xref: /plugin/combo/ComboStrap/OutlineSection.php (revision 7958d4acedb2b7dec2edf9b52ab638b4686d0a1f)
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