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', 'underline', '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