xref: /plugin/combo/renderer/analytics.php (revision 2128d41957a347f460d708a4ede62030dec35ebe)
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 $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
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->metadata[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->metadata[Analytics::TITLE] = $meta[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->metadata[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->metadata[self::DESCRIPTION] = $meta[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->metadata[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
455         * If not set manually
456         */
457        $mandatoryRules = preg_split("/,/", $this->getConf(self::CONF_MANDATORY_QUALITY_RULES));
458        $mandatoryRulesBroken = [];
459        foreach ($mandatoryRules as $lowLevelRule) {
460            if (in_array($lowLevelRule, $brokenRules)) {
461                $mandatoryRulesBroken[] = $lowLevelRule;
462            }
463        }
464        if (empty($this->metadata[Page::LOW_QUALITY_PAGE_INDICATOR])) {
465            $lowLevel = false;
466            if (sizeof($mandatoryRulesBroken) > 0) {
467                $lowLevel = true;
468            }
469            $this->page->setLowQualityIndicator($lowLevel);
470        } else {
471            $lowLevel = $this->metadata[Page::LOW_QUALITY_PAGE_INDICATOR];
472        }
473
474        /**
475         * Building the quality object in order
476         */
477        $quality[Analytics::LOW] = $lowLevel;
478        if (sizeof($mandatoryRulesBroken) > 0) {
479            ksort($mandatoryRulesBroken);
480            $quality[Analytics::FAILED_MANDATORY_RULES] = $mandatoryRulesBroken;
481        }
482        $quality[self::SCORING] = $qualityScoring;
483        $quality[Analytics::RULES][self::RESULT] = $qualityResult;
484        if (!empty($ruleInfo)) {
485            $quality[Analytics::RULES]["info"] = $ruleInfo;
486        }
487
488        ksort($ruleResults);
489        $quality[Analytics::RULES][Analytics::DETAILS] = $ruleResults;
490
491        /**
492         * Metadata
493         */
494        $title = $meta['title'];
495        $this->metadata[Analytics::TITLE] = $title;
496        if ($title != @$meta['h1']) {
497            $this->metadata[Analytics::H1] = $meta['h1'];
498        }
499        $timestampCreation = $meta['date']['created'];
500        $this->metadata[self::DATE_CREATED] = date('Y-m-d h:i:s', $timestampCreation);
501        $timestampModification = $meta['date']['modified'];
502        $this->metadata[Analytics::DATE_MODIFIED] = date('Y-m-d h:i:s', $timestampModification);
503        $this->metadata['age_creation'] = round((time() - $timestampCreation) / 60 / 60 / 24);
504        $this->metadata['age_modification'] = round((time() - $timestampModification) / 60 / 60 / 24);
505
506
507        /**
508         * Building the Top JSON in order
509         */
510        global $ID;
511        $finalStats = array();
512        $finalStats["id"] = $ID;
513        $finalStats["date"] = date('Y-m-d H:i:s', time());
514        $finalStats['metadata'] = $this->metadata;
515        ksort($statExport);
516        $finalStats[Analytics::STATISTICS] = $statExport;
517        $finalStats[Analytics::QUALITY] = $quality; // Quality after the sort to get them at the end
518
519
520        /**
521         * The result can be seen with
522         * doku.php?id=somepage&do=export_combo_analysis
523         *
524         * Set the header temporarily for the export.php file
525         */
526        p_set_metadata(
527            $ID,
528            array("format" => array("combo_" . $this->getPluginComponent() => array("Content-Type" => 'application/json'))),
529            false,
530            false // Persistence is not needed, this is just in case this is an export
531        );
532        $json_encoded = json_encode($finalStats, JSON_PRETTY_PRINT);
533
534        $this->page->saveAnalytics($finalStats);
535        $this->doc .= $json_encoded;
536
537    }
538
539    /**
540     */
541    public function getFormat()
542    {
543        return Analytics::RENDERER_FORMAT;
544    }
545
546    public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content')
547    {
548
549        $link = new LinkUtility($id);
550        $link->setType(LinkUtility::TYPE_INTERNAL);
551        $link->processLinkStats($this->stats);
552
553    }
554
555    public function externallink($url, $name = null)
556    {
557        $link = new LinkUtility($url);
558        $link->setType(LinkUtility::TYPE_EXTERNAL);
559        if ($name != null) {
560            $link->setName($name);
561        }
562        $link->processLinkStats($this->stats);
563    }
564
565    public function header($text, $level, $pos)
566    {
567        if (!array_key_exists(Analytics::HEADERS_COUNT, $this->stats)) {
568            $this->stats[Analytics::HEADERS_COUNT] = [];
569        }
570        $heading = 'h' . $level;
571        if (!array_key_exists(
572            $heading,
573            $this->stats[Analytics::HEADERS_COUNT])) {
574            $this->stats[Analytics::HEADERS_COUNT][$heading] = 0;
575        }
576        $this->stats[Analytics::HEADERS_COUNT][$heading]++;
577
578        $this->headerId++;
579        $this->stats[Analytics::HEADER_POSITION][$this->headerId] = $heading;
580
581        /**
582         * Store the level of each heading
583         * They should only go from low to highest value
584         * for a good outline
585         */
586        if (!array_key_exists(Analytics::HEADERS_COUNT, $this->stats)) {
587            $this->stats[self::HEADER_STRUCT] = [];
588        }
589        $this->stats[self::HEADER_STRUCT][] = $level;
590
591    }
592
593    public function smiley($smiley)
594    {
595        if ($smiley == 'FIXME') $this->stats[self::FIXME]++;
596    }
597
598    public function linebreak()
599    {
600        if (!$this->tableopen) {
601            $this->stats['linebreak']++;
602        }
603    }
604
605    public function table_open($maxcols = null, $numrows = null, $pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
606    {
607        $this->tableopen = true;
608    }
609
610    public function table_close($pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
611    {
612        $this->tableopen = false;
613    }
614
615    public function hr()
616    {
617        $this->stats['hr']++;
618    }
619
620    public function quote_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
621    {
622        $this->stats['quote_count']++;
623        $this->quotelevel++;
624        $this->stats['quote_nest'] = max($this->quotelevel, $this->stats['quote_nest']);
625    }
626
627    public function quote_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
628    {
629        $this->quotelevel--;
630    }
631
632    public function strong_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
633    {
634        $this->formattingBracket++;
635    }
636
637    public function strong_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
638    {
639        $this->formattingBracket--;
640    }
641
642    public function emphasis_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
643    {
644        $this->formattingBracket++;
645    }
646
647    public function emphasis_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
648    {
649        $this->formattingBracket--;
650    }
651
652    public function underline_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
653    {
654        $this->formattingBracket++;
655    }
656
657    public function underline_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
658    {
659        $this->formattingBracket--;
660    }
661
662    public function cdata($text)
663    {
664
665        /**
666         * It seems that you receive cdata
667         * when emphasis_open / underline_open / strong_open
668         * Stats are not for them
669         */
670        if (!$this->formattingBracket) return;
671
672        $this->plainTextId++;
673
674        /**
675         * Length
676         */
677        $len = strlen($text);
678        $this->stats[self::PLAINTEXT][$this->plainTextId]['len'] = $len;
679
680
681        /**
682         * Multi-formatting
683         */
684        if ($this->formattingBracket > 1) {
685            $numberOfFormats = 1 * ($this->formattingBracket - 1);
686            $this->stats[self::PLAINTEXT][$this->plainTextId]['multiformat'] += $numberOfFormats;
687        }
688
689        /**
690         * Total
691         */
692        $this->stats[self::PLAINTEXT][0] += $len;
693    }
694
695    public function internalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
696    {
697        $this->stats[Analytics::INTERNAL_MEDIAS_COUNT]++;
698    }
699
700    public function externalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
701    {
702        $this->stats[Analytics::EXTERNAL_MEDIAS]++;
703    }
704
705    public function reset()
706    {
707        $this->stats = array();
708        $this->metadata = array();
709        $this->headerId = 0;
710    }
711
712    public function setMeta($key, $value)
713    {
714        $this->metadata[$key] = $value;
715    }
716
717
718}
719
720