Syntax coloring with css' mix-blend-mode and tree-sitter
HTML
<!--
This is a proof-of-concept of syntax highlighting of a <textarea> with
css' mix-blend-mode. A bunch of colored blocks are created
on top of the textbox, and are "blended" into the textarea with `mix-blend-mode`.
The code parsing is done with tree sitter.
-->
<textarea id="editor" spellcheck="false">
// This is an actual textarea
// with js syntax highlighting
function foobar(x) {
return x + 4; // A comment
}
const text = "Some text"
console.log(foobar(2));
4 = b; // Error
</textarea>
<div id="blocks"></div>
<br>
<label><input id="debug" type="checkbox"></input>debug</label>
CSS
body {
margin: 0;
background: grey;
}
#blocks,
textarea {
font-family: monospace;
font-size: 24px;
}
#blocks {
position: absolute;
top: 20px;
left: 20px;
pointer-events: none;
}
textarea {
margin: 20px;
white-space: pre;
height: 400px;
width: 400px;
resize: none;
padding: 0;
border-width: 0;
}
#blocks > div {
position: absolute;
mix-blend-mode: lighten;
top: calc(var(--y) * 1lh);
height: 1lh;
left: calc(var(--x) * 1ch);
width: calc(var(--width) * 1ch);
background-color: var(--hi-color);
}
#blocks.debug > div {
mix-blend-mode: unset;
opacity: 0.3;
}
#blocks.debug > div {
mix-blend-mode: unset;
opacity: 0.5;
}
#blocks > .comment {
--hi-color: #909090;
}
#blocks > .ERROR {
--hi-color: #ff0000;
}
#blocks > .bare_key {
--hi-color: #00f;
}
#blocks > .string {
--hi-color: #ffbb00;
}
#blocks > .number {
--hi-color: rgb(23, 162, 39);
}
#blocks > .function,
#blocks > .return,
#blocks > .const {
--hi-color: rgb(23, 179, 184);
}
JavaScript
import {
Parser,
Language,
} from "https://cdn.jsdelivr.net/npm/[email protected]/tree-sitter.js";
await Parser.init();
const parser = new Parser();
const JS = await Language.load(
"https://cdn.jsdelivr.net/npm/[email protected]/tree-sitter-javascript.wasm",
);
parser.setLanguage(JS);
let blocks = document.querySelector("#blocks");
let textarea = document.querySelector("textarea");
function CreateBlock(cursor, lengthes) {
for (let y = cursor.startPosition.row; y <= cursor.endPosition.row; y++) {
let x;
if (y == cursor.startPosition.row) {
x = cursor.startPosition.column;
} else {
x = 0;
}
let width;
if (y == cursor.endPosition.row) {
width = cursor.endPosition.column - x;
} else {
width = lengthes[y] - x;
}
let div = document.createElement("div");
div.style = `--x:${x};--y:${y};--width:${width}`;
div.className = cursor.nodeType;
blocks.appendChild(div);
}
}
function Parse(text) {
let lengthes = text.split("\n").map((line) => line.length);
let cols = Math.max(...lengthes);
let rows = lengthes.length;
textarea.style.minWidth = `${cols + 1}ch`; // room for cursor at end of line
textarea.style.minHeight = `${rows}lh`;
textarea.style.width = `${cols}ch`;
textarea.style.height = `${rows}lh`;
blocks.style.width = `${cols}ch`;
blocks.style.height = `${rows}lh`;
const tree = parser.parse(text);
if (!tree) {
return;
}
blocks.innerHTML = "";
const cursor = tree.walk();
function visit() {
const supported = new Set(["ERROR", "string", "comment", "integer", "number", "function", "return", "const"]);
if (supported.has(cursor.nodeType)) {
CreateBlock(cursor, lengthes);
} else {
if (cursor.gotoFirstChild()) {
visit();
cursor.gotoParent();
}
}
while (cursor.gotoNextSibling()) {
visit();
}
}
...