Bionic Reading on bionic-reading.com
Published: 2022-03-27 17:53:39

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 
```javascript
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
```javascript
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

```javascript
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

```javascript
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
```javascript
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


```javascript
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

```javascript
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


```javascript
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





```javascript
// ==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)

![](https://i.imgur.com/qSF96xe.png)
![](https://i.imgur.com/SSNfGBs.png)
![](https://i.imgur.com/pl7u0zj.png)
![](https://i.imgur.com/HuoOpSq.png)
![](https://i.imgur.com/VTZJPyh.png)

## 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. 😉