A <pre> has no columns, only a font advance. Pad a box-drawn table by string length and it drifts twelve columns; pad it by wcwidth and a real terminal emulator gets it exactly right, while the same table in a <pre> still drifts 39.7px, because Han renders at 1.695x Latin where wcwidth says 2. So this grid measures the advance of the actual rendered face rather than trusting ch, which is the advance of digit zero and therefore a Latin unit; it gives every glyph its own cell so an error cannot push the rest of the row; and it reports the glyphs whose natural advance did not match the cell they were given, because that mismatch is the finding and hiding it would make this the same lie as a <pre> that looks aligned until it is not.
import 'overscan'
import 'overscan/overscan.css'
<ov-grid class="ov-grid--frame"></ov-grid>
const text = await fetch('./data/grid-0-text.json').then(r => r.json())
document.querySelector('ov-grid').setAttribute('text', text)import { OvGrid } from 'overscan/react'
const text = await fetch('./data/grid-0-text.json').then(r => r.json())
<OvGrid
class="ov-grid--frame"
text={text}
/>import { OvGrid } from 'overscan/vue'
const text = await fetch('./data/grid-0-text.json').then(r => r.json())
<OvGrid
class="ov-grid--frame"
:text="text"
/>import OvGrid from 'overscan/svelte/OvGrid.svelte'
const text = await fetch('./data/grid-0-text.json').then(r => r.json())
<OvGrid
class="ov-grid--frame"
text={text}
/>import 'overscan'
import 'overscan/overscan.css'
<ov-grid class="ov-grid--frame"></ov-grid>
const text = await fetch('./data/grid-1-text.json').then(r => r.json())
document.querySelector('ov-grid').setAttribute('text', text)import { OvGrid } from 'overscan/react'
const text = await fetch('./data/grid-1-text.json').then(r => r.json())
<OvGrid
class="ov-grid--frame"
text={text}
/>import { OvGrid } from 'overscan/vue'
const text = await fetch('./data/grid-1-text.json').then(r => r.json())
<OvGrid
class="ov-grid--frame"
:text="text"
/>import OvGrid from 'overscan/svelte/OvGrid.svelte'
const text = await fetch('./data/grid-1-text.json').then(r => r.json())
<OvGrid
class="ov-grid--frame"
text={text}
/>import 'overscan'
import 'overscan/overscan.css'
<ov-grid class="ov-grid--frame"></ov-grid>
const text = await fetch('./data/grid-2-text.json').then(r => r.json())
document.querySelector('ov-grid').setAttribute('text', text)import { OvGrid } from 'overscan/react'
const text = await fetch('./data/grid-2-text.json').then(r => r.json())
<OvGrid
class="ov-grid--frame"
text={text}
/>import { OvGrid } from 'overscan/vue'
const text = await fetch('./data/grid-2-text.json').then(r => r.json())
<OvGrid
class="ov-grid--frame"
:text="text"
/>import OvGrid from 'overscan/svelte/OvGrid.svelte'
const text = await fetch('./data/grid-2-text.json').then(r => r.json())
<OvGrid
class="ov-grid--frame"
text={text}
/>