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