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