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