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