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