1<?php 2 3namespace dokuwiki\test\Parsing; 4 5use dokuwiki\Parsing\Handler; 6use dokuwiki\Parsing\ModeRegistry; 7 8class HandlerTest extends \DokuWikiTest 9{ 10 function testGetModeNameReturnsOriginalName() 11 { 12 $handler = new Handler(new ModeRegistry('dw')); 13 14 // create a simple mode that records what getModeName() returns 15 $mode = new class extends \dokuwiki\Parsing\ParserMode\AbstractMode { 16 public $receivedModeName = ''; 17 public function getSort() { return 0; } 18 public function handle($match, $state, $pos, Handler $handler) 19 { 20 $this->receivedModeName = $handler->getModeName(); 21 return true; 22 } 23 }; 24 25 $handler->registerModeObject('resolved', $mode); 26 27 // simulate dispatch with a remapped name (original differs from resolved) 28 $handler->handleToken('resolved', 'test', DOKU_LEXER_SPECIAL, 0, 'original'); 29 30 $this->assertSame('original', $mode->receivedModeName); 31 } 32 33 function testGetModeNameFallsBackToModeName() 34 { 35 $handler = new Handler(new ModeRegistry('dw')); 36 37 $mode = new class extends \dokuwiki\Parsing\ParserMode\AbstractMode { 38 public $receivedModeName = ''; 39 public function getSort() { return 0; } 40 public function handle($match, $state, $pos, Handler $handler) 41 { 42 $this->receivedModeName = $handler->getModeName(); 43 return true; 44 } 45 }; 46 47 $handler->registerModeObject('mymode', $mode); 48 49 // no original name passed — should fall back to the resolved name 50 $handler->handleToken('mymode', 'test', DOKU_LEXER_SPECIAL, 0); 51 52 $this->assertSame('mymode', $mode->receivedModeName); 53 } 54 55 function testResetClearsCallsAndStatusAndCurrentMode() 56 { 57 $handler = new Handler(new ModeRegistry('dw')); 58 59 // dirty the handler: append a call, mutate status, and prime 60 // currentModeName via a token dispatch. 61 $handler->calls[] = ['cdata', ['x'], 0]; 62 63 self::setInaccessibleProperty($handler, 'status', [ 64 'section' => true, 'doublequote' => 3, 'footnote' => true, 65 ]); 66 67 $mode = new class extends \dokuwiki\Parsing\ParserMode\AbstractMode { 68 public function getSort() 69 { 70 return 0; 71 } 72 73 public function handle($match, $state, $pos, Handler $handler) 74 { 75 return true; 76 } 77 }; 78 $handler->registerModeObject('m', $mode); 79 $handler->handleToken('m', 'x', DOKU_LEXER_SPECIAL, 0, 'm'); 80 $this->assertSame('m', $handler->getModeName()); 81 82 $writerBefore = self::getInaccessibleProperty($handler, 'callWriter'); 83 84 $handler->reset(); 85 86 $this->assertSame([], $handler->calls); 87 $this->assertSame('', $handler->getModeName()); 88 $this->assertSame( 89 ['section' => false, 'doublequote' => 0, 'footnote' => false], 90 self::getInaccessibleProperty($handler, 'status') 91 ); 92 // reset reinstalls a fresh CallWriter — must be a new instance 93 $writerAfter = self::getInaccessibleProperty($handler, 'callWriter'); 94 $this->assertNotSame($writerBefore, $writerAfter); 95 $this->assertInstanceOf(\dokuwiki\Parsing\Handler\CallWriter::class, $writerAfter); 96 } 97 98 /** 99 * Regression test for #4637 100 * 101 * handleToken() must route plugin_* modes through plugin() even when 102 * the same name is also registered as a mode object. 103 * 104 * Before the fix, modeObjects was consulted first, which called 105 * SyntaxPlugin::handle() directly. That returns data but never 106 * emits an instruction via addPluginCall(), so the plugin's rendered 107 * output silently disappeared. 108 */ 109 function testPluginModeIsRoutedThroughPluginHandler() 110 { 111 $handler = new Handler(new ModeRegistry('dw')); 112 113 // Plugins register themselves as mode objects under their plugin_* name. 114 // This reproduces the conflict the bug depended on. 115 $info = plugin_load('syntax', 'info'); 116 $this->assertNotNull($info, 'info plugin must be available for this test'); 117 $handler->registerModeObject('plugin_info', $info); 118 119 $handler->handleToken('plugin_info', '~~INFO:datetime~~', DOKU_LEXER_SPECIAL, 0); 120 121 // After the fix, plugin() runs and emits a plugin instruction. 122 // With the bug, modeObjects['plugin_info']->handle() ran and emitted nothing. 123 $this->assertCount(1, $handler->calls, 'plugin mode must emit exactly one instruction'); 124 [$name, $args] = $handler->calls[0]; 125 $this->assertSame('plugin', $name); 126 $this->assertSame('info', $args[0]); 127 $this->assertSame(['datetime'], $args[1]); 128 $this->assertSame('~~INFO:datetime~~', $args[3]); 129 } 130 131 /** 132 * PARSER_HANDLER_DONE marks a finished document, so finalize() fires it for 133 * a top-level parse; the per-item and per-blockquote sub-parses the built-in 134 * Markdown modes run fire PARSER_SUBHANDLER_DONE instead. Firing the 135 * document event per item let a plugin hooking it (e.g. struct's 136 * auto-output) inject a call into every list item, which defeated 137 * GfmListblock's tight-item detection and wrapped each item in a paragraph. 138 */ 139 function testFinalizeFiresDocumentAndSubParserEventsSeparately() 140 { 141 global $EVENT_HANDLER; 142 $main = 0; 143 $sub = 0; 144 $EVENT_HANDLER->register_hook('PARSER_HANDLER_DONE', 'AFTER', null, function () use (&$main) { 145 $main++; 146 }); 147 $EVENT_HANDLER->register_hook('PARSER_SUBHANDLER_DONE', 'AFTER', null, function () use (&$sub) { 148 $sub++; 149 }); 150 151 // top-level parse fires PARSER_HANDLER_DONE only (also the BC contract 152 // for legacy new Doku_Handler() sub-parsing) 153 (new Handler(new ModeRegistry('md')))->finalize(); 154 $this->assertSame(1, $main, 'main parse must fire PARSER_HANDLER_DONE'); 155 $this->assertSame(0, $sub, 'main parse must not fire PARSER_SUBHANDLER_DONE'); 156 157 // sub-parser handler fires PARSER_SUBHANDLER_DONE only 158 (new Handler(new ModeRegistry('md'), true))->finalize(); 159 $this->assertSame(1, $main, 'sub-parse must not fire PARSER_HANDLER_DONE'); 160 $this->assertSame(1, $sub, 'sub-parse must fire PARSER_SUBHANDLER_DONE'); 161 } 162} 163