1<?php 2 3namespace ComboStrap; 4 5 6use ComboStrap\Meta\Field\PageH1; 7use ComboStrap\Tag\TableTag; 8use ComboStrap\TagAttribute\StyleAttribute; 9use dokuwiki\Extension\SyntaxPlugin; 10use syntax_plugin_combo_analytics; 11use syntax_plugin_combo_header; 12use syntax_plugin_combo_headingatx; 13use syntax_plugin_combo_headingwiki; 14use syntax_plugin_combo_media; 15 16/** 17 * The outline is creating a XML like document 18 * with section 19 * 20 * It's also the post-processing of the instructions 21 */ 22class Outline 23{ 24 25 26 const CANONICAL = "outline"; 27 private const OUTLINE_HEADING_PREFIX = "outline-heading"; 28 const CONTEXT = self::CANONICAL; 29 public const OUTLINE_HEADING_NUMBERING = "outline-heading-numbering"; 30 public const TOC_NUMBERING = "toc-numbering"; 31 /** 32 * As seen on 33 * https://drafts.csswg.org/css-counter-styles-3/#predefined-counters 34 */ 35 public const CONF_COUNTER_STYLES_CHOICES = [ 36 'arabic-indic', 37 'bengali', 38 'cambodian/khmer', 39 'cjk-decimal', 40 'decimal', 41 'decimal-leading-zero', 42 'devanagari', 43 'georgian', 44 'gujarati', 45 'gurmukhi', 46 'hebrew', 47 'hiragana', 48 'hiragana-iroha', 49 'kannada', 50 'katakana', 51 'katakana-iroha', 52 'lao', 53 'lower-alpha', 54 'lower-armenian', 55 'lower-greek', 56 'lower-roman', 57 'malayalam', 58 'mongolian', 59 'myanmar', 60 'oriya', 61 'persian', 62 'tamil', 63 'telugu', 64 'thai', 65 'tibetan', 66 'upper-alpha', 67 'upper-armenian', 68 'upper-roman' 69 ]; 70 public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL4 = "outlineNumberingCounterStyleLevel4"; 71 public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL3 = "outlineNumberingCounterStyleLevel3"; 72 public const CONF_OUTLINE_NUMBERING_SUFFIX = "outlineNumberingSuffix"; 73 public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL2 = "outlineNumberingCounterStyleLevel2"; 74 public const CONF_OUTLINE_NUMBERING_COUNTER_SEPARATOR = "outlineNumberingCounterSeparator"; 75 public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL6 = "outlineNumberingCounterStyleLevel6"; 76 public const CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL5 = "outlineNumberingCounterStyleLevel5"; 77 public const CONF_OUTLINE_NUMBERING_PREFIX = "outlineNumberingPrefix"; 78 public const CONF_OUTLINE_NUMBERING_ENABLE = "outlineNumberingEnable"; 79 /** 80 * To add hash tag to heading 81 */ 82 public const OUTLINE_ANCHOR = "outline-anchor"; 83 const CONF_OUTLINE_NUMBERING_ENABLE_DEFAULT = 1; 84 private OutlineSection $rootSection; 85 86 private OutlineSection $actualSection; // the actual section that is created 87 private Call $actualHeadingCall; // the heading that is parsed 88 private int $actualHeadingParsingState = DOKU_LEXER_EXIT; // the state of the heading parsed (enter, closed), enter if we have entered an heading, exit if not; 89 private ?MarkupPath $markupPath = null; 90 private bool $isFragment; 91 private bool $metaHeaderCapture; 92 93 /** 94 * @param CallStack $callStack 95 * @param MarkupPath|null $markup - needed to store the parsed toc, h1, ... (null if the markup is dynamic) 96 * @param bool $isFragment - needed to control the structure of the outline (if this is a preview, the first heading may be not h1) 97 * @return void 98 */ 99 public function __construct(CallStack $callStack, MarkupPath $markup = null, bool $isFragment = false) 100 { 101 if ($markup !== null) { 102 $this->markupPath = $markup; 103 } 104 $this->isFragment = $isFragment; 105 $this->buildOutline($callStack); 106 $this->storeH1(); 107 $this->storeTocForMarkupIfAny(); 108 } 109 110 /** 111 * @param CallStack $callStack 112 * @param MarkupPath|null $markupPath - needed to store the parsed toc, h1, ... (null if the markup is dynamic) 113 * @param bool $isFragment - needed to control the structure of the outline (if this is a preview, the first heading may be not h1) 114 * @return Outline 115 */ 116 public static function createFromCallStack(CallStack $callStack, MarkupPath $markupPath = null, bool $isFragment = false): Outline 117 { 118 return new Outline($callStack, $markupPath, $isFragment); 119 } 120 121 private function buildOutline(CallStack $callStack) 122 { 123 124 /** 125 * {@link syntax_plugin_combo_analytics tag analytics} 126 * By default, in a test environment 127 * this is set to 0 128 * to speed test and to not pollute 129 */ 130 $analtyicsEnabled = SiteConfig::getConfValue(syntax_plugin_combo_analytics::CONF_SYNTAX_ANALYTICS_ENABLE, true); 131 $analyticsTagUsed = []; 132 133 /** 134 * Processing variable about the context 135 */ 136 $this->rootSection = OutlineSection::createOutlineRoot() 137 ->setStartPosition(0); 138 $this->actualSection = $this->rootSection; 139 $actualLastPosition = 0; 140 $callStack->moveToStart(); 141 while ($actualCall = $callStack->next()) { 142 143 144 $state = $actualCall->getState(); 145 146 /** 147 * Block Post Processing 148 * to not get any unwanted p 149 * to counter {@link Block::process()} 150 * setting dynamically the {@link SyntaxPlugin::getPType()} 151 * 152 * Unfortunately, it can't work because this is called after 153 * {@link Block::process()} 154 */ 155 if ($analtyicsEnabled) { 156 157 if (in_array($state, CallStack::TAG_STATE)) { 158 $tagName = $actualCall->getTagName(); 159 // The dokuwiki component name have open in their name 160 $tagName = str_replace("_open", "", $tagName); 161 $actual = $analyticsTagUsed[$tagName] ?? 0; 162 $analyticsTagUsed[$tagName] = $actual + 1; 163 } 164 165 } 166 167 /** 168 * We don't take the outline and document call if any 169 * This is the case when we build from an actual stored instructions 170 * (to bundle multiple page for instance) 171 */ 172 $tagName = $actualCall->getTagName(); 173 switch ($tagName) { 174 case "document_start": 175 case "document_end": 176 case SectionTag::TAG: 177 continue 2; 178 case syntax_plugin_combo_header::TAG: 179 if ($actualCall->isPluginCall() && $actualCall->getContext() === self::CONTEXT) { 180 continue 2; 181 } 182 } 183 184 /** 185 * Wrap the table 186 */ 187 $componentName = $actualCall->getComponentName(); 188 if ($componentName === "table_open") { 189 $position = $actualCall->getFirstMatchedCharacterPosition(); 190 $originalInstructionCall = &$actualCall->getInstructionCall(); 191 $originalInstructionCall = Call::createComboCall( 192 TableTag::TAG, 193 DOKU_LEXER_ENTER, 194 [PluginUtility::POSITION => $position], 195 null, 196 null, 197 null, 198 $position, 199 \syntax_plugin_combo_xmlblocktag::TAG 200 )->toCallArray(); 201 } 202 203 /** 204 * Enter new section ? 205 */ 206 $shouldWeCreateASection = false; 207 switch ($tagName) { 208 case syntax_plugin_combo_headingatx::TAG: 209 $actualCall->setState(DOKU_LEXER_ENTER); 210 if ($actualCall->getContext() === HeadingTag::TYPE_OUTLINE) { 211 $shouldWeCreateASection = true; 212 } 213 $this->enterHeading($actualCall); 214 break; 215 case HeadingTag::HEADING_TAG: 216 case syntax_plugin_combo_headingwiki::TAG: 217 if ($state == DOKU_LEXER_ENTER 218 && $actualCall->getContext() === HeadingTag::TYPE_OUTLINE) { 219 $shouldWeCreateASection = true; 220 $this->enterHeading($actualCall); 221 } 222 break; 223 case "header": 224 // Should happen only on outline section 225 // we take over inside a component 226 if (!$actualCall->isPluginCall()) { 227 /** 228 * ie not {@link syntax_plugin_combo_header} 229 * but the dokuwiki header (ie heading) 230 */ 231 $shouldWeCreateASection = true; 232 $this->enterHeading($actualCall); 233 } 234 break; 235 } 236 if ($shouldWeCreateASection) { 237 if ($this->actualSection->hasParent()) { 238 // -1 because the actual position is the start of the next section 239 $this->actualSection->setEndPosition($actualCall->getFirstMatchedCharacterPosition() - 1); 240 } 241 $actualSectionLevel = $this->actualSection->getLevel(); 242 243 if ($actualCall->isPluginCall()) { 244 try { 245 $newSectionLevel = DataType::toInteger($actualCall->getAttribute(HeadingTag::LEVEL)); 246 } catch (ExceptionBadArgument $e) { 247 LogUtility::internalError("The level was not present on the heading call", self::CANONICAL); 248 $newSectionLevel = $actualSectionLevel; 249 } 250 } else { 251 $headerTagName = $tagName; 252 if ($headerTagName !== "header") { 253 throw new ExceptionRuntimeInternal("This is not a dokuwiki header call", self::CANONICAL); 254 } 255 $newSectionLevel = $actualCall->getInstructionCall()[1][1]; 256 } 257 258 259 $newOutlineSection = OutlineSection::createFromEnterHeadingCall($actualCall); 260 $sectionDiff = $newSectionLevel - $actualSectionLevel; 261 if ($sectionDiff > 0) { 262 263 /** 264 * A child of the actual section 265 * We append it first before the message check to 266 * build the {@link TreeNode::getTreeIdentifier()} 267 */ 268 try { 269 $this->actualSection->appendChild($newOutlineSection); 270 } catch (ExceptionBadState $e) { 271 throw new ExceptionRuntimeInternal("The node is not added multiple time, this error should not fired. Error:{$e->getMessage()}", self::CANONICAL, 1, $e); 272 } 273 274 if ($sectionDiff > 1 & !($actualSectionLevel === 0 && $newSectionLevel === 2)) { 275 $expectedLevel = $actualSectionLevel + 1; 276 if ($actualSectionLevel === 0) { 277 /** 278 * In a fragment run (preview), 279 * the first heading may not be the first one 280 */ 281 if (!$this->isFragment) { 282 $message = "The first section heading should have the level 1 or 2 (not $newSectionLevel)."; 283 } 284 } else { 285 $message = "The child section heading ($actualSectionLevel) has the level ($newSectionLevel) but is parent ({$this->actualSection->getLabel()}) has the level ($actualSectionLevel). The expected level is ($expectedLevel)."; 286 } 287 if (isset($message)) { 288 LogUtility::warning($message, self::CANONICAL); 289 } 290 $actualCall->setAttribute(HeadingTag::LEVEL, $newSectionLevel); 291 } 292 293 } else { 294 295 /** 296 * A child of the parent section, A sibling of the actual session 297 */ 298 try { 299 $parent = $this->actualSection->getParent(); 300 for ($i = 0; $i < abs($sectionDiff); $i++) { 301 $parent = $parent->getParent(); 302 } 303 try { 304 $parent->appendChild($newOutlineSection); 305 } catch (ExceptionBadState $e) { 306 throw new ExceptionRuntimeInternal("The node is not added multiple time, this error should not fired. Error:{$e->getMessage()}", self::CANONICAL, 1, $e); 307 } 308 } catch (ExceptionNotFound $e) { 309 /** 310 * no parent 311 * May happen in preview (ie fragment) 312 */ 313 if (!$this->isFragment) { 314 LogUtility::internalError("Due to the level logic, the actual section should have a parent"); 315 } 316 try { 317 $this->actualSection->appendChild($newOutlineSection); 318 } catch (ExceptionBadState $e) { 319 throw new ExceptionRuntimeInternal("The node is not added multiple time, this error should not fired. Error:{$e->getMessage()}", self::CANONICAL, 1, $e); 320 } 321 } 322 323 } 324 325 $this->actualSection = $newOutlineSection; 326 continue; 327 } 328 329 /** 330 * Track the number of lines 331 * to inject ads 332 */ 333 switch ($tagName) { 334 case "linebreak": 335 case "tablerow": 336 // linebreak is an inline component 337 $this->actualSection->incrementLineNumber(); 338 break; 339 default: 340 $display = $actualCall->getDisplay(); 341 if ($display === Call::BlOCK_DISPLAY) { 342 $this->actualSection->incrementLineNumber(); 343 } 344 break; 345 } 346 347 /** 348 * Track the position in the file 349 */ 350 $currentLastPosition = $actualCall->getLastMatchedCharacterPosition(); 351 if ($currentLastPosition > $actualLastPosition) { 352 // the position in the stack is not always good 353 $actualLastPosition = $currentLastPosition; 354 } 355 356 357 switch ($actualCall->getComponentName()) { 358 case \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_OPEN: 359 case \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_CLOSE: 360 // we don't store them 361 continue 2; 362 } 363 364 /** 365 * Close/Process the heading description 366 */ 367 if ($this->actualHeadingParsingState === DOKU_LEXER_ENTER) { 368 switch ($tagName) { 369 370 case HeadingTag::HEADING_TAG: 371 case syntax_plugin_combo_headingwiki::TAG: 372 if ($state == DOKU_LEXER_EXIT) { 373 $this->addCallToSection($actualCall); 374 $this->exitHeading(); 375 continue 2; 376 } 377 break; 378 379 case "internalmedia": 380 // no link for media in heading 381 $actualCall->getInstructionCall()[1][6] = MediaMarkup::LINKING_NOLINK_VALUE; 382 break; 383 case syntax_plugin_combo_media::TAG: 384 // no link for media in heading 385 $actualCall->addAttribute(MediaMarkup::LINKING_KEY, MediaMarkup::LINKING_NOLINK_VALUE); 386 break; 387 388 case "header": 389 if (SiteConfig::getConfValue(syntax_plugin_combo_headingwiki::CONF_WIKI_HEADING_ENABLE, syntax_plugin_combo_headingwiki::CONF_DEFAULT_WIKI_ENABLE_VALUE) == 1) { 390 LogUtility::msg("The combo heading wiki is enabled, we should not see `header` calls in the call stack"); 391 } 392 break; 393 394 case "p": 395 396 if ($this->actualHeadingCall->getTagName() === syntax_plugin_combo_headingatx::TAG) { 397 // A new p is the end of an atx call 398 switch ($actualCall->getComponentName()) { 399 case "p_open": 400 // We don't take the p tag inside atx heading 401 // therefore we continue 402 continue 3; 403 case "p_close": 404 $endAtxCall = Call::createComboCall( 405 syntax_plugin_combo_headingatx::TAG, 406 DOKU_LEXER_EXIT, 407 $this->actualHeadingCall->getAttributes(), 408 $this->actualHeadingCall->getContext(), 409 ); 410 $this->addCallToSection($endAtxCall); 411 $this->exitHeading(); 412 // We don't take the p tag inside atx heading 413 // therefore we continue 414 continue 3; 415 } 416 } 417 break; 418 419 } 420 } 421 $this->addCallToSection($actualCall); 422 } 423 424 // empty text 425 if (sizeof($analyticsTagUsed) > 0) { 426 $pluginAnalyticsCall = Call::createComboCall( 427 syntax_plugin_combo_analytics::TAG, 428 DOKU_LEXER_SPECIAL, 429 $analyticsTagUsed 430 ); 431 $this->addCallToSection($pluginAnalyticsCall); 432 } 433 434 // Add label the heading text to the metadata 435 $this->saveOutlineToMetadata(); 436 437 438 } 439 440 public static function getOutlineHeadingClass(): string 441 { 442 return StyleAttribute::addComboStrapSuffix(self::OUTLINE_HEADING_PREFIX); 443 } 444 445 public function getRootOutlineSection(): OutlineSection 446 { 447 return $this->rootSection; 448 449 } 450 451 /** 452 */ 453 public static function merge(Outline $inner, Outline $outer) 454 { 455 /** 456 * Get the inner section where the outer section will be added 457 */ 458 $innerRootOutlineSection = $inner->getRootOutlineSection(); 459 $innerTopSections = $innerRootOutlineSection->getChildren(); 460 if (count($innerTopSections) === 0) { 461 $firstInnerSection = $innerRootOutlineSection; 462 } else { 463 $firstInnerSection = $innerTopSections[count($innerTopSections)]; 464 } 465 $firstInnerSectionLevel = $firstInnerSection->getLevel(); 466 467 /** 468 * Add the outer sections 469 */ 470 $outerRootOutlineSection = $outer->getRootOutlineSection(); 471 foreach ($outerRootOutlineSection->getChildren() as $childOuterSection) { 472 /** 473 * One level less than where the section is included 474 */ 475 $childOuterSection->setLevel($firstInnerSectionLevel + 1); 476 $childOuterSection->detachBeforeAppend(); 477 try { 478 $firstInnerSection->appendChild($childOuterSection); 479 } catch (ExceptionBadState $e) { 480 // We add the node only once. This error should not happen 481 throw new ExceptionRuntimeInternal("Error while adding a section during the outline merge. Error: {$e->getMessage()}", self::CANONICAL, 1, $e); 482 } 483 } 484 485 } 486 487 public static function mergeRecurse(Outline $inner, Outline $outer) 488 { 489 $innerRootOutlineSection = $inner->getRootOutlineSection(); 490 $outerRootOutlineSection = $outer->getRootOutlineSection(); 491 492 } 493 494 /** 495 * Utility class to create a outline from a markup string 496 * @param string $content 497 * @return Outline 498 */ 499 public static function createFromMarkup(string $content, Path $contentPath, WikiPath $contextPath): Outline 500 { 501 $instructions = MarkupRenderer::createFromMarkup($content, $contentPath, $contextPath) 502 ->setRequestedMimeToInstruction() 503 ->getOutput(); 504 $callStack = CallStack::createFromInstructions($instructions); 505 return Outline::createFromCallStack($callStack); 506 } 507 508 /** 509 * Get the heading numbering snippet 510 * @param string $type heading or toc - for {@link Outline::TOC_NUMBERING} or {@link Outline::OUTLINE_HEADING_NUMBERING} 511 * @return string - the css internal stylesheet 512 * @throws ExceptionNotEnabled 513 * @throws ExceptionBadSyntax 514 * Page on DokuWiki 515 * https://www.dokuwiki.org/tips:numbered_headings 516 */ 517 public static function getCssNumberingRulesFor(string $type): string 518 { 519 520 $enable = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_ENABLE, Outline::CONF_OUTLINE_NUMBERING_ENABLE_DEFAULT); 521 if (!$enable) { 522 throw new ExceptionNotEnabled(); 523 } 524 525 $level2CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL2, "decimal"); 526 $level3CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL3, "decimal"); 527 $level4CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL4, "decimal"); 528 $level5CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL5, "decimal"); 529 $level6CounterStyle = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_STYLE_LEVEL6, "decimal"); 530 $counterSeparator = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_COUNTER_SEPARATOR, "."); 531 $prefix = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_PREFIX, ""); 532 $suffix = SiteConfig::getConfValue(self::CONF_OUTLINE_NUMBERING_SUFFIX, " - "); 533 534 switch ($type) { 535 536 case self::OUTLINE_HEADING_NUMBERING: 537 global $ACT; 538 /** 539 * Because the HTML file structure is not really fixed 540 * (we may have section HTML element with a bar, the sectioning heading 541 * may be not enabled) 542 * We can't select via html structure 543 * the outline heading consistently 544 * We do it then with the class value 545 */ 546 $outlineClass = Outline::getOutlineHeadingClass(); 547 $reset = ""; 548 if ($ACT === "preview") { 549 $mainContainerSelector = ".pad"; 550 $reset = <<<EOF 551$mainContainerSelector { counter-reset: h2; } 552$mainContainerSelector h2.$outlineClass { counter-increment: h2 1; counter-reset: h3 h4 h5 h6;} 553$mainContainerSelector h3.$outlineClass { counter-increment: h3 1; counter-reset: h4 h5 h6;} 554$mainContainerSelector h4.$outlineClass { counter-increment: h4 1; counter-reset: h5 h6;} 555$mainContainerSelector h5.$outlineClass { counter-increment: h5 1; counter-reset: h6;} 556$mainContainerSelector h6.$outlineClass { counter-increment: h6 1; } 557EOF; 558 } else { 559 $mainContainerSelector = "#" . TemplateSlot::MAIN_CONTENT_ID; 560 $reset = <<<EOF 561$mainContainerSelector { counter-reset: h2; } 562$mainContainerSelector section.outline-section-cs section.outline-section-cs { counter-increment: h2; counter-reset: h3 h4 h5 h6;} 563$mainContainerSelector section.outline-section-cs section.outline-section-cs section.outline-section-cs { counter-increment: h3; counter-reset: h4 h5 h6;} 564$mainContainerSelector section.outline-section-cs section.outline-section-cs section.outline-section-cs section.outline-section-cs { counter-increment: h4; counter-reset: h5 h6;} 565$mainContainerSelector section.outline-section-cs section.outline-section-cs section.outline-section-cs section.outline-section-cs section.outline-section-cs { counter-increment: h5; counter-reset: h6;} 566EOF; 567 568 } 569 return <<<EOF 570$reset 571$mainContainerSelector h2.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$suffix\A"; } 572$mainContainerSelector h3.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$suffix\A"; } 573$mainContainerSelector h4.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$suffix\A"; } 574$mainContainerSelector h5.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$counterSeparator" counter(h5,$level5CounterStyle) "$suffix\A"; } 575$mainContainerSelector h6.$outlineClass::before { content: "$prefix" counter(h2, $level2CounterStyle) "$counterSeparator" counter(h3,$level3CounterStyle) "$counterSeparator" counter(h4,$level4CounterStyle) "$counterSeparator" counter(h5,$level5CounterStyle) "$counterSeparator" counter(h6,$level6CounterStyle) "$suffix\A"; } 576EOF; 577 578 579 case self::TOC_NUMBERING: 580 /** 581 * The level counter on the toc are based 582 * on the https://www.dokuwiki.org/config:toptoclevel 583 * configuration 584 * if toptoclevel = 2, then level1 = h2 and not h1 585 * @deprecated 586 */ 587 // global $conf; 588 // $topTocLevel = $conf['toptoclevel']; 589 590 $tocSelector = "." . Toc::getClass() . " ul"; 591 return <<<EOF 592$tocSelector li { counter-increment: toc2; } 593$tocSelector li li { counter-increment: toc3; } 594$tocSelector li li li { counter-increment: toc4; } 595$tocSelector li li li li { counter-increment: toc5; } 596$tocSelector li li li li li { counter-increment: toc6; } 597$tocSelector li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$suffix\A"; } 598$tocSelector li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$suffix\A"; } 599$tocSelector li li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$counterSeparator" counter(toc4,$level4CounterStyle) "$suffix\A"; } 600$tocSelector li li li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$counterSeparator" counter(toc4,$level4CounterStyle) "$counterSeparator" counter(toc5,$level5CounterStyle) "$suffix\A"; } 601$tocSelector li li li li li a::before { content: "$prefix" counter(toc2, $level2CounterStyle) "$counterSeparator" counter(toc3,$level3CounterStyle) "$counterSeparator" counter(toc4,$level4CounterStyle) "$counterSeparator" counter(toc5,$level5CounterStyle) "$counterSeparator" counter(toc6,$level6CounterStyle) "$suffix\A"; } 602EOF; 603 604 default: 605 throw new ExceptionBadSyntax("The type ($type) is unknown"); 606 } 607 608 609 } 610 611 /** 612 * @throws ExceptionNotFound 613 */ 614 public static function createFromMarkupPath(MarkupPath $markupPath): Outline 615 { 616 $path = $markupPath->getPathObject(); 617 if (!($path instanceof WikiPath)) { 618 throw new ExceptionRuntimeInternal("The path is not a wiki path"); 619 } 620 $markup = FileSystems::getContent($path); 621 $instructions = MarkupRenderer::createFromMarkup($markup, $path, $path) 622 ->setRequestedMimeToInstruction() 623 ->getOutput(); 624 $callStack = CallStack::createFromInstructions($instructions); 625 return new Outline($callStack, $markupPath); 626 } 627 628 public function getInstructionCalls(): array 629 { 630 $totalInstructionCalls = []; 631 $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) { 632 $instructionCalls = array_map(function (Call $element) { 633 return $element->getInstructionCall(); 634 }, $outlineSection->getCalls()); 635 $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls); 636 }; 637 TreeVisit::visit($this->rootSection, $collectCalls); 638 return $totalInstructionCalls; 639 } 640 641 public function toDokuWikiTemplateInstructionCalls(): array 642 { 643 $totalInstructionCalls = []; 644 $sectionSequenceId = 0; 645 $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls, &$sectionSequenceId) { 646 647 $wikiSectionOpen = Call::createNativeCall( 648 \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_OPEN, 649 array($outlineSection->getLevel()), 650 $outlineSection->getStartPosition() 651 ); 652 $wikiSectionClose = Call::createNativeCall( 653 \action_plugin_combo_instructionspostprocessing::EDIT_SECTION_CLOSE, 654 array(), 655 $outlineSection->getEndPosition() 656 ); 657 658 659 if ($outlineSection->hasParent()) { 660 661 662 $sectionCalls = array_merge( 663 $outlineSection->getHeadingCalls(), 664 [$wikiSectionOpen], 665 $outlineSection->getContentCalls(), 666 [$wikiSectionClose], 667 ); 668 669 if ($this->isSectionEditingEnabled()) { 670 671 /** 672 * Adding sectionedit class to be conform 673 * with the Dokuwiki {@link \Doku_Renderer_xhtml::header()} function 674 */ 675 $sectionSequenceId++; 676 $headingCall = $outlineSection->getEnterHeadingCall(); 677 if ($headingCall->isPluginCall()) { 678 $level = DataType::toIntegerOrDefaultIfNull($headingCall->getAttribute(HeadingTag::LEVEL), 0); 679 if ($level <= $this->getTocMaxLevel()) { 680 $headingCall->addClassName("sectionedit$sectionSequenceId"); 681 } 682 } 683 684 $editButton = EditButton::create($outlineSection->getLabel()) 685 ->setStartPosition($outlineSection->getStartPosition()) 686 ->setEndPosition($outlineSection->getEndPosition()) 687 ->setOutlineHeadingId($outlineSection->getHeadingId()) 688 ->setOutlineSectionId($sectionSequenceId) 689 ->toComboCallDokuWikiForm(); 690 $sectionCalls[] = $editButton; 691 } 692 693 } else { 694 // dokuwiki seems to have no section for the content before the first heading 695 $sectionCalls = $outlineSection->getContentCalls(); 696 } 697 698 $instructionCalls = array_map(function (Call $element) { 699 return $element->getInstructionCall(); 700 }, $sectionCalls); 701 $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls); 702 }; 703 TreeVisit::visit($this->rootSection, $collectCalls); 704 return $totalInstructionCalls; 705 } 706 707 private function addCallToSection(Call $actualCall) 708 { 709 if ($this->actualHeadingParsingState === DOKU_LEXER_ENTER && !$this->actualSection->hasContentCall()) { 710 $this->actualSection->addHeaderCall($actualCall); 711 } else { 712 // an content heading (not outline) or another call 713 $this->actualSection->addContentCall($actualCall); 714 } 715 } 716 717 private function enterHeading(Call $actualCall) 718 { 719 $this->actualHeadingParsingState = DOKU_LEXER_ENTER; 720 $this->actualHeadingCall = $actualCall; 721 } 722 723 private function exitHeading() 724 { 725 $this->actualHeadingParsingState = DOKU_LEXER_EXIT; 726 } 727 728 /** 729 * @return array - Dokuwiki TOC array format 730 */ 731 public function toTocDokuwikiFormat(): array 732 { 733 734 $tableOfContent = []; 735 $collectTableOfContent = function (OutlineSection $outlineSection) use (&$tableOfContent) { 736 737 if (!$outlineSection->hasParent()) { 738 // Root Section, no heading 739 return; 740 } 741 $tableOfContent[] = [ 742 'link' => '#' . $outlineSection->getHeadingId(), 743 'title' => $outlineSection->getLabel(), 744 'type' => 'ul', 745 'level' => $outlineSection->getLevel() 746 ]; 747 748 }; 749 TreeVisit::visit($this->rootSection, $collectTableOfContent); 750 return $tableOfContent; 751 752 } 753 754 755 public 756 function toHtmlSectionOutlineCalls(): array 757 { 758 return OutlineVisitor::create($this)->getCalls(); 759 } 760 761 762 /** 763 * Fragment Rendering 764 * * does not have any section/edit button 765 * * no outline or edit button for dynamic rendering but closing of atx heading 766 * 767 * The outline processing ({@link Outline::buildOutline()} just close the atx heading 768 * 769 * @return array 770 */ 771 public 772 function toFragmentInstructionCalls(): array 773 { 774 $totalInstructionCalls = []; 775 $collectCalls = function (OutlineSection $outlineSection) use (&$totalInstructionCalls) { 776 777 $sectionCalls = array_merge( 778 $outlineSection->getHeadingCalls(), 779 $outlineSection->getContentCalls() 780 ); 781 782 $instructionCalls = array_map(function (Call $element) { 783 return $element->getInstructionCall(); 784 }, $sectionCalls); 785 $totalInstructionCalls = array_merge($totalInstructionCalls, $instructionCalls); 786 }; 787 TreeVisit::visit($this->rootSection, $collectCalls); 788 return $totalInstructionCalls; 789 790 } 791 792 /** 793 * Add the label (ie heading text to the cal attribute) 794 * 795 * @return void 796 */ 797 private 798 function saveOutlineToMetadata() 799 { 800 try { 801 $firstChild = $this->rootSection->getFirstChild(); 802 } catch (ExceptionNotFound $e) { 803 // no child 804 return; 805 } 806 if ($firstChild->getLevel() === 1) { 807 $headingCall = $firstChild->getEnterHeadingCall(); 808 // not dokuwiki header ? 809 if ($headingCall->isPluginCall()) { 810 $headingCall->setAttribute(HeadingTag::HEADING_TEXT_ATTRIBUTE, $firstChild->getLabel()); 811 } 812 } 813 814 } 815 816 817 private 818 function storeH1() 819 { 820 try { 821 $outlineSection = $this->getRootOutlineSection()->getFirstChild(); 822 } catch (ExceptionNotFound $e) { 823 // 824 return; 825 } 826 if ($this->markupPath != null && $outlineSection->getLevel() === 1) { 827 $label = $outlineSection->getLabel(); 828 $call = $outlineSection->getEnterHeadingCall(); 829 if ($call->isPluginCall()) { 830 // we support also the dokwuiki header call that does not need the label 831 $call->addAttribute(HeadingTag::PARSED_LABEL, $label); 832 } 833 PageH1::createForPage($this->markupPath)->setDefaultValue($label); 834 } 835 } 836 837 private 838 function storeTocForMarkupIfAny() 839 { 840 841 $toc = $this->toTocDokuwikiFormat(); 842 843 try { 844 $fetcherMarkup = ExecutionContext::getActualOrCreateFromEnv()->getExecutingMarkupHandler(); 845 $fetcherMarkup->toc = $toc; 846 if ($fetcherMarkup->isDocument()) { 847 /** 848 * We still update the global TOC Dokuwiki variables 849 */ 850 global $TOC; 851 $TOC = $toc; 852 } 853 } catch (ExceptionNotFound $e) { 854 // outline is not runnned from a markup handler 855 } 856 857 if (!isset($this->markupPath)) { 858 return; 859 } 860 861 try { 862 Toc::createForPage($this->markupPath) 863 ->setValue($toc) 864 ->persist(); 865 } catch (ExceptionBadArgument $e) { 866 LogUtility::error("The Toc could not be persisted. Error:{$e->getMessage()}"); 867 } 868 } 869 870 public 871 function getMarkupPath(): ?MarkupPath 872 { 873 return $this->markupPath; 874 } 875 876 877 private 878 function getTocMaxLevel(): int 879 { 880 return ExecutionContext::getActualOrCreateFromEnv() 881 ->getConfig()->getTocMaxLevel(); 882 } 883 884 public function setMetaHeaderCapture(bool $metaHeaderCapture): Outline 885 { 886 $this->metaHeaderCapture = $metaHeaderCapture; 887 return $this; 888 } 889 890 public function getMetaHeaderCapture(): bool 891 { 892 if (isset($this->metaHeaderCapture)) { 893 return $this->metaHeaderCapture; 894 } 895 try { 896 if ($this->markupPath !== null) { 897 $contextPath = $this->markupPath->getPathObject()->toWikiPath(); 898 $hasMainHeaderElement = TemplateForWebPage::create() 899 ->setRequestedContextPath($contextPath) 900 ->hasElement(TemplateSlot::MAIN_HEADER_ID); 901 $isThemeSystemEnabled = ExecutionContext::getActualOrCreateFromEnv() 902 ->getConfig() 903 ->isThemeSystemEnabled(); 904 if ($isThemeSystemEnabled && $hasMainHeaderElement) { 905 return true; 906 } 907 } 908 } catch (ExceptionCast $e) { 909 // to Wiki Path should be good 910 } 911 return false; 912 } 913 914 public function isSectionEditingEnabled(): bool 915 { 916 917 return ExecutionContext::getActualOrCreateFromEnv() 918 ->getConfig()->isSectionEditingEnabled(); 919 920 } 921 922 923} 924