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