I want to learn the Custom Highlight API[1], newly available in browsers as of June 2025. On some of my pages, my code samples have HTML in them (for interactive elements). That means I can't use a standard syntax highlighter that replaces the HTML. Instead, I am hoping to use the Custom Highlight API combined with the Mutation Observer API.
For testing, I used some parts of the hexagon guide:
var {{variant.replace('_', '')}}_direction_differences = [
// {{['even','odd'][parity]}} {{parityField}}s
[[{{neighbor(hex, parity)}}],
],
]
function {{variant.replace('_', '')}}_offset_neighbor(hex, direction):
var parity = hex.{{parityField}} & 1
var diff = {{variant.replace('_', '')}}_direction_differences[parity][direction]
return OffsetCoord(hex.col + diff[0], hex.row + diff[1])
function {{output}}_to_oddr(hex):
var parity = hex.r&1
var col = hex.q + (hex.r - parity) / 2
var row = hex.r
return OffsetCoord(col, row)
function oddr_to_{{output}}(hex):
var parity = hex.row&1
var q = hex.col - (hex.row - parity) / 2
var r = hex.row
return {{output_constructor}}
function {{output}}_to_evenr(hex):
var parity = hex.r&1
var col = hex.q + (hex.r + parity) / 2
var row = hex.r
return OffsetCoord(col, row)
function evenr_to_{{output}}(hex):
var parity = hex.row&1
var q = hex.col - (hex.row + parity) / 2
var r = hex.row
return {{output_constructor}}
function {{output}}_to_oddq(hex):
var parity = hex.q&1
var col = hex.q
var row = hex.r + (hex.q - parity) / 2
return OffsetCoord(col, row)
function oddq_to_{{output}}(hex):
var parity = hex.col&1
var q = hex.col
var r = hex.row - (hex.col - parity) / 2
return {{output_constructor}}
function {{output}}_to_evenq(hex):
var parity = hex.q&1
var col = hex.q
var row = hex.r + (hex.q + parity) / 2
return OffsetCoord(col, row)
function evenq_to_{{output}}(hex):
var parity = hex.col&1
var q = hex.col
var r = hex.row - (hex.col + parity) / 2
return {{output_constructor}}
Array<Cube> results = []
for each -N ≤ q ≤ +N:
for each -N ≤ r ≤ +N:
for each -N ≤ s ≤ +N:
if q + r + s == 0:
results.append(cube_add(center, Cube(q, r, s)))
function hex_reachable(Hex start, int movement): Set<Hex>
Set<Hex> visited = Set()
visited.add(start)
Array<Array<Hex>> fringes = []
fringes.append([start])
for each 1 ≤ k ≤ movement:
fringes.append([])
for each hex in fringes[k-1]:
for each 0 ≤ dir < 6:
Hex neighbor = hex_neighbor(hex, dir)
if neighbor not in visited and not blocked:
visited.add(neighbor)
fringes[k].append(neighbor)
return visited
// note that part of this is editable -- I wanted to test that editing doesn't mess up the highlighting
var oddr_direction_differences = [
// even rows
[[+1, 0], [ 0, -1], [-1, -1],
[-1, 0], , [ 0, +1]],
// odd rows
[[+1, 0], [+1, -1], [ 0, -1],
[-1, 0], [ 0, +1], [+1, +1]],
]
function oddr_offset_neighbor(Hex hex, int direction): OffsetCoord
int parity = hex.row & 1
var diff = oddr_direction_differences[parity][direction]
return OffsetCoord(hex.col + diff[0], hex.row + diff[1])
Syntax highlighting with prism.js or highlight.js generates new HTML. I don't want new HTML. I want the token positions. So instead of a syntax highlighting library, I really want a tokenizer (or parser):
- https://github.com/andreruffert/syntax-highlight-element[2] [2.8kB] - a custom html element that uses the prism.js tokenizer; doesn't let me use other html though
- https://github.com/rse/tokenizr[3] [3.4kB] - a tokenizer that constructs a lexical scanner only, and returns token positions
- https://github.com/no-context/moo[4] [3.2kB] - also a tokenizer that returns token positions
Those sizes are without the grammar. I might want to define my own grammars anyway, especially for the pseudocode I use sometimes. I think I don't need anything nearly as fancy as those libraries, so I made my own.
Limitations:
- The Custom Highlight API will only color, background-color, text-decoration, text-shadow. That means we can't use font-family or font-weight (e.g. bold). :-(
- Some highlight rules like comments work to the end of the line, e.g. <br>. But <br> doesn't show up in
.textContent. I can't use.innerTexteither because it makes it hard to find which element contains a text position, and I need that to create theRangeelements for theHighlightobjects. - MutationObserver watches elements. The hexagon page was creating and deleting <pre> elements. I changed the page to limit interactivity to within the <pre> elements. That will let me observe the <pre> elements at load time. No new <pre> elements are created after loading.
- The Custom Highlight API has a global registry. I need to construct
Highlightobjects statically to put into the global registry, and then I need to add/removeRangeobjects from those globals. To remove theRangeobjects I need to track which ones I had inserted previously.
After the initial (partial) experiments on this page, I put it into production on the Hexagon Guide page.
Source: learning-highlight-api.js and /grids/hexagons/code-highlighting.js