diff --git a/README.md b/README.md
index 3fb1c95..ffdf798 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,11 @@
-# Grav ReadingTime Plugin
+# Grav BetterReadingTime Plugin
-**ReadingTime** is a [Grav](http://github.com/getgrav/grav) plugin which allows Grav to display the reading time of a page's content. This is especially useful for blogs and other sites as it gives the reader a quick idea of how much time they will need to set aside to read the page in full.
+**BetterReadingTime** is a [Grav](http://github.com/getgrav/grav) plugin which allows Grav to display the reading time of a page's content. This is especially useful for blogs and other sites as it gives the reader a quick idea of how much time they will need to set aside to read the page in full.
+
+***This fork differs from the original in three main ways:***
+1. ***Reading label is translated, not just "minutes" and "seconds".***
+2. ***Reading time (and label) is cached, in translated form, in the page headers of every page. This negates the need to run the calculation on every page load if reading time is present in the frontmatter.***
+3. ***Disabled seconds. Reading time is rounded to the nearest minute for a cleaner view.***
Enabling the plugin is very simple. Just install the plugin folder to `/user/plugins/` in your Grav install. By default, the plugin is enabled.
@@ -21,48 +26,14 @@ The contents of the zipped folder should now be located in the `/your/site/grav/
Place the following line of code in the theme file you wish to add the ReadingTime plugin for:
```
-{{ page.content|readingtime }}
-```
-
-You need to pass a Twig Filter 'readingtime' to display the reading time values. You can translate the labels with this example:
-
-```
-{{ page.content|readingtime({'minutes_label': 'Minuti', 'minute_label': 'Minuto', 'seconds_label': 'Secondi', 'second_label': 'Secondo'}) }}
-```
-
-I used Italian translation for the labels, you can change with your language.
-
-If you need you can change the format with this avariable variables (the code is default format):
-
-```
-{{ page.content|readingtime({'format': '{minutes_short_count} {minutes_text}, {seconds_short_count} {seconds_text}'}) }}
+{% if config.plugins.readingtime.enabled %}
+{{ page.header.readingTime }}{{ page|readingtime }}
+{% endif %}
```
-Available variables:
-
-| Variable | Description | Example |
-| :---------------- | :---------------------- | :--------------------------------------------------------------------------- |
-| `{minute_label}` | Minute Label (Singular) | `minute` |
-| `{minutes_label}` | Minutes Label (Plural) | `minutes` |
-| `{second_label}` | Second Label (Singular) | `second` |
-| `{seconds_label}` | Second Label (Plural) | `seconds` |
-| `{format}` | Display Format | `{minutes_text} {minutes_short_count}, {seconds_text} {seconds_short_count}` |
-
-Not available to edit but used in the format variable:
-
-| Variable | Description | Example |
-| :---------------------- | :--------------------------------------- | :------ |
-| `{minutes_short_count}` | Displays Minutes with Abbreviated Digits | `2` |
-| `{seconds_short_count}` | Displays Seconds with Abbreviated Digits | `9` |
-| `{minutes_long_count}` | Displays Minutes in Double Digits | `02` |
-| `{seconds_long_count}` | Displays Seconds in Double Digits | `09` |
-
-Display variables for text labels:
+You need both of these twig variables to make it work. I can't remember why both are required, but if I remove either one it breaks. So just use both.
-| Variable | Description | Example |
-| :--------------- | :------------------------------------------------------------------------------- | :-------- |
-| `{minutes_text}` | Displays the Minutes Text Label (Singular or Plural, Based on Number of Minutes) | `minute` |
-| `{seconds_text}` | Displays the Seconds Text Label (Singular or Plural, Based on Number of Seconds) | `seconds` |
+I haven't tested image views so I can't tell you if they still work. I also dabbled with the "estimated reading time" PR on the original repo but eventually decided not to include it, so you might find vestigial code from that PR here that I forgot to clean up.
>> NOTE: Any time you are making alterations to a theme's files, you will want to duplicate the theme folder in the `user/themes/` directory, rename it, and set the new name as your active theme. This will ensure that you don't lose your customizations in the event that a theme is updated. Once you have tested the change thoroughly, you can delete or back up that folder elsewhere.
diff --git a/classes/TwigReadingTimeFilters.php b/classes/TwigReadingTimeFilters.php
index 7ef5137..9dbf29e 100644
--- a/classes/TwigReadingTimeFilters.php
+++ b/classes/TwigReadingTimeFilters.php
@@ -8,129 +8,138 @@
class TwigReadingTimeFilters extends Twig_Extension
{
- private $grav;
-
- public function __construct()
- {
- $this->grav = Grav::instance();
- }
-
- public function getName()
- {
- return 'TwigReadingTimeFilters';
- }
-
- public function getFilters()
- {
- return [
- new \Twig_SimpleFilter( 'readingtime', [$this, 'getReadingTime'] )
- ];
- }
-
- public function validatePattern($seconds_per_image)
- {
- // Get regex that is used in the user interface
- $pattern = '/' . $this->grav['plugins']->get('readingtime')->blueprints()->schema()->get('seconds_per_image')['validate']['pattern'] . '/';
-
- if (preg_match($pattern, $seconds_per_image, $matches) === false) {
- return false;
- }
-
- // Note: "$matches[0] will contain the text that matched the full pattern"
- // https://www.php.net/manual/en/function.preg-match.php
- return strlen($seconds_per_image) === strlen($matches[0]);
- }
-
- public function getReadingTime( $content, $params = array() )
- {
+ private $grav;
- $this->mergeConfig($this->grav['page']);
- $language = $this->grav['language'];
+ public function __construct()
+ {
+ $this->grav = Grav::instance();
+ }
- $options = array_merge($this->grav['config']->get('plugins.readingtime'), $params);
+ public function getName()
+ {
+ return 'TwigReadingTimeFilters';
+ }
- $words = count(preg_split('/\s+/', strip_tags((string) $content)) ?: []);
- $wpm = $options['words_per_minute'];
+ public function getFilters()
+ {
+ return [
+ new \Twig_SimpleFilter('readingtime', [$this, 'getReadingTime'])
+ ];
+ }
- $minutes_short_count = floor($words / $wpm);
- $seconds_short_count = floor($words % $wpm / ($wpm / 60));
+ public function validatePattern($seconds_per_image)
+ {
+ // Get regex that is used in the user interface
+ $pattern = '/' . $this->grav['plugins']->get('readingtime')->blueprints()->schema()->get('seconds_per_image')['validate']['pattern'] . '/';
- if ($options['include_image_views']) {
- $stripped = strip_tags($content, "
");
- $images_in_content = substr_count($stripped, "
0) {
- if ($this->validatePattern($options['seconds_per_image'])) {
+ // Note: "$matches[0] will contain the text that matched the full pattern"
+ // https://www.php.net/manual/en/function.preg-match.php
+ return strlen($seconds_per_image) === strlen($matches[0]);
+ }
- // assumes string only contains integers, commas, and whitespace
- $spi = preg_split('/\D+/', trim($options['seconds_per_image']));
- $seconds_images = 0;
+ public function getReadingTime($input, $params = array())
+ {
+ $this->mergeConfig($this->grav['page']);
+ $language = $this->grav['language'];
+
+ $options = array_merge($this->grav['config']->get('plugins.readingtime'), $params);
+
+ // --- FIX: Use readingTime from header ---
+ if (is_object($input) && $input instanceof Page && isset($input->header()->readingTime)) {
+ $minutes_short_count = $input->header()->readingTime;
+ } else { // Calculate reading time if not already available
+ $content = is_object($input) ? $input->content() : $input;
+ $words = count(preg_split('/\s+/', strip_tags((string)$content)) ?: []);
+ $wpm = $options['words_per_minute'];
+
+ $minutes_short_count = floor($words / $wpm);
+ $seconds_short_count = floor($words % $wpm / ($wpm / 60));
+
+ if ($options['include_image_views']) {
+ $stripped = strip_tags($content, "
");
+ $images_in_content = substr_count($stripped, "
0) {
+ if ($this->validatePattern($options['seconds_per_image'])) {
+
+ // assumes string only contains integers, commas, and whitespace
+ $spi = preg_split('/\D+/', trim($options['seconds_per_image']));
+ $seconds_images = 0;
+
+ for ($i = 0; $i < $images_in_content; ++$i) {
+ $seconds_images += $i < count($spi) ? $spi[$i] : end($spi);
+ }
+
+ $minutes_short_count += floor($seconds_images / 60);
+ $seconds_short_count += $seconds_images % 60;
+ } else {
+ $this->grav['log']->error("Plugin 'readingtime' - seconds_per_image failed regex vadation");
+ }
+ }
+ }
+
+ $round = $options['round'];
+ if ($round == 'minutes') {
+ $minutes_short_count = round(($minutes_short_count * 60 + $seconds_short_count) / 60);
+
+ if ($minutes_short_count < 1) {
+ $minutes_short_count = 1;
+ }
+
+ $seconds_short_count = 0;
+ }
+ }
- for ($i = 0; $i < $images_in_content; ++$i) {
- $seconds_images += $i < count($spi) ? $spi[$i] : end($spi);
- }
+ $minutes_long_count = number_format($minutes_short_count, 2);
+ $seconds_long_count = number_format($seconds_short_count, 2);
- $minutes_short_count += floor($seconds_images / 60);
- $seconds_short_count += $seconds_images % 60;
+ if (array_key_exists('minute_label', $options) and $minutes_short_count == 1) {
+ $minutes_text = $options['minute_label'];
+ } elseif (array_key_exists('minutes_label', $options) and $minutes_short_count > 1) {
+ $minutes_text = $options['minutes_label'];
} else {
- $this->grav['log']->error("Plugin 'readingtime' - seconds_per_image failed regex vadation");
+ $minutes_text = $language->translate(($minutes_short_count == 1) ? 'PLUGIN_READINGTIME.MINUTE' : 'PLUGIN_READINGTIME.MINUTES');
}
- }
- }
- $round = $options['round'];
- if ($round == 'minutes') {
- $minutes_short_count = round(($minutes_short_count*60 + $seconds_short_count) / 60);
-
- if ( $minutes_short_count < 1 ) {
- $minutes_short_count = 1;
- }
-
- $seconds_short_count = 0;
- }
+ if (array_key_exists('second_label', $options) and $seconds_short_count == 1) {
+ $seconds_text = $options['second_label'];
+ } elseif (array_key_exists('seconds_label', $options) and $seconds_short_count > 1) {
+ $seconds_text = $options['seconds_label'];
+ } else {
+ $seconds_text = $language->translate(($seconds_short_count == 1) ? 'PLUGIN_READINGTIME.SECOND' : 'PLUGIN_READINGTIME.SECONDS');
+ }
- $minutes_long_count = number_format($minutes_short_count, 2);
- $seconds_long_count = number_format($seconds_short_count, 2);
-
- if (array_key_exists('minute_label', $options) and $minutes_short_count == 1) {
- $minutes_text = $options['minute_label'];
- } elseif (array_key_exists('minutes_label', $options) and $minutes_short_count > 1) {
- $minutes_text = $options['minutes_label'];
- } else {
- $minutes_text = $language->translate(( $minutes_short_count == 1 ) ? 'PLUGIN_READINGTIME.MINUTE' : 'PLUGIN_READINGTIME.MINUTES');
- }
+ // Adding translation for reading_label
+ $reading_label = $language->translate('PLUGIN_READINGTIME.READING_LABEL');
- if (array_key_exists('second_label', $options) and $seconds_short_count == 1) {
- $seconds_text = $options['second_label'];
- } elseif (array_key_exists('seconds_label', $options) and $seconds_short_count > 1) {
- $seconds_text = $options['seconds_label'];
- } else {
- $seconds_text = $language->translate(( $seconds_short_count == 1 ) ? 'PLUGIN_READINGTIME.SECOND' : 'PLUGIN_READINGTIME.SECONDS');
- }
+ $replace = [
+ 'minutes_short_count' => $minutes_short_count,
+ 'seconds_short_count' => $seconds_short_count,
+ 'minutes_long_count' => $minutes_long_count,
+ 'seconds_long_count' => $seconds_long_count,
+ 'minutes_text' => $minutes_text,
+ 'seconds_text' => $seconds_text,
+ 'reading_label' => $reading_label,
+ ];
- $replace = [
- 'minutes_short_count' => $minutes_short_count,
- 'seconds_short_count' => $seconds_short_count,
- 'minutes_long_count' => $minutes_long_count,
- 'seconds_long_count' => $seconds_long_count,
- 'minutes_text' => $minutes_text,
- 'seconds_text' => $seconds_text
- ];
+ $result = $options['format'];
- $result = $options['format'];
+ foreach ($replace as $key => $value) {
+ $result = str_replace('{' . $key . '}', $value, $result);
+ }
- foreach ( $replace as $key => $value ) {
- $result = str_replace('{' . $key . '}', $value, $result);
+ return $result;
}
- return $result;
- }
-
- private function mergeConfig( Page $page )
- {
- $defaults = (array) $this->grav['config']->get('plugins.readingtime');
- if ( isset($page->header()->readingtime) ) {
- $this->grav['config']->set('plugins.readingtime', array_merge($defaults, $page->header()->readingtime));
+ private function mergeConfig(Page $page)
+ {
+ $defaults = (array)$this->grav['config']->get('plugins.readingtime');
+ if (isset($page->header()->readingtime)) {
+ $this->grav['config']->set('plugins.readingtime', array_merge($defaults, $page->header()->readingtime));
+ }
}
- }
}
diff --git a/languages.yaml b/languages.yaml
index 3387d7a..9668f93 100644
--- a/languages.yaml
+++ b/languages.yaml
@@ -4,6 +4,7 @@ en:
SECONDS: seconds
MINUTE: minute
MINUTES: minutes
+ READING_LABEL: 'Reading time'
cs:
PLUGIN_READINGTIME:
@@ -11,6 +12,7 @@ cs:
SECONDS: sekundy
MINUTE: minuta
MINUTES: minuty
+ READING_LABEL: 'Čas na čtení'
es:
PLUGIN_READINGTIME:
@@ -18,6 +20,7 @@ es:
SECONDS: segundos
MINUTE: minuto
MINUTES: minutos
+ READING_LABEL: 'Tiempo de lectura'
fr:
PLUGIN_READINGTIME:
@@ -25,6 +28,7 @@ fr:
SECONDS: secondes
MINUTE: minute
MINUTES: minutes
+ READING_LABEL: 'Temps de lecture'
hr:
PLUGIN_READINGTIME:
@@ -32,6 +36,7 @@ hr:
SECONDS: sekundi
MINUTE: minuta
MINUTES: minuta
+ READING_LABEL: 'Olvasási idő'
it:
PLUGIN_READINGTIME:
@@ -39,6 +44,7 @@ it:
SECONDS: secondi
MINUTE: minuto
MINUTES: minuti
+ READING_LABEL: 'Momento della lettura'
nl:
PLUGIN_READINGTIME:
@@ -46,13 +52,23 @@ nl:
SECONDS: seconden
MINUTE: minuut
MINUTES: minuten
+ READING_LABEL: Leestijd
+pt:
+ PLUGIN_READINGTIME:
+ SECOND: segundo
+ SECONDS: segundos
+ MINUTE: minuto
+ MINUTES: minutos
+ READING_LABEL: 'Tempo de leitura'
+
pt-BR:
PLUGIN_READINGTIME:
SECOND: segundo
SECONDS: segundos
MINUTE: minuto
MINUTES: minutos
+ READING_LABEL: 'Tempo de leitura'
de:
PLUGIN_READINGTIME:
@@ -60,3 +76,12 @@ de:
SECONDS: Sekunden
MINUTE: Minute
MINUTES: Minuten
+ READING_LABEL: Lesezeit
+
+ar:
+ PLUGIN_READINGTIME:
+ SECOND: ثانية
+ SECONDS: ثواني
+ MINUTE: دقيقة
+ MINUTES: دقائق
+ READING_LABEL: 'وقت القراءة'
diff --git a/readingtime.php b/readingtime.php
index 230c301..01c7f97 100644
--- a/readingtime.php
+++ b/readingtime.php
@@ -1,49 +1,60 @@
[
- ['autoload', 100000],
- ['onPluginsInitialized', 0]
- ]
+ 'onPageContentProcessed' => ['onPageContentProcessed', 0]
];
}
- /**
- * [onPluginsInitialized:100000] Composer autoload.
- *
- * @return ClassLoader
- */
- public function autoload()
+ public function onPageContentProcessed(Event $event)
{
- return require __DIR__ . '/vendor/autoload.php';
+ $page = $event['page'];
+ $cacheKey = 'readingtime-' . $page->id();
+
+ // Ensure synchronous cache access
+ $readingTime = $this->grav['cache']->fetch($cacheKey);
+ if ($readingTime === false) {
+ $content = $page->content();
+ $readingTime = $this->calculateReadingTime($content);
+ $this->grav['cache']->save($cacheKey, $readingTime);
+ }
+
+ $this->modifyHeader($page, $readingTime);
}
- public function onPluginsInitialized()
+ private function modifyHeader($page, $readingTime)
{
- if ($this->isAdmin()) {
- $this->active = false;
- return;
- }
+ $header = $page->header();
+
+ $minutes_short_count = $readingTime;
+ $minutes_text = ($minutes_short_count == 1) ?
+ $this->grav['language']->translate('PLUGIN_READINGTIME.MINUTE') :
+ $this->grav['language']->translate('PLUGIN_READINGTIME.MINUTES');
- $this->enable([
- 'onTwigExtensions' => [
- ['onTwigExtensions', 0]
- ]
- ]);
+ $readingTimeString = sprintf(
+ '%s: %s %s',
+ $this->grav['language']->translate('PLUGIN_READINGTIME.READING_LABEL'),
+ $minutes_short_count,
+ $minutes_text
+ );
+
+ $header->readingTime = $readingTimeString;
+ $page->header($header);
}
- public function onTwigExtensions()
+ private function calculateReadingTime($text)
{
- $this->grav['twig']->twig->addExtension(new TwigReadingTimeFilters());
+ $wordCount = str_word_count(strip_tags($text));
+ $wordsPerMinute = 200;
+ $readingTime = ceil($wordCount / $wordsPerMinute);
+
+ return $readingTime;
}
}
diff --git a/readingtime.yaml b/readingtime.yaml
index 9247030..824876b 100644
--- a/readingtime.yaml
+++ b/readingtime.yaml
@@ -1,6 +1,6 @@
enabled: true
words_per_minute: 200
-format: "{minutes_short_count} {minutes_text}, {seconds_short_count} {seconds_text}"
+format: "{reading_label}: {minutes_short_count} {minutes_text}, {seconds_short_count} {seconds_text}"
round: seconds
include_image_views: false
seconds_per_image: '12,11,10,9,8,7,6,5,4,3'