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