ILIAS  trunk Revision v11.0_alpha-1715-g7fc467680fb
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
EstimatedReadingTime.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
21 namespace ILIAS\Refinery\String;
22 
23 use ErrorException;
28 use DOMDocument;
29 use DOMText;
30 use DOMCdataSection;
31 use DOMXPath;
32 use 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