xref: /plugin/combo/renderer/analytics.php (revision 71f916b90657b0ec64f620dd9cb434dd66a4bb62)
1<?php
2
3
4use ComboStrap\Analytics;
5use ComboStrap\LinkUtility;
6use ComboStrap\LogUtility;
7use ComboStrap\LowQualityPage;
8use ComboStrap\Sqlite;
9use ComboStrap\Text;
10use ComboStrap\Page;
11use dokuwiki\ChangeLog\PageChangeLog;
12
13require_once(__DIR__ . '/../class/Text.php');
14require_once(__DIR__ . '/../class/LowQualityPage.php');
15require_once(__DIR__ . '/../class/Analytics.php');
16
17
18/**
19 * A analysis Renderer that exports stats/quality/metadata in a json format
20 * You can export the data with
21 * doku.php?id=somepage&do=export_combo_analytics
22 */
23class renderer_plugin_combo_analytics extends Doku_Renderer
24{
25    const DATE_CREATED = 'date_created';
26    const PLAINTEXT = 'formatted';
27    const RESULT = "result";
28    const DESCRIPTION = "description";
29    const PASSED = "Passed";
30    const FAILED = "Failed";
31    const FIXME = 'fixme';
32
33    /**
34     * Rules key
35     */
36    const RULE_WORDS_MINIMAL = 'words_min';
37    const RULE_OUTLINE_STRUCTURE = "outline_structure";
38    const RULE_INTERNAL_BACKLINKS_MIN = 'internal_backlinks_min';
39    const RULE_WORDS_MAXIMAL = "words_max";
40    const RULE_AVERAGE_WORDS_BY_SECTION_MIN = 'words_by_section_avg_min';
41    const RULE_AVERAGE_WORDS_BY_SECTION_MAX = 'words_by_section_avg_max';
42    const RULE_INTERNAL_LINKS_MIN = 'internal_links_min';
43    const RULE_INTERNAL_BROKEN_LINKS_MAX = 'internal_links_broken_max';
44    const RULE_DESCRIPTION_PRESENT = 'description_present';
45    const RULE_FIXME = "fixme_min";
46    const RULE_TITLE_PRESENT = "title_present";
47    const RULE_CANONICAL_PRESENT = "canonical_present";
48
49    /**
50     * The default man
51     */
52    const CONF_MANDATORY_QUALITY_RULES_DEFAULT_VALUE = [
53        self::RULE_WORDS_MINIMAL,
54        self::RULE_INTERNAL_BACKLINKS_MIN,
55        self::RULE_INTERNAL_LINKS_MIN
56    ];
57    const CONF_MANDATORY_QUALITY_RULES = "mandatoryQualityRules";
58
59    /**
60     * Quality Score factors
61     * They are used to calculate the score
62     */
63    const CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR = 'qualityScoreInternalBacklinksFactor';
64    const CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR = 'qualityScoreInternalLinksFactor';
65    const CONF_QUALITY_SCORE_TITLE_PRESENT = 'qualityScoreTitlePresent';
66    const CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE = 'qualityScoreCorrectOutline';
67    const CONF_QUALITY_SCORE_CORRECT_CONTENT = 'qualityScoreCorrectContentLength';
68    const CONF_QUALITY_SCORE_NO_FIXME = 'qualityScoreNoFixMe';
69    const CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE = 'qualityScoreCorrectWordSectionAvg';
70    const CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR = 'qualityScoreNoBrokenLinks';
71    const CONF_QUALITY_SCORE_CHANGES_FACTOR = 'qualityScoreChangesFactor';
72    const CONF_QUALITY_SCORE_DESCRIPTION_PRESENT = 'qualityScoreDescriptionPresent';
73    const CONF_QUALITY_SCORE_CANONICAL_PRESENT = 'qualityScoreCanonicalPresent';
74
75
76    /**
77     * The processing data
78     * that should be {@link  renderer_plugin_combo_analysis::reset()}
79     */
80    public $stats = array(); // the stats
81    protected $metadata = array(); // the metadata
82    protected $headerId = 0; // the id of the header on the page (first, second, ...)
83
84    /**
85     * Don't known this variable ?
86     */
87    protected $quotelevel = 0;
88    protected $formattingBracket = 0;
89    protected $tableopen = false;
90    private $plainTextId = 0;
91
92
93    /**
94     * Here the score is calculated
95     */
96    public function document_end() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
97    {
98        /**
99         * The metadata
100         */
101        global $ID;
102        $meta = p_get_metadata($ID);
103
104        /**
105         * Word and chars count
106         * The word count does not take into account
107         * words with non-words characters such as < =
108         * Therefore the node and attribute are not taken in the count
109         */
110        $text = rawWiki($ID);
111        $this->stats[Analytics::CHARS_COUNT] = strlen($text);
112        $this->stats[Analytics::WORDS_COUNT] = Text::getWordCount($text);
113
114        /**
115         * The exported object
116         */
117        $statExport = $this->stats;
118
119
120        /**
121         * Internal link distance summary calculation
122         */
123        if (array_key_exists(Analytics::INTERNAL_LINK_DISTANCE, $statExport)) {
124            $linkLengths = $statExport[Analytics::INTERNAL_LINK_DISTANCE];
125            unset($statExport[Analytics::INTERNAL_LINK_DISTANCE]);
126            $countBacklinks = count($linkLengths);
127            $statExport[Analytics::INTERNAL_LINK_DISTANCE]['avg'] = null;
128            $statExport[Analytics::INTERNAL_LINK_DISTANCE]['max'] = null;
129            $statExport[Analytics::INTERNAL_LINK_DISTANCE]['min'] = null;
130            if ($countBacklinks > 0) {
131                $statExport[Analytics::INTERNAL_LINK_DISTANCE]['avg'] = array_sum($linkLengths) / $countBacklinks;
132                $statExport[Analytics::INTERNAL_LINK_DISTANCE]['max'] = max($linkLengths);
133                $statExport[Analytics::INTERNAL_LINK_DISTANCE]['min'] = min($linkLengths);
134            }
135        }
136
137        /**
138         * Quality Report / Rules
139         */
140        // The array that hold the results of the quality rules
141        $ruleResults = array();
142        // The array that hold the quality score details
143        $qualityScores = array();
144
145
146        /**
147         * No fixme
148         */
149        $fixmeCount = $this->stats[self::FIXME];
150        $statExport[self::FIXME] = $fixmeCount == null ? 0 : $fixmeCount;
151        if ($fixmeCount != 0) {
152            $ruleResults[self::RULE_FIXME] = self::FAILED;
153            $qualityScores['no_' . self::FIXME] = 0;
154        } else {
155            $ruleResults[self::RULE_FIXME] = self::PASSED;
156            $qualityScores['no_' . self::FIXME] = $this->getConf(self::CONF_QUALITY_SCORE_NO_FIXME, 1);;
157        }
158
159        /**
160         * A title should be present
161         */
162        if (empty($this->metadata[Analytics::TITLE])) {
163            $ruleResults[self::RULE_TITLE_PRESENT] = self::FAILED;
164            $ruleInfo[self::RULE_TITLE_PRESENT] = "A title is not present in the frontmatter";
165            $this->metadata[Analytics::TITLE] = $meta[Analytics::TITLE];
166            $qualityScores[self::RULE_TITLE_PRESENT] = 0;
167        } else {
168            $qualityScores[self::RULE_TITLE_PRESENT] = $this->getConf(self::CONF_QUALITY_SCORE_TITLE_PRESENT, 10);;
169            $ruleResults[self::RULE_TITLE_PRESENT] = self::PASSED;
170        }
171
172        /**
173         * A description should be present
174         */
175        if (empty($this->metadata[self::DESCRIPTION])) {
176            $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::FAILED;
177            $ruleInfo[self::RULE_CANONICAL_PRESENT] = "A description is not present in the frontmatter";
178            $this->metadata[self::DESCRIPTION] = $meta[self::DESCRIPTION]["abstract"];
179            $qualityScores[self::RULE_DESCRIPTION_PRESENT] = 0;
180        } else {
181            $qualityScores[self::RULE_DESCRIPTION_PRESENT] = $this->getConf(self::CONF_QUALITY_SCORE_DESCRIPTION_PRESENT, 8);;
182            $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::PASSED;
183        }
184
185        /**
186         * A canonical should be present
187         */
188        if (empty($this->metadata[Page::CANONICAL_PROPERTY])) {
189            $qualityScores[self::RULE_CANONICAL_PRESENT] = 0;
190            $ruleResults[self::RULE_CANONICAL_PRESENT] = self::FAILED;
191            $ruleInfo[self::RULE_CANONICAL_PRESENT] = "A canonical is not present in the frontmatter";
192        } else {
193            $qualityScores[self::RULE_CANONICAL_PRESENT] = $this->getConf(self::CONF_QUALITY_SCORE_CANONICAL_PRESENT, 5);;
194            $ruleResults[self::RULE_CANONICAL_PRESENT] = self::PASSED;
195        }
196
197        /**
198         * Outline / Header structure
199         */
200        $treeError = 0;
201        $headersCount = 0;
202        if (array_key_exists(Analytics::HEADER_POSITION, $this->stats)) {
203            $headersCount = count($this->stats[Analytics::HEADER_POSITION]);
204            unset($statExport[Analytics::HEADER_POSITION]);
205            for ($i = 1; $i < $headersCount; $i++) {
206                $currentHeaderLevel = $this->stats['header_struct'][$i];
207                $previousHeaderLevel = $this->stats['header_struct'][$i - 1];
208                if ($currentHeaderLevel - $previousHeaderLevel > 1) {
209                    $treeError += 1;
210                    $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "The " . $i . " header (h" . $currentHeaderLevel . ") has a level bigger than its precedent (" . $previousHeaderLevel . ")";
211                }
212            }
213        }
214        if ($treeError > 0 || $headersCount == 0) {
215            $qualityScores['correct_outline'] = 0;
216            $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::FAILED;
217            if ($headersCount==0){
218                $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "There is no header";
219            }
220        } else {
221            $qualityScores['correct_outline'] = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE, 3);
222            $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::PASSED;
223        }
224
225
226        /**
227         * Document length
228         */
229        $minimalWordCount = 50;
230        $maximalWordCount = 1500;
231        $correctContentLength = true;
232        if ($this->stats[Analytics::WORDS_COUNT] < $minimalWordCount) {
233            $ruleResults[self::RULE_WORDS_MINIMAL] = self::FAILED;
234            $correctContentLength = false;
235            $ruleInfo[self::RULE_WORDS_MINIMAL] = "The number of words is less than {$minimalWordCount}";
236        } else {
237            $ruleResults[self::RULE_WORDS_MINIMAL] = self::PASSED;
238        }
239        if ($this->stats[Analytics::WORDS_COUNT] > $maximalWordCount) {
240            $ruleResults[self::RULE_WORDS_MAXIMAL] = self::FAILED;
241            $ruleInfo[self::RULE_WORDS_MAXIMAL] = "The number of words is more than {$maximalWordCount}";
242            $correctContentLength = false;
243        } else {
244            $ruleResults[self::RULE_WORDS_MAXIMAL] = self::PASSED;
245        }
246        if ($correctContentLength) {
247            $qualityScores['correct_content_length'] = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_CONTENT, 10);
248        } else {
249            $qualityScores['correct_content_length'] = 0;
250        }
251
252
253        /**
254         * Average Number of words by header section to text ratio
255         */
256        $headers = $this->stats[Analytics::HEADERS_COUNT];
257        if ($headers != null) {
258            $headerCount = array_sum($headers);
259            $headerCount--; // h1 is supposed to have no words
260            if ($headerCount > 0) {
261
262                $avgWordsCountBySection = round($this->stats[Analytics::WORDS_COUNT] / $headerCount);
263                $statExport['word_section_count']['avg'] = $avgWordsCountBySection;
264
265                /**
266                 * Min words by header section
267                 */
268                $wordsByHeaderMin = 20;
269                /**
270                 * Max words by header section
271                 */
272                $wordsByHeaderMax = 300;
273                $correctAverageWordsBySection = true;
274                if ($avgWordsCountBySection < $wordsByHeaderMin) {
275                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::FAILED;
276                    $correctAverageWordsBySection = false;
277                    $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = "The number of words by section is less than {$wordsByHeaderMin}";
278                } else {
279                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::PASSED;
280                }
281                if ($avgWordsCountBySection > $wordsByHeaderMax) {
282                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::FAILED;
283                    $correctAverageWordsBySection = false;
284                    $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = "The number of words by section is more than {$wordsByHeaderMax}";
285                } else {
286                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::PASSED;
287                }
288                if ($correctAverageWordsBySection) {
289                    $qualityScores['correct_word_avg_by_section'] = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE, 10);
290                } else {
291                    $qualityScores['correct_word_avg_by_section'] = 0;
292                }
293
294            }
295        }
296
297        /**
298         * Internal Backlinks rule
299         *
300         * If a page is a low quality page, if the process run
301         * anonymous, we will not see all {@link ft_backlinks()}
302         * we use then the index directly to avoid confusion
303         */
304        $backlinks = idx_get_indexer()->lookupKey('relation_references', $ID);
305        $countBacklinks = count($backlinks);
306        $statExport[Analytics::INTERNAL_BACKLINKS_COUNT] = $countBacklinks;
307        if ($countBacklinks == 0) {
308            $qualityScores[Analytics::INTERNAL_BACKLINKS_COUNT] = 0;
309            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::FAILED;
310            $ruleInfo[self::RULE_INTERNAL_BACKLINKS_MIN] = "There is no backlinks";
311        } else {
312            $qualityScores[Analytics::INTERNAL_BACKLINKS_COUNT] = $countBacklinks * $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR, 1);
313            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::PASSED;
314        }
315
316        /**
317         * Internal links
318         */
319        $internalLinksCount = $this->stats[Analytics::INTERNAL_LINKS_COUNT];
320        if ($internalLinksCount == 0) {
321            $qualityScores[Analytics::INTERNAL_LINKS_COUNT] = 0;
322            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::FAILED;
323            $ruleInfo[self::RULE_INTERNAL_BACKLINKS_MIN] = "There is no internal links";
324        } else {
325            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::PASSED;
326            $qualityScores[Analytics::INTERNAL_LINKS_COUNT] = $countBacklinks * $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR, 1);;
327        }
328
329        /**
330         * Broken Links
331         */
332        $brokenLinksCount = $this->stats[Analytics::INTERNAL_LINKS_BROKEN_COUNT];
333        if ($brokenLinksCount > 2) {
334            $qualityScores['no_' . Analytics::INTERNAL_LINKS_BROKEN_COUNT] = 0;
335            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::FAILED;
336            $ruleInfo[self::RULE_INTERNAL_BACKLINKS_MIN] = "There is {$brokenLinksCount} broken links";
337        } else {
338            $qualityScores['no_' . Analytics::INTERNAL_LINKS_BROKEN_COUNT] = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR, 2);;;
339            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::PASSED;
340        }
341
342        /**
343         * Changes, the more changes the better
344         */
345        $qualityScores[Analytics::EDITS_COUNT] = $this->stats[Analytics::EDITS_COUNT] * $this->getConf(self::CONF_QUALITY_SCORE_CHANGES_FACTOR, 0.25);;;
346
347
348        /**
349         * Rules that comes from the qc plugin
350         * but are not yet fully implemented
351         */
352
353//        // 2 points for lot's of formatting
354//        if ($this->stats[self::PLAINTEXT] && $this->stats['chars'] / $this->stats[self::PLAINTEXT] < 3) {
355//            $ruleResults['manyformat'] = 2;
356//        }
357//
358//        // 1/2 points for deeply nested quotations
359//        if ($this->stats['quote_nest'] > 2) {
360//            $ruleResults['deepquote'] += $this->stats['quote_nest'] / 2;
361//        }
362//
363//        // 1/2 points for too many hr
364//        if ($this->stats['hr'] > 2) {
365//            $ruleResults['manyhr'] = ($this->stats['hr'] - 2) / 2;
366//        }
367//
368//        // 1 point for too many line breaks
369//        if ($this->stats['linebreak'] > 2) {
370//            $ruleResults['manybr'] = $this->stats['linebreak'] - 2;
371//        }
372//
373//        // 1 point for single author only
374//        if (!$this->getConf('single_author_only') && count($this->stats['authors']) == 1) {
375//            $ruleResults['singleauthor'] = 1;
376//        }
377
378        // Too much cdata (plaintext), see cdata
379        // if ($len > 500) $statExport[self::QUALITY][self::ERROR]['plaintext']++;
380        // if ($len > 500) $statExport[self::QUALITY][self::ERROR]['plaintext']++;
381        //
382        // // 1 point for formattings longer than 500 chars
383        // $statExport[self::QUALITY][self::ERROR]['multiformat']
384
385        /**
386         * Quality Score
387         */
388        ksort($qualityScores);
389        $qualityScoring = array();
390        $qualityScoring["score"] = array_sum($qualityScores);
391        $qualityScoring["scores"] = $qualityScores;
392
393
394        /**
395         * The rule that if broken will set the quality level to low
396         */
397        $brokenRules = array();
398        foreach ($ruleResults as $ruleName => $ruleResult) {
399            if ($ruleResult == self::FAILED) {
400                $brokenRules[] = $ruleName;
401            }
402        }
403        $ruleErrorCount = sizeof($brokenRules);
404        if ($ruleErrorCount > 0) {
405            $qualityResult = $ruleErrorCount . " quality rules errors";
406        } else {
407            $qualityResult = "All quality rules passed";
408        }
409
410        /**
411         * Low level
412         */
413        $mandatoryRules = preg_split("/,/", $this->getConf(self::CONF_MANDATORY_QUALITY_RULES));
414        $mandatoryRulesBroken = [];
415        foreach ($mandatoryRules as $lowLevelRule) {
416            if (in_array($lowLevelRule, $brokenRules)) {
417                $mandatoryRulesBroken[] = $lowLevelRule;
418            }
419        }
420        $lowLevel = false;
421        if (sizeof($mandatoryRulesBroken) > 0) {
422            $lowLevel = true;
423        }
424        LowQualityPage::setLowQualityPage($ID, $lowLevel);
425
426        /**
427         * Building the quality object in order
428         */
429        $quality["low"] = $lowLevel;
430        if (sizeof($mandatoryRulesBroken) > 0) {
431            ksort($mandatoryRulesBroken);
432            $quality['failed_mandatory_rules'] = $mandatoryRulesBroken;
433        }
434        $quality["scoring"] = $qualityScoring;
435        $quality["rules"][self::RESULT] = $qualityResult;
436        if (!empty($ruleInfo)) {
437            $quality["rules"]["info"] = $ruleInfo;
438        }
439
440        ksort($ruleResults);
441        $quality["rules"]['details'] = $ruleResults;
442
443        /**
444         * Metadata
445         */
446        $this->metadata[Analytics::TITLE] = $meta['title'];
447        $timestampCreation = $meta['date']['created'];
448        $this->metadata[self::DATE_CREATED] = date('Y-m-d h:i:s', $timestampCreation);
449        $timestampModification = $meta['date']['modified'];
450        $this->metadata[Analytics::DATE_MODIFIED] = date('Y-m-d h:i:s', $timestampModification);
451        $this->metadata['age_creation'] = round((time() - $timestampCreation) / 60 / 60 / 24);
452        $this->metadata['age_modification'] = round((time() - $timestampModification) / 60 / 60 / 24);
453
454
455        // get author info
456        $changelog = new PageChangeLog($ID);
457        $revs = $changelog->getRevisions(0, 10000);
458        array_push($revs, $meta['last_change']['date']);
459        $this->stats[Analytics::EDITS_COUNT] = count($revs);
460        foreach ($revs as $rev) {
461            $info = $changelog->getRevisionInfo($rev);
462            if ($info['user']) {
463                $this->stats['authors'][$info['user']] += 1;
464            } else {
465                $this->stats['authors']['*'] += 1;
466            }
467        }
468
469        /**
470         * Building the Top JSON in order
471         */
472        global $ID;
473        $json = array();
474        $json["id"] = $ID;
475        $json['metadata'] = $this->metadata;
476        ksort($statExport);
477        $json[Analytics::STATISTICS] = $statExport;
478        $json[Analytics::QUALITY] = $quality; // Quality after the sort to get them at the end
479
480
481        /**
482         * The result can be seen with
483         * doku.php?id=somepage&do=export_combo_analysis
484         */
485        /**
486         * Set the header for the export.php file
487         */
488        p_set_metadata($ID, array("format" =>
489            array("combo_" . $this->getPluginComponent() => array("Content-Type" => 'application/json'))
490        ));
491        $json_encoded = json_encode($json, JSON_PRETTY_PRINT);
492
493        $sqlite = Sqlite::getSqlite();
494        if ($sqlite != null) {
495            /**
496             * Sqlite Plugin installed
497             */
498            $canonical = $this->metadata[Page::CANONICAL_PROPERTY];
499            if (empty($canonical)) {
500                $canonical = $ID; // not null constraint unfortunately
501            }
502            $entry = array(
503                'CANONICAL' => $canonical,
504                'ANALYTICS' => $json_encoded,
505                'ID' => $ID
506            );
507            $res = $sqlite->query("SELECT count(*) FROM PAGES where ID = ?", $ID);
508            if ($sqlite->res2single($res) == 1) {
509                // Upset not supported on all version
510                //$upsert = 'insert into PAGES (ID,CANONICAL,ANALYTICS) values (?,?,?) on conflict (ID,CANONICAL) do update set ANALYTICS = EXCLUDED.ANALYTICS';
511                $update = 'update PAGES SET CANONICAL = ?, ANALYTICS = ? where ID=?';
512                $res = $sqlite->query($update, $entry);
513            } else {
514                $res = $sqlite->storeEntry('PAGES', $entry);
515            }
516            if (!$res) {
517                LogUtility::msg("There was a problem during the upsert: {$sqlite->getAdapter()->getDb()->errorInfo()}");
518            }
519            $sqlite->res_close($res);
520        }
521        $this->doc .= $json_encoded;
522
523    }
524
525    /**
526     */
527    public function getFormat()
528    {
529        return Analytics::RENDERER_FORMAT;
530    }
531
532    public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content')
533    {
534
535        LinkUtility::processInternalLinkStats($id, $this->stats);
536
537    }
538
539    public function externallink($url, $name = null)
540    {
541        $this->stats[Analytics::EXTERNAL_LINKS_COUNT]++;
542    }
543
544    public function header($text, $level, $pos)
545    {
546        $this->stats[Analytics::HEADERS_COUNT]['h' . $level]++;
547        $this->headerId++;
548        $this->stats[Analytics::HEADER_POSITION][$this->headerId] = 'h' . $level;
549
550    }
551
552    public function smiley($smiley)
553    {
554        if ($smiley == 'FIXME') $this->stats[self::FIXME]++;
555    }
556
557    public function linebreak()
558    {
559        if (!$this->tableopen) {
560            $this->stats['linebreak']++;
561        }
562    }
563
564    public function table_open($maxcols = null, $numrows = null, $pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
565    {
566        $this->tableopen = true;
567    }
568
569    public function table_close($pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
570    {
571        $this->tableopen = false;
572    }
573
574    public function hr()
575    {
576        $this->stats['hr']++;
577    }
578
579    public function quote_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
580    {
581        $this->stats['quote_count']++;
582        $this->quotelevel++;
583        $this->stats['quote_nest'] = max($this->quotelevel, $this->stats['quote_nest']);
584    }
585
586    public function quote_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
587    {
588        $this->quotelevel--;
589    }
590
591    public function strong_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
592    {
593        $this->formattingBracket++;
594    }
595
596    public function strong_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
597    {
598        $this->formattingBracket--;
599    }
600
601    public function emphasis_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
602    {
603        $this->formattingBracket++;
604    }
605
606    public function emphasis_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
607    {
608        $this->formattingBracket--;
609    }
610
611    public function underline_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
612    {
613        $this->formattingBracket++;
614    }
615
616    public function underline_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
617    {
618        $this->formattingBracket--;
619    }
620
621    public function cdata($text)
622    {
623
624        /**
625         * It seems that you receive cdata
626         * when emphasis_open / underline_open / strong_open
627         * Stats are not for them
628         */
629        if (!$this->formattingBracket) return;
630
631        $this->plainTextId++;
632
633        /**
634         * Length
635         */
636        $len = strlen($text);
637        $this->stats[self::PLAINTEXT][$this->plainTextId]['len'] = $len;
638
639
640        /**
641         * Multi-formatting
642         */
643        if ($this->formattingBracket > 1) {
644            $numberOfFormats = 1 * ($this->formattingBracket - 1);
645            $this->stats[self::PLAINTEXT][$this->plainTextId]['multiformat'] += $numberOfFormats;
646        }
647
648        /**
649         * Total
650         */
651        $this->stats[self::PLAINTEXT][0] += $len;
652    }
653
654    public function internalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
655    {
656        $this->stats[Analytics::INTERNAL_MEDIAS_COUNT]++;
657    }
658
659    public function externalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
660    {
661        $this->stats[Analytics::EXTERNAL_MEDIAS]++;
662    }
663
664    public function reset()
665    {
666        $this->stats = array();
667        $this->metadata = array();
668        $this->headerId = 0;
669    }
670
671    public function setMeta($key, $value)
672    {
673        $this->metadata[$key] = $value;
674    }
675
676
677}
678
679