I recently visited https://bionic-reading.com/. Here comes a new idea that I find interesting that is making the first few letters of a word bold in the text will make the it easier to read. 😃 I'm not sure if this actually makes the text easier to read or if it's just easier for some people but I like this idea and I find it applicable to websites, especially https://bionic-reading.com/ itself. 😄 So what is the method? Userscript then. 😄 A Userscript running on each web page and bionic reading mode enabled would be a reasonable solution. So in this article, I will show you how to write a simple userscript to enable bionic reading in thick text web pages and hopefully it will make the text easier to read for you. This is a rough idea, so the algorithm used in this userscript will be simple and not as complete as the original page with the main purpose for you to understand how to do it can customize at your discretion then. 😁
Idea
We will build code based on the ideas outlined and that is the first
Decompose a string into words
Given you a string, how can you decompose them into words? The solution I use is a regular expression combined with a generator.
Build a regular expression
I built a regular expression named PIECE_PATTERN
to capture words. Here, I want to preserve spaces so this regular expression will be
const PIECE_PATTERN = /(?<space>\s+)|(?<word>\S+)/y;
This regular expression will capture words and spaces in turn, so remember to pay attention to the sticky mode (the ending y
).
Next we will
Build a generator to generate a string
Generator will have the following code
function *wordize(str) {
let pattern = new RegExp(PIECE_PATTERN);
let m;
while (m = pattern.exec(str)) {
if (m.groups.space) {
yield {
text: m.groups.space,
space: true
};
continue;
}
yield {
text: m.groups.word,
space: false
};
}
}
Using regular expression's exec
and sticky suffix, we can easily split string into words
Making the first few letters bold
The bionic reading site's algorithm is quite sophisticated and as I said before, I implemented just a simple algorithm. Its idea is: you have a value of level
to indicate the number of characters you want to make them bold at the beginning. If level == 1
you make half the characters bold, If level == 2
you make one quarter of the characters bold and so on. I should have built my own function for this but integrated version would be better so I make the code into the makeBionicTextSpan
function, which takes a string and will create a span
element corresponding to that string including the head and tail part. To do that we do the following:
Decompose strings into words
Using the for
loop as below will do the job
for (let piece of wordize(text))
And with every word we fetch
Word processing
If the fetched word is spaces, we simply create a text node and add the resulting span
element. If it's is the opposite then
Calculate the number of bold characters of the word
By default, the number of bold characters n
will be 1
. Based on level
, if the length of the string is significantly greater than 2^level
or 1 << level
, we will halve that number of characters until level
reaches the value 0
. During that process, if the number of characters is odd, we will add one to that number. And so our code to find n
is going to be
let str = piece.text;
let n = 1;
if (str.length > 1 << level) {
n = str.length;
let l = level;
while (l) {
if (n & 1)
n++;
n >>= 1;
l--;
}
}
Having calculated n
we
Split string into two parts
It's span.bionic-head
and span.bionic-tail
with the code
let head = document.createElement("span");
let tail = document.createElement("span");
head.textContent = str.substr(0, n);
tail.textContent = str.slice(n);
head.classList.add(HEAD_CLASS_NAME);
tail.classList.add(TAIL_CLASS_NAME);
span.appendChild(head);
span.appendChild(tail);
And the full code is
function makeBionicTextSpan(text, level = 1) {
if (!text.length || level < 0)
return null;
let span = document.createElement("span");
for (let piece of wordize(text)) {
if (piece.space) {
span.appendChild(document.createTextNode(piece.text));
continue;
}
let str = piece.text;
let n = 1;
if (str.length > 1 << level) {
n = str.length;
let l = level;
while (l) {
if (n & 1)
n++;
n >>= 1;
l--;
}
}
let head = document.createElement("span");
let tail = document.createElement("span");
head.textContent = str.substr(0, n);
tail.textContent = str.slice(n);
head.classList.add(HEAD_CLASS_NAME);
tail.classList.add(TAIL_CLASS_NAME);
span.appendChild(head);
span.appendChild(tail);
}
return span;
}
Replace text nodes below any element
The technique I use here is recursion with the function makeBionicElement(el, level)
where el
is the element to be processed.
Replaces the text nodes of the current element
I use XPath here to list all the text nodes of the current element and replace them with the span generated from the makeBionicTextSpan
function above. So our code is
let texts = document.evaluate("./text()", el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < texts.snapshotLength; i++) {
let node = texts.snapshotItem(i);
if (el.classList.contains(HEAD_CLASS_NAME) || el.classList.contains(TAIL_CLASS_NAME) || FULL_SPACES_PATTERN.test(node.textContent))
continue;
let bionic = makeBionicTextSpan(node.textContent, level);
if (bionic)
el.replaceChild(bionic, node);
}
Same applies to elements below the current element
This is done by calling the makeBionicElement(el, level)
function itself for successive elements like code below
for (let child of el.children)
if (!IGNORED_TAGS.includes(child.tagName.toLowerCase()))
makeBionicElement(child, level);
Here you will see that I do not apply to some tags like code
or pre
Add style and apply to body
I used a simple style here for testing, you can tweak it if you want
let sty = document.head.appendChild(document.createElement("style"));
sty.textContent =
`
span.bionic-head{
font-weight: 450 !important;
}
span.bionic-tail{
font-weight: lighter !important;
opacity: 40% !important;
}
`
makeBionicElement(document.body, 1);
And such,
Our complete script will be
// ==UserScript==
// @name Bionic reading
// @version 1
// @grant none
// ==/UserScript==
const PIECE_PATTERN = /(?<space>\s+)|(?<word>\S+)/y;
const FULL_SPACES_PATTERN = /^\s+$/g;
const HEAD_CLASS_NAME = "bionic-head";
const TAIL_CLASS_NAME = "bionic-tail";
const IGNORED_TAGS = Object.freeze(["code", "kbd", "pre"]);
function * wordize(str) {
let pattern = new RegExp(PIECE_PATTERN);
let m;
while (m = pattern.exec(str)) {
if (m.groups.space) {
yield {
text: m.groups.space,
space: true
};
continue;
}
yield {
text: m.groups.word,
space: false
};
}
}
function makeBionicTextSpan(text, level = 1) {
if (!text.length || level < 0)
return null;
let span = document.createElement("span");
for (let piece of wordize(text)) {
if (piece.space) {
span.appendChild(document.createTextNode(piece.text));
continue;
}
let str = piece.text;
let n = 1;
if (str.length > 1 << level) {
n = str.length;
let l = level;
while (l) {
if (n & 1)
n++;
n >>= 1;
l--;
}
}
let head = document.createElement("span");
let tail = document.createElement("span");
head.textContent = str.substr(0, n);
tail.textContent = str.slice(n);
head.classList.add(HEAD_CLASS_NAME);
tail.classList.add(TAIL_CLASS_NAME);
span.appendChild(head);
span.appendChild(tail);
}
return span;
}
function makeBionicElement(el, level = 1) {
if (!el.tagName)
return;
let texts = document.evaluate("./text()", el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < texts.snapshotLength; i++) {
let node = texts.snapshotItem(i);
if (el.classList.contains(HEAD_CLASS_NAME) || el.classList.contains(TAIL_CLASS_NAME) || FULL_SPACES_PATTERN.test(node.textContent))
continue;
let bionic = makeBionicTextSpan(node.textContent, level);
if (bionic)
el.replaceChild(bionic, node);
}
for (let child of el.children)
if (!IGNORED_TAGS.includes(child.tagName.toLowerCase()))
makeBionicElement(child, level);
}
let sty = document.head.appendChild(document.createElement("style"));
sty.textContent =
`
span.bionic-head{
font-weight: 450 !important;
}
span.bionic-tail{
font-weight: lighter !important;
opacity: 40% !important;
}
`
makeBionicElement(document.body, 1);
Results achieved on bionic reading page
When I apply the script on bionic reading page, the result will be as follows (you can compare with the original website)
Thought
This is an interesting idea although I am not sure if it will benefit you but I hope it brings a new experience to you. 😊 Have a nice day with this userscript then. 😊
PS: This userscript is broken on some pages so maybe I will update in the near future. 😉
Top comments (1)
BionicScript is a website that i use to convert plain text to bionic