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