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