DEV Community

Nam Phạm
Nam Phạm

Posted on

Bionic Reading on bionic-reading.com

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;
Enter fullscreen mode Exit fullscreen mode

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
        };
    }
}

Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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--;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
bionicscript profile image
bionicscript

BionicScript is a website that i use to convert plain text to bionic