1<?php 2 3namespace dokuwiki\test\Parsing; 4 5use dokuwiki\Parsing\ModeRegistry; 6use dokuwiki\Parsing\ParserMode\ModeInterface; 7 8class ModeRegistryTest extends \DokuWikiTest 9{ 10 /** @var ModeRegistry */ 11 private $registry; 12 13 function setUp(): void 14 { 15 parent::setUp(); 16 ModeRegistry::reset(); 17 $this->registry = ModeRegistry::getInstance(); 18 } 19 20 function tearDown(): void 21 { 22 ModeRegistry::reset(); 23 parent::tearDown(); 24 } 25 26 function testSingleton() 27 { 28 $this->assertSame( 29 ModeRegistry::getInstance(), 30 ModeRegistry::getInstance() 31 ); 32 } 33 34 function testResetCreatesFreshInstance() 35 { 36 $first = ModeRegistry::getInstance(); 37 ModeRegistry::reset(); 38 $second = ModeRegistry::getInstance(); 39 $this->assertNotSame($first, $second); 40 } 41 42 function testConstructorPopulatesGlobal() 43 { 44 global $PARSER_MODES; 45 $this->assertIsArray($PARSER_MODES); 46 $this->assertArrayHasKey('container', $PARSER_MODES); 47 $this->assertArrayHasKey('formatting', $PARSER_MODES); 48 $this->assertArrayHasKey('substition', $PARSER_MODES); 49 $this->assertArrayHasKey('protected', $PARSER_MODES); 50 $this->assertArrayHasKey('disabled', $PARSER_MODES); 51 $this->assertArrayHasKey('paragraphs', $PARSER_MODES); 52 $this->assertArrayHasKey('baseonly', $PARSER_MODES); 53 } 54 55 function testGetCategories() 56 { 57 global $PARSER_MODES; 58 $this->assertSame($PARSER_MODES, $this->registry->getCategories()); 59 } 60 61 function testGetModesForSingleCategory() 62 { 63 $modes = $this->registry->getModesForCategories([ModeRegistry::CATEGORY_CONTAINER]); 64 $this->assertContains('listblock', $modes); 65 $this->assertContains('table', $modes); 66 $this->assertContains('quote', $modes); 67 $this->assertContains('hr', $modes); 68 } 69 70 function testGetModesForMultipleCategories() 71 { 72 $modes = $this->registry->getModesForCategories([ 73 ModeRegistry::CATEGORY_CONTAINER, 74 ModeRegistry::CATEGORY_BASEONLY, 75 ]); 76 $this->assertContains('listblock', $modes); 77 $this->assertContains('header', $modes); 78 } 79 80 function testGetModesForCategoriesDeduplicates() 81 { 82 $modes = $this->registry->getModesForCategories([ 83 ModeRegistry::CATEGORY_CONTAINER, 84 ModeRegistry::CATEGORY_CONTAINER, 85 ]); 86 $counts = array_count_values($modes); 87 foreach ($counts as $count) { 88 $this->assertEquals(1, $count); 89 } 90 } 91 92 function testGetModesForUnknownCategoryReturnsEmpty() 93 { 94 $modes = $this->registry->getModesForCategories(['nonexistent']); 95 $this->assertSame([], $modes); 96 } 97 98 function testRegisterMode() 99 { 100 global $PARSER_MODES; 101 $this->registry->registerMode(ModeRegistry::CATEGORY_CONTAINER, 'testmode'); 102 $this->assertContains('testmode', $PARSER_MODES[ModeRegistry::CATEGORY_CONTAINER]); 103 $this->assertContains( 104 'testmode', 105 $this->registry->getModesForCategories([ModeRegistry::CATEGORY_CONTAINER]) 106 ); 107 } 108 109 function testGlobalModificationsAreVisible() 110 { 111 global $PARSER_MODES; 112 $PARSER_MODES[ModeRegistry::CATEGORY_FORMATTING][] = 'custom_format'; 113 $modes = $this->registry->getModesForCategories([ModeRegistry::CATEGORY_FORMATTING]); 114 $this->assertContains('custom_format', $modes); 115 } 116 117 function testGetModesReturnsSortedArray() 118 { 119 $modes = $this->registry->getModes(); 120 $this->assertNotEmpty($modes); 121 122 $sortValues = array_column($modes, 'sort'); 123 $sorted = $sortValues; 124 sort($sorted); 125 $this->assertSame($sorted, $sortValues); 126 } 127 128 function testGetModesContainsExpectedKeys() 129 { 130 $modes = $this->registry->getModes(); 131 foreach ($modes as $entry) { 132 $this->assertArrayHasKey('sort', $entry); 133 $this->assertArrayHasKey('mode', $entry); 134 $this->assertArrayHasKey('obj', $entry); 135 $this->assertIsInt($entry['sort']); 136 $this->assertIsString($entry['mode']); 137 $this->assertInstanceOf(ModeInterface::class, $entry['obj']); 138 } 139 } 140 141 function testGetModesContainsBuiltinModes() 142 { 143 $modes = $this->registry->getModes(); 144 $modeNames = array_column($modes, 'mode'); 145 $this->assertContains('strong', $modeNames); 146 $this->assertContains('header', $modeNames); 147 $this->assertContains('listblock', $modeNames); 148 $this->assertContains('eol', $modeNames); 149 $this->assertContains('smiley', $modeNames); 150 $this->assertContains('acronym', $modeNames); 151 $this->assertContains('entity', $modeNames); 152 } 153 154 function testSortModes() 155 { 156 $a = ['sort' => 10, 'mode' => 'a']; 157 $b = ['sort' => 20, 'mode' => 'b']; 158 $this->assertLessThan(0, ModeRegistry::sortModes($a, $b)); 159 $this->assertGreaterThan(0, ModeRegistry::sortModes($b, $a)); 160 $this->assertEquals(0, ModeRegistry::sortModes($a, $a)); 161 } 162 163 function testBlockEolModesEmptyByDefault() 164 { 165 $this->assertSame([], $this->registry->getBlockEolModes()); 166 } 167 168 function testRegisterBlockEolMode() 169 { 170 $this->registry->registerBlockEolMode('listblock'); 171 $this->registry->registerBlockEolMode('table'); 172 $this->assertSame(['listblock', 'table'], $this->registry->getBlockEolModes()); 173 } 174 175 function testLineStartMarkersEmptyByDefault() 176 { 177 $this->assertSame([], $this->registry->getLineStartMarkers()); 178 } 179 180 function testRegisterLineStartMarkers() 181 { 182 $this->registry->registerLineStartMarkers('listblock', ['\\*', '\\-']); 183 $markers = $this->registry->getLineStartMarkers(); 184 $this->assertContains('\\*', $markers); 185 $this->assertContains('\\-', $markers); 186 } 187 188 function testLineStartMarkersDeduplicates() 189 { 190 $this->registry->registerLineStartMarkers('mode_a', ['\\*', '\\-']); 191 $this->registry->registerLineStartMarkers('mode_b', ['\\-', '\\+']); 192 $markers = $this->registry->getLineStartMarkers(); 193 $this->assertCount(3, $markers); 194 $this->assertContains('\\*', $markers); 195 $this->assertContains('\\-', $markers); 196 $this->assertContains('\\+', $markers); 197 } 198 199 function testBlockEolModesResetWithInstance() 200 { 201 $this->registry->registerBlockEolMode('listblock'); 202 ModeRegistry::reset(); 203 $fresh = ModeRegistry::getInstance(); 204 $this->assertSame([], $fresh->getBlockEolModes()); 205 } 206 207 /** 208 * The default syntax setting must produce the exact same mode set as before 209 * the syntax setting was introduced (no-op guarantee). 210 */ 211 function testGetModesDefaultSyntaxMatchesLegacy() 212 { 213 global $conf; 214 $conf['syntax'] = 'dokuwiki'; 215 ModeRegistry::reset(); 216 $registry = ModeRegistry::getInstance(); 217 $modes = $registry->getModes(); 218 $modeNames = array_column($modes, 'mode'); 219 220 // All original built-in modes must be present 221 $expected = [ 222 'listblock', 'preformatted', 'notoc', 'nocache', 223 'header', 'table', 'linebreak', 'footnote', 224 'hr', 'unformatted', 'code', 'file', 'quote', 225 'internallink', 'rss', 'media', 'externallink', 226 'emaillink', 'windowssharelink', 'eol', 227 'strong', 'emphasis', 'underline', 'monospace', 228 'subscript', 'superscript', 'deleted', 229 'smiley', 'acronym', 'entity', 230 ]; 231 foreach ($expected as $mode) { 232 $this->assertContains($mode, $modeNames, "Mode '$mode' missing in dokuwiki syntax setting"); 233 } 234 } 235 236 /** DW-only modes must be absent when syntax is 'markdown' */ 237 function testGetModesDwModesSkippedInMarkdownOnly() 238 { 239 global $conf; 240 $conf['syntax'] = 'markdown'; 241 ModeRegistry::reset(); 242 $registry = ModeRegistry::getInstance(); 243 $modes = $registry->getModes(); 244 $modeNames = array_column($modes, 'mode'); 245 246 $dwOnly = [ 247 'emphasis', 'deleted', 'code', 'header', 'hr', 248 'linebreak', 'internallink', 'media', 'listblock', 'table', 249 'monospace', 'unformatted', 'file', 250 ]; 251 foreach ($dwOnly as $mode) { 252 $this->assertNotContains($mode, $modeNames, "DW mode '$mode' should not load in markdown-only mode"); 253 } 254 } 255 256 /** Always-loaded modes must still be present in markdown-only mode */ 257 function testGetModesAlwaysModesPresentInMarkdownOnly() 258 { 259 global $conf; 260 $conf['syntax'] = 'markdown'; 261 ModeRegistry::reset(); 262 $registry = ModeRegistry::getInstance(); 263 $modes = $registry->getModes(); 264 $modeNames = array_column($modes, 'mode'); 265 266 $always = [ 267 'strong', 'subscript', 'superscript', 268 'footnote', 'eol', 'preformatted', 269 'quote', 'externallink', 'emaillink', 'windowssharelink', 270 'notoc', 'nocache', 'rss', 271 'smiley', 'acronym', 'entity', 272 ]; 273 foreach ($always as $mode) { 274 $this->assertContains($mode, $modeNames, "Always-loaded mode '$mode' missing in markdown syntax setting"); 275 } 276 } 277 278 /** In mixed modes, DW modes must still load */ 279 function testGetModesMixedModesLoadDwModes() 280 { 281 $dwOnly = [ 282 'emphasis', 'deleted', 'code', 'header', 'hr', 283 'linebreak', 'internallink', 'media', 'listblock', 'table', 284 'monospace', 'unformatted', 'file', 285 ]; 286 287 foreach (['dw+md', 'md+dw'] as $syntax) { 288 global $conf; 289 $conf['syntax'] = $syntax; 290 ModeRegistry::reset(); 291 $registry = ModeRegistry::getInstance(); 292 $modes = $registry->getModes(); 293 $modeNames = array_column($modes, 'mode'); 294 295 foreach ($dwOnly as $mode) { 296 $this->assertContains($mode, $modeNames, "DW mode '$mode' missing in '$syntax' syntax setting"); 297 } 298 } 299 } 300 301 /** 302 * Verifies that each mode is loaded in the expected combinations of 303 * `$conf['syntax']`. One data set per (mode, syntax) pair. 304 * 305 * Add new mode-gating rules to {@see provideModeLoadingCases} — each 306 * entry lists the four syntax settings and whether the mode should be 307 * loaded there. 308 * 309 * @dataProvider provideModeLoadingCases 310 */ 311 function testModeLoadingBySyntax(string $mode, string $syntax, bool $shouldLoad): void 312 { 313 global $conf; 314 $conf['syntax'] = $syntax; 315 ModeRegistry::reset(); 316 $modeNames = array_column(ModeRegistry::getInstance()->getModes(), 'mode'); 317 318 if ($shouldLoad) { 319 $this->assertContains($mode, $modeNames, "$mode must load in '$syntax'"); 320 } else { 321 $this->assertNotContains($mode, $modeNames, "$mode must NOT load in '$syntax'"); 322 } 323 } 324 325 /** 326 * Data provider for {@see testModeLoadingBySyntax}. 327 * 328 * Declares, per parser mode, whether it should be loaded in each of the 329 * four `$conf['syntax']` settings (`dokuwiki`, `markdown`, `dw+md`, 330 * `md+dw`). Entries are expanded into one data set per (mode, syntax) 331 * pair so PHPUnit reports failures with a specific label. 332 * 333 * Five gating categories are represented: 334 * 335 * - **Always**: loaded unconditionally (no syntax-specific counterpart 336 * or conflict). Covers core formatting, paragraphs, and data-driven 337 * modes (smileys, acronyms, entities). 338 * - **DW-always**: loaded whenever DokuWiki is part of the syntax. Used 339 * for features that have a Markdown counterpart but no delimiter 340 * conflict (e.g. `**bold**` for emphasis). 341 * - **DW-preferred**: loaded only when DokuWiki is the primary syntax. 342 * Used when the delimiter conflicts with a Markdown mode in MD- 343 * preferred settings (e.g. `__` clashes with GFM strong). 344 * - **MD-always**: mirror — loaded whenever Markdown is part of the 345 * syntax. Used when the delimiter has no DokuWiki counterpart (e.g. 346 * `*` for emphasis). 347 * - **MD-preferred**: mirror — loaded only when Markdown is primary. 348 * Used when the delimiter conflicts with a DokuWiki mode in DW- 349 * preferred settings (e.g. `_`, `__`, `___` clash with Underline). 350 * 351 * Add a new line to the `$rules` table to register additional mode- 352 * gating rules. 353 * 354 * @return array<string, array{0: string, 1: string, 2: bool}> map from 355 * test-case label to [mode name, syntax setting, should-load] 356 */ 357 public static function provideModeLoadingCases(): array 358 { 359 $rules = [ 360 // Always-loaded (unconditional — no syntax-specific counterpart) 361 'strong' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 362 'subscript' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 363 'superscript' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 364 'footnote' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 365 'eol' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 366 'preformatted' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 367 'quote' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 368 'externallink' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 369 'emaillink' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 370 'windowssharelink' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 371 'notoc' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 372 'nocache' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 373 'rss' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 374 'smiley' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 375 'acronym' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 376 'entity' => ['dokuwiki' => true, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 377 // DW-always (features with MD counterparts but no delimiter clash) 378 'emphasis' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 379 'deleted' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 380 'code' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 381 'header' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 382 'hr' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 383 'linebreak' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 384 'internallink' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 385 'media' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 386 'listblock' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 387 'table' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 388 'monospace' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 389 'unformatted' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 390 'file' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => true ], 391 // MD-always (`*` / `~~` have no conflicting DW counterpart) 392 'gfm_emphasis' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 393 'gfm_emphasis_strong' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 394 'gfm_deleted' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 395 'gfm_backtick_single' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 396 'gfm_backtick_double' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 397 'gfm_header' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 398 'gfm_link' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 399 'gfm_media' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 400 'gfm_code' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 401 'gfm_file' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 402 // MD-preferred (`_`, `__`, `___` clash with Underline in DW) 403 'gfm_emphasis_underscore' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => false, 'md+dw' => true ], 404 'gfm_strong_underscore' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => false, 'md+dw' => true ], 405 'gfm_emphasis_strong_underscore' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => false, 'md+dw' => true ], 406 // DW-preferred (Underline's `__` clashes with GFM strong) 407 'underline' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => false], 408 ]; 409 410 $cases = []; 411 foreach ($rules as $mode => $bySyntax) { 412 foreach ($bySyntax as $syntax => $shouldLoad) { 413 $cases["$mode in $syntax"] = [$mode, $syntax, $shouldLoad]; 414 } 415 } 416 return $cases; 417 } 418 419} 420