From cd96e43e0fe2ad8712611ee2532774498e0d47db Mon Sep 17 00:00:00 2001
From: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date: Sun, 24 May 2020 21:47:40 +0100
Subject: [PATCH] Add reading time estimation Disable with readingtime_enabled.
 Also add new class system-text-insert for all things that appear below a wiki
 page's title <h1> and the beginning of the body text

---
 Changelog.md                    |  2 +
 modules/feature-readingtime.php | 71 +++++++++++++++++++++++++++++++++
 modules/page-view.php           |  6 +--
 peppermint.guiconfig.json       |  3 ++
 4 files changed, 79 insertions(+), 3 deletions(-)
 create mode 100644 modules/feature-readingtime.php

diff --git a/Changelog.md b/Changelog.md
index 8b39875..53d110e 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -16,6 +16,8 @@ This file holds the changelog for Pepperminty Wiki. This is the master list of t
      - Put `[__TOC__]` on a line by itself to insert an automatic table of contents
      - Note that the level of heading generated can be controlled (or even removed) by the new `parser_toc_heading_level` setting
  - Add `<meta name="theme-color" content="value" />` support with the new `theme_colour` setting. More information: [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color), [caniuse](https://caniuse.com/#feat=meta-theme-color). Also used by some platforms to customise embed accents when generating a rich snippet (e.g. Discord).
+ - Added reading time estimate to the top of wiki pages - control it with the new `readingtime_enabled` setting (#172)
+     - The algorithm used to estimate reading times is the as the one used in Firefox's reader mode
 
 ### Changed
  - Fiddled with Parsedown & ParsedownExtra versions
diff --git a/modules/feature-readingtime.php b/modules/feature-readingtime.php
new file mode 100644
index 0000000..85deeeb
--- /dev/null
+++ b/modules/feature-readingtime.php
@@ -0,0 +1,71 @@
+<?php
+register_module([
+	"name" => "Reading time estimator",
+	"version" => "0.1",
+	"author" => "Starbeamrainbowlabs",
+	"description" => "Displays the approximate reading time for a page beneath it's title.",
+	"id" => "feature-readingtime",
+	"code" => function() {
+		
+		page_renderer::register_part_preprocessor(function(&$parts) {
+			global $env, $settings;;
+			error_log("[readingtime] part_preprocessor, matching $settings->readingtime_action");
+			// Only insert for the view action
+			if($env->action !== $settings->readingtime_action || !$settings->readingtime_enabled)
+				return;
+			error_log("[readingtime] go ahead");
+			
+			$reading_time = estimate_reading_time(
+				file_get_contents($env->page_filename),
+				$settings->readingtime_language
+			);
+			error_log(var_export($reading_time, true));
+			
+			$insert = "<small><em>{$reading_time[0]} - {$reading_time[1]} minute read</em></small>";
+			if($reading_time[0] === $reading_time[1])
+				$insert = "<small><em>{$reading_time[0]} minute read</em></small>";
+			
+			// TODO: Create a canonical way to insert something just below the header - this might be tough though 'cause the that isn't handled by the page_renderer though
+			$parts["{content}"] = str_replace("</h1>", "</h1><p class='system-text-insert readingtime-estimate'>$insert</p>", $parts["{content}"]);
+		});
+	}
+]);
+
+/**
+ * Estimates the reading time for a given lump of text.
+ * Ref https://github.com/sbrl/Pepperminty-Wiki/issues/172 (has snippet of
+ * original code from Firefox & link to study from which the numbers are
+ * taken).
+ * @param	string	$text	The text to estimate for.
+ * @param	string	$lang	The language code of the text - defaults to "en"
+ * @return	array	An array in the form [ low_time, high_time ] in minutes
+ */
+function estimate_reading_time(string $text, string $lang = "en") : array {
+	$chars_count = mb_strlen($text);
+	$langs = [
+		"en" => (object) [ "cpm" => 987, "variance" => 118 ],
+		"ar" => (object) [ "cpm" => 612, "variance" => 88 ],
+		"de" => (object) [ "cpm" => 920, "variance" => 86 ],
+		"es" => (object) [ "cpm" => 1025, "variance" => 127 ],
+		"fi" => (object) [ "cpm" => 1078, "variance" => 121 ],
+		"fr" => (object) [ "cpm" => 998, "variance" => 126 ],
+		"he" => (object) [ "cpm" => 833, "variance" => 130 ],
+		"it" => (object) [ "cpm" => 950, "variance" => 140 ],
+		"jw" => (object) [ "cpm" => 357, "variance" => 56 ],
+		"nl" => (object) [ "cpm" => 978, "variance" => 143 ],
+		"pl" => (object) [ "cpm" => 916, "variance" => 126 ],
+		"pt" => (object) [ "cpm" => 913, "variance" => 145 ],
+		"ru" => (object) [ "cpm" => 986, "variance" => 175 ],
+		"sk" => (object) [ "cpm" => 885, "variance" => 145 ],
+		"sv" => (object) [ "cpm" => 917, "variance" => 156 ],
+		"tr" => (object) [ "cpm" => 1054, "variance" => 156 ],
+		"zh" => (object) [ "cpm" => 255, "variance" => 29 ],
+	];
+	if(!isset($langs[$lang]))
+		return null;
+	
+	return [
+		ceil($chars_count / ($langs[$lang]->cpm + $langs[$lang]->variance)),
+		ceil($chars_count / ($langs[$lang]->cpm - $langs[$lang]->variance))
+	];
+}
diff --git a/modules/page-view.php b/modules/page-view.php
index 2016d35..4e91acc 100644
--- a/modules/page-view.php
+++ b/modules/page-view.php
@@ -92,16 +92,16 @@ register_module([
 				$content .= "<h1>$env->page</h1>\n";
 			else {
 				$content .= "<h1>Revision #{$env->history->revision_number} of $env->page</h1>\n";
-				$content .= "<p class='revision-note'><em>(Revision saved by {$env->history->revision_data->editor} " . render_timestamp($env->history->revision_data->timestamp) . ". <a href='?page=" . rawurlencode($env->page) . "'>Jump to the current revision</a> or see a <a href='?action=history&page=" . rawurlencode($env->page) . "'>list of all revisions</a> for this page.)</em></p>\n";
+				$content .= "<p class='system-text-insert revision-note'><em>(Revision saved by {$env->history->revision_data->editor} " . render_timestamp($env->history->revision_data->timestamp) . ". <a href='?page=" . rawurlencode($env->page) . "'>Jump to the current revision</a> or see a <a href='?action=history&page=" . rawurlencode($env->page) . "'>list of all revisions</a> for this page.)</em></p>\n";
 			}
 			
 			// Add a visit parent page link if we're a subpage
 			if(get_page_parent($env->page) !== false)
-				$content .= "<p class='link-parent-page'><em><a href='?action=view&page=" . rawurlencode(get_page_parent($env->page)) . "'>&laquo; " . htmlentities(get_page_parent($env->page)) . "</a></em></p>\n";
+				$content .= "<p class='system-text-insert link-parent-page'><em><a href='?action=view&page=" . rawurlencode(get_page_parent($env->page)) . "'>&laquo; " . htmlentities(get_page_parent($env->page)) . "</a></em></p>\n";
 			
 			// Add an extra message if the requester was redirected from another page
 			if(isset($_GET["redirected_from"]))
-				$content .= "<p><em>Redirected from <a href='?page=" . rawurlencode($_GET["redirected_from"]) . "&redirect=no'>" . $_GET["redirected_from"] . "</a>.</em></p>\n";
+				$content .= "<p class='system-text-insert'><em>Redirected from <a href='?page=" . rawurlencode($_GET["redirected_from"]) . "&redirect=no'>" . $_GET["redirected_from"] . "</a>.</em></p>\n";
 			
 			$parsing_start = microtime(true);
 			
diff --git a/peppermint.guiconfig.json b/peppermint.guiconfig.json
index 6db41e9..b5e5486 100644
--- a/peppermint.guiconfig.json
+++ b/peppermint.guiconfig.json
@@ -98,6 +98,9 @@
 	"password_cost_time_lastcheck": { "type": "number", "description": "Pseudo-setting used to keep track of the last recalculation of password_cost. Is updated with the current unix timestamp every time password_cost is recalculated.", "default": 0},
 	"new_password_length": { "type": "number", "description": "The length of newly-generated passwords. This is currently used in the user table when creating new accounts.", "default": 32},
 	"require_login_view": { "type": "checkbox", "description": "Whether to require that users login before they do anything else. Best used with the data_storage_dir option.", "default": false},
+	"readingtime_enabled": { "type": "checkbox", "description": "Whether to display the estimated reading time beneath the header of every wiki page.", "default": true },
+	"readingtime_language": { "type": "text", "description": "The language code to use when estimating the reading time. Possible values: en, ar, de, es, fi, fr, he, it, jw, nl, pl, pt, ru, sk, sv, tr, zh. Unfrotuantely adding multi-language support to the user interface is an absolutely massive undertaking that would take ages, as Peppermitny Wiki waasn't designed with that in mind :-/", "default": "en" },
+	"readingtime_action": { "type": "text", "description": "The name of the action to enable the reading time estimation on. You probably shouldn't change this unless you know what you're doing.", "default": "view" },
 	"data_storage_dir": { "type": "text", "description": "The directory in which to store all files, except the main index.php.", "default": "." },
 	"watchlists_enable": { "type": "checkbox", "description": "Whether the watchlists feature should be enabled or not.", "default": true },
 	"delayed_indexing_time": { "type": "number", "description": "The amount of time, in seconds, that pages should be blocked from being indexed by search engines after their last edit. Aka delayed indexing.", "default": 0},
