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