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