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