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