xref: /plugin/combo/renderer/analytics.php (revision 70bbd7f1f72440223cc13f3495efdcb2b0a11514)
1007225e5Sgerardnico<?php
2007225e5Sgerardnico
3007225e5Sgerardnico
404fd306cSNickeauuse ComboStrap\Meta\Field\BacklinkCount;
5c3437056SNickeauuse ComboStrap\Canonical;
604fd306cSNickeauuse ComboStrap\ExceptionCompile;
704fd306cSNickeauuse ComboStrap\ExceptionNotExists;
804fd306cSNickeauuse ComboStrap\ExceptionNotFound;
904fd306cSNickeauuse ComboStrap\ExceptionRuntimeInternal;
1004fd306cSNickeauuse ComboStrap\FetcherMarkup;
1104fd306cSNickeauuse ComboStrap\LogUtility;
1204fd306cSNickeauuse ComboStrap\MarkupPath;
1304fd306cSNickeauuse ComboStrap\Meta\Field\PageH1;
1404fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDbStore;
1504fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDokuWikiStore;
1604fd306cSNickeauuse ComboStrap\Mime;
17c3437056SNickeauuse ComboStrap\PageTitle;
1837748cd8SNickeauuse ComboStrap\StringUtility;
1904fd306cSNickeauuse ComboStrap\WikiPath;
20007225e5Sgerardnicouse dokuwiki\ChangeLog\PageChangeLog;
21007225e5Sgerardnico
2237748cd8SNickeau
2337748cd8SNickeaurequire_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
24007225e5Sgerardnico
25007225e5Sgerardnico
26007225e5Sgerardnico/**
27007225e5Sgerardnico * A analysis Renderer that exports stats/quality/metadata in a json format
28007225e5Sgerardnico * You can export the data with
29007225e5Sgerardnico * doku.php?id=somepage&do=export_combo_analytics
3004fd306cSNickeau *
3104fd306cSNickeau * TODO: Move the metadata part to the metadata render and the quality part to the indexer.
32007225e5Sgerardnico */
33007225e5Sgerardnicoclass renderer_plugin_combo_analytics extends Doku_Renderer
34007225e5Sgerardnico{
357c33ecc6Sgerardnico
36007225e5Sgerardnico    const PLAINTEXT = 'formatted';
37007225e5Sgerardnico    const RESULT = "result";
38007225e5Sgerardnico    const DESCRIPTION = "description";
39007225e5Sgerardnico    const PASSED = "Passed";
40007225e5Sgerardnico    const FAILED = "Failed";
41007225e5Sgerardnico    const FIXME = 'fixme';
42007225e5Sgerardnico
43007225e5Sgerardnico    /**
44007225e5Sgerardnico     * Rules key
45007225e5Sgerardnico     */
46007225e5Sgerardnico    const RULE_WORDS_MINIMAL = 'words_min';
47007225e5Sgerardnico    const RULE_OUTLINE_STRUCTURE = "outline_structure";
48007225e5Sgerardnico    const RULE_INTERNAL_BACKLINKS_MIN = 'internal_backlinks_min';
49007225e5Sgerardnico    const RULE_WORDS_MAXIMAL = "words_max";
50007225e5Sgerardnico    const RULE_AVERAGE_WORDS_BY_SECTION_MIN = 'words_by_section_avg_min';
51007225e5Sgerardnico    const RULE_AVERAGE_WORDS_BY_SECTION_MAX = 'words_by_section_avg_max';
52007225e5Sgerardnico    const RULE_INTERNAL_LINKS_MIN = 'internal_links_min';
53007225e5Sgerardnico    const RULE_INTERNAL_BROKEN_LINKS_MAX = 'internal_links_broken_max';
54007225e5Sgerardnico    const RULE_DESCRIPTION_PRESENT = 'description_present';
55007225e5Sgerardnico    const RULE_FIXME = "fixme_min";
56007225e5Sgerardnico    const RULE_TITLE_PRESENT = "title_present";
57007225e5Sgerardnico    const RULE_CANONICAL_PRESENT = "canonical_present";
58aa3cb38fSgerardnico    const QUALITY_RULES = [
59aa3cb38fSgerardnico        self::RULE_CANONICAL_PRESENT,
60aa3cb38fSgerardnico        self::RULE_DESCRIPTION_PRESENT,
61aa3cb38fSgerardnico        self::RULE_FIXME,
62aa3cb38fSgerardnico        self::RULE_INTERNAL_BACKLINKS_MIN,
63aa3cb38fSgerardnico        self::RULE_INTERNAL_BROKEN_LINKS_MAX,
64aa3cb38fSgerardnico        self::RULE_INTERNAL_LINKS_MIN,
65aa3cb38fSgerardnico        self::RULE_OUTLINE_STRUCTURE,
66aa3cb38fSgerardnico        self::RULE_TITLE_PRESENT,
67aa3cb38fSgerardnico        self::RULE_WORDS_MINIMAL,
68aa3cb38fSgerardnico        self::RULE_WORDS_MAXIMAL,
69aa3cb38fSgerardnico        self::RULE_AVERAGE_WORDS_BY_SECTION_MIN,
70aa3cb38fSgerardnico        self::RULE_AVERAGE_WORDS_BY_SECTION_MAX
71aa3cb38fSgerardnico    ];
72007225e5Sgerardnico
73007225e5Sgerardnico    /**
74007225e5Sgerardnico     * The default man
75007225e5Sgerardnico     */
76007225e5Sgerardnico    const CONF_MANDATORY_QUALITY_RULES_DEFAULT_VALUE = [
77007225e5Sgerardnico        self::RULE_WORDS_MINIMAL,
78007225e5Sgerardnico        self::RULE_INTERNAL_BACKLINKS_MIN,
79007225e5Sgerardnico        self::RULE_INTERNAL_LINKS_MIN
80007225e5Sgerardnico    ];
81007225e5Sgerardnico    const CONF_MANDATORY_QUALITY_RULES = "mandatoryQualityRules";
82007225e5Sgerardnico
83007225e5Sgerardnico    /**
84007225e5Sgerardnico     * Quality Score factors
85007225e5Sgerardnico     * They are used to calculate the score
86007225e5Sgerardnico     */
87007225e5Sgerardnico    const CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR = 'qualityScoreInternalBacklinksFactor';
88007225e5Sgerardnico    const CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR = 'qualityScoreInternalLinksFactor';
89007225e5Sgerardnico    const CONF_QUALITY_SCORE_TITLE_PRESENT = 'qualityScoreTitlePresent';
90007225e5Sgerardnico    const CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE = 'qualityScoreCorrectOutline';
91007225e5Sgerardnico    const CONF_QUALITY_SCORE_CORRECT_CONTENT = 'qualityScoreCorrectContentLength';
92007225e5Sgerardnico    const CONF_QUALITY_SCORE_NO_FIXME = 'qualityScoreNoFixMe';
93007225e5Sgerardnico    const CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE = 'qualityScoreCorrectWordSectionAvg';
94007225e5Sgerardnico    const CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR = 'qualityScoreNoBrokenLinks';
95007225e5Sgerardnico    const CONF_QUALITY_SCORE_CHANGES_FACTOR = 'qualityScoreChangesFactor';
96007225e5Sgerardnico    const CONF_QUALITY_SCORE_DESCRIPTION_PRESENT = 'qualityScoreDescriptionPresent';
97007225e5Sgerardnico    const CONF_QUALITY_SCORE_CANONICAL_PRESENT = 'qualityScoreCanonicalPresent';
9808ca4f85Sgerardnico    const SCORING = "scoring";
9908ca4f85Sgerardnico    const SCORE = "score";
100ebdc69ceSgerardnico    const HEADER_STRUCT = 'header_struct';
101531e725cSNickeau    const RENDERER_NAME_MODE = "combo_" . renderer_plugin_combo_analytics::RENDERER_FORMAT;
102c3437056SNickeau
10304fd306cSNickeau
104531e725cSNickeau    /**
105531e725cSNickeau     * The format returned by the renderer
106531e725cSNickeau     */
107531e725cSNickeau    const RENDERER_FORMAT = "analytics";
10804fd306cSNickeau    public const QUALITY = 'quality';
10904fd306cSNickeau    public const DETAILS = 'details';
11004fd306cSNickeau    /**
11104fd306cSNickeau     * An array of info for errors mostly
11204fd306cSNickeau     */
11304fd306cSNickeau    public const INFO = "info";
11404fd306cSNickeau    public const INTERNAL_LINK_COUNT = 'internal_link_count';
11504fd306cSNickeau    public const CHAR_COUNT = 'char_count';
11604fd306cSNickeau    public const FAILED_MANDATORY_RULES = 'failed_mandatory_rules';
11704fd306cSNickeau    public const EDITS_COUNT = 'edits_count';
11804fd306cSNickeau    public const LOCAL_LINK_COUNT = "local_link_count";
11904fd306cSNickeau    public const WINDOWS_SHARE_COUNT = "windows_share_count";
12004fd306cSNickeau    public const SYNTAX_COUNT = "syntax_count";
12104fd306cSNickeau    /**
12204fd306cSNickeau     * Constant in Key or value
12304fd306cSNickeau     */
12404fd306cSNickeau    public const HEADER_POSITION = 'header_id';
12504fd306cSNickeau    public const INTERNAL_BROKEN_MEDIA_COUNT = 'internal_broken_media_count';
12604fd306cSNickeau    public const TEMPLATE_LINK_COUNT = 'template_link_count';
12704fd306cSNickeau    public const STATISTICS = "statistics";
12804fd306cSNickeau    public const INTERWIKI_LINK_COUNT = "interwiki_link_count";
12904fd306cSNickeau    public const HEADING_COUNT = 'heading_count';
13004fd306cSNickeau    public const MEDIA_COUNT = 'media_count';
13104fd306cSNickeau    public const EXTERNAL_MEDIA_COUNT = 'external_media_count';
13204fd306cSNickeau    public const INTERNAL_LINK_DISTANCE = 'internal_link_distance';
13304fd306cSNickeau    public const INTERNAL_LINK_BROKEN_COUNT = 'internal_broken_link_count';
13404fd306cSNickeau    public const EMAIL_COUNT = "email_count";
13504fd306cSNickeau    public const EXTERNAL_LINK_COUNT = 'external_link_count';
13604fd306cSNickeau    public const LOW = "low";
13704fd306cSNickeau    public const WORD_COUNT = 'word_count';
13804fd306cSNickeau    public const RULES = "rules";
13904fd306cSNickeau    public const METADATA = 'metadata';
14004fd306cSNickeau    public const INTERNAL_MEDIA_COUNT = 'internal_media_count';
141007225e5Sgerardnico
142aa3cb38fSgerardnico
143007225e5Sgerardnico    /**
144007225e5Sgerardnico     * The processing data
145007225e5Sgerardnico     * that should be {@link  renderer_plugin_combo_analysis::reset()}
146007225e5Sgerardnico     */
147007225e5Sgerardnico    public $stats = array(); // the stats
14837748cd8SNickeau    protected $metadata = array(); // the metadata in frontmatter
149007225e5Sgerardnico    protected $headerId = 0; // the id of the header on the page (first, second, ...)
150007225e5Sgerardnico
151007225e5Sgerardnico    /**
152007225e5Sgerardnico     * Don't known this variable ?
153007225e5Sgerardnico     */
154007225e5Sgerardnico    protected $quotelevel = 0;
155007225e5Sgerardnico    protected $formattingBracket = 0;
156007225e5Sgerardnico    protected $tableopen = false;
157007225e5Sgerardnico    private $plainTextId = 0;
1582c067407Sgerardnico    /**
15904fd306cSNickeau     * @var MarkupPath
1602c067407Sgerardnico     */
16104fd306cSNickeau    private MarkupPath $page;
16204fd306cSNickeau
16304fd306cSNickeau    /**
16404fd306cSNickeau     * @throws ExceptionNotExists - if the file does not exists
16504fd306cSNickeau     */
16604fd306cSNickeau    public static function createAnalyticsFetcherForPageFragment(MarkupPath $markupPath): FetcherMarkup
16704fd306cSNickeau    {
16804fd306cSNickeau        $path = $markupPath->getPathObject();
16904fd306cSNickeau        if (!($path instanceof WikiPath)) {
17004fd306cSNickeau            throw new ExceptionRuntimeInternal("The path ($path) is not a wiki path");
17104fd306cSNickeau        }
17204fd306cSNickeau        return FetcherMarkup::confRoot()
17304fd306cSNickeau            ->setRequestedExecutingPath($path)
17404fd306cSNickeau            ->setRequestedContextPath($path)
17504fd306cSNickeau            ->setRequestedMime(Mime::getJson())
17604fd306cSNickeau            ->setRequestedRenderer(self::RENDERER_NAME_MODE)
17704fd306cSNickeau            ->build();
17804fd306cSNickeau
17904fd306cSNickeau    }
18004fd306cSNickeau
18104fd306cSNickeau    public static function getMime(): Mime
18204fd306cSNickeau    {
18304fd306cSNickeau        return Mime::create(self::RENDERER_NAME_MODE . "/json");
18404fd306cSNickeau    }
1852c067407Sgerardnico
186e8b2ff59SNickeau    /**
187e8b2ff59SNickeau     * Get and unset a value from an array
188e8b2ff59SNickeau     * @param array $array
189e8b2ff59SNickeau     * @param $key
190e8b2ff59SNickeau     * @param $default
191e8b2ff59SNickeau     * @return mixed
192e8b2ff59SNickeau     */
193e8b2ff59SNickeau    private static function getAndUnset(array &$array, $key, $default)
194e8b2ff59SNickeau    {
195e8b2ff59SNickeau        if (isset($array[$key])) {
196e8b2ff59SNickeau            $value = $array[$key];
197e8b2ff59SNickeau            unset($array[$key]);
198e8b2ff59SNickeau            return $value;
199e8b2ff59SNickeau        }
200e8b2ff59SNickeau        return $default;
201e8b2ff59SNickeau
202e8b2ff59SNickeau    }
203e8b2ff59SNickeau
2042c067407Sgerardnico    public function document_start()
2052c067407Sgerardnico    {
2067c33ecc6Sgerardnico        $this->reset();
20704fd306cSNickeau        try {
20804fd306cSNickeau            $this->page = MarkupPath::createPageFromExecutingId();
20904fd306cSNickeau        } catch (ExceptionCompile $e) {
21004fd306cSNickeau            LogUtility::msg("The global ID is unknown, we were unable to instantiate the requested page in analytics");
21104fd306cSNickeau        }
2122c067407Sgerardnico
2132c067407Sgerardnico    }
214007225e5Sgerardnico
215007225e5Sgerardnico
216007225e5Sgerardnico    /**
217007225e5Sgerardnico     * Here the score is calculated
218007225e5Sgerardnico     */
219007225e5Sgerardnico    public function document_end() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
220007225e5Sgerardnico    {
221007225e5Sgerardnico        /**
222f3748b38Sgerardnico         * The exported object
223f3748b38Sgerardnico         */
224f3748b38Sgerardnico        $statExport = $this->stats;
225f3748b38Sgerardnico
226f3748b38Sgerardnico        /**
227007225e5Sgerardnico         * The metadata
228007225e5Sgerardnico         */
229007225e5Sgerardnico        global $ID;
230fa5961eaSgerardnico        $dokuWikiMetadata = p_get_metadata($ID);
231007225e5Sgerardnico
232007225e5Sgerardnico        /**
233f3748b38Sgerardnico         * Edit author stats
234f3748b38Sgerardnico         */
235f3748b38Sgerardnico        $changelog = new PageChangeLog($ID);
236f3748b38Sgerardnico        $revs = $changelog->getRevisions(0, 10000);
23704fd306cSNickeau        $statExport[self::EDITS_COUNT] = count($revs);
238f3748b38Sgerardnico        foreach ($revs as $rev) {
2392128d419Sgerardnico
240ebdc69ceSgerardnico
241ebdc69ceSgerardnico            /**
242ebdc69ceSgerardnico             * Init the authors array
243ebdc69ceSgerardnico             */
244ebdc69ceSgerardnico            if (!array_key_exists('authors', $statExport)) {
245ebdc69ceSgerardnico                $statExport['authors'] = [];
246f3748b38Sgerardnico            }
247ebdc69ceSgerardnico            /**
248ebdc69ceSgerardnico             * Analytics by users
249ebdc69ceSgerardnico             */
2502128d419Sgerardnico            $info = $changelog->getRevisionInfo($rev);
2512128d419Sgerardnico            if (is_array($info)) {
252ebdc69ceSgerardnico                $user = "*";
253ebdc69ceSgerardnico                if (array_key_exists('user', $info)) {
254ebdc69ceSgerardnico                    $user = $info['user'];
255ebdc69ceSgerardnico                }
256ebdc69ceSgerardnico                if (!array_key_exists('authors', $statExport['authors'])) {
257ebdc69ceSgerardnico                    $statExport['authors'][$user] = 0;
258ebdc69ceSgerardnico                }
259ebdc69ceSgerardnico                $statExport['authors'][$user] += 1;
260f3748b38Sgerardnico            }
2612128d419Sgerardnico        }
262f3748b38Sgerardnico
263f3748b38Sgerardnico        /**
264007225e5Sgerardnico         * Word and chars count
265007225e5Sgerardnico         * The word count does not take into account
266007225e5Sgerardnico         * words with non-words characters such as < =
267007225e5Sgerardnico         * Therefore the node and attribute are not taken in the count
268007225e5Sgerardnico         */
269007225e5Sgerardnico        $text = rawWiki($ID);
27004fd306cSNickeau        $statExport[self::CHAR_COUNT] = strlen($text);
27104fd306cSNickeau        $statExport[self::WORD_COUNT] = StringUtility::getWordCount($text);
272007225e5Sgerardnico
273007225e5Sgerardnico
274007225e5Sgerardnico        /**
275007225e5Sgerardnico         * Internal link distance summary calculation
276007225e5Sgerardnico         */
27704fd306cSNickeau        if (array_key_exists(self::INTERNAL_LINK_DISTANCE, $statExport)) {
27804fd306cSNickeau            $linkLengths = $statExport[self::INTERNAL_LINK_DISTANCE];
27904fd306cSNickeau            unset($statExport[self::INTERNAL_LINK_DISTANCE]);
280007225e5Sgerardnico            $countBacklinks = count($linkLengths);
28104fd306cSNickeau            $statExport[self::INTERNAL_LINK_DISTANCE]['avg'] = null;
28204fd306cSNickeau            $statExport[self::INTERNAL_LINK_DISTANCE]['max'] = null;
28304fd306cSNickeau            $statExport[self::INTERNAL_LINK_DISTANCE]['min'] = null;
284007225e5Sgerardnico            if ($countBacklinks > 0) {
28504fd306cSNickeau                $statExport[self::INTERNAL_LINK_DISTANCE]['avg'] = array_sum($linkLengths) / $countBacklinks;
28604fd306cSNickeau                $statExport[self::INTERNAL_LINK_DISTANCE]['max'] = max($linkLengths);
28704fd306cSNickeau                $statExport[self::INTERNAL_LINK_DISTANCE]['min'] = min($linkLengths);
288007225e5Sgerardnico            }
289007225e5Sgerardnico        }
290007225e5Sgerardnico
291007225e5Sgerardnico        /**
292007225e5Sgerardnico         * Quality Report / Rules
293007225e5Sgerardnico         */
294007225e5Sgerardnico        // The array that hold the results of the quality rules
295007225e5Sgerardnico        $ruleResults = array();
296007225e5Sgerardnico        // The array that hold the quality score details
297007225e5Sgerardnico        $qualityScores = array();
298007225e5Sgerardnico
299007225e5Sgerardnico
300007225e5Sgerardnico        /**
301007225e5Sgerardnico         * No fixme
302007225e5Sgerardnico         */
303ebdc69ceSgerardnico        if (array_key_exists(self::FIXME, $this->stats)) {
304007225e5Sgerardnico            $fixmeCount = $this->stats[self::FIXME];
305007225e5Sgerardnico            $statExport[self::FIXME] = $fixmeCount == null ? 0 : $fixmeCount;
306007225e5Sgerardnico            if ($fixmeCount != 0) {
307007225e5Sgerardnico                $ruleResults[self::RULE_FIXME] = self::FAILED;
308007225e5Sgerardnico                $qualityScores['no_' . self::FIXME] = 0;
309007225e5Sgerardnico            } else {
310007225e5Sgerardnico                $ruleResults[self::RULE_FIXME] = self::PASSED;
3117c33ecc6Sgerardnico                $qualityScores['no_' . self::FIXME] = $this->getConf(self::CONF_QUALITY_SCORE_NO_FIXME, 1);
312007225e5Sgerardnico            }
313ebdc69ceSgerardnico        }
314007225e5Sgerardnico
315007225e5Sgerardnico        /**
316007225e5Sgerardnico         * A title should be present
317007225e5Sgerardnico         */
31808ca4f85Sgerardnico        $titleScore = $this->getConf(self::CONF_QUALITY_SCORE_TITLE_PRESENT, 10);
319c3437056SNickeau        if (empty($this->metadata[PageTitle::TITLE])) {
320007225e5Sgerardnico            $ruleResults[self::RULE_TITLE_PRESENT] = self::FAILED;
321c3437056SNickeau            $ruleInfo[self::RULE_TITLE_PRESENT] = "Add a title for {$titleScore} points";
322*70bbd7f1Sgerardnico            $this->metadata[PageTitle::TITLE] = $dokuWikiMetadata[PageTitle::TITLE] ?? null;
323007225e5Sgerardnico            $qualityScores[self::RULE_TITLE_PRESENT] = 0;
324007225e5Sgerardnico        } else {
3257c33ecc6Sgerardnico            $qualityScores[self::RULE_TITLE_PRESENT] = $titleScore;
326007225e5Sgerardnico            $ruleResults[self::RULE_TITLE_PRESENT] = self::PASSED;
327007225e5Sgerardnico        }
328007225e5Sgerardnico
329007225e5Sgerardnico        /**
330007225e5Sgerardnico         * A description should be present
331007225e5Sgerardnico         */
33208ca4f85Sgerardnico        $descScore = $this->getConf(self::CONF_QUALITY_SCORE_DESCRIPTION_PRESENT, 8);
33337748cd8SNickeau        if (empty($this->metadata[self::DESCRIPTION])) {
334007225e5Sgerardnico            $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::FAILED;
335c3437056SNickeau            $ruleInfo[self::RULE_DESCRIPTION_PRESENT] = "Add a description for {$descScore} points";
33637748cd8SNickeau            $this->metadata[self::DESCRIPTION] = $dokuWikiMetadata[self::DESCRIPTION]["abstract"];
337007225e5Sgerardnico            $qualityScores[self::RULE_DESCRIPTION_PRESENT] = 0;
338007225e5Sgerardnico        } else {
3397c33ecc6Sgerardnico            $qualityScores[self::RULE_DESCRIPTION_PRESENT] = $descScore;
340007225e5Sgerardnico            $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::PASSED;
341007225e5Sgerardnico        }
342007225e5Sgerardnico
343007225e5Sgerardnico        /**
344007225e5Sgerardnico         * A canonical should be present
345007225e5Sgerardnico         */
34608ca4f85Sgerardnico        $canonicalScore = $this->getConf(self::CONF_QUALITY_SCORE_CANONICAL_PRESENT, 5);
347c3437056SNickeau        if (empty($this->metadata[Canonical::PROPERTY_NAME])) {
348f3748b38Sgerardnico            global $conf;
349f3748b38Sgerardnico            $root = $conf['start'];
3504cadd4f8SNickeau            if ($ID !== $root) {
351007225e5Sgerardnico                $qualityScores[self::RULE_CANONICAL_PRESENT] = 0;
352007225e5Sgerardnico                $ruleResults[self::RULE_CANONICAL_PRESENT] = self::FAILED;
353c3437056SNickeau                // no link to the documentation because we don't want any html in the json
354c3437056SNickeau                $ruleInfo[self::RULE_CANONICAL_PRESENT] = "Add a canonical for {$canonicalScore} points";
355f3748b38Sgerardnico            }
356007225e5Sgerardnico        } else {
3577c33ecc6Sgerardnico            $qualityScores[self::RULE_CANONICAL_PRESENT] = $canonicalScore;
358007225e5Sgerardnico            $ruleResults[self::RULE_CANONICAL_PRESENT] = self::PASSED;
359007225e5Sgerardnico        }
360007225e5Sgerardnico
361007225e5Sgerardnico        /**
362007225e5Sgerardnico         * Outline / Header structure
363007225e5Sgerardnico         */
364007225e5Sgerardnico        $treeError = 0;
365007225e5Sgerardnico        $headersCount = 0;
36604fd306cSNickeau        if (array_key_exists(self::HEADER_POSITION, $this->stats)) {
36704fd306cSNickeau            $headersCount = count($this->stats[self::HEADER_POSITION]);
36804fd306cSNickeau            unset($statExport[self::HEADER_POSITION]);
369007225e5Sgerardnico            for ($i = 1; $i < $headersCount; $i++) {
370ebdc69ceSgerardnico                $currentHeaderLevel = $this->stats[self::HEADER_STRUCT][$i];
371ebdc69ceSgerardnico                $previousHeaderLevel = $this->stats[self::HEADER_STRUCT][$i - 1];
372007225e5Sgerardnico                if ($currentHeaderLevel - $previousHeaderLevel > 1) {
373007225e5Sgerardnico                    $treeError += 1;
374007225e5Sgerardnico                    $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "The " . $i . " header (h" . $currentHeaderLevel . ") has a level bigger than its precedent (" . $previousHeaderLevel . ")";
375007225e5Sgerardnico                }
376007225e5Sgerardnico            }
377ebdc69ceSgerardnico            unset($statExport[self::HEADER_STRUCT]);
378007225e5Sgerardnico        }
379eee76a3dSgerardnico        $outlinePoints = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE, 3);
380007225e5Sgerardnico        if ($treeError > 0 || $headersCount == 0) {
381007225e5Sgerardnico            $qualityScores['correct_outline'] = 0;
382007225e5Sgerardnico            $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::FAILED;
383007225e5Sgerardnico            if ($headersCount == 0) {
384eee76a3dSgerardnico                $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "Add headings to create a document outline for {$outlinePoints} points";
385007225e5Sgerardnico            }
386007225e5Sgerardnico        } else {
387eee76a3dSgerardnico            $qualityScores['correct_outline'] = $outlinePoints;
388007225e5Sgerardnico            $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::PASSED;
389007225e5Sgerardnico        }
390007225e5Sgerardnico
391007225e5Sgerardnico
392007225e5Sgerardnico        /**
393007225e5Sgerardnico         * Document length
394007225e5Sgerardnico         */
395007225e5Sgerardnico        $minimalWordCount = 50;
396007225e5Sgerardnico        $maximalWordCount = 1500;
397007225e5Sgerardnico        $correctContentLength = true;
39808ca4f85Sgerardnico        $correctLengthScore = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_CONTENT, 10);
39904fd306cSNickeau        $missingWords = $minimalWordCount - $statExport[self::WORD_COUNT];
40008ca4f85Sgerardnico        if ($missingWords > 0) {
401007225e5Sgerardnico            $ruleResults[self::RULE_WORDS_MINIMAL] = self::FAILED;
402007225e5Sgerardnico            $correctContentLength = false;
40308ca4f85Sgerardnico            $ruleInfo[self::RULE_WORDS_MINIMAL] = "Add {$missingWords} words to get {$correctLengthScore} points";
404007225e5Sgerardnico        } else {
405007225e5Sgerardnico            $ruleResults[self::RULE_WORDS_MINIMAL] = self::PASSED;
406007225e5Sgerardnico        }
40704fd306cSNickeau        $tooMuchWords = $statExport[self::WORD_COUNT] - $maximalWordCount;
40808ca4f85Sgerardnico        if ($tooMuchWords > 0) {
409007225e5Sgerardnico            $ruleResults[self::RULE_WORDS_MAXIMAL] = self::FAILED;
41008ca4f85Sgerardnico            $ruleInfo[self::RULE_WORDS_MAXIMAL] = "Delete {$tooMuchWords} words to get {$correctLengthScore} points";
411007225e5Sgerardnico            $correctContentLength = false;
412007225e5Sgerardnico        } else {
413007225e5Sgerardnico            $ruleResults[self::RULE_WORDS_MAXIMAL] = self::PASSED;
414007225e5Sgerardnico        }
415007225e5Sgerardnico        if ($correctContentLength) {
41608ca4f85Sgerardnico            $qualityScores['correct_content_length'] = $correctLengthScore;
417007225e5Sgerardnico        } else {
418007225e5Sgerardnico            $qualityScores['correct_content_length'] = 0;
419007225e5Sgerardnico        }
420007225e5Sgerardnico
421007225e5Sgerardnico
422007225e5Sgerardnico        /**
423007225e5Sgerardnico         * Average Number of words by header section to text ratio
424007225e5Sgerardnico         */
425*70bbd7f1Sgerardnico        $headers = $this->stats[self::HEADING_COUNT] ?? null;
426007225e5Sgerardnico        if ($headers != null) {
427007225e5Sgerardnico            $headerCount = array_sum($headers);
428007225e5Sgerardnico            $headerCount--; // h1 is supposed to have no words
429007225e5Sgerardnico            if ($headerCount > 0) {
430007225e5Sgerardnico
431*70bbd7f1Sgerardnico                $wordCount = $this->stats[self::WORD_COUNT] ?? 0;
432*70bbd7f1Sgerardnico                $avgWordsCountBySection = round($wordCount / $headerCount);
433007225e5Sgerardnico                $statExport['word_section_count']['avg'] = $avgWordsCountBySection;
434007225e5Sgerardnico
435007225e5Sgerardnico                /**
436007225e5Sgerardnico                 * Min words by header section
437007225e5Sgerardnico                 */
438007225e5Sgerardnico                $wordsByHeaderMin = 20;
439007225e5Sgerardnico                /**
440007225e5Sgerardnico                 * Max words by header section
441007225e5Sgerardnico                 */
442007225e5Sgerardnico                $wordsByHeaderMax = 300;
443007225e5Sgerardnico                $correctAverageWordsBySection = true;
444007225e5Sgerardnico                if ($avgWordsCountBySection < $wordsByHeaderMin) {
445007225e5Sgerardnico                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::FAILED;
446007225e5Sgerardnico                    $correctAverageWordsBySection = false;
44708ca4f85Sgerardnico                    $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = "The number of words by section is less than {$wordsByHeaderMin}";
448007225e5Sgerardnico                } else {
449007225e5Sgerardnico                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::PASSED;
450007225e5Sgerardnico                }
451007225e5Sgerardnico                if ($avgWordsCountBySection > $wordsByHeaderMax) {
452007225e5Sgerardnico                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::FAILED;
453007225e5Sgerardnico                    $correctAverageWordsBySection = false;
454007225e5Sgerardnico                    $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = "The number of words by section is more than {$wordsByHeaderMax}";
455007225e5Sgerardnico                } else {
456007225e5Sgerardnico                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::PASSED;
457007225e5Sgerardnico                }
458007225e5Sgerardnico                if ($correctAverageWordsBySection) {
459007225e5Sgerardnico                    $qualityScores['correct_word_avg_by_section'] = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE, 10);
460007225e5Sgerardnico                } else {
461007225e5Sgerardnico                    $qualityScores['correct_word_avg_by_section'] = 0;
462007225e5Sgerardnico                }
463007225e5Sgerardnico
464007225e5Sgerardnico            }
465007225e5Sgerardnico        }
466007225e5Sgerardnico
467007225e5Sgerardnico        /**
468007225e5Sgerardnico         * Internal Backlinks rule
469007225e5Sgerardnico         *
470c3437056SNickeau         * We used the database table to get the backlinks
471c3437056SNickeau         * because the replication is based on it
472c3437056SNickeau         * If the dokuwiki index is not up to date, we may got
473c3437056SNickeau         * inconsistency
474007225e5Sgerardnico         */
47504fd306cSNickeau        try {
476c3437056SNickeau            $countBacklinks = BacklinkCount::createFromResource($this->page)
477c3437056SNickeau                ->setReadStore(MetadataDbStore::class)
478c3437056SNickeau                ->getValueOrDefault();
47904fd306cSNickeau        } catch (ExceptionNotFound $e) {
48004fd306cSNickeau            $countBacklinks = 0;
48104fd306cSNickeau        }
482c3437056SNickeau        $statExport[BacklinkCount::getPersistentName()] = $countBacklinks;
483d262537cSgerardnico        $backlinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR, 1);
484007225e5Sgerardnico        if ($countBacklinks == 0) {
485c3437056SNickeau
486c3437056SNickeau            $qualityScores[BacklinkCount::getPersistentName()] = 0;
487007225e5Sgerardnico            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::FAILED;
488d262537cSgerardnico            $ruleInfo[self::RULE_INTERNAL_BACKLINKS_MIN] = "Add backlinks for {$backlinkScore} point each";
489c3437056SNickeau
490007225e5Sgerardnico        } else {
491d262537cSgerardnico
492c3437056SNickeau            $qualityScores[BacklinkCount::getPersistentName()] = $countBacklinks * $backlinkScore;
493007225e5Sgerardnico            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::PASSED;
494007225e5Sgerardnico        }
495007225e5Sgerardnico
496007225e5Sgerardnico        /**
497007225e5Sgerardnico         * Internal links
498007225e5Sgerardnico         */
499*70bbd7f1Sgerardnico        $internalLinksCount = $this->stats[self::INTERNAL_LINK_COUNT] ?? null;
500d262537cSgerardnico        $internalLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR, 1);
501007225e5Sgerardnico        if ($internalLinksCount == 0) {
50204fd306cSNickeau            $qualityScores[self::INTERNAL_LINK_COUNT] = 0;
503007225e5Sgerardnico            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::FAILED;
504d262537cSgerardnico            $ruleInfo[self::RULE_INTERNAL_LINKS_MIN] = "Add internal links for {$internalLinkScore} point each";
505007225e5Sgerardnico        } else {
506007225e5Sgerardnico            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::PASSED;
50704fd306cSNickeau            $qualityScores[self::INTERNAL_LINK_COUNT] = $countBacklinks * $internalLinkScore;
508007225e5Sgerardnico        }
509007225e5Sgerardnico
510007225e5Sgerardnico        /**
511007225e5Sgerardnico         * Broken Links
512007225e5Sgerardnico         */
513d262537cSgerardnico        $brokenLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR, 2);
514ebdc69ceSgerardnico        $brokenLinksCount = 0;
51504fd306cSNickeau        if (array_key_exists(self::INTERNAL_LINK_BROKEN_COUNT, $this->stats)) {
51604fd306cSNickeau            $brokenLinksCount = $this->stats[self::INTERNAL_LINK_BROKEN_COUNT];
517ebdc69ceSgerardnico        }
518007225e5Sgerardnico        if ($brokenLinksCount > 2) {
51904fd306cSNickeau            $qualityScores['no_' . self::INTERNAL_LINK_BROKEN_COUNT] = 0;
520007225e5Sgerardnico            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::FAILED;
521d262537cSgerardnico            $ruleInfo[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = "Delete the {$brokenLinksCount} broken links and add {$brokenLinkScore} points";
522007225e5Sgerardnico        } else {
52304fd306cSNickeau            $qualityScores['no_' . self::INTERNAL_LINK_BROKEN_COUNT] = $brokenLinkScore;
524007225e5Sgerardnico            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::PASSED;
525007225e5Sgerardnico        }
526007225e5Sgerardnico
527007225e5Sgerardnico        /**
528e8b2ff59SNickeau         * Media
529e8b2ff59SNickeau         */
530e8b2ff59SNickeau        $mediasStats = [
53104fd306cSNickeau            "total_count" => self::getAndUnset($statExport, self::MEDIA_COUNT, 0),
53204fd306cSNickeau            "internal_count" => self::getAndUnset($statExport, self::INTERNAL_MEDIA_COUNT, 0),
53304fd306cSNickeau            "internal_broken_count" => self::getAndUnset($statExport, self::INTERNAL_BROKEN_MEDIA_COUNT, 0),
53404fd306cSNickeau            "external_count" => self::getAndUnset($statExport, self::EXTERNAL_MEDIA_COUNT, 0)
535e8b2ff59SNickeau        ];
536e8b2ff59SNickeau        $statExport['media'] = $mediasStats;
537e8b2ff59SNickeau
538e8b2ff59SNickeau        /**
539007225e5Sgerardnico         * Changes, the more changes the better
540007225e5Sgerardnico         */
54104fd306cSNickeau        $qualityScores[self::EDITS_COUNT] = $statExport[self::EDITS_COUNT] * $this->getConf(self::CONF_QUALITY_SCORE_CHANGES_FACTOR, 0.25);
542007225e5Sgerardnico
543007225e5Sgerardnico
544007225e5Sgerardnico        /**
545007225e5Sgerardnico         * Quality Score
546007225e5Sgerardnico         */
547007225e5Sgerardnico        ksort($qualityScores);
548007225e5Sgerardnico        $qualityScoring = array();
54908ca4f85Sgerardnico        $qualityScoring[self::SCORE] = array_sum($qualityScores);
550007225e5Sgerardnico        $qualityScoring["scores"] = $qualityScores;
551007225e5Sgerardnico
552007225e5Sgerardnico
553007225e5Sgerardnico        /**
554007225e5Sgerardnico         * The rule that if broken will set the quality level to low
555007225e5Sgerardnico         */
556007225e5Sgerardnico        $brokenRules = array();
557007225e5Sgerardnico        foreach ($ruleResults as $ruleName => $ruleResult) {
558007225e5Sgerardnico            if ($ruleResult == self::FAILED) {
559007225e5Sgerardnico                $brokenRules[] = $ruleName;
560007225e5Sgerardnico            }
561007225e5Sgerardnico        }
562007225e5Sgerardnico        $ruleErrorCount = sizeof($brokenRules);
563007225e5Sgerardnico        if ($ruleErrorCount > 0) {
564007225e5Sgerardnico            $qualityResult = $ruleErrorCount . " quality rules errors";
565007225e5Sgerardnico        } else {
566007225e5Sgerardnico            $qualityResult = "All quality rules passed";
567007225e5Sgerardnico        }
568007225e5Sgerardnico
569007225e5Sgerardnico        /**
570fa5961eaSgerardnico         * Low level Computation
571007225e5Sgerardnico         */
572007225e5Sgerardnico        $mandatoryRules = preg_split("/,/", $this->getConf(self::CONF_MANDATORY_QUALITY_RULES));
573007225e5Sgerardnico        $mandatoryRulesBroken = [];
574007225e5Sgerardnico        foreach ($mandatoryRules as $lowLevelRule) {
575007225e5Sgerardnico            if (in_array($lowLevelRule, $brokenRules)) {
576007225e5Sgerardnico                $mandatoryRulesBroken[] = $lowLevelRule;
577007225e5Sgerardnico            }
578007225e5Sgerardnico        }
579fa5961eaSgerardnico        /**
580c3437056SNickeau         * Low Level
581fa5961eaSgerardnico         */
582007225e5Sgerardnico        $lowLevel = false;
58385e82846SNickeau        $brokenRulesCount = sizeof($mandatoryRulesBroken);
58485e82846SNickeau        if ($brokenRulesCount > 0) {
585007225e5Sgerardnico            $lowLevel = true;
58685e82846SNickeau            $quality["message"] = "$brokenRulesCount mandatory rules broken.";
58785e82846SNickeau        } else {
58885e82846SNickeau            $quality["message"] = "No mandatory rules broken";
589007225e5Sgerardnico        }
59004fd306cSNickeau        if ($this->page->isSlot()) {
591c3437056SNickeau            $lowLevel = false;
5929b9e6d1fSgerardnico        }
59304fd306cSNickeau        try {
594c3437056SNickeau            $this->page->setLowQualityIndicatorCalculation($lowLevel);
59504fd306cSNickeau        } catch (ExceptionCompile $e) {
59604fd306cSNickeau            LogUtility::msg("An error has occurred while saving the low quality level. Error: {$e->getMessage()}");
59704fd306cSNickeau        }
598007225e5Sgerardnico
599007225e5Sgerardnico        /**
600007225e5Sgerardnico         * Building the quality object in order
601007225e5Sgerardnico         */
60204fd306cSNickeau        $quality[self::LOW] = $lowLevel;
603007225e5Sgerardnico        if (sizeof($mandatoryRulesBroken) > 0) {
604007225e5Sgerardnico            ksort($mandatoryRulesBroken);
60504fd306cSNickeau            $quality[self::FAILED_MANDATORY_RULES] = $mandatoryRulesBroken;
606007225e5Sgerardnico        }
60708ca4f85Sgerardnico        $quality[self::SCORING] = $qualityScoring;
60804fd306cSNickeau        $quality[self::RULES][self::RESULT] = $qualityResult;
609007225e5Sgerardnico        if (!empty($ruleInfo)) {
61004fd306cSNickeau            $quality[self::RULES]["info"] = $ruleInfo;
611007225e5Sgerardnico        }
612007225e5Sgerardnico
613007225e5Sgerardnico        ksort($ruleResults);
61404fd306cSNickeau        $quality[self::RULES][self::DETAILS] = $ruleResults;
615007225e5Sgerardnico
616007225e5Sgerardnico        /**
617007225e5Sgerardnico         * Metadata
618007225e5Sgerardnico         */
61904fd306cSNickeau        try {
62004fd306cSNickeau            $requestedPage = MarkupPath::createPageFromExecutingId();
62104fd306cSNickeau        } catch (ExceptionCompile $e) {
62204fd306cSNickeau            LogUtility::msg("The global ID is unknown, we can't find the requested page. Analytics was stopped");
62304fd306cSNickeau            return;
62404fd306cSNickeau        }
62504fd306cSNickeau        $meta = $requestedPage->getMetadataForRendering();
62637748cd8SNickeau        foreach ($meta as $key => $value) {
62737748cd8SNickeau            /**
62837748cd8SNickeau             * The metadata may have been set
62937748cd8SNickeau             * by frontmatter
63037748cd8SNickeau             */
63137748cd8SNickeau            if (!isset($this->metadata[$key])) {
63237748cd8SNickeau                $this->metadata[$key] = $value;
63304fd306cSNickeau                if ($key === PageH1::getName()) {
63404fd306cSNickeau                    $this->metadata[PageH1::H1_PARSED] = MetadataDokuWikiStore::getOrCreateFromResource($requestedPage)->getFromName(PageH1::H1_PARSED);
63504fd306cSNickeau                }
636c42a1196Sgerardnico            }
63737748cd8SNickeau        }
638007225e5Sgerardnico
639007225e5Sgerardnico
640007225e5Sgerardnico        /**
641007225e5Sgerardnico         * Building the Top JSON in order
642007225e5Sgerardnico         */
6432c067407Sgerardnico        $finalStats = array();
644c42a1196Sgerardnico        $finalStats["date"] = date('Y-m-d H:i:s', time());
64537748cd8SNickeau        ksort($this->metadata);
64604fd306cSNickeau        $finalStats[self::METADATA] = $this->metadata;
647007225e5Sgerardnico        ksort($statExport);
64804fd306cSNickeau        $finalStats[self::STATISTICS] = $statExport;
64904fd306cSNickeau        $finalStats[self::QUALITY] = $quality; // Quality after the sort to get them at the end
650007225e5Sgerardnico
651007225e5Sgerardnico
652007225e5Sgerardnico        /**
653007225e5Sgerardnico         * The result can be seen with
654007225e5Sgerardnico         * doku.php?id=somepage&do=export_combo_analysis
6557c33ecc6Sgerardnico         *
6567c33ecc6Sgerardnico         * Set the header temporarily for the export.php file
65785e82846SNickeau         *
65885e82846SNickeau         * The mode in the export is
659007225e5Sgerardnico         */
66085e82846SNickeau        $mode = "combo_" . $this->getPluginComponent();
6617c33ecc6Sgerardnico        p_set_metadata(
66204fd306cSNickeau            $requestedPage->getPageId(),
66385e82846SNickeau            array("format" => array($mode => array("Content-Type" => 'application/json'))),
6647c33ecc6Sgerardnico            false,
665c3437056SNickeau            false // Persistence is needed because there is a cache
6667c33ecc6Sgerardnico        );
6672c067407Sgerardnico        $json_encoded = json_encode($finalStats, JSON_PRETTY_PRINT);
668007225e5Sgerardnico
66904fd306cSNickeau        $this->doc = $json_encoded;
670007225e5Sgerardnico
671007225e5Sgerardnico    }
672007225e5Sgerardnico
673007225e5Sgerardnico    /**
674007225e5Sgerardnico     */
675007225e5Sgerardnico    public function getFormat()
676007225e5Sgerardnico    {
677531e725cSNickeau        return self::RENDERER_FORMAT;
678007225e5Sgerardnico    }
679007225e5Sgerardnico
680007225e5Sgerardnico
681007225e5Sgerardnico    public function header($text, $level, $pos)
682007225e5Sgerardnico    {
68304fd306cSNickeau        if (!array_key_exists(self::HEADING_COUNT, $this->stats)) {
68404fd306cSNickeau            $this->stats[self::HEADING_COUNT] = [];
685ebdc69ceSgerardnico        }
686ebdc69ceSgerardnico        $heading = 'h' . $level;
687ebdc69ceSgerardnico        if (!array_key_exists(
688ebdc69ceSgerardnico            $heading,
68904fd306cSNickeau            $this->stats[self::HEADING_COUNT])) {
69004fd306cSNickeau            $this->stats[self::HEADING_COUNT][$heading] = 0;
691ebdc69ceSgerardnico        }
69204fd306cSNickeau        $this->stats[self::HEADING_COUNT][$heading]++;
693ebdc69ceSgerardnico
694007225e5Sgerardnico        $this->headerId++;
69504fd306cSNickeau        $this->stats[self::HEADER_POSITION][$this->headerId] = $heading;
696ebdc69ceSgerardnico
697ebdc69ceSgerardnico        /**
698ebdc69ceSgerardnico         * Store the level of each heading
699ebdc69ceSgerardnico         * They should only go from low to highest value
700ebdc69ceSgerardnico         * for a good outline
701ebdc69ceSgerardnico         */
70204fd306cSNickeau        if (!array_key_exists(self::HEADING_COUNT, $this->stats)) {
703ebdc69ceSgerardnico            $this->stats[self::HEADER_STRUCT] = [];
704ebdc69ceSgerardnico        }
705ebdc69ceSgerardnico        $this->stats[self::HEADER_STRUCT][] = $level;
706007225e5Sgerardnico
707007225e5Sgerardnico    }
708007225e5Sgerardnico
709007225e5Sgerardnico    public function smiley($smiley)
710007225e5Sgerardnico    {
711*70bbd7f1Sgerardnico        if ($smiley == 'FIXME') {
712*70bbd7f1Sgerardnico            $totalFixme = $this->stats[self::FIXME] ?? 0;
713*70bbd7f1Sgerardnico            $this->stats[self::FIXME] = $totalFixme + 1;
714*70bbd7f1Sgerardnico        }
715007225e5Sgerardnico    }
716007225e5Sgerardnico
717007225e5Sgerardnico    public function linebreak()
718007225e5Sgerardnico    {
719007225e5Sgerardnico        if (!$this->tableopen) {
720*70bbd7f1Sgerardnico            $linebreak = $this->stats['linebreak'] ?? 0;
721*70bbd7f1Sgerardnico            $this->stats['linebreak'] = $linebreak + 1;
722007225e5Sgerardnico        }
723007225e5Sgerardnico    }
724007225e5Sgerardnico
725007225e5Sgerardnico    public function table_open($maxcols = null, $numrows = null, $pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
726007225e5Sgerardnico    {
727007225e5Sgerardnico        $this->tableopen = true;
728007225e5Sgerardnico    }
729007225e5Sgerardnico
730007225e5Sgerardnico    public function table_close($pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
731007225e5Sgerardnico    {
732007225e5Sgerardnico        $this->tableopen = false;
733007225e5Sgerardnico    }
734007225e5Sgerardnico
735007225e5Sgerardnico    public function hr()
736007225e5Sgerardnico    {
737*70bbd7f1Sgerardnico        $hr = $this->stats['hr'] ?? 0;
738*70bbd7f1Sgerardnico        $this->stats['hr'] = $hr + 1;
739007225e5Sgerardnico    }
740007225e5Sgerardnico
741007225e5Sgerardnico    public function quote_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
742007225e5Sgerardnico    {
743*70bbd7f1Sgerardnico        $quoteCount = $this->stats['quote_count'] ?? 0;
744*70bbd7f1Sgerardnico        $this->stats['quote_count'] = $quoteCount + 1;
745007225e5Sgerardnico        $this->quotelevel++;
746*70bbd7f1Sgerardnico        $quoteNest = $this->stats['quote_nest'] ?? 0;
747*70bbd7f1Sgerardnico        $this->stats['quote_nest'] = max($this->quotelevel, $quoteNest);
748007225e5Sgerardnico    }
749007225e5Sgerardnico
750007225e5Sgerardnico    public function quote_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
751007225e5Sgerardnico    {
752007225e5Sgerardnico        $this->quotelevel--;
753007225e5Sgerardnico    }
754007225e5Sgerardnico
755007225e5Sgerardnico    public function strong_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
756007225e5Sgerardnico    {
757007225e5Sgerardnico        $this->formattingBracket++;
758007225e5Sgerardnico    }
759007225e5Sgerardnico
760007225e5Sgerardnico    public function strong_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
761007225e5Sgerardnico    {
762007225e5Sgerardnico        $this->formattingBracket--;
763007225e5Sgerardnico    }
764007225e5Sgerardnico
765007225e5Sgerardnico    public function emphasis_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
766007225e5Sgerardnico    {
767007225e5Sgerardnico        $this->formattingBracket++;
768007225e5Sgerardnico    }
769007225e5Sgerardnico
770007225e5Sgerardnico    public function emphasis_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
771007225e5Sgerardnico    {
772007225e5Sgerardnico        $this->formattingBracket--;
773007225e5Sgerardnico    }
774007225e5Sgerardnico
775007225e5Sgerardnico    public function underline_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
776007225e5Sgerardnico    {
777007225e5Sgerardnico        $this->formattingBracket++;
778007225e5Sgerardnico    }
779007225e5Sgerardnico
78004fd306cSNickeau    public function addToDescription($text)
78104fd306cSNickeau    {
7824cadd4f8SNickeau
7834cadd4f8SNickeau    }
7844cadd4f8SNickeau
785007225e5Sgerardnico    public function underline_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
786007225e5Sgerardnico    {
787007225e5Sgerardnico        $this->formattingBracket--;
788007225e5Sgerardnico    }
789007225e5Sgerardnico
790007225e5Sgerardnico    public function cdata($text)
791007225e5Sgerardnico    {
792007225e5Sgerardnico
793007225e5Sgerardnico        /**
794007225e5Sgerardnico         * It seems that you receive cdata
795007225e5Sgerardnico         * when emphasis_open / underline_open / strong_open
796007225e5Sgerardnico         * Stats are not for them
797007225e5Sgerardnico         */
798007225e5Sgerardnico        if (!$this->formattingBracket) return;
799007225e5Sgerardnico
800007225e5Sgerardnico        $this->plainTextId++;
801007225e5Sgerardnico
802007225e5Sgerardnico        /**
803007225e5Sgerardnico         * Length
804007225e5Sgerardnico         */
805007225e5Sgerardnico        $len = strlen($text);
806007225e5Sgerardnico        $this->stats[self::PLAINTEXT][$this->plainTextId]['len'] = $len;
807007225e5Sgerardnico
808007225e5Sgerardnico
809007225e5Sgerardnico        /**
810007225e5Sgerardnico         * Multi-formatting
811007225e5Sgerardnico         */
812007225e5Sgerardnico        if ($this->formattingBracket > 1) {
813007225e5Sgerardnico            $numberOfFormats = 1 * ($this->formattingBracket - 1);
814007225e5Sgerardnico            $this->stats[self::PLAINTEXT][$this->plainTextId]['multiformat'] += $numberOfFormats;
815007225e5Sgerardnico        }
816007225e5Sgerardnico
817007225e5Sgerardnico        /**
818007225e5Sgerardnico         * Total
819007225e5Sgerardnico         */
820*70bbd7f1Sgerardnico        $totalLen = $this->stats[self::PLAINTEXT][0] ?? 0;
821*70bbd7f1Sgerardnico        $this->stats[self::PLAINTEXT][0] = $totalLen + $len;
822*70bbd7f1Sgerardnico
823007225e5Sgerardnico    }
824007225e5Sgerardnico
825007225e5Sgerardnico    public function internalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
826007225e5Sgerardnico    {
82704fd306cSNickeau        $this->stats[self::INTERNAL_MEDIA_COUNT]++;
828007225e5Sgerardnico    }
829007225e5Sgerardnico
830007225e5Sgerardnico    public function externalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
831007225e5Sgerardnico    {
83204fd306cSNickeau        $this->stats[self::EXTERNAL_MEDIA_COUNT]++;
833007225e5Sgerardnico    }
834007225e5Sgerardnico
835007225e5Sgerardnico    public function reset()
836007225e5Sgerardnico    {
837007225e5Sgerardnico        $this->stats = array();
83837748cd8SNickeau        $this->metadata = array();
839007225e5Sgerardnico        $this->headerId = 0;
840007225e5Sgerardnico    }
841007225e5Sgerardnico
842c3437056SNickeau    public function setAnalyticsMetaForReporting($key, $value)
843007225e5Sgerardnico    {
84437748cd8SNickeau        $this->metadata[$key] = $value;
845007225e5Sgerardnico    }
846007225e5Sgerardnico
847007225e5Sgerardnico
848007225e5Sgerardnico}
849007225e5Sgerardnico
850