xref: /plugin/combo/renderer/analytics.php (revision d262537c0e3e167c29541034d59d4a6edf2ffecf)
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        $backlinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR, 1);
351        if ($countBacklinks == 0) {
352            $qualityScores[Analytics::INTERNAL_BACKLINKS_COUNT] = 0;
353            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::FAILED;
354            $ruleInfo[self::RULE_INTERNAL_BACKLINKS_MIN] = "Add backlinks for {$backlinkScore} point each";
355        } else {
356
357            $qualityScores[Analytics::INTERNAL_BACKLINKS_COUNT] = $countBacklinks * $backlinkScore;
358            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::PASSED;
359        }
360
361        /**
362         * Internal links
363         */
364        $internalLinksCount = $this->stats[Analytics::INTERNAL_LINKS_COUNT];
365        $internalLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR, 1);
366        if ($internalLinksCount == 0) {
367            $qualityScores[Analytics::INTERNAL_LINKS_COUNT] = 0;
368            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::FAILED;
369            $ruleInfo[self::RULE_INTERNAL_LINKS_MIN] = "Add internal links for {$internalLinkScore} point each";
370        } else {
371            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::PASSED;
372            $qualityScores[Analytics::INTERNAL_LINKS_COUNT] = $countBacklinks * $internalLinkScore;
373        }
374
375        /**
376         * Broken Links
377         */
378        $brokenLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR, 2);
379        $brokenLinksCount = $this->stats[Analytics::INTERNAL_LINKS_BROKEN_COUNT];
380        if ($brokenLinksCount > 2) {
381            $qualityScores['no_' . Analytics::INTERNAL_LINKS_BROKEN_COUNT] = 0;
382            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::FAILED;
383            $ruleInfo[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = "Delete the {$brokenLinksCount} broken links and add {$brokenLinkScore} points";
384        } else {
385            $qualityScores['no_' . Analytics::INTERNAL_LINKS_BROKEN_COUNT] = $brokenLinkScore;
386            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::PASSED;
387        }
388
389        /**
390         * Changes, the more changes the better
391         */
392        $qualityScores[Analytics::EDITS_COUNT] = $this->stats[Analytics::EDITS_COUNT] * $this->getConf(self::CONF_QUALITY_SCORE_CHANGES_FACTOR, 0.25);;;
393
394
395
396
397        /**
398         * Quality Score
399         */
400        ksort($qualityScores);
401        $qualityScoring = array();
402        $qualityScoring[self::SCORE] = array_sum($qualityScores);
403        $qualityScoring["scores"] = $qualityScores;
404
405
406        /**
407         * The rule that if broken will set the quality level to low
408         */
409        $brokenRules = array();
410        foreach ($ruleResults as $ruleName => $ruleResult) {
411            if ($ruleResult == self::FAILED) {
412                $brokenRules[] = $ruleName;
413            }
414        }
415        $ruleErrorCount = sizeof($brokenRules);
416        if ($ruleErrorCount > 0) {
417            $qualityResult = $ruleErrorCount . " quality rules errors";
418        } else {
419            $qualityResult = "All quality rules passed";
420        }
421
422        /**
423         * Low level
424         */
425        $mandatoryRules = preg_split("/,/", $this->getConf(self::CONF_MANDATORY_QUALITY_RULES));
426        $mandatoryRulesBroken = [];
427        foreach ($mandatoryRules as $lowLevelRule) {
428            if (in_array($lowLevelRule, $brokenRules)) {
429                $mandatoryRulesBroken[] = $lowLevelRule;
430            }
431        }
432        $lowLevel = false;
433        if (sizeof($mandatoryRulesBroken) > 0) {
434            $lowLevel = true;
435        }
436        LowQualityPage::setLowQualityPage($ID, $lowLevel);
437
438        /**
439         * Building the quality object in order
440         */
441        $quality[Analytics::LOW] = $lowLevel;
442        if (sizeof($mandatoryRulesBroken) > 0) {
443            ksort($mandatoryRulesBroken);
444            $quality['failed_mandatory_rules'] = $mandatoryRulesBroken;
445        }
446        $quality[self::SCORING] = $qualityScoring;
447        $quality[Analytics::RULES][self::RESULT] = $qualityResult;
448        if (!empty($ruleInfo)) {
449            $quality[Analytics::RULES]["info"] = $ruleInfo;
450        }
451
452        ksort($ruleResults);
453        $quality[Analytics::RULES][Analytics::DETAILS] = $ruleResults;
454
455        /**
456         * Metadata
457         */
458        $title = $meta['title'];
459        $this->metadata[Analytics::TITLE] = $title;
460        if ($title != $meta['h1']) {
461            $this->metadata[Analytics::H1] = $meta['h1'];
462        }
463        $timestampCreation = $meta['date']['created'];
464        $this->metadata[self::DATE_CREATED] = date('Y-m-d h:i:s', $timestampCreation);
465        $timestampModification = $meta['date']['modified'];
466        $this->metadata[Analytics::DATE_MODIFIED] = date('Y-m-d h:i:s', $timestampModification);
467        $this->metadata['age_creation'] = round((time() - $timestampCreation) / 60 / 60 / 24);
468        $this->metadata['age_modification'] = round((time() - $timestampModification) / 60 / 60 / 24);
469
470
471
472
473        /**
474         * Building the Top JSON in order
475         */
476        global $ID;
477        $finalStats = array();
478        $finalStats["id"] = $ID;
479        $finalStats["date"] = date('Y-m-d H:i:s', time());
480        $finalStats['metadata'] = $this->metadata;
481        ksort($statExport);
482        $finalStats[Analytics::STATISTICS] = $statExport;
483        $finalStats[Analytics::QUALITY] = $quality; // Quality after the sort to get them at the end
484
485
486        /**
487         * The result can be seen with
488         * doku.php?id=somepage&do=export_combo_analysis
489         */
490        /**
491         * Set the header for the export.php file
492         */
493        p_set_metadata($ID, array("format" =>
494            array("combo_" . $this->getPluginComponent() => array("Content-Type" => 'application/json'))
495        ));
496        $json_encoded = json_encode($finalStats, JSON_PRETTY_PRINT);
497
498        $page = new Page($ID);
499        $page->saveAnalytics($finalStats);
500        $this->doc .= $json_encoded;
501
502    }
503
504    /**
505     */
506    public function getFormat()
507    {
508        return Analytics::RENDERER_FORMAT;
509    }
510
511    public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content')
512    {
513
514        $attribute = array(
515            LinkUtility::ATTRIBUTE_ID => $id,
516            LinkUtility::ATTRIBUTE_TYPE => LinkUtility::TYPE_INTERNAL
517        );
518        LinkUtility::processLinkStats($attribute, $this->stats);
519
520    }
521
522    public function externallink($url, $name = null)
523    {
524        $attribute = array(
525            LinkUtility::ATTRIBUTE_ID => $url,
526            LinkUtility::ATTRIBUTE_TYPE => LinkUtility::TYPE_EXTERNAL,
527            LinkUtility::ATTRIBUTE_TITLE => $name
528        );
529        LinkUtility::processLinkStats($attribute, $this->stats);
530    }
531
532    public function header($text, $level, $pos)
533    {
534        $this->stats[Analytics::HEADERS_COUNT]['h' . $level]++;
535        $this->headerId++;
536        $this->stats[Analytics::HEADER_POSITION][$this->headerId] = 'h' . $level;
537
538    }
539
540    public function smiley($smiley)
541    {
542        if ($smiley == 'FIXME') $this->stats[self::FIXME]++;
543    }
544
545    public function linebreak()
546    {
547        if (!$this->tableopen) {
548            $this->stats['linebreak']++;
549        }
550    }
551
552    public function table_open($maxcols = null, $numrows = null, $pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
553    {
554        $this->tableopen = true;
555    }
556
557    public function table_close($pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
558    {
559        $this->tableopen = false;
560    }
561
562    public function hr()
563    {
564        $this->stats['hr']++;
565    }
566
567    public function quote_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
568    {
569        $this->stats['quote_count']++;
570        $this->quotelevel++;
571        $this->stats['quote_nest'] = max($this->quotelevel, $this->stats['quote_nest']);
572    }
573
574    public function quote_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
575    {
576        $this->quotelevel--;
577    }
578
579    public function strong_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
580    {
581        $this->formattingBracket++;
582    }
583
584    public function strong_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
585    {
586        $this->formattingBracket--;
587    }
588
589    public function emphasis_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
590    {
591        $this->formattingBracket++;
592    }
593
594    public function emphasis_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
595    {
596        $this->formattingBracket--;
597    }
598
599    public function underline_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
600    {
601        $this->formattingBracket++;
602    }
603
604    public function underline_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
605    {
606        $this->formattingBracket--;
607    }
608
609    public function cdata($text)
610    {
611
612        /**
613         * It seems that you receive cdata
614         * when emphasis_open / underline_open / strong_open
615         * Stats are not for them
616         */
617        if (!$this->formattingBracket) return;
618
619        $this->plainTextId++;
620
621        /**
622         * Length
623         */
624        $len = strlen($text);
625        $this->stats[self::PLAINTEXT][$this->plainTextId]['len'] = $len;
626
627
628        /**
629         * Multi-formatting
630         */
631        if ($this->formattingBracket > 1) {
632            $numberOfFormats = 1 * ($this->formattingBracket - 1);
633            $this->stats[self::PLAINTEXT][$this->plainTextId]['multiformat'] += $numberOfFormats;
634        }
635
636        /**
637         * Total
638         */
639        $this->stats[self::PLAINTEXT][0] += $len;
640    }
641
642    public function internalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
643    {
644        $this->stats[Analytics::INTERNAL_MEDIAS_COUNT]++;
645    }
646
647    public function externalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
648    {
649        $this->stats[Analytics::EXTERNAL_MEDIAS]++;
650    }
651
652    public function reset()
653    {
654        $this->stats = array();
655        $this->metadata = array();
656        $this->headerId = 0;
657    }
658
659    public function setMeta($key, $value)
660    {
661        $this->metadata[$key] = $value;
662    }
663
664
665}
666
667