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