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