1<?php
2
3
4use ComboStrap\AnalyticsDocument;
5use ComboStrap\BacklinkCount;
6use ComboStrap\Canonical;
7use ComboStrap\MarkupRef;
8use ComboStrap\MetadataDbStore;
9use ComboStrap\Page;
10use ComboStrap\PageTitle;
11use ComboStrap\PluginUtility;
12use ComboStrap\StringUtility;
13use dokuwiki\ChangeLog\PageChangeLog;
14
15
16require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
17
18
19/**
20 * A analysis Renderer that exports stats/quality/metadata in a json format
21 * You can export the data with
22 * doku.php?id=somepage&do=export_combo_analytics
23 */
24class renderer_plugin_combo_analytics extends Doku_Renderer
25{
26
27    const PLAINTEXT = 'formatted';
28    const RESULT = "result";
29    const DESCRIPTION = "description";
30    const PASSED = "Passed";
31    const FAILED = "Failed";
32    const FIXME = 'fixme';
33
34    /**
35     * Rules key
36     */
37    const RULE_WORDS_MINIMAL = 'words_min';
38    const RULE_OUTLINE_STRUCTURE = "outline_structure";
39    const RULE_INTERNAL_BACKLINKS_MIN = 'internal_backlinks_min';
40    const RULE_WORDS_MAXIMAL = "words_max";
41    const RULE_AVERAGE_WORDS_BY_SECTION_MIN = 'words_by_section_avg_min';
42    const RULE_AVERAGE_WORDS_BY_SECTION_MAX = 'words_by_section_avg_max';
43    const RULE_INTERNAL_LINKS_MIN = 'internal_links_min';
44    const RULE_INTERNAL_BROKEN_LINKS_MAX = 'internal_links_broken_max';
45    const RULE_DESCRIPTION_PRESENT = 'description_present';
46    const RULE_FIXME = "fixme_min";
47    const RULE_TITLE_PRESENT = "title_present";
48    const RULE_CANONICAL_PRESENT = "canonical_present";
49    const QUALITY_RULES = [
50        self::RULE_CANONICAL_PRESENT,
51        self::RULE_DESCRIPTION_PRESENT,
52        self::RULE_FIXME,
53        self::RULE_INTERNAL_BACKLINKS_MIN,
54        self::RULE_INTERNAL_BROKEN_LINKS_MAX,
55        self::RULE_INTERNAL_LINKS_MIN,
56        self::RULE_OUTLINE_STRUCTURE,
57        self::RULE_TITLE_PRESENT,
58        self::RULE_WORDS_MINIMAL,
59        self::RULE_WORDS_MAXIMAL,
60        self::RULE_AVERAGE_WORDS_BY_SECTION_MIN,
61        self::RULE_AVERAGE_WORDS_BY_SECTION_MAX
62    ];
63
64    /**
65     * The default man
66     */
67    const CONF_MANDATORY_QUALITY_RULES_DEFAULT_VALUE = [
68        self::RULE_WORDS_MINIMAL,
69        self::RULE_INTERNAL_BACKLINKS_MIN,
70        self::RULE_INTERNAL_LINKS_MIN
71    ];
72    const CONF_MANDATORY_QUALITY_RULES = "mandatoryQualityRules";
73
74    /**
75     * Quality Score factors
76     * They are used to calculate the score
77     */
78    const CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR = 'qualityScoreInternalBacklinksFactor';
79    const CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR = 'qualityScoreInternalLinksFactor';
80    const CONF_QUALITY_SCORE_TITLE_PRESENT = 'qualityScoreTitlePresent';
81    const CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE = 'qualityScoreCorrectOutline';
82    const CONF_QUALITY_SCORE_CORRECT_CONTENT = 'qualityScoreCorrectContentLength';
83    const CONF_QUALITY_SCORE_NO_FIXME = 'qualityScoreNoFixMe';
84    const CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE = 'qualityScoreCorrectWordSectionAvg';
85    const CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR = 'qualityScoreNoBrokenLinks';
86    const CONF_QUALITY_SCORE_CHANGES_FACTOR = 'qualityScoreChangesFactor';
87    const CONF_QUALITY_SCORE_DESCRIPTION_PRESENT = 'qualityScoreDescriptionPresent';
88    const CONF_QUALITY_SCORE_CANONICAL_PRESENT = 'qualityScoreCanonicalPresent';
89    const SCORING = "scoring";
90    const SCORE = "score";
91    const HEADER_STRUCT = 'header_struct';
92    const RENDERER_NAME_MODE = "combo_" . renderer_plugin_combo_analytics::RENDERER_FORMAT;
93
94    /**
95     * The format returned by the renderer
96     */
97    const RENDERER_FORMAT = "analytics";
98
99
100    /**
101     * The processing data
102     * that should be {@link  renderer_plugin_combo_analysis::reset()}
103     */
104    public $stats = array(); // the stats
105    protected $metadata = array(); // the metadata in frontmatter
106    protected $headerId = 0; // the id of the header on the page (first, second, ...)
107
108    /**
109     * Don't known this variable ?
110     */
111    protected $quotelevel = 0;
112    protected $formattingBracket = 0;
113    protected $tableopen = false;
114    private $plainTextId = 0;
115    /**
116     * @var Page
117     */
118    private $page;
119
120    /**
121     * Get and unset a value from an array
122     * @param array $array
123     * @param $key
124     * @param $default
125     * @return mixed
126     */
127    private static function getAndUnset(array &$array, $key, $default)
128    {
129        if (isset($array[$key])) {
130            $value = $array[$key];
131            unset($array[$key]);
132            return $value;
133        }
134        return $default;
135
136    }
137
138    public function document_start()
139    {
140        $this->reset();
141        $this->page = Page::createPageFromGlobalDokuwikiId();
142
143    }
144
145
146    /**
147     * Here the score is calculated
148     * @throws \ComboStrap\ExceptionCombo
149     */
150    public function document_end() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
151    {
152        /**
153         * The exported object
154         */
155        $statExport = $this->stats;
156
157        /**
158         * The metadata
159         */
160        global $ID;
161        $dokuWikiMetadata = p_get_metadata($ID);
162
163        /**
164         * Edit author stats
165         */
166        $changelog = new PageChangeLog($ID);
167        $revs = $changelog->getRevisions(0, 10000);
168        array_push($revs, $dokuWikiMetadata['last_change']['date']);
169        $statExport[AnalyticsDocument::EDITS_COUNT] = count($revs);
170        foreach ($revs as $rev) {
171
172
173            /**
174             * Init the authors array
175             */
176            if (!array_key_exists('authors', $statExport)) {
177                $statExport['authors'] = [];
178            }
179            /**
180             * Analytics by users
181             */
182            $info = $changelog->getRevisionInfo($rev);
183            if (is_array($info)) {
184                $user = "*";
185                if (array_key_exists('user', $info)) {
186                    $user = $info['user'];
187                }
188                if (!array_key_exists('authors', $statExport['authors'])) {
189                    $statExport['authors'][$user] = 0;
190                }
191                $statExport['authors'][$user] += 1;
192            }
193        }
194
195        /**
196         * Word and chars count
197         * The word count does not take into account
198         * words with non-words characters such as < =
199         * Therefore the node and attribute are not taken in the count
200         */
201        $text = rawWiki($ID);
202        $statExport[AnalyticsDocument::CHAR_COUNT] = strlen($text);
203        $statExport[AnalyticsDocument::WORD_COUNT] = StringUtility::getWordCount($text);
204
205
206        /**
207         * Internal link distance summary calculation
208         */
209        if (array_key_exists(AnalyticsDocument::INTERNAL_LINK_DISTANCE, $statExport)) {
210            $linkLengths = $statExport[AnalyticsDocument::INTERNAL_LINK_DISTANCE];
211            unset($statExport[AnalyticsDocument::INTERNAL_LINK_DISTANCE]);
212            $countBacklinks = count($linkLengths);
213            $statExport[AnalyticsDocument::INTERNAL_LINK_DISTANCE]['avg'] = null;
214            $statExport[AnalyticsDocument::INTERNAL_LINK_DISTANCE]['max'] = null;
215            $statExport[AnalyticsDocument::INTERNAL_LINK_DISTANCE]['min'] = null;
216            if ($countBacklinks > 0) {
217                $statExport[AnalyticsDocument::INTERNAL_LINK_DISTANCE]['avg'] = array_sum($linkLengths) / $countBacklinks;
218                $statExport[AnalyticsDocument::INTERNAL_LINK_DISTANCE]['max'] = max($linkLengths);
219                $statExport[AnalyticsDocument::INTERNAL_LINK_DISTANCE]['min'] = min($linkLengths);
220            }
221        }
222
223        /**
224         * Quality Report / Rules
225         */
226        // The array that hold the results of the quality rules
227        $ruleResults = array();
228        // The array that hold the quality score details
229        $qualityScores = array();
230
231
232        /**
233         * No fixme
234         */
235        if (array_key_exists(self::FIXME, $this->stats)) {
236            $fixmeCount = $this->stats[self::FIXME];
237            $statExport[self::FIXME] = $fixmeCount == null ? 0 : $fixmeCount;
238            if ($fixmeCount != 0) {
239                $ruleResults[self::RULE_FIXME] = self::FAILED;
240                $qualityScores['no_' . self::FIXME] = 0;
241            } else {
242                $ruleResults[self::RULE_FIXME] = self::PASSED;
243                $qualityScores['no_' . self::FIXME] = $this->getConf(self::CONF_QUALITY_SCORE_NO_FIXME, 1);
244            }
245        }
246
247        /**
248         * A title should be present
249         */
250        $titleScore = $this->getConf(self::CONF_QUALITY_SCORE_TITLE_PRESENT, 10);
251        if (empty($this->metadata[PageTitle::TITLE])) {
252            $ruleResults[self::RULE_TITLE_PRESENT] = self::FAILED;
253            $ruleInfo[self::RULE_TITLE_PRESENT] = "Add a title for {$titleScore} points";
254            $this->metadata[PageTitle::TITLE] = $dokuWikiMetadata[PageTitle::TITLE];
255            $qualityScores[self::RULE_TITLE_PRESENT] = 0;
256        } else {
257            $qualityScores[self::RULE_TITLE_PRESENT] = $titleScore;
258            $ruleResults[self::RULE_TITLE_PRESENT] = self::PASSED;
259        }
260
261        /**
262         * A description should be present
263         */
264        $descScore = $this->getConf(self::CONF_QUALITY_SCORE_DESCRIPTION_PRESENT, 8);
265        if (empty($this->metadata[self::DESCRIPTION])) {
266            $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::FAILED;
267            $ruleInfo[self::RULE_DESCRIPTION_PRESENT] = "Add a description for {$descScore} points";
268            $this->metadata[self::DESCRIPTION] = $dokuWikiMetadata[self::DESCRIPTION]["abstract"];
269            $qualityScores[self::RULE_DESCRIPTION_PRESENT] = 0;
270        } else {
271            $qualityScores[self::RULE_DESCRIPTION_PRESENT] = $descScore;
272            $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::PASSED;
273        }
274
275        /**
276         * A canonical should be present
277         */
278        $canonicalScore = $this->getConf(self::CONF_QUALITY_SCORE_CANONICAL_PRESENT, 5);
279        if (empty($this->metadata[Canonical::PROPERTY_NAME])) {
280            global $conf;
281            $root = $conf['start'];
282            if ($ID !== $root) {
283                $qualityScores[self::RULE_CANONICAL_PRESENT] = 0;
284                $ruleResults[self::RULE_CANONICAL_PRESENT] = self::FAILED;
285                // no link to the documentation because we don't want any html in the json
286                $ruleInfo[self::RULE_CANONICAL_PRESENT] = "Add a canonical for {$canonicalScore} points";
287            }
288        } else {
289            $qualityScores[self::RULE_CANONICAL_PRESENT] = $canonicalScore;
290            $ruleResults[self::RULE_CANONICAL_PRESENT] = self::PASSED;
291        }
292
293        /**
294         * Outline / Header structure
295         */
296        $treeError = 0;
297        $headersCount = 0;
298        if (array_key_exists(AnalyticsDocument::HEADER_POSITION, $this->stats)) {
299            $headersCount = count($this->stats[AnalyticsDocument::HEADER_POSITION]);
300            unset($statExport[AnalyticsDocument::HEADER_POSITION]);
301            for ($i = 1; $i < $headersCount; $i++) {
302                $currentHeaderLevel = $this->stats[self::HEADER_STRUCT][$i];
303                $previousHeaderLevel = $this->stats[self::HEADER_STRUCT][$i - 1];
304                if ($currentHeaderLevel - $previousHeaderLevel > 1) {
305                    $treeError += 1;
306                    $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "The " . $i . " header (h" . $currentHeaderLevel . ") has a level bigger than its precedent (" . $previousHeaderLevel . ")";
307                }
308            }
309            unset($statExport[self::HEADER_STRUCT]);
310        }
311        $outlinePoints = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE, 3);
312        if ($treeError > 0 || $headersCount == 0) {
313            $qualityScores['correct_outline'] = 0;
314            $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::FAILED;
315            if ($headersCount == 0) {
316                $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "Add headings to create a document outline for {$outlinePoints} points";
317            }
318        } else {
319            $qualityScores['correct_outline'] = $outlinePoints;
320            $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::PASSED;
321        }
322
323
324        /**
325         * Document length
326         */
327        $minimalWordCount = 50;
328        $maximalWordCount = 1500;
329        $correctContentLength = true;
330        $correctLengthScore = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_CONTENT, 10);
331        $missingWords = $minimalWordCount - $statExport[AnalyticsDocument::WORD_COUNT];
332        if ($missingWords > 0) {
333            $ruleResults[self::RULE_WORDS_MINIMAL] = self::FAILED;
334            $correctContentLength = false;
335            $ruleInfo[self::RULE_WORDS_MINIMAL] = "Add {$missingWords} words to get {$correctLengthScore} points";
336        } else {
337            $ruleResults[self::RULE_WORDS_MINIMAL] = self::PASSED;
338        }
339        $tooMuchWords = $statExport[AnalyticsDocument::WORD_COUNT] - $maximalWordCount;
340        if ($tooMuchWords > 0) {
341            $ruleResults[self::RULE_WORDS_MAXIMAL] = self::FAILED;
342            $ruleInfo[self::RULE_WORDS_MAXIMAL] = "Delete {$tooMuchWords} words to get {$correctLengthScore} points";
343            $correctContentLength = false;
344        } else {
345            $ruleResults[self::RULE_WORDS_MAXIMAL] = self::PASSED;
346        }
347        if ($correctContentLength) {
348            $qualityScores['correct_content_length'] = $correctLengthScore;
349        } else {
350            $qualityScores['correct_content_length'] = 0;
351        }
352
353
354        /**
355         * Average Number of words by header section to text ratio
356         */
357        $headers = $this->stats[AnalyticsDocument::HEADING_COUNT];
358        if ($headers != null) {
359            $headerCount = array_sum($headers);
360            $headerCount--; // h1 is supposed to have no words
361            if ($headerCount > 0) {
362
363                $avgWordsCountBySection = round($this->stats[AnalyticsDocument::WORD_COUNT] / $headerCount);
364                $statExport['word_section_count']['avg'] = $avgWordsCountBySection;
365
366                /**
367                 * Min words by header section
368                 */
369                $wordsByHeaderMin = 20;
370                /**
371                 * Max words by header section
372                 */
373                $wordsByHeaderMax = 300;
374                $correctAverageWordsBySection = true;
375                if ($avgWordsCountBySection < $wordsByHeaderMin) {
376                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::FAILED;
377                    $correctAverageWordsBySection = false;
378                    $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = "The number of words by section is less than {$wordsByHeaderMin}";
379                } else {
380                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::PASSED;
381                }
382                if ($avgWordsCountBySection > $wordsByHeaderMax) {
383                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::FAILED;
384                    $correctAverageWordsBySection = false;
385                    $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = "The number of words by section is more than {$wordsByHeaderMax}";
386                } else {
387                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::PASSED;
388                }
389                if ($correctAverageWordsBySection) {
390                    $qualityScores['correct_word_avg_by_section'] = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE, 10);
391                } else {
392                    $qualityScores['correct_word_avg_by_section'] = 0;
393                }
394
395            }
396        }
397
398        /**
399         * Internal Backlinks rule
400         *
401         * We used the database table to get the backlinks
402         * because the replication is based on it
403         * If the dokuwiki index is not up to date, we may got
404         * inconsistency
405         */
406        $countBacklinks = BacklinkCount::createFromResource($this->page)
407            ->setReadStore(MetadataDbStore::class)
408            ->getValueOrDefault();
409        $statExport[BacklinkCount::getPersistentName()] = $countBacklinks;
410        $backlinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR, 1);
411        if ($countBacklinks == 0) {
412
413            $qualityScores[BacklinkCount::getPersistentName()] = 0;
414            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::FAILED;
415            $ruleInfo[self::RULE_INTERNAL_BACKLINKS_MIN] = "Add backlinks for {$backlinkScore} point each";
416
417        } else {
418
419            $qualityScores[BacklinkCount::getPersistentName()] = $countBacklinks * $backlinkScore;
420            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::PASSED;
421        }
422
423        /**
424         * Internal links
425         */
426        $internalLinksCount = $this->stats[AnalyticsDocument::INTERNAL_LINK_COUNT];
427        $internalLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR, 1);
428        if ($internalLinksCount == 0) {
429            $qualityScores[AnalyticsDocument::INTERNAL_LINK_COUNT] = 0;
430            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::FAILED;
431            $ruleInfo[self::RULE_INTERNAL_LINKS_MIN] = "Add internal links for {$internalLinkScore} point each";
432        } else {
433            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::PASSED;
434            $qualityScores[AnalyticsDocument::INTERNAL_LINK_COUNT] = $countBacklinks * $internalLinkScore;
435        }
436
437        /**
438         * Broken Links
439         */
440        $brokenLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR, 2);
441        $brokenLinksCount = 0;
442        if (array_key_exists(AnalyticsDocument::INTERNAL_LINK_BROKEN_COUNT, $this->stats)) {
443            $brokenLinksCount = $this->stats[AnalyticsDocument::INTERNAL_LINK_BROKEN_COUNT];
444        }
445        if ($brokenLinksCount > 2) {
446            $qualityScores['no_' . AnalyticsDocument::INTERNAL_LINK_BROKEN_COUNT] = 0;
447            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::FAILED;
448            $ruleInfo[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = "Delete the {$brokenLinksCount} broken links and add {$brokenLinkScore} points";
449        } else {
450            $qualityScores['no_' . AnalyticsDocument::INTERNAL_LINK_BROKEN_COUNT] = $brokenLinkScore;
451            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::PASSED;
452        }
453
454        /**
455         * Media
456         */
457        $mediasStats = [
458            "total_count" => self::getAndUnset($statExport, AnalyticsDocument::MEDIA_COUNT, 0),
459            "internal_count" => self::getAndUnset($statExport, AnalyticsDocument::INTERNAL_MEDIA_COUNT, 0),
460            "internal_broken_count" => self::getAndUnset($statExport, AnalyticsDocument::INTERNAL_BROKEN_MEDIA_COUNT, 0),
461            "external_count" => self::getAndUnset($statExport, AnalyticsDocument::EXTERNAL_MEDIA_COUNT, 0)
462        ];
463        $statExport['media'] = $mediasStats;
464
465        /**
466         * Changes, the more changes the better
467         */
468        $qualityScores[AnalyticsDocument::EDITS_COUNT] = $statExport[AnalyticsDocument::EDITS_COUNT] * $this->getConf(self::CONF_QUALITY_SCORE_CHANGES_FACTOR, 0.25);
469
470
471        /**
472         * Quality Score
473         */
474        ksort($qualityScores);
475        $qualityScoring = array();
476        $qualityScoring[self::SCORE] = array_sum($qualityScores);
477        $qualityScoring["scores"] = $qualityScores;
478
479
480        /**
481         * The rule that if broken will set the quality level to low
482         */
483        $brokenRules = array();
484        foreach ($ruleResults as $ruleName => $ruleResult) {
485            if ($ruleResult == self::FAILED) {
486                $brokenRules[] = $ruleName;
487            }
488        }
489        $ruleErrorCount = sizeof($brokenRules);
490        if ($ruleErrorCount > 0) {
491            $qualityResult = $ruleErrorCount . " quality rules errors";
492        } else {
493            $qualityResult = "All quality rules passed";
494        }
495
496        /**
497         * Low level Computation
498         */
499        $mandatoryRules = preg_split("/,/", $this->getConf(self::CONF_MANDATORY_QUALITY_RULES));
500        $mandatoryRulesBroken = [];
501        foreach ($mandatoryRules as $lowLevelRule) {
502            if (in_array($lowLevelRule, $brokenRules)) {
503                $mandatoryRulesBroken[] = $lowLevelRule;
504            }
505        }
506        /**
507         * Low Level
508         */
509        $lowLevel = false;
510        $brokenRulesCount = sizeof($mandatoryRulesBroken);
511        if ($brokenRulesCount > 0) {
512            $lowLevel = true;
513            $quality["message"] = "$brokenRulesCount mandatory rules broken.";
514        } else {
515            $quality["message"] = "No mandatory rules broken";
516        }
517        if ($this->page->isSecondarySlot()) {
518            $lowLevel = false;
519        }
520        $this->page->setLowQualityIndicatorCalculation($lowLevel);
521
522        /**
523         * Building the quality object in order
524         */
525        $quality[AnalyticsDocument::LOW] = $lowLevel;
526        if (sizeof($mandatoryRulesBroken) > 0) {
527            ksort($mandatoryRulesBroken);
528            $quality[AnalyticsDocument::FAILED_MANDATORY_RULES] = $mandatoryRulesBroken;
529        }
530        $quality[self::SCORING] = $qualityScoring;
531        $quality[AnalyticsDocument::RULES][self::RESULT] = $qualityResult;
532        if (!empty($ruleInfo)) {
533            $quality[AnalyticsDocument::RULES]["info"] = $ruleInfo;
534        }
535
536        ksort($ruleResults);
537        $quality[AnalyticsDocument::RULES][AnalyticsDocument::DETAILS] = $ruleResults;
538
539        /**
540         * Metadata
541         */
542        $page = Page::createPageFromGlobalDokuwikiId();
543        $meta = $page->getMetadataForRendering();
544        foreach ($meta as $key => $value) {
545            /**
546             * The metadata may have been set
547             * by frontmatter
548             */
549            if (!isset($this->metadata[$key])) {
550                $this->metadata[$key] = $value;
551            }
552        }
553
554
555        /**
556         * Building the Top JSON in order
557         */
558        global $ID;
559        $finalStats = array();
560        $finalStats["date"] = date('Y-m-d H:i:s', time());
561        ksort($this->metadata);
562        $finalStats[AnalyticsDocument::METADATA] = $this->metadata;
563        ksort($statExport);
564        $finalStats[AnalyticsDocument::STATISTICS] = $statExport;
565        $finalStats[AnalyticsDocument::QUALITY] = $quality; // Quality after the sort to get them at the end
566
567
568        /**
569         * The result can be seen with
570         * doku.php?id=somepage&do=export_combo_analysis
571         *
572         * Set the header temporarily for the export.php file
573         *
574         * The mode in the export is
575         */
576        $mode = "combo_" . $this->getPluginComponent();
577        p_set_metadata(
578            $ID,
579            array("format" => array($mode => array("Content-Type" => 'application/json'))),
580            false,
581            false // Persistence is needed because there is a cache
582        );
583        $json_encoded = json_encode($finalStats, JSON_PRETTY_PRINT);
584
585        $this->doc .= $json_encoded;
586
587    }
588
589    /**
590     */
591    public function getFormat()
592    {
593        return self::RENDERER_FORMAT;
594    }
595
596
597
598    public function header($text, $level, $pos)
599    {
600        if (!array_key_exists(AnalyticsDocument::HEADING_COUNT, $this->stats)) {
601            $this->stats[AnalyticsDocument::HEADING_COUNT] = [];
602        }
603        $heading = 'h' . $level;
604        if (!array_key_exists(
605            $heading,
606            $this->stats[AnalyticsDocument::HEADING_COUNT])) {
607            $this->stats[AnalyticsDocument::HEADING_COUNT][$heading] = 0;
608        }
609        $this->stats[AnalyticsDocument::HEADING_COUNT][$heading]++;
610
611        $this->headerId++;
612        $this->stats[AnalyticsDocument::HEADER_POSITION][$this->headerId] = $heading;
613
614        /**
615         * Store the level of each heading
616         * They should only go from low to highest value
617         * for a good outline
618         */
619        if (!array_key_exists(AnalyticsDocument::HEADING_COUNT, $this->stats)) {
620            $this->stats[self::HEADER_STRUCT] = [];
621        }
622        $this->stats[self::HEADER_STRUCT][] = $level;
623
624    }
625
626    public function smiley($smiley)
627    {
628        if ($smiley == 'FIXME') $this->stats[self::FIXME]++;
629    }
630
631    public function linebreak()
632    {
633        if (!$this->tableopen) {
634            $this->stats['linebreak']++;
635        }
636    }
637
638    public function table_open($maxcols = null, $numrows = null, $pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
639    {
640        $this->tableopen = true;
641    }
642
643    public function table_close($pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
644    {
645        $this->tableopen = false;
646    }
647
648    public function hr()
649    {
650        $this->stats['hr']++;
651    }
652
653    public function quote_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
654    {
655        $this->stats['quote_count']++;
656        $this->quotelevel++;
657        $this->stats['quote_nest'] = max($this->quotelevel, $this->stats['quote_nest']);
658    }
659
660    public function quote_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
661    {
662        $this->quotelevel--;
663    }
664
665    public function strong_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
666    {
667        $this->formattingBracket++;
668    }
669
670    public function strong_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
671    {
672        $this->formattingBracket--;
673    }
674
675    public function emphasis_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
676    {
677        $this->formattingBracket++;
678    }
679
680    public function emphasis_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
681    {
682        $this->formattingBracket--;
683    }
684
685    public function underline_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
686    {
687        $this->formattingBracket++;
688    }
689
690    public function addToDescription($text){
691
692    }
693
694    public function underline_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
695    {
696        $this->formattingBracket--;
697    }
698
699    public function cdata($text)
700    {
701
702        /**
703         * It seems that you receive cdata
704         * when emphasis_open / underline_open / strong_open
705         * Stats are not for them
706         */
707        if (!$this->formattingBracket) return;
708
709        $this->plainTextId++;
710
711        /**
712         * Length
713         */
714        $len = strlen($text);
715        $this->stats[self::PLAINTEXT][$this->plainTextId]['len'] = $len;
716
717
718        /**
719         * Multi-formatting
720         */
721        if ($this->formattingBracket > 1) {
722            $numberOfFormats = 1 * ($this->formattingBracket - 1);
723            $this->stats[self::PLAINTEXT][$this->plainTextId]['multiformat'] += $numberOfFormats;
724        }
725
726        /**
727         * Total
728         */
729        $this->stats[self::PLAINTEXT][0] += $len;
730    }
731
732    public function internalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
733    {
734        $this->stats[AnalyticsDocument::INTERNAL_MEDIA_COUNT]++;
735    }
736
737    public function externalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
738    {
739        $this->stats[AnalyticsDocument::EXTERNAL_MEDIA_COUNT]++;
740    }
741
742    public function reset()
743    {
744        $this->stats = array();
745        $this->metadata = array();
746        $this->headerId = 0;
747    }
748
749    public function setAnalyticsMetaForReporting($key, $value)
750    {
751        $this->metadata[$key] = $value;
752    }
753
754
755}
756
757