ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
EstimatedReadingTime.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21namespace ILIAS\Refinery\String;
22
23use ErrorException;
27use InvalidArgumentException;
28use DOMDocument;
29use DOMText;
30use DOMCdataSection;
31use DOMXPath;
32use LibXMLError;
33
35{
38
39 private int $wordsPerMinute = 275;
40 private bool $withImages;
41 private bool $xmlErrorState = false;
43 private array $xmlErrors = [];
44
45 public function __construct(bool $withImages)
46 {
47 $this->withImages = $withImages;
48 }
49
53 public function transform($from): int
54 {
55 if (!is_string($from)) {
56 throw new InvalidArgumentException(__METHOD__ . ' the argument is not a string.');
57 }
58
59 return $this->calculate($from);
60 }
61
62 private function calculate(string $text): int
63 {
64 $text = mb_encode_numericentity(
65 '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>' . $text . '</body></html>',
66 [0x80, 0x10FFFF, 0, ~0],
67 'UTF-8'
68 );
69
70 $document = new DOMDocument();
71
72 try {
73 set_error_handler(static function (int $severity, string $message, string $file, int $line): void {
74 throw new ErrorException($message, $severity, $severity, $file, $line);
75 });
76
77 $this->beginXmlLogging();
78
79 if (!$document->loadHTML($text)) {
80 throw new InvalidArgumentException(__METHOD__ . ' the argument is not a parsable XHTML string.');
81 }
82 } catch (ErrorException $e) {
83 throw new InvalidArgumentException(__METHOD__ . ' the argument is not a parsable XHTML string: ' . $e->getMessage());
84 } finally {
85 restore_error_handler();
86 $this->addErrors();
87 $this->endXmlLogging();
88 }
89
90 $numberOfWords = 0;
91
92 $xpath = new DOMXPath($document);
93 $textNodes = $xpath->query('//text()');
94 if ($textNodes->length > 0) {
95 foreach ($textNodes as $textNode) {
97 if ($textNode instanceof DOMCdataSection) {
98 continue;
99 }
100
101 $wordsInContent = array_filter(preg_split('/\s+/', $textNode->textContent));
102
103 $wordsInContent = array_filter($wordsInContent, static function (string $word): bool {
104 return preg_replace('/^\pP$/u', '', $word) !== '';
105 });
106
107 $numberOfWords += count($wordsInContent);
108 }
109 }
110
111 if ($this->withImages) {
112 $imageNodes = $document->getElementsByTagName('img');
113 $numberOfWords += $this->calculateWordsForImages($imageNodes->length);
114 }
115
116 $readingTime = ceil($numberOfWords / $this->wordsPerMinute);
117
118 return (int) $readingTime;
119 }
120
126 private function calculateWordsForImages(int $numberOfImages): float
127 {
128 $time = 0.0;
129
130 for ($i = 1; $i <= $numberOfImages; $i++) {
131 if ($i >= 10) {
132 $time += 3 * ($this->wordsPerMinute / 60);
133 } else {
134 $time += (12 - ($i - 1)) * ($this->wordsPerMinute / 60);
135 }
136 }
137
138 return $time;
139 }
140
141 private function beginXmlLogging(): void
142 {
143 $this->xmlErrorState = libxml_use_internal_errors(true);
144 libxml_clear_errors();
145 }
146
147 private function addErrors(): void
148 {
149 $currentErrors = libxml_get_errors();
150 libxml_clear_errors();
151
152 $this->xmlErrors = $currentErrors;
153 }
154
155 private function endXmlLogging(): void
156 {
157 libxml_use_internal_errors($this->xmlErrorState);
158 }
159
160 private function xmlErrorsOccurred(): bool
161 {
162 return $this->xmlErrors !== [];
163 }
164
165 private function xmlErrorsToString(): string
166 {
167 $text = '';
168 foreach ($this->xmlErrors as $error) {
169 $text .= implode(',', [
170 'level=' . $error->level,
171 'code=' . $error->code,
172 'line=' . $error->line,
173 'col=' . $error->column,
174 'msg=' . trim($error->message)
175 ]) . "\n";
176 }
177
178 return $text;
179 }
180}
ilErrorHandling $error
Definition: class.ilias.php:69
A transformation is a function from one datatype to another.
$message
Definition: xapiexit.php:31