P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("- A\n- B\n- C\n"); $expected = [ ['document_start', []], ['listu_open', []], ['listitem_open', [1]], ['listcontent_open', []], ['nest', [[ ['cdata', ['A']] ]]], ['listcontent_close', []], ['listitem_close', []], ['listitem_open', [1]], ['listcontent_open', []], ['nest', [[ ['cdata', ['B']] ]]], ['listcontent_close', []], ['listitem_close', []], ['listitem_open', [1]], ['listcontent_open', []], ['nest', [[ ['cdata', ['C']] ]]], ['listcontent_close', []], ['listitem_close', []], ['listu_close', []], ['document_end', []], ]; $this->assertCalls($expected, $this->H->calls); } public function testUnorderedAsterisk() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("* A\n* B\n"); $names = array_column($this->H->calls, 0); $this->assertContains('listu_open', $names); $this->assertNotContains('listo_open', $names); } public function testUnorderedPlus() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("+ A\n+ B\n"); $names = array_column($this->H->calls, 0); $this->assertContains('listu_open', $names); $this->assertNotContains('listo_open', $names); } public function testOrderedDot() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("1. A\n2. B\n3. C\n"); $names = array_column($this->H->calls, 0); $this->assertContains('listo_open', $names); $this->assertNotContains('listu_open', $names); } public function testOrderedParen() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("1) A\n2) B\n"); $names = array_column($this->H->calls, 0); $this->assertContains('listo_open', $names); } public function testOrderedStartNumber() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("5. A\n6. B\n"); $opens = array_filter($this->H->calls, static fn($c) => $c[0] === 'listo_open_start'); $this->assertCount(1, $opens, 'non-default start emits listo_open_start, not listo_open'); $open = array_values($opens)[0]; $this->assertSame([5], $open[1], 'listo_open_start must carry the first item start number'); $plainOpens = array_filter($this->H->calls, static fn($c) => $c[0] === 'listo_open'); $this->assertCount(0, $plainOpens, 'plain listo_open is not emitted when start != 1'); } public function testOrderedDefaultStartNotEmittedSpecially() { // For start=1 the rewriter emits the plain listo_open instruction so // unmodified plugin renderers (which only override listo_open) keep // working. The wire shape is bare []. $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("1. A\n2. B\n"); $opens = array_filter($this->H->calls, static fn($c) => $c[0] === 'listo_open'); $open = array_values($opens)[0]; $this->assertSame([], $open[1]); $startOpens = array_filter($this->H->calls, static fn($c) => $c[0] === 'listo_open_start'); $this->assertCount(0, $startOpens, 'start=1 must not emit listo_open_start'); } public function testNestedTwoLevels() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("- A\n - B\n- C\n"); $expected = [ ['document_start', []], ['listu_open', []], ['listitem_open', [1, GfmLists::NODE]], ['listcontent_open', []], ['nest', [[ ['cdata', ['A']] ]]], ['listcontent_close', []], ['listu_open', []], ['listitem_open', [2]], ['listcontent_open', []], ['nest', [[ ['cdata', ['B']] ]]], ['listcontent_close', []], ['listitem_close', []], ['listu_close', []], ['listitem_close', []], ['listitem_open', [1]], ['listcontent_open', []], ['nest', [[ ['cdata', ['C']] ]]], ['listcontent_close', []], ['listitem_close', []], ['listu_close', []], ['document_end', []], ]; $this->assertCalls($expected, $this->H->calls); } /** * Flatten a call list, recursing into `nest` calls' inner content. * Useful for tests that just want to verify a particular instruction * appears somewhere in the rendered output regardless of nesting. */ private function flatNames(array $calls): array { $names = []; foreach ($calls as $call) { $names[] = $call[0]; if ($call[0] === 'nest') { $names = array_merge($names, $this->flatNames($call[1][0])); } } return $names; } public function testNestedThreeLevels() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("- A\n - B\n - C\n"); $itemOpens = array_filter($this->H->calls, static fn($c) => $c[0] === 'listitem_open'); $levels = array_map(static fn($c) => $c[1][0], array_values($itemOpens)); $this->assertSame([1, 2, 3], $levels); } public function testInlineFormatting() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("- **bold** text\n"); $names = $this->flatNames($this->H->calls); $this->assertContains('strong_open', $names, 'inline strong must be parsed inside item'); $this->assertContains('strong_close', $names); } public function testMarkerCharSwitchKeepsOneList() { // CommonMark: changing marker character (`-` → `+`) starts a new list. // Our simpler model groups by type ('u' / 'o') only, so `-` and `+` // share one
` (it already has * its own `
` from the sub-parser), producing nested paragraph tags. */ public function testItemContentIsWrappedInNest() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("- foo\n"); $nests = array_filter($this->H->calls, static fn($c) => $c[0] === 'nest'); $this->assertCount(1, $nests, 'each item body should land in one nest call'); } /** * Regression: multiple consecutive blank lines inside a list block must * NOT terminate the list. Spec example 242 (`- Foo\n\n bar\n\n\n * baz`) ends with a triple blank between two indented continuations and * expects all three to remain inside one list item. */ public function testTripleBlankBetweenContinuationsKeepsListOpen() { $this->P->addMode('gfm_listblock', new GfmListblock()); $this->P->parse("- Foo\n\n bar\n\n\n baz\n"); // The list should bracket all three indented lines: `- Foo`, `bar`, // and `baz` all live inside a single `