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