1309a0852SAndreas Gohr<?php 2309a0852SAndreas Gohr 3309a0852SAndreas Gohrnamespace dokuwiki\test\Parsing\ParserMode; 4309a0852SAndreas Gohr 5309a0852SAndreas Gohruse dokuwiki\Parsing\ModeRegistry; 6309a0852SAndreas Gohruse dokuwiki\Parsing\ParserMode\GfmQuote; 7*dccbd514SAndreas Gohruse dokuwiki\Parsing\ParserMode\GfmTable; 8*dccbd514SAndreas Gohruse dokuwiki\Parsing\ParserMode\Listblock; 9*dccbd514SAndreas Gohruse dokuwiki\Parsing\ParserMode\Table; 10309a0852SAndreas Gohr 11309a0852SAndreas Gohr/** 12309a0852SAndreas Gohr * Tests for GFM-style block quotes. 13309a0852SAndreas Gohr * 14309a0852SAndreas Gohr * GfmQuote is the unified blockquote implementation covering both DW and 15309a0852SAndreas Gohr * GFM dialects. The mode captures the entire quote via addSpecialPattern 16309a0852SAndreas Gohr * and sub-parses the stripped body, so the outer parser only needs 17309a0852SAndreas Gohr * gfm_quote attached; inline modes and block modes (lists, code blocks, 18309a0852SAndreas Gohr * nested quotes) are picked up by the sub-parser. 19309a0852SAndreas Gohr * 20309a0852SAndreas Gohr * Two rendering shapes are exercised. Under DW-preferred syntax, a 21309a0852SAndreas Gohr * post-pass flattens the sub-parser's paragraph wrapping into linebreak- 22309a0852SAndreas Gohr * separated cdata so existing DW pages keep their `<br/>`-between-lines 23309a0852SAndreas Gohr * rendering. Under MD-preferred syntax the sub-parser's paragraph 24309a0852SAndreas Gohr * wrapping survives — a quote with one paragraph emits 25309a0852SAndreas Gohr * `<blockquote><p>...</p></blockquote>`. 26309a0852SAndreas Gohr */ 27309a0852SAndreas Gohrclass GfmQuoteTest extends ParserTestBase 28309a0852SAndreas Gohr{ 29309a0852SAndreas Gohr public function tearDown(): void 30309a0852SAndreas Gohr { 31309a0852SAndreas Gohr ModeRegistry::reset(); 32309a0852SAndreas Gohr parent::tearDown(); 33309a0852SAndreas Gohr } 34309a0852SAndreas Gohr 35309a0852SAndreas Gohr private function setSyntax(string $syntax): void 36309a0852SAndreas Gohr { 37309a0852SAndreas Gohr global $conf; 38309a0852SAndreas Gohr $conf['syntax'] = $syntax; 39309a0852SAndreas Gohr ModeRegistry::reset(); 40309a0852SAndreas Gohr } 41309a0852SAndreas Gohr 42309a0852SAndreas Gohr /** 43309a0852SAndreas Gohr * Recursively flatten call lists, descending into `nest` content. 44309a0852SAndreas Gohr * Useful for tests that just check whether an instruction appears 45309a0852SAndreas Gohr * somewhere in the rendered output regardless of nesting depth. 46309a0852SAndreas Gohr */ 47309a0852SAndreas Gohr private function flatNames(array $calls): array 48309a0852SAndreas Gohr { 49309a0852SAndreas Gohr $names = []; 50309a0852SAndreas Gohr foreach ($calls as $call) { 51309a0852SAndreas Gohr $names[] = $call[0]; 52309a0852SAndreas Gohr if ($call[0] === 'nest') { 53309a0852SAndreas Gohr $names = array_merge($names, $this->flatNames($call[1][0])); 54309a0852SAndreas Gohr } 55309a0852SAndreas Gohr } 56309a0852SAndreas Gohr return $names; 57309a0852SAndreas Gohr } 58309a0852SAndreas Gohr 59309a0852SAndreas Gohr public function testSortValue() 60309a0852SAndreas Gohr { 61309a0852SAndreas Gohr $mode = new GfmQuote(); 62309a0852SAndreas Gohr $this->assertSame(220, $mode->getSort()); 63309a0852SAndreas Gohr } 64309a0852SAndreas Gohr 65309a0852SAndreas Gohr // ----- DW-preferred rendering: linebreak-separated, no <p> ------------ 66309a0852SAndreas Gohr 67309a0852SAndreas Gohr public function testDwSingleLine() 68309a0852SAndreas Gohr { 6913a62f81SAndreas Gohr $this->setSyntax('dw'); 70309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 71309a0852SAndreas Gohr $this->P->parse("> foo\n"); 72309a0852SAndreas Gohr 73309a0852SAndreas Gohr $expected = [ 74309a0852SAndreas Gohr ['document_start', []], 75309a0852SAndreas Gohr ['quote_open', []], 76309a0852SAndreas Gohr ['nest', [[ ['cdata', ['foo']] ]]], 77309a0852SAndreas Gohr ['quote_close', []], 78309a0852SAndreas Gohr ['document_end', []], 79309a0852SAndreas Gohr ]; 80309a0852SAndreas Gohr $this->assertCalls($expected, $this->H->calls); 81309a0852SAndreas Gohr } 82309a0852SAndreas Gohr 83309a0852SAndreas Gohr public function testDwSpaceAfterMarkerOptional() 84309a0852SAndreas Gohr { 85309a0852SAndreas Gohr // GFM allows omitting the space after `>`; DW always did. Strip 86309a0852SAndreas Gohr // logic removes one optional space after the `>`, so `>foo` and 87309a0852SAndreas Gohr // `> foo` both produce cdata "foo". 8813a62f81SAndreas Gohr $this->setSyntax('dw'); 89309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 90309a0852SAndreas Gohr $this->P->parse(">foo\n"); 91309a0852SAndreas Gohr 92309a0852SAndreas Gohr $names = $this->flatNames($this->H->calls); 93309a0852SAndreas Gohr $this->assertContains('quote_open', $names); 94309a0852SAndreas Gohr $this->assertContains('cdata', $names); 95309a0852SAndreas Gohr } 96309a0852SAndreas Gohr 97309a0852SAndreas Gohr public function testDwTwoLinesEmitLinebreak() 98309a0852SAndreas Gohr { 99309a0852SAndreas Gohr // The DW-preferred post-pass converts the sub-parser's paragraph 100309a0852SAndreas Gohr // wrapping into a linebreak between the two cdata calls, matching 101309a0852SAndreas Gohr // the historical `<blockquote>foo<br/>bar</blockquote>` shape. 10213a62f81SAndreas Gohr $this->setSyntax('dw'); 103309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 104309a0852SAndreas Gohr $this->P->parse("> foo\n> bar\n"); 105309a0852SAndreas Gohr 106309a0852SAndreas Gohr $expected = [ 107309a0852SAndreas Gohr ['document_start', []], 108309a0852SAndreas Gohr ['quote_open', []], 109309a0852SAndreas Gohr ['nest', [[ 110309a0852SAndreas Gohr ['cdata', ['foo']], 111309a0852SAndreas Gohr ['linebreak', []], 112309a0852SAndreas Gohr ['cdata', ['bar']], 113309a0852SAndreas Gohr ]]], 114309a0852SAndreas Gohr ['quote_close', []], 115309a0852SAndreas Gohr ['document_end', []], 116309a0852SAndreas Gohr ]; 117309a0852SAndreas Gohr $this->assertCalls($expected, $this->H->calls); 118309a0852SAndreas Gohr } 119309a0852SAndreas Gohr 120309a0852SAndreas Gohr public function testDwBlankMarkerLineEmitsTwoLinebreaks() 121309a0852SAndreas Gohr { 122309a0852SAndreas Gohr // `>` alone between content lines is a paragraph break in GFM. 123309a0852SAndreas Gohr // The DW post-pass replaces each p_open and each p_close with a 124309a0852SAndreas Gohr // linebreak, producing two adjacent linebreak calls between the 125309a0852SAndreas Gohr // two content cdata — matches the historical DW two-`<br/>` shape. 12613a62f81SAndreas Gohr $this->setSyntax('dw'); 127309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 128309a0852SAndreas Gohr $this->P->parse("> foo\n>\n> bar\n"); 129309a0852SAndreas Gohr 130309a0852SAndreas Gohr $expected = [ 131309a0852SAndreas Gohr ['document_start', []], 132309a0852SAndreas Gohr ['quote_open', []], 133309a0852SAndreas Gohr ['nest', [[ 134309a0852SAndreas Gohr ['cdata', ['foo']], 135309a0852SAndreas Gohr ['linebreak', []], 136309a0852SAndreas Gohr ['linebreak', []], 137309a0852SAndreas Gohr ['cdata', ['bar']], 138309a0852SAndreas Gohr ]]], 139309a0852SAndreas Gohr ['quote_close', []], 140309a0852SAndreas Gohr ['document_end', []], 141309a0852SAndreas Gohr ]; 142309a0852SAndreas Gohr $this->assertCalls($expected, $this->H->calls); 143309a0852SAndreas Gohr } 144309a0852SAndreas Gohr 145309a0852SAndreas Gohr public function testDwNested() 146309a0852SAndreas Gohr { 14713a62f81SAndreas Gohr $this->setSyntax('dw'); 148309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 149309a0852SAndreas Gohr $this->P->parse("> > foo\n"); 150309a0852SAndreas Gohr 151309a0852SAndreas Gohr // The outer captures a single line `> > foo`. Stripping the 152309a0852SAndreas Gohr // outer marker leaves `> foo`, which the sub-parser feeds back 153309a0852SAndreas Gohr // through GfmQuote — recursion produces a nested quote_open / 154309a0852SAndreas Gohr // quote_close pair carrying the cdata. 155309a0852SAndreas Gohr $names = $this->flatNames($this->H->calls); 156309a0852SAndreas Gohr $opens = array_filter($names, static fn($n) => $n === 'quote_open'); 157309a0852SAndreas Gohr $closes = array_filter($names, static fn($n) => $n === 'quote_close'); 158309a0852SAndreas Gohr $this->assertCount(2, $opens, 'two levels of quote_open expected'); 159309a0852SAndreas Gohr $this->assertCount(2, $closes, 'two levels of quote_close expected'); 160309a0852SAndreas Gohr } 161309a0852SAndreas Gohr 162309a0852SAndreas Gohr public function testDwNoLazyContinuation() 163309a0852SAndreas Gohr { 164309a0852SAndreas Gohr // GfmQuote does not implement lazy continuation: every quote 165309a0852SAndreas Gohr // line must begin with `>`. `bar` without a `>` prefix terminates 166309a0852SAndreas Gohr // the quote, so it ends up as a separate paragraph — matching 167309a0852SAndreas Gohr // today's DW behavior. 16813a62f81SAndreas Gohr $this->setSyntax('dw'); 169309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 170309a0852SAndreas Gohr $this->P->parse("> foo\nbar\n"); 171309a0852SAndreas Gohr 172309a0852SAndreas Gohr $opens = array_filter($this->H->calls, static fn($c) => $c[0] === 'quote_open'); 173309a0852SAndreas Gohr $this->assertCount(1, $opens, 'quote opens once and stops at the non-`>` line'); 174309a0852SAndreas Gohr 175309a0852SAndreas Gohr // `bar` is outside the quote — find a top-level cdata after the close 176309a0852SAndreas Gohr $afterClose = false; 177309a0852SAndreas Gohr $sawBarOutside = false; 178309a0852SAndreas Gohr foreach ($this->H->calls as $call) { 179309a0852SAndreas Gohr if ($call[0] === 'quote_close') $afterClose = true; 180309a0852SAndreas Gohr if ($afterClose && $call[0] === 'cdata' && str_contains($call[1][0], 'bar')) { 181309a0852SAndreas Gohr $sawBarOutside = true; 182309a0852SAndreas Gohr } 183309a0852SAndreas Gohr } 184309a0852SAndreas Gohr $this->assertTrue($sawBarOutside, '`bar` must appear as cdata outside the quote'); 185309a0852SAndreas Gohr } 186309a0852SAndreas Gohr 187309a0852SAndreas Gohr public function testDwBlankLineSeparatesQuotes() 188309a0852SAndreas Gohr { 189309a0852SAndreas Gohr // A truly blank line ends the quote. The next `>` starts a new 190309a0852SAndreas Gohr // quote, producing two distinct quote_open / quote_close pairs. 19113a62f81SAndreas Gohr $this->setSyntax('dw'); 192309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 193309a0852SAndreas Gohr $this->P->parse("> foo\n\n> bar\n"); 194309a0852SAndreas Gohr 195309a0852SAndreas Gohr $opens = array_filter($this->H->calls, static fn($c) => $c[0] === 'quote_open'); 196309a0852SAndreas Gohr $closes = array_filter($this->H->calls, static fn($c) => $c[0] === 'quote_close'); 197309a0852SAndreas Gohr $this->assertCount(2, $opens, 'two distinct quote blocks'); 198309a0852SAndreas Gohr $this->assertCount(2, $closes); 199309a0852SAndreas Gohr } 200309a0852SAndreas Gohr 201309a0852SAndreas Gohr public function testDwHeaderInsideQuoteStaysCdata() 202309a0852SAndreas Gohr { 203309a0852SAndreas Gohr // Sub-parser excludes BASEONLY (Header / GfmHeader). Header 204309a0852SAndreas Gohr // instructions drive section-edit anchors and TOC entries that 205309a0852SAndreas Gohr // do not compose with `<blockquote>`. `# Foo` therefore stays 206309a0852SAndreas Gohr // as plain cdata text. 20713a62f81SAndreas Gohr $this->setSyntax('dw'); 208309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 209309a0852SAndreas Gohr $this->P->parse("> # Foo\n"); 210309a0852SAndreas Gohr 211309a0852SAndreas Gohr $names = $this->flatNames($this->H->calls); 212309a0852SAndreas Gohr $this->assertNotContains('header', $names); 213309a0852SAndreas Gohr $this->assertNotContains('section_open', $names); 214309a0852SAndreas Gohr $this->assertContains('cdata', $names); 215309a0852SAndreas Gohr } 216309a0852SAndreas Gohr 217309a0852SAndreas Gohr // ----- MD-preferred rendering: paragraph wrapping survives ------------ 218309a0852SAndreas Gohr 219309a0852SAndreas Gohr public function testMdSingleParagraph() 220309a0852SAndreas Gohr { 22113a62f81SAndreas Gohr $this->setSyntax('md'); 222309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 223309a0852SAndreas Gohr $this->P->parse("> foo\n> bar\n"); 224309a0852SAndreas Gohr 225309a0852SAndreas Gohr // Sub-parser wraps the body in `p_open` / `p_close`. The outer 226309a0852SAndreas Gohr // wraps them inside a `nest`, and Block treats the nest as 227309a0852SAndreas Gohr // opaque. Two `>`-content lines join into one paragraph. 228309a0852SAndreas Gohr $expected = [ 229309a0852SAndreas Gohr ['document_start', []], 230309a0852SAndreas Gohr ['quote_open', []], 231309a0852SAndreas Gohr ['nest', [[ 232309a0852SAndreas Gohr ['p_open', []], 233309a0852SAndreas Gohr ['cdata', ["foo\nbar"]], 234309a0852SAndreas Gohr ['p_close', []], 235309a0852SAndreas Gohr ]]], 236309a0852SAndreas Gohr ['quote_close', []], 237309a0852SAndreas Gohr ['document_end', []], 238309a0852SAndreas Gohr ]; 239309a0852SAndreas Gohr $this->assertCalls($expected, $this->H->calls); 240309a0852SAndreas Gohr } 241309a0852SAndreas Gohr 242309a0852SAndreas Gohr public function testMdMultiParagraph() 243309a0852SAndreas Gohr { 244309a0852SAndreas Gohr // `>` alone between content lines creates two paragraphs in one 245309a0852SAndreas Gohr // blockquote — under MD-preferred the post-pass does not run, so 246309a0852SAndreas Gohr // the sub-parser's `p_open` / `p_close` pairs survive intact. 24713a62f81SAndreas Gohr $this->setSyntax('md'); 248309a0852SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 249309a0852SAndreas Gohr $this->P->parse("> foo\n>\n> bar\n"); 250309a0852SAndreas Gohr 251309a0852SAndreas Gohr $names = $this->flatNames($this->H->calls); 252309a0852SAndreas Gohr $pOpens = array_filter($names, static fn($n) => $n === 'p_open'); 253309a0852SAndreas Gohr $pCloses = array_filter($names, static fn($n) => $n === 'p_close'); 254309a0852SAndreas Gohr $this->assertCount(2, $pOpens, 'two paragraphs inside one blockquote'); 255309a0852SAndreas Gohr $this->assertCount(2, $pCloses); 256309a0852SAndreas Gohr } 257309a0852SAndreas Gohr 258*dccbd514SAndreas Gohr // ----- Handoff from preceding block modes ---------------------------- 259*dccbd514SAndreas Gohr // 260*dccbd514SAndreas Gohr // GfmTable, DW Table, and DW Listblock all consume the boundary \n on 261*dccbd514SAndreas Gohr // their way out (their exit pattern is \n by structural necessity: at 262*dccbd514SAndreas Gohr // the boundary there is no leading unmatched content for a zero-width 263*dccbd514SAndreas Gohr // lookahead exit to attach to). The pattern (?:^|\n)>... lets GfmQuote 264*dccbd514SAndreas Gohr // open at the line that starts the blockquote regardless of whether 265*dccbd514SAndreas Gohr // the preceding mode left the \n in the stream. 266*dccbd514SAndreas Gohr 267*dccbd514SAndreas Gohr public function testHandoffFromGfmTable() 268*dccbd514SAndreas Gohr { 269*dccbd514SAndreas Gohr // Spec example 201: a `>` line immediately following a GFM table 270*dccbd514SAndreas Gohr // ends the table and opens a blockquote. GfmTable's exit consumes 271*dccbd514SAndreas Gohr // the boundary \n, so GfmQuote relies on the line-start (^) 272*dccbd514SAndreas Gohr // alternative to fire here. 273*dccbd514SAndreas Gohr $this->setSyntax('md'); 274*dccbd514SAndreas Gohr $this->P->addMode('gfm_table', new GfmTable()); 275*dccbd514SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 276*dccbd514SAndreas Gohr $this->P->parse("| abc | def |\n| --- | --- |\n| bar | baz |\n> bar"); 277*dccbd514SAndreas Gohr 278*dccbd514SAndreas Gohr $names = array_map(static fn($c) => $c[0], $this->H->calls); 279*dccbd514SAndreas Gohr $this->assertContains('table_open', $names); 280*dccbd514SAndreas Gohr $this->assertContains('table_close', $names); 281*dccbd514SAndreas Gohr $this->assertContains('quote_open', $names); 282*dccbd514SAndreas Gohr $this->assertContains('quote_close', $names); 283*dccbd514SAndreas Gohr 284*dccbd514SAndreas Gohr // Order: the quote must open after the table closes. 285*dccbd514SAndreas Gohr $tableCloseIdx = array_search('table_close', $names, true); 286*dccbd514SAndreas Gohr $quoteOpenIdx = array_search('quote_open', $names, true); 287*dccbd514SAndreas Gohr $this->assertGreaterThan($tableCloseIdx, $quoteOpenIdx); 288*dccbd514SAndreas Gohr } 289*dccbd514SAndreas Gohr 290*dccbd514SAndreas Gohr public function testHandoffFromDwTable() 291*dccbd514SAndreas Gohr { 292*dccbd514SAndreas Gohr // Same as the GFM-table case for a DW-style table. DW Table's 293*dccbd514SAndreas Gohr // exit also consumes \n, so the line-start (^) alternative is 294*dccbd514SAndreas Gohr // what lets the blockquote open. 295*dccbd514SAndreas Gohr $this->setSyntax('dw'); 296*dccbd514SAndreas Gohr $this->P->addMode('table', new Table()); 297*dccbd514SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 298*dccbd514SAndreas Gohr $this->P->parse("| foo | bar |\n> baz"); 299*dccbd514SAndreas Gohr 300*dccbd514SAndreas Gohr $names = array_map(static fn($c) => $c[0], $this->H->calls); 301*dccbd514SAndreas Gohr $this->assertContains('table_open', $names); 302*dccbd514SAndreas Gohr $this->assertContains('table_close', $names); 303*dccbd514SAndreas Gohr $this->assertContains('quote_open', $names); 304*dccbd514SAndreas Gohr $this->assertContains('quote_close', $names); 305*dccbd514SAndreas Gohr 306*dccbd514SAndreas Gohr $tableCloseIdx = array_search('table_close', $names, true); 307*dccbd514SAndreas Gohr $quoteOpenIdx = array_search('quote_open', $names, true); 308*dccbd514SAndreas Gohr $this->assertGreaterThan($tableCloseIdx, $quoteOpenIdx); 309*dccbd514SAndreas Gohr } 310*dccbd514SAndreas Gohr 311*dccbd514SAndreas Gohr public function testHandoffFromDwListblock() 312*dccbd514SAndreas Gohr { 313*dccbd514SAndreas Gohr // DW Listblock also exits on \n, consuming the boundary. The 314*dccbd514SAndreas Gohr // line-start alternative lets a `>` line right after the list 315*dccbd514SAndreas Gohr // open a blockquote without an intervening blank line. 316*dccbd514SAndreas Gohr $this->setSyntax('dw'); 317*dccbd514SAndreas Gohr $this->P->addMode('listblock', new Listblock()); 318*dccbd514SAndreas Gohr $this->P->addMode('gfm_quote', new GfmQuote()); 319*dccbd514SAndreas Gohr $this->P->parse(" * foo\n * bar\n> baz"); 320*dccbd514SAndreas Gohr 321*dccbd514SAndreas Gohr $names = array_map(static fn($c) => $c[0], $this->H->calls); 322*dccbd514SAndreas Gohr $this->assertContains('listu_open', $names); 323*dccbd514SAndreas Gohr $this->assertContains('listu_close', $names); 324*dccbd514SAndreas Gohr $this->assertContains('quote_open', $names); 325*dccbd514SAndreas Gohr $this->assertContains('quote_close', $names); 326*dccbd514SAndreas Gohr 327*dccbd514SAndreas Gohr $listCloseIdx = array_search('listu_close', $names, true); 328*dccbd514SAndreas Gohr $quoteOpenIdx = array_search('quote_open', $names, true); 329*dccbd514SAndreas Gohr $this->assertGreaterThan($listCloseIdx, $quoteOpenIdx); 330*dccbd514SAndreas Gohr } 331*dccbd514SAndreas Gohr 332309a0852SAndreas Gohr public function testMdListInsideQuote() 333309a0852SAndreas Gohr { 334309a0852SAndreas Gohr // GfmListblock is loaded under MD-preferred syntax, so a list 335309a0852SAndreas Gohr // inside a quote parses as a real list. The sub-parser's list 336309a0852SAndreas Gohr // calls land inside the outer `nest` wrapper. 33713a62f81SAndreas Gohr $this->setSyntax('md'); 338309a0852SAndreas Gohr ModeRegistry::reset(); 339309a0852SAndreas Gohr // Add the registry's full mode set so gfm_listblock is reachable 340309a0852SAndreas Gohr // via the sub-parser (the sub-parser uses ModeRegistry::getModes, 341309a0852SAndreas Gohr // which honors $conf['syntax']). 342309a0852SAndreas Gohr foreach (ModeRegistry::getInstance()->getModes() as $m) { 343309a0852SAndreas Gohr $this->P->addMode($m['mode'], $m['obj']); 344309a0852SAndreas Gohr } 345309a0852SAndreas Gohr 346309a0852SAndreas Gohr $this->P->parse("> - foo\n> - bar\n"); 347309a0852SAndreas Gohr 348309a0852SAndreas Gohr $names = $this->flatNames($this->H->calls); 349309a0852SAndreas Gohr $this->assertContains('quote_open', $names); 350309a0852SAndreas Gohr $this->assertContains('listu_open', $names, 'list inside quote must parse'); 351309a0852SAndreas Gohr $this->assertContains('listu_close', $names); 352309a0852SAndreas Gohr $this->assertContains('quote_close', $names); 353309a0852SAndreas Gohr } 354309a0852SAndreas Gohr} 355