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