`-between-lines * rendering. Under MD-preferred syntax the sub-parser's paragraph * wrapping survives — a quote with one paragraph emits * `
`. */ class GfmQuoteTest extends ParserTestBase { /** * Recursively flatten call lists, descending into `nest` content. * Useful for tests that just check whether an instruction appears * somewhere in the rendered output regardless of nesting depth. */ 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 testSortValue() { $mode = new GfmQuote(); $this->assertSame(220, $mode->getSort()); } // ----- DW-preferred rendering: linebreak-separated, no...
------------ public function testDwSingleLine() { $this->setSyntax('dw'); $this->P->addMode('gfm_quote', new GfmQuote()); $this->P->parse("> foo\n"); $expected = [ ['document_start', []], ['quote_open', []], ['nest', [[ ['cdata', ['foo']] ]]], ['quote_close', []], ['document_end', []], ]; $this->assertCalls($expected, $this->H->calls); } public function testDwSpaceAfterMarkerOptional() { // GFM allows omitting the space after `>`; DW always did. Strip // logic removes one optional space after the `>`, so `>foo` and // `> foo` both produce cdata "foo". $this->setSyntax('dw'); $this->P->addMode('gfm_quote', new GfmQuote()); $this->P->parse(">foo\n"); $names = $this->flatNames($this->H->calls); $this->assertContains('quote_open', $names); $this->assertContains('cdata', $names); } public function testDwTwoLinesEmitLinebreak() { // The DW-preferred post-pass converts the sub-parser's paragraph // wrapping into a linebreak between the two cdata calls, matching // the historical `
foo` shape. $this->setSyntax('dw'); $this->P->addMode('gfm_quote', new GfmQuote()); $this->P->parse("> foo\n> bar\n"); $expected = [ ['document_start', []], ['quote_open', []], ['nest', [[ ['cdata', ['foo']], ['linebreak', []], ['cdata', ['bar']], ]]], ['quote_close', []], ['document_end', []], ]; $this->assertCalls($expected, $this->H->calls); } public function testDwBlankMarkerLineEmitsTwoLinebreaks() { // `>` alone between content lines is a paragraph break in GFM. // The DW post-pass replaces each p_open and each p_close with a // linebreak, producing two adjacent linebreak calls between the // two content cdata — matches the historical DW two-`
bar
`. `# Foo` therefore stays // as plain cdata text. $this->setSyntax('dw'); $this->P->addMode('gfm_quote', new GfmQuote()); $this->P->parse("> # Foo\n"); $names = $this->flatNames($this->H->calls); $this->assertNotContains('header', $names); $this->assertNotContains('section_open', $names); $this->assertContains('cdata', $names); } // ----- MD-preferred rendering: paragraph wrapping survives ------------ public function testMdSingleParagraph() { $this->setSyntax('md'); $this->P->addMode('gfm_quote', new GfmQuote()); $this->P->parse("> foo\n> bar\n"); // Sub-parser wraps the body in `p_open` / `p_close`. The outer // wraps them inside a `nest`, and Block treats the nest as // opaque. Two `>`-content lines join into one paragraph. $expected = [ ['document_start', []], ['quote_open', []], ['nest', [[ ['p_open', []], ['cdata', ["foo\nbar"]], ['p_close', []], ]]], ['quote_close', []], ['document_end', []], ]; $this->assertCalls($expected, $this->H->calls); } public function testMdMultiParagraph() { // `>` alone between content lines creates two paragraphs in one // blockquote — under MD-preferred the post-pass does not run, so // the sub-parser's `p_open` / `p_close` pairs survive intact. $this->setSyntax('md'); $this->P->addMode('gfm_quote', new GfmQuote()); $this->P->parse("> foo\n>\n> bar\n"); $names = $this->flatNames($this->H->calls); $pOpens = array_filter($names, static fn($n) => $n === 'p_open'); $pCloses = array_filter($names, static fn($n) => $n === 'p_close'); $this->assertCount(2, $pOpens, 'two paragraphs inside one blockquote'); $this->assertCount(2, $pCloses); } // ----- Handoff from preceding block modes ---------------------------- // // GfmTable, DW Table, and DW Listblock all consume the boundary \n on // their way out (their exit pattern is \n by structural necessity: at // the boundary there is no leading unmatched content for a zero-width // lookahead exit to attach to). The pattern (?:^|\n)>... lets GfmQuote // open at the line that starts the blockquote regardless of whether // the preceding mode left the \n in the stream. public function testHandoffFromGfmTable() { // Spec example 201: a `>` line immediately following a GFM table // ends the table and opens a blockquote. GfmTable's exit consumes // the boundary \n, so GfmQuote relies on the line-start (^) // alternative to fire here. $this->setSyntax('md'); $this->P->addMode('gfm_table', new GfmTable()); $this->P->addMode('gfm_quote', new GfmQuote()); $this->P->parse("| abc | def |\n| --- | --- |\n| bar | baz |\n> bar"); $names = array_map(static fn($c) => $c[0], $this->H->calls); $this->assertContains('table_open', $names); $this->assertContains('table_close', $names); $this->assertContains('quote_open', $names); $this->assertContains('quote_close', $names); // Order: the quote must open after the table closes. $tableCloseIdx = array_search('table_close', $names, true); $quoteOpenIdx = array_search('quote_open', $names, true); $this->assertGreaterThan($tableCloseIdx, $quoteOpenIdx); } public function testHandoffFromDwTable() { // Same as the GFM-table case for a DW-style table. DW Table's // exit also consumes \n, so the line-start (^) alternative is // what lets the blockquote open. $this->setSyntax('dw'); $this->P->addMode('table', new Table()); $this->P->addMode('gfm_quote', new GfmQuote()); $this->P->parse("| foo | bar |\n> baz"); $names = array_map(static fn($c) => $c[0], $this->H->calls); $this->assertContains('table_open', $names); $this->assertContains('table_close', $names); $this->assertContains('quote_open', $names); $this->assertContains('quote_close', $names); $tableCloseIdx = array_search('table_close', $names, true); $quoteOpenIdx = array_search('quote_open', $names, true); $this->assertGreaterThan($tableCloseIdx, $quoteOpenIdx); } public function testHandoffFromDwListblock() { // DW Listblock also exits on \n, consuming the boundary. The // line-start alternative lets a `>` line right after the list // open a blockquote without an intervening blank line. $this->setSyntax('dw'); $this->P->addMode('listblock', new Listblock()); $this->P->addMode('gfm_quote', new GfmQuote()); $this->P->parse(" * foo\n * bar\n> baz"); $names = array_map(static fn($c) => $c[0], $this->H->calls); $this->assertContains('listu_open', $names); $this->assertContains('listu_close', $names); $this->assertContains('quote_open', $names); $this->assertContains('quote_close', $names); $listCloseIdx = array_search('listu_close', $names, true); $quoteOpenIdx = array_search('quote_open', $names, true); $this->assertGreaterThan($listCloseIdx, $quoteOpenIdx); } public function testMdListInsideQuote() { // GfmListblock is loaded under MD-preferred syntax, so a list // inside a quote parses as a real list. The sub-parser's list // calls land inside the outer `nest` wrapper. $this->setSyntax('md'); // Add the registry's full mode set so gfm_listblock is reachable // via the sub-parser (the sub-parser inherits this registry, whose // syntax determines which modes load). foreach ($this->registry->getModes() as $m) { $this->P->addMode($m['mode'], $m['obj']); } $this->P->parse("> - foo\n> - bar\n"); $names = $this->flatNames($this->H->calls); $this->assertContains('quote_open', $names); $this->assertContains('listu_open', $names, 'list inside quote must parse'); $this->assertContains('listu_close', $names); $this->assertContains('quote_close', $names); } }