1<?php
2
3/**
4 * Plugin QnA: Block syntax
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Mykola Ostrovskyy <dwpforge@gmail.com>
8 */
9
10class syntax_plugin_qna_toc extends DokuWiki_Syntax_Plugin {
11
12    private $mode;
13
14    /**
15     * Construct
16     */
17    public function __construct() {
18        $this->mode = substr(get_class($this), 7);
19    }
20
21    /**
22     * What kind of syntax are we?
23     */
24    public function getType() {
25        return 'container';
26    }
27
28    public function getPType() {
29        return 'block';
30    }
31
32    /**
33     * Where to sort in?
34     */
35    public function getSort() {
36        return 55;
37    }
38
39    /**
40     *
41     */
42    public function connectTo($mode) {
43        $this->Lexer->addSpecialPattern('~~(?:QNA|FAQ)(?:\s[^\n]*?)?~~', $mode, $this->mode);
44    }
45
46    /**
47     *
48     */
49    public function handle($match, $state, $pos, Doku_Handler $handler) {
50        global $ID;
51
52        if ($state == DOKU_LEXER_SPECIAL) {
53            preg_match('/~~(?:QNA|FAQ)(.*?)~~/', $match, $match);
54            $data = preg_split('/\s+/', $match[1], -1, PREG_SPLIT_NO_EMPTY);
55            $resolver = new dokuwiki\File\PageResolver($ID);
56
57            foreach ($data as &$pageId) {
58                $pageId = $resolver->resolveId($pageId);
59            }
60        }
61        else {
62            $data = false;
63        }
64
65        return $data;
66    }
67
68    /**
69     *
70     */
71    public function render($mode, Doku_Renderer $renderer, $data) {
72        global $ID;
73
74        if ($mode == 'xhtml') {
75            if (empty($data)) {
76                /* If no page is specified, render the questions from the current one */
77                $data[0] = $ID;
78            }
79
80            $toc = $this->buildToc($data);
81
82            if (!empty($toc)) {
83                $this->compressToc($toc);
84                $this->normalizeToc($toc);
85                $this->renderToc($renderer, $toc);
86            }
87
88            return true;
89        }
90        elseif ($mode == 'metadata') {
91            if (!empty($data)) {
92                $this->addCacheDependencies($renderer, $data);
93            }
94
95            return true;
96        }
97
98        return false;
99    }
100
101    /**
102     * Assemble questions from all pages into a single TOC
103     */
104    private function buildToc($pageId) {
105        $toc = array();
106
107        foreach ($pageId as $id) {
108            if (auth_quickaclcheck($id) < AUTH_READ) {
109                continue;
110            }
111
112            $pageToc = p_get_metadata($id, 'description tableofquestions');
113
114            if (!empty($pageToc)) {
115                foreach ($pageToc as $item) {
116                    $item['link'] = $id . '#' . $item['id'];
117                    $toc[] = $item;
118                }
119            }
120        }
121
122        return $toc;
123    }
124
125    /**
126     * Remove not used list levels
127     */
128    private function compressToc(&$toc) {
129        $maxLevel = 0;
130
131        foreach ($toc as $item) {
132            if ($maxLevel < $item['level']) {
133                $maxLevel = $item['level'];
134            }
135        }
136
137        /* Build list level usage histogram */
138        $level = array_fill(1, $maxLevel, 0);
139
140        foreach ($toc as $item) {
141            $level[$item['level']]++;
142        }
143
144        /* Determine how many unused list levels have to be skipped for each used one */
145        $skipCount = 0;
146
147        for ($l = 1; $l <= $maxLevel; $l++) {
148            if ($level[$l] == 0) {
149                $skipCount++;
150            }
151            else {
152                $level[$l] = $skipCount;
153            }
154        }
155
156        /* Remove unused list levels */
157        foreach ($toc as &$item) {
158            $item['level'] -= $level[$item['level']];
159        }
160    }
161
162    /**
163     * Make sure that list starts with a first level item
164     */
165    private function normalizeToc(&$toc) {
166        $offset = 9;
167
168        for ($i = 0; $toc[$i]['level'] > 1; $i++) {
169            if (($toc[$i]['level'] - $offset) < 1) {
170                $offset = $toc[$i]['level'] - 1;
171            }
172
173            $toc[$i]['level'] -= $offset;
174        }
175    }
176
177    /**
178     *
179     */
180    private function renderToc($renderer, $toc) {
181        $renderer->doc .= '<div class="qna-toc">' . DOKU_LF;
182        $this->renderList($renderer, $toc, 0);
183        $renderer->doc .= '</div>' . DOKU_LF;
184    }
185
186    /**
187     *
188     */
189    private function renderList($renderer, $toc, $index) {
190        $items = count( $toc );
191        $level = $toc[$index]['level'];
192
193        $renderer->listu_open();
194
195        for ($i = $index; ($i < $items) && ($toc[$i]['level'] == $level); $i++) {
196            $renderer->listitem_open($level);
197            $renderer->listcontent_open();
198            $renderer->doc .= '<span class="qna-toc-' . $toc[$i]['class'] . '">';
199            $renderer->internallink($toc[$i]['link'], $toc[$i]['title']);
200            $renderer->doc .= '</span>';
201            $renderer->listcontent_close();
202
203            if ((($i + 1) < $items) && ($toc[$i + 1]['level'] > $level)) {
204                $i = $this->renderList($renderer, $toc, $i + 1);
205            }
206
207            $renderer->listitem_close();
208        }
209
210        $renderer->listu_close();
211
212        return $i - 1;
213    }
214
215    /**
216     *
217     */
218    private function addCacheDependencies($renderer, $pageId) {
219        foreach ($pageId as $id) {
220            $metafile = metaFN($id, '.meta');
221
222            $renderer->meta['relation']['depends']['rendering'][$metafile] = true;
223        }
224    }
225}
226