xref: /dokuwiki/inc/Parsing/Handler/Lists.php (revision 093fe67e98c0cdb4b73fd46938e49b64971483c2)
1<?php
2
3namespace dokuwiki\Parsing\Handler;
4
5class Lists extends AbstractRewriter
6{
7    protected $listCalls = [];
8    protected $listStack = [];
9
10    protected $initialDepth = 0;
11
12    public const NODE = 1;
13
14    /** @inheritdoc */
15    public function finalise()
16    {
17        $last_call = end($this->calls);
18        $this->writeCall(['list_close', [], $last_call[2]]);
19
20        $this->process();
21        $this->callWriter->finalise();
22        unset($this->callWriter);
23    }
24
25    /** @inheritdoc */
26    public function process()
27    {
28
29        foreach ($this->calls as $call) {
30            match ($call[0]) {
31                'list_item' => $this->listOpen($call),
32                'list_open' => $this->listStart($call),
33                'list_close' => $this->listEnd($call),
34                default => $this->listContent($call),
35            };
36        }
37
38        $this->callWriter->writeCalls($this->listCalls);
39        return $this->callWriter;
40    }
41
42    protected function listStart($call)
43    {
44        $depth = $this->interpretSyntax($call[1][0], $listType);
45
46        $this->initialDepth = $depth;
47        //                   array(list type, current depth, index of current listitem_open)
48        $this->listStack[] = [$listType, $depth, 1];
49
50        $this->listCalls[] = ['list' . $listType . '_open', [], $call[2]];
51        $this->listCalls[] = ['listitem_open', [1], $call[2]];
52        $this->listCalls[] = ['listcontent_open', [], $call[2]];
53    }
54
55
56    protected function listEnd($call)
57    {
58        $closeContent = true;
59
60        while ($list = array_pop($this->listStack)) {
61            if ($closeContent) {
62                $this->listCalls[] = ['listcontent_close', [], $call[2]];
63                $closeContent = false;
64            }
65            $this->listCalls[] = ['listitem_close', [], $call[2]];
66            $this->listCalls[] = ['list' . $list[0] . '_close', [], $call[2]];
67        }
68    }
69
70    protected function listOpen($call)
71    {
72        $depth = $this->interpretSyntax($call[1][0], $listType);
73        $end = end($this->listStack);
74        $key = key($this->listStack);
75
76        // Not allowed to be shallower than initialDepth
77        if ($depth < $this->initialDepth) {
78            $depth = $this->initialDepth;
79        }
80
81        if ($depth == $end[1]) {
82            // Just another item in the list...
83            if ($listType == $end[0]) {
84                $this->listCalls[] = ['listcontent_close', [], $call[2]];
85                $this->listCalls[] = ['listitem_close', [], $call[2]];
86                $this->listCalls[] = ['listitem_open', [$depth - 1], $call[2]];
87                $this->listCalls[] = ['listcontent_open', [], $call[2]];
88
89                // new list item, update list stack's index into current listitem_open
90                $this->listStack[$key][2] = count($this->listCalls) - 2;
91
92                // Switched list type...
93            } else {
94                $this->listCalls[] = ['listcontent_close', [], $call[2]];
95                $this->listCalls[] = ['listitem_close', [], $call[2]];
96                $this->listCalls[] = ['list' . $end[0] . '_close', [], $call[2]];
97                $this->listCalls[] = ['list' . $listType . '_open', [], $call[2]];
98                $this->listCalls[] = ['listitem_open', [$depth - 1], $call[2]];
99                $this->listCalls[] = ['listcontent_open', [], $call[2]];
100
101                array_pop($this->listStack);
102                $this->listStack[] = [$listType, $depth, count($this->listCalls) - 2];
103            }
104        } elseif ($depth > $end[1]) { // Getting deeper...
105            $this->listCalls[] = ['listcontent_close', [], $call[2]];
106            $this->listCalls[] = ['list' . $listType . '_open', [], $call[2]];
107            $this->listCalls[] = ['listitem_open', [$depth - 1], $call[2]];
108            $this->listCalls[] = ['listcontent_open', [], $call[2]];
109
110            // set the node/leaf state of this item's parent listitem_open to NODE
111            $this->listCalls[$this->listStack[$key][2]][1][1] = self::NODE;
112
113            $this->listStack[] = [$listType, $depth, count($this->listCalls) - 2];
114        } else { // Getting shallower ( $depth < $end[1] )
115            $this->listCalls[] = ['listcontent_close', [], $call[2]];
116            $this->listCalls[] = ['listitem_close', [], $call[2]];
117            $this->listCalls[] = ['list' . $end[0] . '_close', [], $call[2]];
118
119            // Throw away the end - done
120            array_pop($this->listStack);
121
122            while (1) {
123                $end = end($this->listStack);
124                $key = key($this->listStack);
125
126                if ($end[1] <= $depth) {
127                    // Normalize depths
128                    $depth = $end[1];
129
130                    $this->listCalls[] = ['listitem_close', [], $call[2]];
131
132                    if ($end[0] == $listType) {
133                        $this->listCalls[] = ['listitem_open', [$depth - 1], $call[2]];
134                        $this->listCalls[] = ['listcontent_open', [], $call[2]];
135
136                        // new list item, update list stack's index into current listitem_open
137                        $this->listStack[$key][2] = count($this->listCalls) - 2;
138                    } else {
139                        // Switching list type...
140                        $this->listCalls[] = ['list' . $end[0] . '_close', [], $call[2]];
141                        $this->listCalls[] = ['list' . $listType . '_open', [], $call[2]];
142                        $this->listCalls[] = ['listitem_open', [$depth - 1], $call[2]];
143                        $this->listCalls[] = ['listcontent_open', [], $call[2]];
144
145                        array_pop($this->listStack);
146                        $this->listStack[] = [$listType, $depth, count($this->listCalls) - 2];
147                    }
148
149                    break;
150
151                    // Haven't dropped down far enough yet.... ( $end[1] > $depth )
152                } else {
153                    $this->listCalls[] = ['listitem_close', [], $call[2]];
154                    $this->listCalls[] = ['list' . $end[0] . '_close', [], $call[2]];
155
156                    array_pop($this->listStack);
157                }
158            }
159        }
160    }
161
162    protected function listContent($call)
163    {
164        $this->listCalls[] = $call;
165    }
166
167    protected function interpretSyntax($match, &$type)
168    {
169        if (str_ends_with($match, '*')) {
170            $type = 'u';
171        } else {
172            $type = 'o';
173        }
174        // Is the +1 needed? It used to be count(explode(...))
175        // but I don't think the number is seen outside this handler
176        return substr_count(str_replace("\t", '  ', $match), '  ') + 1;
177    }
178}
179