104fd306cSNickeau<?php 204fd306cSNickeau 304fd306cSNickeaunamespace ComboStrap; 404fd306cSNickeau 504fd306cSNickeau 604fd306cSNickeauclass OutlineSection extends TreeNode 704fd306cSNickeau{ 804fd306cSNickeau const CANONICAL = "outline"; 904fd306cSNickeau 1004fd306cSNickeau 1104fd306cSNickeau /** 1204fd306cSNickeau * Not to confound with header calls that are {@link OutlineSection::getContentCalls()} 1304fd306cSNickeau * of a section that has children 1404fd306cSNickeau * 1504fd306cSNickeau * @var Call[] $headingCalls 1604fd306cSNickeau */ 1704fd306cSNickeau private array $headingCalls = []; 1804fd306cSNickeau /** 1904fd306cSNickeau * 2004fd306cSNickeau * @var Call[] $contentCalls 2104fd306cSNickeau */ 2204fd306cSNickeau private array $contentCalls = []; 2304fd306cSNickeau 2404fd306cSNickeau 2570bbd7f1Sgerardnico private string $headingId; 2670bbd7f1Sgerardnico 2704fd306cSNickeau private int $startFileIndex; 2804fd306cSNickeau private ?int $endFileIndex = null; 2904fd306cSNickeau 30912a1845Sgerardnico /** 31912a1845Sgerardnico * @var Call|null - the first heading call for the section 32912a1845Sgerardnico */ 3304fd306cSNickeau private ?Call $headingEnterCall; 3404fd306cSNickeau /** 3504fd306cSNickeau * @var array an array to make sure that the id are unique 3604fd306cSNickeau */ 3704fd306cSNickeau private array $tocUniqueId = []; 3804fd306cSNickeau 3904fd306cSNickeau /** 4004fd306cSNickeau * @var int - a best guess on the number of 4104fd306cSNickeau */ 4204fd306cSNickeau private int $lineNumber; 43dff3a8c8SNico /** 44dff3a8c8SNico * @var Outline - the outline that created this section (only on root, this is to get the path for the heading) 45dff3a8c8SNico */ 46dff3a8c8SNico private Outline $outlineContext; 4704fd306cSNickeau 4804fd306cSNickeau 4904fd306cSNickeau /** 5004fd306cSNickeau * @param Call|null $headingEnterCall - null if the section is the root 5104fd306cSNickeau */ 527958d4acSNico private function __construct(Outline $outlineContext, Call $headingEnterCall = null) 5304fd306cSNickeau { 547958d4acSNico $this->outlineContext = $outlineContext; 5504fd306cSNickeau $this->headingEnterCall = $headingEnterCall; 5604fd306cSNickeau if ($headingEnterCall !== null) { 5704fd306cSNickeau $position = $headingEnterCall->getFirstMatchedCharacterPosition(); 5804fd306cSNickeau if ($position === null) { 5904fd306cSNickeau $this->startFileIndex = 0; 6004fd306cSNickeau } else { 6104fd306cSNickeau $this->startFileIndex = $position; 6204fd306cSNickeau } 6304fd306cSNickeau $this->addHeaderCall($headingEnterCall); 647958d4acSNico // We persist the id for level 1 because the heading tag may be deleted 657958d4acSNico if ($this->getLevel() === 1) { 66*313de40aSNicolas GERARD /** 67*313de40aSNicolas GERARD * Bug in {@link \Doku_Renderer_xhtml} header method, there is 4 attributes 68*313de40aSNicolas GERARD * and the 4 element may be not present 69*313de40aSNicolas GERARD */ 70*313de40aSNicolas GERARD if ($this->headingEnterCall->getTagName() == 'header') { 71*313de40aSNicolas GERARD if (count($this->headingEnterCall->getAttributes()) == 3) { 72*313de40aSNicolas GERARD $this->headingEnterCall->setAttribute(3, false); 73*313de40aSNicolas GERARD } 74*313de40aSNicolas GERARD } 75*313de40aSNicolas GERARD $this->headingEnterCall->setAttribute('id', $this->getHeadingId()); 767958d4acSNico } 7704fd306cSNickeau } else { 7804fd306cSNickeau $this->startFileIndex = 0; 7904fd306cSNickeau } 8004fd306cSNickeau $this->lineNumber = 1; // the heading 8104fd306cSNickeau 8204fd306cSNickeau } 8304fd306cSNickeau 8404fd306cSNickeau 857958d4acSNico public static function createOutlineRoot(Outline $outlineContext): OutlineSection 8604fd306cSNickeau { 877958d4acSNico return new OutlineSection($outlineContext, null); 8804fd306cSNickeau } 8904fd306cSNickeau 9004fd306cSNickeau 9104fd306cSNickeau /** 9204fd306cSNickeau * Return a text to an HTML Id 9304fd306cSNickeau * @param string $fragment 9404fd306cSNickeau * @return string 9504fd306cSNickeau */ 9604fd306cSNickeau public static function textToHtmlSectionId(string $fragment): string 9704fd306cSNickeau { 9804fd306cSNickeau $check = false; 9904fd306cSNickeau // for empty string, the below function returns `section` 10004fd306cSNickeau return sectionID($fragment, $check); 10104fd306cSNickeau } 10204fd306cSNickeau 1037958d4acSNico public static function createFromEnterHeadingCall(Outline $outline, Call $enterHeadingCall): OutlineSection 10404fd306cSNickeau { 1057958d4acSNico return new OutlineSection($outline, $enterHeadingCall); 10604fd306cSNickeau } 10704fd306cSNickeau 10804fd306cSNickeau public function getFirstChild(): OutlineSection 10904fd306cSNickeau { 11004fd306cSNickeau 11104fd306cSNickeau /** @noinspection PhpIncompatibleReturnTypeInspection */ 11204fd306cSNickeau return parent::getFirstChild(); 11304fd306cSNickeau 11404fd306cSNickeau } 11504fd306cSNickeau 11604fd306cSNickeau 11704fd306cSNickeau public function addContentCall(Call $actualCall): OutlineSection 11804fd306cSNickeau { 11904fd306cSNickeau 12004fd306cSNickeau $this->contentCalls[] = $actualCall; 12104fd306cSNickeau return $this; 12204fd306cSNickeau 12304fd306cSNickeau 12404fd306cSNickeau } 12504fd306cSNickeau 12604fd306cSNickeau public function addHeaderCall(Call $actualCall): OutlineSection 12704fd306cSNickeau { 12804fd306cSNickeau 12904fd306cSNickeau $this->headingCalls[] = $actualCall; 13004fd306cSNickeau return $this; 13104fd306cSNickeau } 13204fd306cSNickeau 13304fd306cSNickeau public function getLabel(): string 13404fd306cSNickeau { 13504fd306cSNickeau $label = ""; 13604fd306cSNickeau foreach ($this->headingCalls as $call) { 137912a1845Sgerardnico if ($call->getTagName() === Outline::DOKUWIKI_HEADING_CALL_NAME) { 138912a1845Sgerardnico $label = $call->getInstructionCall()[1][0]; 139912a1845Sgerardnico // no more label call 140912a1845Sgerardnico break; 141912a1845Sgerardnico } 14204fd306cSNickeau if ($call->isTextCall()) { 14304fd306cSNickeau // Building the text for the toc 14404fd306cSNickeau // only cdata for now 14504fd306cSNickeau // no image, ... 14604fd306cSNickeau if ($label != "") { 14704fd306cSNickeau $label .= " "; 14804fd306cSNickeau } 14904fd306cSNickeau $label .= trim($call->getCapturedContent()); 15004fd306cSNickeau } 15104fd306cSNickeau } 15204fd306cSNickeau return trim($label); 15304fd306cSNickeau } 15404fd306cSNickeau 15504fd306cSNickeau public function setStartPosition(int $startPosition): OutlineSection 15604fd306cSNickeau { 15704fd306cSNickeau $this->startFileIndex = $startPosition; 15804fd306cSNickeau return $this; 15904fd306cSNickeau } 16004fd306cSNickeau 16104fd306cSNickeau public function setEndPosition(int $endFileIndex): OutlineSection 16204fd306cSNickeau { 16304fd306cSNickeau $this->endFileIndex = $endFileIndex; 16404fd306cSNickeau return $this; 16504fd306cSNickeau } 16604fd306cSNickeau 16704fd306cSNickeau /** 16804fd306cSNickeau * @return Call[] 16904fd306cSNickeau */ 17004fd306cSNickeau public function getHeadingCalls(): array 17104fd306cSNickeau { 17204fd306cSNickeau if ( 17304fd306cSNickeau $this->headingEnterCall !== null && 17404fd306cSNickeau $this->headingEnterCall->isPluginCall() && 17504fd306cSNickeau !$this->headingEnterCall->hasAttribute("id") 17604fd306cSNickeau ) { 17704fd306cSNickeau $this->headingEnterCall->addAttribute("id", $this->getHeadingId()); 17804fd306cSNickeau } 17904fd306cSNickeau return $this->headingCalls; 18004fd306cSNickeau } 18104fd306cSNickeau 18204fd306cSNickeau 18304fd306cSNickeau public 18404fd306cSNickeau function getEnterHeadingCall(): ?Call 18504fd306cSNickeau { 18604fd306cSNickeau return $this->headingEnterCall; 18704fd306cSNickeau } 18804fd306cSNickeau 18904fd306cSNickeau 19004fd306cSNickeau public 19104fd306cSNickeau function getCalls(): array 19204fd306cSNickeau { 19304fd306cSNickeau return array_merge($this->headingCalls, $this->contentCalls); 19404fd306cSNickeau } 19504fd306cSNickeau 19604fd306cSNickeau public 19704fd306cSNickeau function getContentCalls(): array 19804fd306cSNickeau { 19904fd306cSNickeau return $this->contentCalls; 20004fd306cSNickeau } 20104fd306cSNickeau 20204fd306cSNickeau /** 20304fd306cSNickeau * @return int 20404fd306cSNickeau */ 20504fd306cSNickeau public 20604fd306cSNickeau function getLevel(): int 20704fd306cSNickeau { 20804fd306cSNickeau if ($this->headingEnterCall === null) { 20904fd306cSNickeau return 0; 21004fd306cSNickeau } 21104fd306cSNickeau switch ($this->headingEnterCall->getTagName()) { 212912a1845Sgerardnico case Outline::DOKUWIKI_HEADING_CALL_NAME: 21304fd306cSNickeau $level = $this->headingEnterCall->getInstructionCall()[1][1]; 21404fd306cSNickeau break; 21504fd306cSNickeau default: 21604fd306cSNickeau $level = $this->headingEnterCall->getAttribute(HeadingTag::LEVEL); 21704fd306cSNickeau break; 21804fd306cSNickeau } 21904fd306cSNickeau 22004fd306cSNickeau try { 22104fd306cSNickeau return DataType::toInteger($level); 22204fd306cSNickeau } catch (ExceptionBadArgument $e) { 22304fd306cSNickeau // should not happen 22404fd306cSNickeau LogUtility::internalError("The level ($level) could not be cast to an integer", self::CANONICAL); 22504fd306cSNickeau return 0; 22604fd306cSNickeau } 22704fd306cSNickeau } 22804fd306cSNickeau 22904fd306cSNickeau public 23004fd306cSNickeau function getStartPosition(): int 23104fd306cSNickeau { 23204fd306cSNickeau return $this->startFileIndex; 23304fd306cSNickeau } 23404fd306cSNickeau 23504fd306cSNickeau public 23604fd306cSNickeau function getEndPosition(): ?int 23704fd306cSNickeau { 23804fd306cSNickeau return $this->endFileIndex; 23904fd306cSNickeau } 24004fd306cSNickeau 24104fd306cSNickeau public 24204fd306cSNickeau function hasContentCall(): bool 24304fd306cSNickeau { 24404fd306cSNickeau return sizeof($this->contentCalls) > 0; 24504fd306cSNickeau } 24604fd306cSNickeau 24704fd306cSNickeau /** 24804fd306cSNickeau */ 24904fd306cSNickeau public 25004fd306cSNickeau function getHeadingId() 25104fd306cSNickeau { 25204fd306cSNickeau 25304fd306cSNickeau if (!isset($this->headingId)) { 25404fd306cSNickeau $id = $this->headingEnterCall->getAttribute("id"); 25504fd306cSNickeau if ($id !== null) { 25604fd306cSNickeau return $id; 25704fd306cSNickeau } 258dff3a8c8SNico 25904fd306cSNickeau $label = $this->getLabel(); 260dff3a8c8SNico 261dff3a8c8SNico /** 262dff3a8c8SNico * For Level 1 (ie Heading 1), we use the path as id and not the label 263dff3a8c8SNico * Why? because when we bundle all pages in a single page 264dff3a8c8SNico * (With {@link FetcherPageBundler} 265dff3a8c8SNico * we can transform a wiki link to an internal link 266dff3a8c8SNico */ 267dff3a8c8SNico $level = $this->getLevel(); 268dff3a8c8SNico if ($level === 1) { 269dff3a8c8SNico // id is the path id 270dff3a8c8SNico $markupPath = $this->getRoot()->outlineContext->getMarkupPath(); 271dff3a8c8SNico if ($markupPath !== null) { 272dff3a8c8SNico $label = $markupPath->toAbsoluteId(); 273dff3a8c8SNico } 274dff3a8c8SNico } 275dff3a8c8SNico 27604fd306cSNickeau $this->headingId = sectionID($label, $this->tocUniqueId); 27704fd306cSNickeau } 27804fd306cSNickeau return $this->headingId; 27904fd306cSNickeau 28004fd306cSNickeau } 28104fd306cSNickeau 28204fd306cSNickeau /** 28304fd306cSNickeau * A HTML section should have a heading 28404fd306cSNickeau * but in a markup document, we may have data before the first 28504fd306cSNickeau * heading making a section without heading 28604fd306cSNickeau * @return bool 28704fd306cSNickeau */ 28804fd306cSNickeau public 28904fd306cSNickeau function hasHeading(): bool 29004fd306cSNickeau { 29104fd306cSNickeau return $this->headingEnterCall !== null; 29204fd306cSNickeau } 29304fd306cSNickeau 29404fd306cSNickeau /** 29504fd306cSNickeau * @return OutlineSection[] 29604fd306cSNickeau */ 29704fd306cSNickeau public 29804fd306cSNickeau function getChildren(): array 29904fd306cSNickeau { 30004fd306cSNickeau return parent::getChildren(); 30104fd306cSNickeau } 30204fd306cSNickeau 30304fd306cSNickeau public function setLevel(int $level): OutlineSection 30404fd306cSNickeau { 3057958d4acSNico 30604fd306cSNickeau switch ($this->headingEnterCall->getTagName()) { 307912a1845Sgerardnico case Outline::DOKUWIKI_HEADING_CALL_NAME: 30804fd306cSNickeau $this->headingEnterCall->getInstructionCall()[1][1] = $level; 30904fd306cSNickeau break; 31004fd306cSNickeau default: 31104fd306cSNickeau $this->headingEnterCall->setAttribute(HeadingTag::LEVEL, $level); 31204fd306cSNickeau $headingExitCall = $this->headingCalls[count($this->headingCalls) - 1]; 31304fd306cSNickeau $headingExitCall->setAttribute(HeadingTag::LEVEL, $level); 31404fd306cSNickeau break; 31504fd306cSNickeau } 31604fd306cSNickeau 31704fd306cSNickeau /** 318dff3a8c8SNico * Update the descendants sections 31904fd306cSNickeau * @param OutlineSection $parentSection 32004fd306cSNickeau * @return void 32104fd306cSNickeau */ 32204fd306cSNickeau $updateLevel = function (OutlineSection $parentSection) { 32304fd306cSNickeau foreach ($parentSection->getChildren() as $child) { 32404fd306cSNickeau $child->setLevel($parentSection->getLevel() + 1); 32504fd306cSNickeau } 32604fd306cSNickeau }; 32704fd306cSNickeau TreeVisit::visit($this, $updateLevel); 32804fd306cSNickeau 32904fd306cSNickeau return $this; 33004fd306cSNickeau } 33104fd306cSNickeau 33204fd306cSNickeau 33304fd306cSNickeau public function deleteContentCalls(): OutlineSection 33404fd306cSNickeau { 33504fd306cSNickeau $this->contentCalls = []; 33604fd306cSNickeau return $this; 33704fd306cSNickeau } 33804fd306cSNickeau 33904fd306cSNickeau public function incrementLineNumber(): OutlineSection 34004fd306cSNickeau { 34104fd306cSNickeau $this->lineNumber++; 34204fd306cSNickeau return $this; 34304fd306cSNickeau } 34404fd306cSNickeau 34504fd306cSNickeau public function getLineCount(): int 34604fd306cSNickeau { 34704fd306cSNickeau return $this->lineNumber; 34804fd306cSNickeau } 34904fd306cSNickeau 350dff3a8c8SNico private function getRoot() 351dff3a8c8SNico { 352dff3a8c8SNico $actual = $this; 353dff3a8c8SNico while ($actual->hasParent()) { 354dff3a8c8SNico try { 355dff3a8c8SNico $actual = $actual->getParent(); 356dff3a8c8SNico } catch (ExceptionNotFound $e) { 357dff3a8c8SNico // should not as we check before 358dff3a8c8SNico } 359dff3a8c8SNico } 360dff3a8c8SNico return $actual; 361dff3a8c8SNico } 362dff3a8c8SNico 363dff3a8c8SNico /** 364dff3a8c8SNico * @param MarkupPath|null $startPath - the path from where the page bundle is started to see if the link is of a page that was bundled 365dff3a8c8SNico * @return $this - when merging 2 page, we need to make sure that the link becomes internal 366dff3a8c8SNico * if the page was bundled 367dff3a8c8SNico * (ie a link to :page:yolo become #pageyolo) 368dff3a8c8SNico */ 369dff3a8c8SNico public function updatePageLinkToInternal(?MarkupPath $startPath): OutlineSection 370dff3a8c8SNico { 371dff3a8c8SNico foreach ($this->contentCalls as $contentCall) { 372dff3a8c8SNico 373dff3a8c8SNico if (!$contentCall->isPluginCall()) { 374dff3a8c8SNico continue; 375dff3a8c8SNico } 376dff3a8c8SNico $componentName = $contentCall->getComponentName(); 377dff3a8c8SNico if ($componentName === "combo_link" && $contentCall->getState() === DOKU_LEXER_ENTER) { 378dff3a8c8SNico $refString = $contentCall->getAttribute("ref"); 379dff3a8c8SNico if ($refString === null) { 380dff3a8c8SNico continue; 381dff3a8c8SNico } 382dff3a8c8SNico try { 383dff3a8c8SNico $markupRef = MarkupRef::createLinkFromRef($refString); 384dff3a8c8SNico } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotFound $e) { 385dff3a8c8SNico // pffff 386dff3a8c8SNico continue; 387dff3a8c8SNico } 388dff3a8c8SNico if ($markupRef->getSchemeType() !== MarkupRef::WIKI_URI) { 389dff3a8c8SNico continue; 390dff3a8c8SNico } 391dff3a8c8SNico try { 392dff3a8c8SNico $parentPath = $startPath->toWikiPath()->getParent()->toAbsoluteId(); 393dff3a8c8SNico } catch (ExceptionNotFound $e) { 394dff3a8c8SNico // root then 395dff3a8c8SNico $parentPath = ":"; 396dff3a8c8SNico } 397dff3a8c8SNico if (!StringUtility::startWiths($refString, $parentPath)) { 398dff3a8c8SNico continue; 399dff3a8c8SNico } 400dff3a8c8SNico $noCheck = false; 401dff3a8c8SNico $expectedH1ID = sectionID($refString, $noCheck); 402dff3a8c8SNico $contentCall->setAttribute("ref", "#" . $expectedH1ID); 403dff3a8c8SNico 404dff3a8c8SNico } 405dff3a8c8SNico } 406dff3a8c8SNico 407dff3a8c8SNico /** 408dff3a8c8SNico * Update the links to internal 409dff3a8c8SNico */ 410dff3a8c8SNico $updateLink = function (OutlineSection $parentSection) use ($startPath) { 411dff3a8c8SNico foreach ($parentSection->getChildren() as $child) { 412dff3a8c8SNico $child->updatePageLinkToInternal($startPath); 413dff3a8c8SNico } 414dff3a8c8SNico }; 415dff3a8c8SNico TreeVisit::visit($this, $updateLink); 416dff3a8c8SNico return $this; 417dff3a8c8SNico } 418dff3a8c8SNico 41904fd306cSNickeau 42004fd306cSNickeau} 421