parse($this->fixture('synthetic-basic.html'))); $outline = new Outline(['a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d' => 'Hero-Titel']); $rows = $outline->rows($index); self::assertFalse($outline->wasTruncated()); self::assertSame( ['id' => 's0.r0.c1.m0', 'type' => 'heading', 'label' => '', 'excerpt' => 'Willkommen bei Müller & Söhne', 'info' => '', 'presets' => ['Hero-Titel'], 'children' => 0, 'editable' => true, 'depth' => 3, 'truncated' => false], $rows[5] ); self::assertSame('Spalten 8_24+16_24', $rows[1]['info']); self::assertSame('Seit 1987 zuverlässig – jetzt anfragen.', $rows[6]['excerpt']); self::assertSame( 's0 section "Hero"' . "\n" . ' s0.r0 row "Hero – Zeile" [Spalten 8_24+16_24]' . "\n" . ' s0.r0.c0 column' . "\n" . ' s0.r0.c0.m0 image "Hero – Bild" – Unser Team vor dem Büro', implode("\n", array_slice(explode("\n", $outline->text($rows)), 0, 4)) ); } public function testSubtreeAndDepthLimit(): void { $index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-basic.html'))); $outline = new Outline(); $rows = $outline->rows($index, 's1.r0.c1', 1); self::assertSame(['s1.r0.c1', 's1.r0.c1.m0'], array_column($rows, 'id')); self::assertSame([0, 1], array_column($rows, 'depth')); self::assertSame(2, $rows[1]['children']); } /** Ruling S18: outline breadth limit – caps children per parent, marks the parent truncated. */ public function testBreadthLimitCapsChildrenPerParentAndMarksTruncation(): void { $index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-basic.html'))); $outline = new Outline(); // s0.r0.c1 has three children (heading, text, button); cap them at two. $rows = $outline->rows($index, 's0.r0.c1', null, 2); self::assertSame(['s0.r0.c1', 's0.r0.c1.m0', 's0.r0.c1.m1'], array_column($rows, 'id')); self::assertTrue($rows[0]['truncated'], 'the parent whose children were cut is marked'); self::assertFalse($rows[1]['truncated']); self::assertTrue($outline->wasTruncated()); $full = (new Outline())->rows($index, 's0.r0.c1'); self::assertSame(4, count($full), 'the default limit (50) does not cut three children'); } /** The page root has no row of its own, so a top-level cap can only show up in wasTruncated(). */ public function testBreadthLimitAtTheTopLevelStillReportsTruncation(): void { $index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-basic.html'))); $outline = new Outline(); $rows = $outline->rows($index, null, 0, 1); self::assertSame(['s0'], array_column($rows, 'id')); self::assertTrue($outline->wasTruncated()); } public function testReadOnlyNodesAreMarked(): void { $index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-mixed.html'))); $text = (new Outline())->text((new Outline())->rows($index)); self::assertStringContainsString('x0 core/paragraph (nur lesbar)', $text); self::assertStringContainsString('s1.g1 row "Globale Zeile" (nur lesbar)', $text); self::assertStringContainsString('s0 section [specialty]', $text); self::assertStringContainsString('s0.c1.r0 row-inner [Spalten 1_2+1_2]', $text); } public function testSummaryHelpers(): void { self::assertSame('{post_title}', NodeSummary::textOf( '$variable({"type":"content","value":{"name":"post_title","settings":{}}})$' )); self::assertSame('Mehr', NodeSummary::textOf(['linkUrl' => '/x', 'text' => 'Mehr'])); self::assertSame('', NodeSummary::textOf(new stdClass())); } public function testLabelIsWhitespaceNormalizedAndBounded(): void { $block = self::blockWithAdminLabel("Hero \n Titel\twith space"); self::assertSame('Hero Titel with space', NodeSummary::label($block)); $long = str_repeat('a', 200); $shortened = NodeSummary::label(self::blockWithAdminLabel($long)); self::assertLessThan(200, mb_strlen($shortened)); self::assertStringEndsWith('…', $shortened); } private static function blockWithAdminLabel(string $label): Block { return Block::create('divi/text', [ 'module' => ['meta' => ['adminLabel' => ['desktop' => ['value' => $label]]]], ]); } public function testAttrMapFlattenGetSetRemove(): void { $attrs = [ 'builderVersion' => '5.2.1', 'title' => ['innerContent' => ['desktop' => ['value' => 'Hallo']], 'decoration' => ['font' => [ 'font' => ['desktop' => ['value' => ['headingLevel' => 'h2']], 'phone' => ['value' => ['size' => '20px']]], ]]], 'module' => ['decoration' => ['spacing' => ['desktop' => ['value' => new stdClass()]]]], ]; self::assertSame( ['module.decoration.spacing', 'title.decoration.font.font', 'title.innerContent'], array_keys(AttrMap::flatten($attrs)) ); self::assertSame('Hallo', AttrMap::desktop($attrs, 'title.innerContent')); self::assertTrue(AttrMap::isBreakpointMap(['desktop' => [], 'phone' => []])); self::assertFalse(AttrMap::isBreakpointMap(['desktop' => [], 'font' => []])); $changed = AttrMap::set($attrs, 'module.decoration.spacing.desktop.value.padding', '10px'); self::assertSame('10px', AttrMap::get($changed, 'module.decoration.spacing.desktop.value.padding')); $removed = AttrMap::remove($attrs, 'title.decoration.font.font'); self::assertArrayNotHasKey('decoration', $removed['title']); self::assertSame($attrs, AttrMap::remove($attrs, 'nope.path')); } private function fixture(string $name): string { return (string) file_get_contents(dirname(__DIR__, 4) . '/Fixtures/divi/' . $name); } }