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