Prototype · lexer-to-font compiler

Fontlex

The box below is a plain <textarea> with no highlighting code attached. The colors come from the font. Fontlex compiled a small JavaScript lexer into OpenType substitution rules, so the browser's text shaper runs the lexer every time it draws a line.

example.js
number regex comment string keyword constant builtin

Edit anything. Turning the font lexer off disables the font's calt feature, and the same text goes back to one color. Custom colors use CSS @font-palette-values, so retheming needs no new font.

What got compiled

28DFA states
14character classes
254lexer rules emitted (132 dropped)
67keywords and builtins
11,351variant glyphs added
144 KBfont as WOFF2

Checked by shaping 20,000 random snippets with HarfBuzz, the shaper Chrome and Firefox use, and comparing every glyph to a reference implementation of the lexer: 20,000 of 20,000 matched.

The lexer, as a transition table

Each row is the state of the previous character. Each column is the class of the next one. The cell is the new state, shown in the color the character will be drawn in.

after \ nextletterdigitdotdqsqbtbackslashslashstarspacecloselbracketrbracketother
code (start)identnumbercodedqsqtplcodere_startcodecodeaftercodeaftercode
afteridentnumbercodedqsqtplcodedivcodeafteraftercodeaftercode
identidentidentcodedqsqtplcodedivcodeafteraftercodeaftercode
numbernumbernumbernumberdqsqtplcodedivcodeafteraftercodeaftercode
dividentnumbercodedqsqtplcodeline_commentvblockcodeaftercodeaftercode
re_startregexregexregexregexregexregexre_escline_commentblockregexregexre_classregexregex
regexregexregexregexregexregexregexre_escre_endregexregexregexre_classregexregex
re_escregexregexregexregexregexregexregexregexregexregexregexregexregexregex
re_classre_classre_classre_classre_classre_classre_classre_class_escre_classre_classre_classre_classre_classregexre_class
re_class_escre_classre_classre_classre_classre_classre_classre_classre_classre_classre_classre_classre_classre_classre_class
re_endre_flagsnumbercodedqsqtplcodedivcodeafteraftercodeaftercode
re_flagsre_flagsnumbercodedqsqtplcodedivcodeafteraftercodeaftercode
line_commentline_commentline_commentline_commentline_commentline_commentline_commentline_commentline_commentline_commentline_commentline_commentline_commentline_commentline_comment
blockblockblockblockblockblockblockblockblockblock_starblockblockblockblockblock
block_starblockblockblockblockblockblockblockblock_endblock_starblockblockblockblockblock
block_endidentnumbercodedqsqtplcodere_startcodecodeaftercodeaftercode
vblockvblockvblockvblockvblockvblockvblockvblockvblockvblock_starvblockvblockvblockvblockvblock
vblock_starvblockvblockvblockvblockvblockvblockvblockvblock_endvblock_starvblockvblockvblockvblockvblock
vblock_endidentnumbercodedqsqtplcodedivcodeafteraftercodeaftercode
dqdqdqdqdq_enddqdqdq_escdqdqdqdqdqdqdq
dq_escdqdqdqdqdqdqdqdqdqdqdqdqdqdq
dq_endidentnumbercodedqsqtplcodedivcodeafteraftercodeaftercode
sqsqsqsqsqsq_endsqsq_escsqsqsqsqsqsqsq
sq_escsqsqsqsqsqsqsqsqsqsqsqsqsqsq
sq_endidentnumbercodedqsqtplcodedivcodeafteraftercodeaftercode
tpltpltpltpltpltpltpl_endtpl_esctpltpltpltpltpltpltpl
tpl_esctpltpltpltpltpltpltpltpltpltpltpltpltpltpl
tpl_endidentnumbercodedqsqtplcodedivcodeafteraftercodeaftercode

The highlighted row is the start state. It becomes the fallback rules at the end of the lookup. Faded cells give the same answer as the fallback, so the compiler emits no rule for them.

How a font can run a lexer

1

Every character gets a copy of its glyph for each state it can be in, named like zero.lx_number. Copies are composites that point at the original outline, so they cost a few bytes each.

2

A chaining contextual lookup walks the text left to right. Its backtrack context sees glyphs it already replaced, so the previous glyph's copy carries the automaton's state forward. One rule per table cell:

lookup LEX {
  …
  sub @S_after @C_slash' lookup TO_div;
  sub @S_ident @C_digit' lookup TO_ident;
  sub @S_number @C_letter' lookup TO_number;
  sub @S_div @C_slash' lookup TO_line_comment;
  sub @S_re_start @C_letter' lookup TO_regex;
  …
  # fallbacks: the start state's row
  sub @C_letter' lookup TO_ident;
  sub @C_digit' lookup TO_number;
  sub @C_dq' lookup TO_dq;
  …
} LEX;
3

A second lookup recolors whole identifiers that spell a keyword. The first letter of every identifier is marked, and a word's rule runs from that letter to the identifier's last, so in can't match inside inland; a last pass spreads the color back over the word. Longer words go first, so instanceof isn't read as in:

lookup START {
  ignore sub @WORDLIKE @S_ident';
  sub @S_ident' lookup TO_START;
} START;
lookup WORDS {
  ignore sub […]' @WORDLIKE;
  …
  sub \i.lxs_ident \n.lx_ident \s.lx_ident \t.lx_ident \a.lx_ident \n.lx_ident \c.lx_ident \e.lx_ident \o.lx_ident \f.lx_ident' lookup W_keyword;
  …
  sub \i.lxs_ident \n.lx_ident' lookup W_keyword;
} WORDS;
4

Each colored copy gets a COLR record that paints the original outline with a palette entry. The font carries a light and a dark palette.

Limits of this prototype