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 */ 52*7958d4acSNico private function __construct(Outline $outlineContext,Call $headingEnterCall = null) 5304fd306cSNickeau { 54*7958d4acSNico $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); 64*7958d4acSNico // We persist the id for level 1 because the heading tag may be deleted 65*7958d4acSNico if ($this->getLevel() === 1) { 66*7958d4acSNico $this->headingEnterCall->setAttribute("id", $this->getHeadingId()); 67*7958d4acSNico } 6804fd306cSNickeau } else { 6904fd306cSNickeau $this->startFileIndex = 0; 7004fd306cSNickeau } 7104fd306cSNickeau $this->lineNumber = 1; // the heading 7204fd306cSNickeau 7304fd306cSNickeau } 7404fd306cSNickeau 7504fd306cSNickeau 76*7958d4acSNico public static function createOutlineRoot(Outline $outlineContext): OutlineSection 7704fd306cSNickeau { 78*7958d4acSNico return new OutlineSection($outlineContext,null); 7904fd306cSNickeau } 8004fd306cSNickeau 8104fd306cSNickeau 8204fd306cSNickeau /** 8304fd306cSNickeau * Return a text to an HTML Id 8404fd306cSNickeau * @param string $fragment 8504fd306cSNickeau * @return string 8604fd306cSNickeau */ 8704fd306cSNickeau public static function textToHtmlSectionId(string $fragment): string 8804fd306cSNickeau { 8904fd306cSNickeau $check = false; 9004fd306cSNickeau // for empty string, the below function returns `section` 9104fd306cSNickeau return sectionID($fragment, $check); 9204fd306cSNickeau } 9304fd306cSNickeau 94*7958d4acSNico public static function createFromEnterHeadingCall(Outline $outline,Call $enterHeadingCall): OutlineSection 9504fd306cSNickeau { 96*7958d4acSNico return new OutlineSection($outline, $enterHeadingCall); 9704fd306cSNickeau } 9804fd306cSNickeau 9904fd306cSNickeau public function getFirstChild(): OutlineSection 10004fd306cSNickeau { 10104fd306cSNickeau 10204fd306cSNickeau /** @noinspection PhpIncompatibleReturnTypeInspection */ 10304fd306cSNickeau return parent::getFirstChild(); 10404fd306cSNickeau 10504fd306cSNickeau } 10604fd306cSNickeau 10704fd306cSNickeau 10804fd306cSNickeau public function addContentCall(Call $actualCall): OutlineSection 10904fd306cSNickeau { 11004fd306cSNickeau 11104fd306cSNickeau $this->contentCalls[] = $actualCall; 11204fd306cSNickeau return $this; 11304fd306cSNickeau 11404fd306cSNickeau 11504fd306cSNickeau } 11604fd306cSNickeau 11704fd306cSNickeau public function addHeaderCall(Call $actualCall): OutlineSection 11804fd306cSNickeau { 11904fd306cSNickeau 12004fd306cSNickeau $this->headingCalls[] = $actualCall; 12104fd306cSNickeau return $this; 12204fd306cSNickeau } 12304fd306cSNickeau 12404fd306cSNickeau public function getLabel(): string 12504fd306cSNickeau { 12604fd306cSNickeau $label = ""; 12704fd306cSNickeau foreach ($this->headingCalls as $call) { 128912a1845Sgerardnico if ($call->getTagName() === Outline::DOKUWIKI_HEADING_CALL_NAME) { 129912a1845Sgerardnico $label = $call->getInstructionCall()[1][0]; 130912a1845Sgerardnico // no more label call 131912a1845Sgerardnico break; 132912a1845Sgerardnico } 13304fd306cSNickeau if ($call->isTextCall()) { 13404fd306cSNickeau // Building the text for the toc 13504fd306cSNickeau // only cdata for now 13604fd306cSNickeau // no image, ... 13704fd306cSNickeau if ($label != "") { 13804fd306cSNickeau $label .= " "; 13904fd306cSNickeau } 14004fd306cSNickeau $label .= trim($call->getCapturedContent()); 14104fd306cSNickeau } 14204fd306cSNickeau } 14304fd306cSNickeau return trim($label); 14404fd306cSNickeau } 14504fd306cSNickeau 14604fd306cSNickeau public function setStartPosition(int $startPosition): OutlineSection 14704fd306cSNickeau { 14804fd306cSNickeau $this->startFileIndex = $startPosition; 14904fd306cSNickeau return $this; 15004fd306cSNickeau } 15104fd306cSNickeau 15204fd306cSNickeau public function setEndPosition(int $endFileIndex): OutlineSection 15304fd306cSNickeau { 15404fd306cSNickeau $this->endFileIndex = $endFileIndex; 15504fd306cSNickeau return $this; 15604fd306cSNickeau } 15704fd306cSNickeau 15804fd306cSNickeau /** 15904fd306cSNickeau * @return Call[] 16004fd306cSNickeau */ 16104fd306cSNickeau public function getHeadingCalls(): array 16204fd306cSNickeau { 16304fd306cSNickeau if ( 16404fd306cSNickeau $this->headingEnterCall !== null && 16504fd306cSNickeau $this->headingEnterCall->isPluginCall() && 16604fd306cSNickeau !$this->headingEnterCall->hasAttribute("id") 16704fd306cSNickeau ) { 16804fd306cSNickeau $this->headingEnterCall->addAttribute("id", $this->getHeadingId()); 16904fd306cSNickeau } 17004fd306cSNickeau return $this->headingCalls; 17104fd306cSNickeau } 17204fd306cSNickeau 17304fd306cSNickeau 17404fd306cSNickeau public 17504fd306cSNickeau function getEnterHeadingCall(): ?Call 17604fd306cSNickeau { 17704fd306cSNickeau return $this->headingEnterCall; 17804fd306cSNickeau } 17904fd306cSNickeau 18004fd306cSNickeau 18104fd306cSNickeau public 18204fd306cSNickeau function getCalls(): array 18304fd306cSNickeau { 18404fd306cSNickeau return array_merge($this->headingCalls, $this->contentCalls); 18504fd306cSNickeau } 18604fd306cSNickeau 18704fd306cSNickeau public 18804fd306cSNickeau function getContentCalls(): array 18904fd306cSNickeau { 19004fd306cSNickeau return $this->contentCalls; 19104fd306cSNickeau } 19204fd306cSNickeau 19304fd306cSNickeau /** 19404fd306cSNickeau * @return int 19504fd306cSNickeau */ 19604fd306cSNickeau public 19704fd306cSNickeau function getLevel(): int 19804fd306cSNickeau { 19904fd306cSNickeau if ($this->headingEnterCall === null) { 20004fd306cSNickeau return 0; 20104fd306cSNickeau } 20204fd306cSNickeau switch ($this->headingEnterCall->getTagName()) { 203912a1845Sgerardnico case Outline::DOKUWIKI_HEADING_CALL_NAME: 20404fd306cSNickeau $level = $this->headingEnterCall->getInstructionCall()[1][1]; 20504fd306cSNickeau break; 20604fd306cSNickeau default: 20704fd306cSNickeau $level = $this->headingEnterCall->getAttribute(HeadingTag::LEVEL); 20804fd306cSNickeau break; 20904fd306cSNickeau } 21004fd306cSNickeau 21104fd306cSNickeau try { 21204fd306cSNickeau return DataType::toInteger($level); 21304fd306cSNickeau } catch (ExceptionBadArgument $e) { 21404fd306cSNickeau // should not happen 21504fd306cSNickeau LogUtility::internalError("The level ($level) could not be cast to an integer", self::CANONICAL); 21604fd306cSNickeau return 0; 21704fd306cSNickeau } 21804fd306cSNickeau } 21904fd306cSNickeau 22004fd306cSNickeau public 22104fd306cSNickeau function getStartPosition(): int 22204fd306cSNickeau { 22304fd306cSNickeau return $this->startFileIndex; 22404fd306cSNickeau } 22504fd306cSNickeau 22604fd306cSNickeau public 22704fd306cSNickeau function getEndPosition(): ?int 22804fd306cSNickeau { 22904fd306cSNickeau return $this->endFileIndex; 23004fd306cSNickeau } 23104fd306cSNickeau 23204fd306cSNickeau public 23304fd306cSNickeau function hasContentCall(): bool 23404fd306cSNickeau { 23504fd306cSNickeau return sizeof($this->contentCalls) > 0; 23604fd306cSNickeau } 23704fd306cSNickeau 23804fd306cSNickeau /** 23904fd306cSNickeau */ 24004fd306cSNickeau public 24104fd306cSNickeau function getHeadingId() 24204fd306cSNickeau { 24304fd306cSNickeau 24404fd306cSNickeau if (!isset($this->headingId)) { 24504fd306cSNickeau $id = $this->headingEnterCall->getAttribute("id"); 24604fd306cSNickeau if ($id !== null) { 24704fd306cSNickeau return $id; 24804fd306cSNickeau } 249dff3a8c8SNico 25004fd306cSNickeau $label = $this->getLabel(); 251dff3a8c8SNico 252dff3a8c8SNico /** 253dff3a8c8SNico * For Level 1 (ie Heading 1), we use the path as id and not the label 254dff3a8c8SNico * Why? because when we bundle all pages in a single page 255dff3a8c8SNico * (With {@link FetcherPageBundler} 256dff3a8c8SNico * we can transform a wiki link to an internal link 257dff3a8c8SNico */ 258dff3a8c8SNico $level = $this->getLevel(); 259dff3a8c8SNico if ($level === 1) { 260dff3a8c8SNico // id is the path id 261dff3a8c8SNico $markupPath = $this->getRoot()->outlineContext->getMarkupPath(); 262dff3a8c8SNico if ($markupPath !== null) { 263dff3a8c8SNico $label = $markupPath->toAbsoluteId(); 264dff3a8c8SNico } 265dff3a8c8SNico } 266dff3a8c8SNico 26704fd306cSNickeau $this->headingId = sectionID($label, $this->tocUniqueId); 26804fd306cSNickeau } 26904fd306cSNickeau return $this->headingId; 27004fd306cSNickeau 27104fd306cSNickeau } 27204fd306cSNickeau 27304fd306cSNickeau /** 27404fd306cSNickeau * A HTML section should have a heading 27504fd306cSNickeau * but in a markup document, we may have data before the first 27604fd306cSNickeau * heading making a section without heading 27704fd306cSNickeau * @return bool 27804fd306cSNickeau */ 27904fd306cSNickeau public 28004fd306cSNickeau function hasHeading(): bool 28104fd306cSNickeau { 28204fd306cSNickeau return $this->headingEnterCall !== null; 28304fd306cSNickeau } 28404fd306cSNickeau 28504fd306cSNickeau /** 28604fd306cSNickeau * @return OutlineSection[] 28704fd306cSNickeau */ 28804fd306cSNickeau public 28904fd306cSNickeau function getChildren(): array 29004fd306cSNickeau { 29104fd306cSNickeau return parent::getChildren(); 29204fd306cSNickeau } 29304fd306cSNickeau 29404fd306cSNickeau public function setLevel(int $level): OutlineSection 29504fd306cSNickeau { 296*7958d4acSNico 29704fd306cSNickeau switch ($this->headingEnterCall->getTagName()) { 298912a1845Sgerardnico case Outline::DOKUWIKI_HEADING_CALL_NAME: 29904fd306cSNickeau $this->headingEnterCall->getInstructionCall()[1][1] = $level; 30004fd306cSNickeau break; 30104fd306cSNickeau default: 30204fd306cSNickeau $this->headingEnterCall->setAttribute(HeadingTag::LEVEL, $level); 30304fd306cSNickeau $headingExitCall = $this->headingCalls[count($this->headingCalls) - 1]; 30404fd306cSNickeau $headingExitCall->setAttribute(HeadingTag::LEVEL, $level); 30504fd306cSNickeau break; 30604fd306cSNickeau } 30704fd306cSNickeau 30804fd306cSNickeau /** 309dff3a8c8SNico * Update the descendants sections 31004fd306cSNickeau * @param OutlineSection $parentSection 31104fd306cSNickeau * @return void 31204fd306cSNickeau */ 31304fd306cSNickeau $updateLevel = function (OutlineSection $parentSection) { 31404fd306cSNickeau foreach ($parentSection->getChildren() as $child) { 31504fd306cSNickeau $child->setLevel($parentSection->getLevel() + 1); 31604fd306cSNickeau } 31704fd306cSNickeau }; 31804fd306cSNickeau TreeVisit::visit($this, $updateLevel); 31904fd306cSNickeau 32004fd306cSNickeau return $this; 32104fd306cSNickeau } 32204fd306cSNickeau 32304fd306cSNickeau 32404fd306cSNickeau public function deleteContentCalls(): OutlineSection 32504fd306cSNickeau { 32604fd306cSNickeau $this->contentCalls = []; 32704fd306cSNickeau return $this; 32804fd306cSNickeau } 32904fd306cSNickeau 33004fd306cSNickeau public function incrementLineNumber(): OutlineSection 33104fd306cSNickeau { 33204fd306cSNickeau $this->lineNumber++; 33304fd306cSNickeau return $this; 33404fd306cSNickeau } 33504fd306cSNickeau 33604fd306cSNickeau public function getLineCount(): int 33704fd306cSNickeau { 33804fd306cSNickeau return $this->lineNumber; 33904fd306cSNickeau } 34004fd306cSNickeau 341dff3a8c8SNico private function getRoot() 342dff3a8c8SNico { 343dff3a8c8SNico $actual = $this; 344dff3a8c8SNico while ($actual->hasParent()) { 345dff3a8c8SNico try { 346dff3a8c8SNico $actual = $actual->getParent(); 347dff3a8c8SNico } catch (ExceptionNotFound $e) { 348dff3a8c8SNico // should not as we check before 349dff3a8c8SNico } 350dff3a8c8SNico } 351dff3a8c8SNico return $actual; 352dff3a8c8SNico } 353dff3a8c8SNico 354dff3a8c8SNico /** 355dff3a8c8SNico * @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 356dff3a8c8SNico * @return $this - when merging 2 page, we need to make sure that the link becomes internal 357dff3a8c8SNico * if the page was bundled 358dff3a8c8SNico * (ie a link to :page:yolo become #pageyolo) 359dff3a8c8SNico */ 360dff3a8c8SNico public function updatePageLinkToInternal(?MarkupPath $startPath): OutlineSection 361dff3a8c8SNico { 362dff3a8c8SNico foreach ($this->contentCalls as $contentCall) { 363dff3a8c8SNico 364dff3a8c8SNico if (!$contentCall->isPluginCall()) { 365dff3a8c8SNico continue; 366dff3a8c8SNico } 367dff3a8c8SNico $componentName = $contentCall->getComponentName(); 368dff3a8c8SNico if ($componentName === "combo_link" && $contentCall->getState() === DOKU_LEXER_ENTER) { 369dff3a8c8SNico $refString = $contentCall->getAttribute("ref"); 370dff3a8c8SNico if ($refString === null) { 371dff3a8c8SNico continue; 372dff3a8c8SNico } 373dff3a8c8SNico try { 374dff3a8c8SNico $markupRef = MarkupRef::createLinkFromRef($refString); 375dff3a8c8SNico } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotFound $e) { 376dff3a8c8SNico // pffff 377dff3a8c8SNico continue; 378dff3a8c8SNico } 379dff3a8c8SNico if ($markupRef->getSchemeType() !== MarkupRef::WIKI_URI) { 380dff3a8c8SNico continue; 381dff3a8c8SNico } 382dff3a8c8SNico try { 383dff3a8c8SNico $parentPath = $startPath->toWikiPath()->getParent()->toAbsoluteId(); 384dff3a8c8SNico } catch (ExceptionNotFound $e) { 385dff3a8c8SNico // root then 386dff3a8c8SNico $parentPath = ":"; 387dff3a8c8SNico } 388dff3a8c8SNico if (!StringUtility::startWiths($refString, $parentPath)) { 389dff3a8c8SNico continue; 390dff3a8c8SNico } 391dff3a8c8SNico $noCheck = false; 392dff3a8c8SNico $expectedH1ID = sectionID($refString, $noCheck); 393dff3a8c8SNico $contentCall->setAttribute("ref", "#" . $expectedH1ID); 394dff3a8c8SNico 395dff3a8c8SNico } 396dff3a8c8SNico } 397dff3a8c8SNico 398dff3a8c8SNico /** 399dff3a8c8SNico * Update the links to internal 400dff3a8c8SNico */ 401dff3a8c8SNico $updateLink = function (OutlineSection $parentSection) use ($startPath) { 402dff3a8c8SNico foreach ($parentSection->getChildren() as $child) { 403dff3a8c8SNico $child->updatePageLinkToInternal($startPath); 404dff3a8c8SNico } 405dff3a8c8SNico }; 406dff3a8c8SNico TreeVisit::visit($this, $updateLink); 407dff3a8c8SNico return $this; 408dff3a8c8SNico } 409dff3a8c8SNico 41004fd306cSNickeau 41104fd306cSNickeau} 412