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