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