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