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