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