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