P->addMode('file',new File());
$this->P->parse('Foo testing Bar');
$calls = [
['document_start',[]],
['p_open',[]],
['cdata',["\n".'Foo ']],
['p_close',[]],
['code',['testing', null, null]],
['p_open',[]],
['cdata',['Bar']],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testCodeWhitespace() {
$this->P->addMode('code',new Code());
$this->P->parse("Foo testing Bar");
$calls = [
['document_start',[]],
['p_open',[]],
['cdata',["\n".'Foo ']],
['p_close',[]],
['code',['testing', null, null]],
['p_open',[]],
['cdata',['Bar']],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testCodeLang() {
$this->P->addMode('code',new Code());
$this->P->parse("Foo testing Bar");
$calls = [
['document_start',[]],
['p_open',[]],
['cdata',["\n".'Foo ']],
['p_close',[]],
['code',['testing', 'php', null]],
['p_open',[]],
['cdata',['Bar']],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testPreformatted() {
$this->P->addMode('preformatted',new Preformatted());
$this->P->parse("F oo\n x \n y \nBar\n");
$calls = [
['document_start',[]],
['p_open',[]],
['cdata',["\nF oo"]],
['p_close',[]],
['preformatted',["x \n y "]],
['p_open',[]],
['cdata',["\nBar"]],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testPreformattedWinEOL() {
$this->P->addMode('preformatted',new Preformatted());
$this->P->parse("F oo\r\n x \r\n y \r\nBar\r\n");
$calls = [
['document_start',[]],
['p_open',[]],
['cdata',["\nF oo"]],
['p_close',[]],
['preformatted',["x \n y "]],
['p_open',[]],
['cdata',["\nBar"]],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testPreformattedTab() {
$this->P->addMode('preformatted',new Preformatted());
$this->P->parse("F oo\n\tx\t\n\t\ty\t\nBar\n");
$calls = [
['document_start',[]],
['p_open',[]],
['cdata',["\nF oo"]],
['p_close',[]],
['preformatted',["x\t\n\ty\t"]],
['p_open',[]],
['cdata',["\nBar"]],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testPreformattedTabWinEOL() {
$this->P->addMode('preformatted',new Preformatted());
$this->P->parse("F oo\r\n\tx\t\r\n\t\ty\t\r\nBar\r\n");
$calls = [
['document_start',[]],
['p_open',[]],
['cdata',["\nF oo"]],
['p_close',[]],
['preformatted',["x\t\n\ty\t"]],
['p_open',[]],
['cdata',["\nBar"]],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testPreformattedList() {
// Listblock (sort 10) must be added before Preformatted (sort 20) so
// the resulting PCRE alternation matches the canonical mode order.
// PCRE picks the first alternative that matches at a given position,
// and an indented bullet line like " - x" matches both modes at the
// same offset; Listblock has to come first to win the tie.
$this->P->addMode('listblock',new Listblock());
$this->P->addMode('preformatted',new Preformatted());
$this->P->parse(" - x \n * y \nF oo\n x \n y \n -X\n *Y\nBar\n");
$calls = [
['document_start',[]],
['listo_open',[]],
['listitem_open',[1]],
['listcontent_open',[]],
['cdata',[" x "]],
['listcontent_close',[]],
['listitem_close',[]],
['listo_close',[]],
['listu_open',[]],
['listitem_open',[1]],
['listcontent_open',[]],
['cdata',[" y "]],
['listcontent_close',[]],
['listitem_close',[]],
['listu_close',[]],
['p_open',[]],
['cdata',["F oo"]],
['p_close',[]],
['preformatted',["x \n y \n-X\n*Y"]],
['p_open',[]],
['cdata',["\nBar"]],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testIndentedListEndsTableNotSwallowedAsPreformatted() {
// The list-awareness lookahead on preformatted's entry patterns is
// what keeps an indented list line from being read as an indented code
// block. In base mode sort order already gives listblock the tie, but
// table mode accepts preformatted (a protected mode) without connecting
// listblock, so inside a table only the lookahead stops "\n *" from
// entering preformatted and swallowing the list. Without it the list
// lines vanish and the table's section-edit range grows to cover them.
$this->P->addMode('table', new Table());
$this->P->addMode('preformatted', new Preformatted());
$this->P->addMode('listblock', new Listblock());
$this->P->parse("| a |\n * one\n * two\nafter\n");
$calls = [
['document_start', []],
['table_open', [1, 1, 1]],
['tablerow_open', []],
['tablecell_open', [1, null, 1]],
['cdata', [" a "]],
['tablecell_close', []],
['tablerow_close', []],
['table_close', [6]],
['p_open', []],
['cdata', ["* one"]],
['p_close', []],
['listu_open', []],
['listitem_open', [1]],
['listcontent_open', []],
['cdata', [" two"]],
['listcontent_close', []],
['listitem_close', []],
['listu_close', []],
['p_open', []],
['cdata', ["after"]],
['p_close', []],
['document_end', []],
];
$this->assertCalls($calls, $this->H->calls);
}
function testIndentedListEndsTableInMixedMarkdownSyntax() {
// Same table/preformatted regression, but in a Markdown-preferred mixed
// syntax (md+dw): the DokuWiki table still exists, but GfmListblock owns
// lists (with +, ordered markers and tab indents) and is not connected
// inside the table either. The list-guard's marker set must follow the
// active list mode, so a tab-indented list still ends the table here
// instead of being swallowed as an indented code block.
$this->setSyntax('md+dw');
$this->P->addMode('table', new Table());
$this->P->addMode('preformatted', new Preformatted());
$this->P->addMode('gfm_listblock', new GfmListblock());
$this->P->parse("| a |\n\t* one\n\t* two\nafter\n");
$calls = [
['document_start', []],
['table_open', [1, 1, 1]],
['tablerow_open', []],
['tablecell_open', [1, null, 1]],
['cdata', [" a "]],
['tablecell_close', []],
['tablerow_close', []],
['table_close', [6]],
['p_open', []],
['cdata', ["* one"]],
['p_close', []],
['listu_open', []],
['listitem_open', [1]],
['listcontent_open', []],
['nest', [[['cdata', ["two"]]]]],
['listcontent_close', []],
['listitem_close', []],
['listu_close', []],
['p_open', []],
['cdata', ["\nafter"]],
['p_close', []],
['document_end', []],
];
$this->assertCalls($calls, $this->H->calls);
}
function testMarkdownPreferredUsesFourSpaces() {
// In `md` and `md+dw` settings the indent threshold is 4,
// matching GFM's indented code block rule. Lines with only 2-3
// leading spaces stay as paragraph text.
$this->setSyntax('md');
$this->P->addMode('preformatted', new Preformatted());
$this->P->parse("F oo\n x \n y \nBar\n");
$calls = [
['document_start', []],
['p_open', []],
['cdata', ["\nF oo"]],
['p_close', []],
['preformatted', ["x \n y "]],
['p_open', []],
['cdata', ["\nBar"]],
['p_close', []],
['document_end', []],
];
$this->assertCalls($calls, $this->H->calls);
}
function testMarkdownPreferredRejectsTwoSpaces() {
// 2-space indent in MD-preferred mode does NOT trigger preformatted.
$this->setSyntax('md');
$this->P->addMode('preformatted', new Preformatted());
$this->P->parse("F oo\n x\nBar\n");
$modes = array_column($this->H->calls, 0);
$this->assertNotContains('preformatted', $modes,
'2-space indent must not trigger preformatted when Markdown is preferred');
}
function testMarkdownPreferredTabStillTriggers() {
// Tab is a trigger regardless of the space threshold.
$this->setSyntax('md');
$this->P->addMode('preformatted', new Preformatted());
$this->P->parse("F oo\n\tx\nBar\n");
$modes = array_column($this->H->calls, 0);
$this->assertContains('preformatted', $modes,
'A single tab must still trigger preformatted in MD-preferred mode');
}
function testStripsLeadingAndTrailingBlankIndentedLines() {
// GFM example #87: leading and trailing blank-but-indented lines
// should not appear in the preformatted body. The lexer's
// continuation pattern eats their indents, leaving padding `\n`
// runs in the rewriter buffer; the rewriter trims them so the
// emitted text starts and ends on a non-blank line.
$this->setSyntax('md');
$this->P->addMode('preformatted', new Preformatted());
$this->P->parse("\n \n foo\n \n\n");
$calls = [
['document_start', []],
['preformatted', ['foo']],
['document_end', []],
];
$this->assertCalls($calls, $this->H->calls);
}
function testWhitespaceOnlyBlockIsSkipped() {
// A run of only blank-but-indented lines must not emit a
// preformatted call at all - the body would be pure whitespace
// and visually meaningless.
$this->setSyntax('md');
$this->P->addMode('preformatted', new Preformatted());
$this->P->parse("\n \n \n\n");
$modes = array_column($this->H->calls, 0);
$this->assertNotContains('preformatted', $modes);
}
function testBlankIndentedLineKeepsFollowingContent() {
// A "blank" line that is empty after its indent (classic trailing
// whitespace) followed by a column-0 line used to make the
// zero-width preformatted exit fire with nothing consumed, tripping
// the lexer's no-advance guard and silently dropping the rest of the
// document. The block is whitespace-only so it emits no preformatted
// call; the following paragraph must survive.
$this->P->addMode('preformatted',new Preformatted());
$this->P->parse("abc\n \nmore\n");
$calls = [
['document_start',[]],
['p_open',[]],
['cdata',["\nabc"]],
['p_close',[]],
['p_open',[]],
['cdata',["\nmore"]],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testBlankIndentedLineAfterCodeKeepsFollowingContent() {
// Same guard, but the blank indented line is a continuation line of
// a non-empty preformatted block: the block renders and the trailing
// paragraph must still survive.
$this->P->addMode('preformatted',new Preformatted());
$this->P->parse("abc\n code\n \nmore\n");
$calls = [
['document_start',[]],
['p_open',[]],
['cdata',["\nabc"]],
['p_close',[]],
['preformatted',['code']],
['p_open',[]],
['cdata',["\nmore"]],
['p_close',[]],
['document_end',[]],
];
$this->assertCalls($calls, $this->H->calls);
}
function testBlankIndentedLineKeepsFollowingBlockBoundary() {
// The zero-width preformatted exit exists to leave the boundary \n in
// the stream so a following block mode can anchor on it (e.g. an