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