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"; 32270bbd7f1Sgerardnico $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 */ 42570bbd7f1Sgerardnico $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 43170bbd7f1Sgerardnico $wordCount = $this->stats[self::WORD_COUNT] ?? 0; 43270bbd7f1Sgerardnico $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 */ 49970bbd7f1Sgerardnico $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( 662*cc610584Sgerardnico $requestedPage->getWikiId(), 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 { 71170bbd7f1Sgerardnico if ($smiley == 'FIXME') { 71270bbd7f1Sgerardnico $totalFixme = $this->stats[self::FIXME] ?? 0; 71370bbd7f1Sgerardnico $this->stats[self::FIXME] = $totalFixme + 1; 71470bbd7f1Sgerardnico } 715007225e5Sgerardnico } 716007225e5Sgerardnico 717007225e5Sgerardnico public function linebreak() 718007225e5Sgerardnico { 719007225e5Sgerardnico if (!$this->tableopen) { 72070bbd7f1Sgerardnico $linebreak = $this->stats['linebreak'] ?? 0; 72170bbd7f1Sgerardnico $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 { 73770bbd7f1Sgerardnico $hr = $this->stats['hr'] ?? 0; 73870bbd7f1Sgerardnico $this->stats['hr'] = $hr + 1; 739007225e5Sgerardnico } 740007225e5Sgerardnico 741007225e5Sgerardnico public function quote_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 742007225e5Sgerardnico { 74370bbd7f1Sgerardnico $quoteCount = $this->stats['quote_count'] ?? 0; 74470bbd7f1Sgerardnico $this->stats['quote_count'] = $quoteCount + 1; 745007225e5Sgerardnico $this->quotelevel++; 74670bbd7f1Sgerardnico $quoteNest = $this->stats['quote_nest'] ?? 0; 74770bbd7f1Sgerardnico $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 */ 82070bbd7f1Sgerardnico $totalLen = $this->stats[self::PLAINTEXT][0] ?? 0; 82170bbd7f1Sgerardnico $this->stats[self::PLAINTEXT][0] = $totalLen + $len; 82270bbd7f1Sgerardnico 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