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