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