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 ]; 250 foreach ($dwOnly as $mode) { 251 $this->assertNotContains($mode, $modeNames, "DW mode '$mode' should not load in markdown-only mode"); 252 } 253 } 254 255 /** Always-loaded modes must still be present in markdown-only mode */ 256 function testGetModesAlwaysModesPresentInMarkdownOnly() 257 { 258 global $conf; 259 $conf['syntax'] = 'markdown'; 260 ModeRegistry::reset(); 261 $registry = ModeRegistry::getInstance(); 262 $modes = $registry->getModes(); 263 $modeNames = array_column($modes, 'mode'); 264 265 $always = [ 266 'strong', 'monospace', 'subscript', 'superscript', 267 'footnote', 'eol', 'unformatted', 'preformatted', 'file', 268 'quote', 'externallink', 'emaillink', 'windowssharelink', 269 'notoc', 'nocache', 'rss', 270 'smiley', 'acronym', 'entity', 271 ]; 272 foreach ($always as $mode) { 273 $this->assertContains($mode, $modeNames, "Always-loaded mode '$mode' missing in markdown syntax setting"); 274 } 275 } 276 277 /** In mixed modes, DW modes must still load */ 278 function testGetModesMixedModesLoadDwModes() 279 { 280 $dwOnly = [ 281 'emphasis', 'deleted', 'code', 'header', 'hr', 282 'linebreak', 'internallink', 'media', 'listblock', 'table', 283 ]; 284 285 foreach (['dw+md', 'md+dw'] as $syntax) { 286 global $conf; 287 $conf['syntax'] = $syntax; 288 ModeRegistry::reset(); 289 $registry = ModeRegistry::getInstance(); 290 $modes = $registry->getModes(); 291 $modeNames = array_column($modes, 'mode'); 292 293 foreach ($dwOnly as $mode) { 294 $this->assertContains($mode, $modeNames, "DW mode '$mode' missing in '$syntax' syntax setting"); 295 } 296 } 297 } 298 299 /** 300 * Verifies that each mode is loaded in the expected combinations of 301 * `$conf['syntax']`. One data set per (mode, syntax) pair. 302 * 303 * Add new mode-gating rules to {@see provideModeLoadingCases} — each 304 * entry lists the four syntax settings and whether the mode should be 305 * loaded there. 306 * 307 * @dataProvider provideModeLoadingCases 308 */ 309 function testModeLoadingBySyntax(string $mode, string $syntax, bool $shouldLoad): void 310 { 311 global $conf; 312 $conf['syntax'] = $syntax; 313 ModeRegistry::reset(); 314 $modeNames = array_column(ModeRegistry::getInstance()->getModes(), 'mode'); 315 316 if ($shouldLoad) { 317 $this->assertContains($mode, $modeNames, "$mode must load in '$syntax'"); 318 } else { 319 $this->assertNotContains($mode, $modeNames, "$mode must NOT load in '$syntax'"); 320 } 321 } 322 323 /** 324 * Data provider for {@see testModeLoadingBySyntax}. 325 * 326 * Declares, per parser mode, whether it should be loaded in each of the 327 * four `$conf['syntax']` settings (`dokuwiki`, `markdown`, `dw+md`, 328 * `md+dw`). Entries are expanded into one data set per (mode, syntax) 329 * pair so PHPUnit reports failures with a specific label. 330 * 331 * Three gating categories are represented: 332 * 333 * - **MD-always**: loaded whenever Markdown is part of the syntax. Used 334 * when the delimiter has no DokuWiki counterpart (e.g. `*` for 335 * emphasis). 336 * - **MD-preferred**: loaded only when Markdown is the primary syntax. 337 * Used when the delimiter conflicts with a DokuWiki mode in DW- 338 * preferred settings (e.g. `_`, `__`, `___` clash with Underline). 339 * - **DW-preferred**: mirror — loaded only when DokuWiki is primary. 340 * 341 * Add a new line to the `$rules` table to register additional mode- 342 * gating rules. 343 * 344 * @return array<string, array{0: string, 1: string, 2: bool}> map from 345 * test-case label to [mode name, syntax setting, should-load] 346 */ 347 public static function provideModeLoadingCases(): array 348 { 349 $rules = [ 350 // MD-always (`*` / `~~` have no conflicting DW counterpart) 351 'gfm_emphasis' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 352 'gfm_emphasis_strong' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 353 'gfm_deleted' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 354 'gfm_backtick_single' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 355 'gfm_backtick_double' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 356 'gfm_header' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 357 'gfm_link' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 358 'gfm_media' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => true, 'md+dw' => true ], 359 // MD-preferred (`_`, `__`, `___` clash with Underline in DW) 360 'gfm_emphasis_underscore' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => false, 'md+dw' => true ], 361 'gfm_strong_underscore' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => false, 'md+dw' => true ], 362 'gfm_emphasis_strong_underscore' => ['dokuwiki' => false, 'markdown' => true, 'dw+md' => false, 'md+dw' => true ], 363 // DW-preferred (Underline's `__` clashes with GFM strong) 364 'underline' => ['dokuwiki' => true, 'markdown' => false, 'dw+md' => true, 'md+dw' => false], 365 ]; 366 367 $cases = []; 368 foreach ($rules as $mode => $bySyntax) { 369 foreach ($bySyntax as $syntax => $shouldLoad) { 370 $cases["$mode in $syntax"] = [$mode, $syntax, $shouldLoad]; 371 } 372 } 373 return $cases; 374 } 375 376} 377