xref: /plugin/combo/ComboStrap/OutlineSection.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1*04fd306cSNickeau<?php
2*04fd306cSNickeau
3*04fd306cSNickeaunamespace ComboStrap;
4*04fd306cSNickeau
5*04fd306cSNickeau
6*04fd306cSNickeauclass OutlineSection extends TreeNode
7*04fd306cSNickeau{
8*04fd306cSNickeau    const CANONICAL = "outline";
9*04fd306cSNickeau    const HEADER_DOKUWIKI_CALL = "header";
10*04fd306cSNickeau
11*04fd306cSNickeau
12*04fd306cSNickeau    /**
13*04fd306cSNickeau     * Not to confound with header calls that are {@link OutlineSection::getContentCalls()}
14*04fd306cSNickeau     * of a section that has children
15*04fd306cSNickeau     *
16*04fd306cSNickeau     * @var Call[] $headingCalls
17*04fd306cSNickeau     */
18*04fd306cSNickeau    private array $headingCalls = [];
19*04fd306cSNickeau    /**
20*04fd306cSNickeau     *
21*04fd306cSNickeau     * @var Call[] $contentCalls
22*04fd306cSNickeau     */
23*04fd306cSNickeau    private array $contentCalls = [];
24*04fd306cSNickeau
25*04fd306cSNickeau
26*04fd306cSNickeau    private int $startFileIndex;
27*04fd306cSNickeau    private ?int $endFileIndex = null;
28*04fd306cSNickeau
29*04fd306cSNickeau    private ?Call $headingEnterCall;
30*04fd306cSNickeau    /**
31*04fd306cSNickeau     * @var array an array to make sure that the id are unique
32*04fd306cSNickeau     */
33*04fd306cSNickeau    private array $tocUniqueId = [];
34*04fd306cSNickeau
35*04fd306cSNickeau    /**
36*04fd306cSNickeau     * @var int - a best guess on the number of
37*04fd306cSNickeau     */
38*04fd306cSNickeau    private int $lineNumber;
39*04fd306cSNickeau
40*04fd306cSNickeau
41*04fd306cSNickeau    /**
42*04fd306cSNickeau     * @param Call|null $headingEnterCall - null if the section is the root
43*04fd306cSNickeau     */
44*04fd306cSNickeau    private function __construct(Call $headingEnterCall = null)
45*04fd306cSNickeau    {
46*04fd306cSNickeau        $this->headingEnterCall = $headingEnterCall;
47*04fd306cSNickeau        if ($headingEnterCall !== null) {
48*04fd306cSNickeau            $position = $headingEnterCall->getFirstMatchedCharacterPosition();
49*04fd306cSNickeau            if ($position === null) {
50*04fd306cSNickeau                $this->startFileIndex = 0;
51*04fd306cSNickeau            } else {
52*04fd306cSNickeau                $this->startFileIndex = $position;
53*04fd306cSNickeau            }
54*04fd306cSNickeau            $this->addHeaderCall($headingEnterCall);
55*04fd306cSNickeau        } else {
56*04fd306cSNickeau            $this->startFileIndex = 0;
57*04fd306cSNickeau        }
58*04fd306cSNickeau        $this->lineNumber = 1; // the heading
59*04fd306cSNickeau
60*04fd306cSNickeau    }
61*04fd306cSNickeau
62*04fd306cSNickeau
63*04fd306cSNickeau    public static function createOutlineRoot(): OutlineSection
64*04fd306cSNickeau    {
65*04fd306cSNickeau        return new OutlineSection(null);
66*04fd306cSNickeau    }
67*04fd306cSNickeau
68*04fd306cSNickeau
69*04fd306cSNickeau    /**
70*04fd306cSNickeau     * Return a text to an HTML Id
71*04fd306cSNickeau     * @param string $fragment
72*04fd306cSNickeau     * @return string
73*04fd306cSNickeau     */
74*04fd306cSNickeau    public static function textToHtmlSectionId(string $fragment): string
75*04fd306cSNickeau    {
76*04fd306cSNickeau        $check = false;
77*04fd306cSNickeau        // for empty string, the below function returns `section`
78*04fd306cSNickeau        return sectionID($fragment, $check);
79*04fd306cSNickeau    }
80*04fd306cSNickeau
81*04fd306cSNickeau    public static function createFromEnterHeadingCall(Call $enterHeadingCall): OutlineSection
82*04fd306cSNickeau    {
83*04fd306cSNickeau        return new OutlineSection($enterHeadingCall);
84*04fd306cSNickeau    }
85*04fd306cSNickeau
86*04fd306cSNickeau    public function getFirstChild(): OutlineSection
87*04fd306cSNickeau    {
88*04fd306cSNickeau
89*04fd306cSNickeau        /** @noinspection PhpIncompatibleReturnTypeInspection */
90*04fd306cSNickeau        return parent::getFirstChild();
91*04fd306cSNickeau
92*04fd306cSNickeau    }
93*04fd306cSNickeau
94*04fd306cSNickeau
95*04fd306cSNickeau    public function addContentCall(Call $actualCall): OutlineSection
96*04fd306cSNickeau    {
97*04fd306cSNickeau
98*04fd306cSNickeau        $this->contentCalls[] = $actualCall;
99*04fd306cSNickeau        return $this;
100*04fd306cSNickeau
101*04fd306cSNickeau
102*04fd306cSNickeau    }
103*04fd306cSNickeau
104*04fd306cSNickeau    public function addHeaderCall(Call $actualCall): OutlineSection
105*04fd306cSNickeau    {
106*04fd306cSNickeau
107*04fd306cSNickeau        $this->headingCalls[] = $actualCall;
108*04fd306cSNickeau        return $this;
109*04fd306cSNickeau    }
110*04fd306cSNickeau
111*04fd306cSNickeau    public function getLabel(): string
112*04fd306cSNickeau    {
113*04fd306cSNickeau        $label = "";
114*04fd306cSNickeau        foreach ($this->headingCalls as $call) {
115*04fd306cSNickeau            if ($call->isTextCall()) {
116*04fd306cSNickeau                // Building the text for the toc
117*04fd306cSNickeau                // only cdata for now
118*04fd306cSNickeau                // no image, ...
119*04fd306cSNickeau                if ($label != "") {
120*04fd306cSNickeau                    $label .= " ";
121*04fd306cSNickeau                }
122*04fd306cSNickeau                $label .= trim($call->getCapturedContent());
123*04fd306cSNickeau            }
124*04fd306cSNickeau        }
125*04fd306cSNickeau        return trim($label);
126*04fd306cSNickeau    }
127*04fd306cSNickeau
128*04fd306cSNickeau    public function setStartPosition(int $startPosition): OutlineSection
129*04fd306cSNickeau    {
130*04fd306cSNickeau        $this->startFileIndex = $startPosition;
131*04fd306cSNickeau        return $this;
132*04fd306cSNickeau    }
133*04fd306cSNickeau
134*04fd306cSNickeau    public function setEndPosition(int $endFileIndex): OutlineSection
135*04fd306cSNickeau    {
136*04fd306cSNickeau        $this->endFileIndex = $endFileIndex;
137*04fd306cSNickeau        return $this;
138*04fd306cSNickeau    }
139*04fd306cSNickeau
140*04fd306cSNickeau    /**
141*04fd306cSNickeau     * @return Call[]
142*04fd306cSNickeau     */
143*04fd306cSNickeau    public function getHeadingCalls(): array
144*04fd306cSNickeau    {
145*04fd306cSNickeau        if (
146*04fd306cSNickeau            $this->headingEnterCall !== null &&
147*04fd306cSNickeau            $this->headingEnterCall->isPluginCall() &&
148*04fd306cSNickeau            !$this->headingEnterCall->hasAttribute("id")
149*04fd306cSNickeau        ) {
150*04fd306cSNickeau            $this->headingEnterCall->addAttribute("id", $this->getHeadingId());
151*04fd306cSNickeau        }
152*04fd306cSNickeau        return $this->headingCalls;
153*04fd306cSNickeau    }
154*04fd306cSNickeau
155*04fd306cSNickeau
156*04fd306cSNickeau    public
157*04fd306cSNickeau    function getEnterHeadingCall(): ?Call
158*04fd306cSNickeau    {
159*04fd306cSNickeau        return $this->headingEnterCall;
160*04fd306cSNickeau    }
161*04fd306cSNickeau
162*04fd306cSNickeau
163*04fd306cSNickeau    public
164*04fd306cSNickeau    function getCalls(): array
165*04fd306cSNickeau    {
166*04fd306cSNickeau        return array_merge($this->headingCalls, $this->contentCalls);
167*04fd306cSNickeau    }
168*04fd306cSNickeau
169*04fd306cSNickeau    public
170*04fd306cSNickeau    function getContentCalls(): array
171*04fd306cSNickeau    {
172*04fd306cSNickeau        return $this->contentCalls;
173*04fd306cSNickeau    }
174*04fd306cSNickeau
175*04fd306cSNickeau    /**
176*04fd306cSNickeau     * @return int
177*04fd306cSNickeau     */
178*04fd306cSNickeau    public
179*04fd306cSNickeau    function getLevel(): int
180*04fd306cSNickeau    {
181*04fd306cSNickeau        if ($this->headingEnterCall === null) {
182*04fd306cSNickeau            return 0;
183*04fd306cSNickeau        }
184*04fd306cSNickeau        switch ($this->headingEnterCall->getTagName()) {
185*04fd306cSNickeau            case self::HEADER_DOKUWIKI_CALL:
186*04fd306cSNickeau                $level = $this->headingEnterCall->getInstructionCall()[1][1];
187*04fd306cSNickeau                break;
188*04fd306cSNickeau            default:
189*04fd306cSNickeau                $level = $this->headingEnterCall->getAttribute(HeadingTag::LEVEL);
190*04fd306cSNickeau                break;
191*04fd306cSNickeau        }
192*04fd306cSNickeau
193*04fd306cSNickeau        try {
194*04fd306cSNickeau            return DataType::toInteger($level);
195*04fd306cSNickeau        } catch (ExceptionBadArgument $e) {
196*04fd306cSNickeau            // should not happen
197*04fd306cSNickeau            LogUtility::internalError("The level ($level) could not be cast to an integer", self::CANONICAL);
198*04fd306cSNickeau            return 0;
199*04fd306cSNickeau        }
200*04fd306cSNickeau    }
201*04fd306cSNickeau
202*04fd306cSNickeau    public
203*04fd306cSNickeau    function getStartPosition(): int
204*04fd306cSNickeau    {
205*04fd306cSNickeau        return $this->startFileIndex;
206*04fd306cSNickeau    }
207*04fd306cSNickeau
208*04fd306cSNickeau    public
209*04fd306cSNickeau    function getEndPosition(): ?int
210*04fd306cSNickeau    {
211*04fd306cSNickeau        return $this->endFileIndex;
212*04fd306cSNickeau    }
213*04fd306cSNickeau
214*04fd306cSNickeau    public
215*04fd306cSNickeau    function hasContentCall(): bool
216*04fd306cSNickeau    {
217*04fd306cSNickeau        return sizeof($this->contentCalls) > 0;
218*04fd306cSNickeau    }
219*04fd306cSNickeau
220*04fd306cSNickeau    /**
221*04fd306cSNickeau     */
222*04fd306cSNickeau    public
223*04fd306cSNickeau    function getHeadingId()
224*04fd306cSNickeau    {
225*04fd306cSNickeau
226*04fd306cSNickeau        if (!isset($this->headingId)) {
227*04fd306cSNickeau            $id = $this->headingEnterCall->getAttribute("id");
228*04fd306cSNickeau            if ($id !== null) {
229*04fd306cSNickeau                return $id;
230*04fd306cSNickeau            }
231*04fd306cSNickeau            $label = $this->getLabel();
232*04fd306cSNickeau            $this->headingId = sectionID($label, $this->tocUniqueId);
233*04fd306cSNickeau        }
234*04fd306cSNickeau        return $this->headingId;
235*04fd306cSNickeau
236*04fd306cSNickeau    }
237*04fd306cSNickeau
238*04fd306cSNickeau    /**
239*04fd306cSNickeau     * A HTML section should have a heading
240*04fd306cSNickeau     * but in a markup document, we may have data before the first
241*04fd306cSNickeau     * heading making a section without heading
242*04fd306cSNickeau     * @return bool
243*04fd306cSNickeau     */
244*04fd306cSNickeau    public
245*04fd306cSNickeau    function hasHeading(): bool
246*04fd306cSNickeau    {
247*04fd306cSNickeau        return $this->headingEnterCall !== null;
248*04fd306cSNickeau    }
249*04fd306cSNickeau
250*04fd306cSNickeau    /**
251*04fd306cSNickeau     * @return OutlineSection[]
252*04fd306cSNickeau     */
253*04fd306cSNickeau    public
254*04fd306cSNickeau    function getChildren(): array
255*04fd306cSNickeau    {
256*04fd306cSNickeau        return parent::getChildren();
257*04fd306cSNickeau    }
258*04fd306cSNickeau
259*04fd306cSNickeau    public function setLevel(int $level): OutlineSection
260*04fd306cSNickeau    {
261*04fd306cSNickeau        switch ($this->headingEnterCall->getTagName()) {
262*04fd306cSNickeau            case self::HEADER_DOKUWIKI_CALL:
263*04fd306cSNickeau                $this->headingEnterCall->getInstructionCall()[1][1] = $level;
264*04fd306cSNickeau                break;
265*04fd306cSNickeau            default:
266*04fd306cSNickeau                $this->headingEnterCall->setAttribute(HeadingTag::LEVEL, $level);
267*04fd306cSNickeau                $headingExitCall = $this->headingCalls[count($this->headingCalls) - 1];
268*04fd306cSNickeau                $headingExitCall->setAttribute(HeadingTag::LEVEL, $level);
269*04fd306cSNickeau                break;
270*04fd306cSNickeau        }
271*04fd306cSNickeau
272*04fd306cSNickeau        /**
273*04fd306cSNickeau         * Update the descdenants sections
274*04fd306cSNickeau         * @param OutlineSection $parentSection
275*04fd306cSNickeau         * @return void
276*04fd306cSNickeau         */
277*04fd306cSNickeau        $updateLevel = function (OutlineSection $parentSection) {
278*04fd306cSNickeau            foreach ($parentSection->getChildren() as $child) {
279*04fd306cSNickeau                $child->setLevel($parentSection->getLevel() + 1);
280*04fd306cSNickeau            }
281*04fd306cSNickeau        };
282*04fd306cSNickeau        TreeVisit::visit($this, $updateLevel);
283*04fd306cSNickeau
284*04fd306cSNickeau        return $this;
285*04fd306cSNickeau    }
286*04fd306cSNickeau
287*04fd306cSNickeau
288*04fd306cSNickeau    public function deleteContentCalls(): OutlineSection
289*04fd306cSNickeau    {
290*04fd306cSNickeau        $this->contentCalls = [];
291*04fd306cSNickeau        return $this;
292*04fd306cSNickeau    }
293*04fd306cSNickeau
294*04fd306cSNickeau    public function incrementLineNumber(): OutlineSection
295*04fd306cSNickeau    {
296*04fd306cSNickeau        $this->lineNumber++;
297*04fd306cSNickeau        return $this;
298*04fd306cSNickeau    }
299*04fd306cSNickeau
300*04fd306cSNickeau    public function getLineCount(): int
301*04fd306cSNickeau    {
302*04fd306cSNickeau        return $this->lineNumber;
303*04fd306cSNickeau    }
304*04fd306cSNickeau
305*04fd306cSNickeau
306*04fd306cSNickeau}
307