1<?php
2
3// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
4
5use dokuwiki\ChangeLog\PageChangeLog;
6use dokuwiki\File\PageResolver;
7use dokuwiki\Utf8\PhpString;
8
9/**
10 * The Renderer
11 */
12class renderer_plugin_qc extends Doku_Renderer
13{
14    /**
15     * We store all our data in an array
16     */
17    public $docArray = [
18        // raw statistics
19        'header_count' => [0, 0, 0, 0, 0, 0],
20        'header_struct' => [],
21        'linebreak' => 0,
22        'quote_nest' => 0,
23        'quote_count' => 0,
24        'fixme' => 0,
25        'hr' => 0,
26        'formatted' => 0,
27        'created' => 0,
28        'modified' => 0,
29        'changes' => 0,
30        'authors' => [],
31        'internal_links' => 0,
32        'broken_links' => 0,
33        'external_links' => 0,
34        'link_lengths' => [],
35        'chars' => 0,
36        'words' => 0,
37        'score' => 0,
38        // calculated error scores
39        'err' => [
40            'fixme' => 0,
41            'noh1' => 0,
42            'manyh1' => 0,
43            'headernest' => 0,
44            'manyhr' => 0,
45            'manybr' => 0,
46            'longformat' => 0,
47            'multiformat' => 0
48        ],
49    ];
50
51    protected $quotelevel = 0;
52    protected $formatting = 0;
53    protected $tableopen = false;
54
55    /** @inheritdoc */
56    public function document_start()
57    {
58        global $ID;
59        $meta = p_get_metadata($ID);
60
61        // get some dates from meta data
62        $this->docArray['created'] = $meta['date']['created'];
63        $this->docArray['modified'] = $meta['date']['modified'];
64        $this->docArray['authors']['*'] = 0;
65
66        // get author info
67        $changelog = new PageChangeLog($ID);
68        $revs = $changelog->getRevisions(0, 10000); //FIXME find a good solution for 'get ALL revisions'
69        $revs[] = $meta['last_change']['date'];
70        $this->docArray['changes'] = count($revs);
71        foreach ($revs as $rev) {
72            $info = $changelog->getRevisionInfo($rev);
73            if ($info && !empty($info['user'])) {
74                $authorUserCnt = empty($this->docArray['authors'][$info['user']])
75                    ? 0
76                    : $this->docArray['authors'][$info['user']];
77                $this->docArray['authors'][$info['user']] = $authorUserCnt + 1;
78            } else {
79                ++$this->docArray['authors']['*'];
80            }
81        }
82
83        // work on raw text
84        $text = rawWiki($ID);
85        $this->docArray['chars'] = PhpString::strlen($text);
86        $this->docArray['words'] = count(array_filter(preg_split('/[^\w\-_]/u', $text)));
87    }
88
89
90    /**
91     * Here the score is calculated
92     * @inheritdoc
93     */
94    public function document_end()
95    {
96        global $ID;
97
98        // 2 points for missing backlinks
99        if (ft_backlinks($ID) === []) {
100            $this->docArray['err']['nobacklink'] += 2;
101        }
102
103        // 1 point for each FIXME
104        $this->docArray['err']['fixme'] += $this->docArray['fixme'];
105
106        // 5 points for missing H1
107        if ($this->docArray['header_count'][1] == 0) {
108            $this->docArray['err']['noh1'] += 5;
109        }
110        // 1 point for each H1 too much
111        if ($this->docArray['header_count'][1] > 1) {
112            $this->docArray['err']['manyh1'] += $this->docArray['header_count'][1];
113        }
114
115        // 1 point for each incorrectly nested headline
116        $cnt = count($this->docArray['header_struct']);
117        for ($i = 1; $i < $cnt; $i++) {
118            if ($this->docArray['header_struct'][$i] - $this->docArray['header_struct'][$i - 1] > 1) {
119                ++$this->docArray['err']['headernest'];
120            }
121        }
122
123        // 1/2 points for deeply nested quotations
124        if ($this->docArray['quote_nest'] > 2) {
125            $this->docArray['err']['deepquote'] = $this->docArray['quote_nest'] / 2;
126        }
127
128        // FIXME points for many quotes?
129
130        // 1/2 points for too many hr
131        if ($this->docArray['hr'] > 2) {
132            $this->docArray['err']['manyhr'] = ($this->docArray['hr'] - 2) / 2;
133        }
134
135        // 1 point for too many line breaks
136        if ($this->docArray['linebreak'] > 2) {
137            $this->docArray['err']['manybr'] = $this->docArray['linebreak'] - 2;
138        }
139
140        // 1 point for single author only
141        if (!$this->getConf('single_author_only') && count($this->docArray['authors']) == 1) {
142            $this->docArray['err']['singleauthor'] = 1;
143        }
144
145        // 1 point for too small document
146        if ($this->docArray['chars'] < 150) {
147            $this->docArray['err']['toosmall'] = 1;
148        }
149
150        // 1 point for too large document
151        if ($this->docArray['chars'] > 100000) {
152            $this->docArray['err']['toolarge'] = 1;
153        }
154
155        // header to text ratio
156        $hc = $this->docArray['header_count'][1] +
157            $this->docArray['header_count'][2] +
158            $this->docArray['header_count'][3] +
159            $this->docArray['header_count'][4] +
160            $this->docArray['header_count'][5];
161        $hc--; //we expect at least 1
162        if ($hc > 0) {
163            $hr = $this->docArray['chars'] / $hc;
164
165            // 1 point for too many headers
166            if ($hr < 200) {
167                $this->docArray['err']['manyheaders'] = 1;
168            }
169
170            // 1 point for too few headers
171            if ($hr > 2000) {
172                $this->docArray['err']['fewheaders'] = 1;
173            }
174        }
175
176        // 1 point when no link at all
177        if (!$this->docArray['internal_links']) {
178            $this->docArray['err']['nolink'] = 1;
179        }
180
181        // 0.5 for broken links when too many
182        if ($this->docArray['broken_links'] > 2) {
183            $this->docArray['err']['brokenlink'] = $this->docArray['broken_links'] * 0.5;
184        }
185
186        // 2 points for lot's of formatting
187        if ($this->docArray['formatted'] && $this->docArray['chars'] / $this->docArray['formatted'] < 3) {
188            $this->docArray['err']['manyformat'] = 2;
189        }
190
191        // add up all scores
192        foreach ($this->docArray['err'] as $val) $this->docArray['score'] += $val;
193
194
195        //we're done here
196        $this->doc = serialize($this->docArray);
197    }
198
199    /** @inheritdoc */
200    public function getFormat()
201    {
202        return 'qc';
203    }
204
205    /** @inheritdoc */
206    public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content')
207    {
208        global $ID;
209
210        $resolver = new PageResolver($ID);
211        $id = $resolver->resolveId($id);
212        $exists = page_exists($id);
213
214        // calculate link width
215        $a = explode(':', getNS($ID));
216        $b = explode(':', getNS($id));
217        while (isset($a[0]) && $a[0] === $b[0]) {
218            array_shift($a);
219            array_shift($b);
220        }
221        $length = count($a) + count($b);
222        $this->docArray['link_lengths'][] = $length;
223
224        $this->docArray['internal_links']++;
225        if (!$exists) $this->docArray['broken_links']++;
226    }
227
228    /** @inheritdoc */
229    public function externallink($url, $name = null)
230    {
231        $this->docArray['external_links']++;
232    }
233
234    /** @inheritdoc */
235    public function header($text, $level, $pos)
236    {
237        $this->docArray['header_count'][$level]++;
238        $this->docArray['header_struct'][] = $level;
239    }
240
241    /** @inheritdoc */
242    public function smiley($smiley)
243    {
244        if ($smiley == 'FIXME') $this->docArray['fixme']++;
245    }
246
247    /** @inheritdoc */
248    public function linebreak()
249    {
250        if (!$this->tableopen) {
251            $this->docArray['linebreak']++;
252        }
253    }
254
255    /** @inheritdoc */
256    public function table_open($maxcols = null, $numrows = null, $pos = null)
257    {
258        $this->tableopen = true;
259    }
260
261    /** @inheritdoc */
262    public function table_close($pos = null)
263    {
264        $this->tableopen = false;
265    }
266
267    /** @inheritdoc */
268    public function hr()
269    {
270        $this->docArray['hr']++;
271    }
272
273    /** @inheritdoc */
274    public function quote_open()
275    {
276        $this->docArray['quote_count']++;
277        $this->quotelevel++;
278        $this->docArray['quote_nest'] = max($this->quotelevel, $this->docArray['quote_nest']);
279    }
280
281    /** @inheritdoc */
282    public function quote_close()
283    {
284        $this->quotelevel--;
285    }
286
287    /** @inheritdoc */
288    public function strong_open()
289    {
290        $this->formatting++;
291    }
292
293    /** @inheritdoc */
294    public function strong_close()
295    {
296        $this->formatting--;
297    }
298
299    /** @inheritdoc */
300    public function emphasis_open()
301    {
302        $this->formatting++;
303    }
304
305    /** @inheritdoc */
306    public function emphasis_close()
307    {
308        $this->formatting--;
309    }
310
311    /** @inheritdoc */
312    public function underline_open()
313    {
314        $this->formatting++;
315    }
316
317    /** @inheritdoc */
318    public function underline_close()
319    {
320        $this->formatting--;
321    }
322
323    /** @inheritdoc */
324    public function cdata($text)
325    {
326        if (!$this->formatting) return;
327
328        $len = PhpString::strlen($text);
329
330        // 1 point for formattings longer than 500 chars
331        if ($len > 500) $this->docArray['err']['longformat']++;
332
333        // 1 point for each multiformatting
334        if ($this->formatting > 1) $this->docArray['err']['multiformat'] += 1 * ($this->formatting - 1);
335
336        $this->docArray['formatted'] += $len;
337    }
338}
339