fix
This commit is contained in:
+73
-30
@@ -31,50 +31,89 @@ export function describe(bytes, i) {
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
// Row layout, in character cells: offset, gap, hex, gap, ASCII.
|
||||
const BYTES_PER_ROW = [16, 8, 4]; // widest first
|
||||
const GAP = 1.2;
|
||||
const rowChars = (n) => 4 + GAP * 2 + n * 3 + (n === 16 ? 1 : 0) + n;
|
||||
|
||||
/**
|
||||
* Render a hex dump. Returns { el, inspector } where inspector is the element
|
||||
* that shows the decoded value of the hovered/selected byte.
|
||||
* Render a hex dump that always fits its container: it picks 16, 8 or 4 bytes
|
||||
* per row and sizes the font so a whole row fits without sideways scrolling, and
|
||||
* re-fits whenever the container changes size (rotation, window resize).
|
||||
* Returns { el, inspector }; the inspector shows the decoded hovered/selected byte.
|
||||
*
|
||||
* Options: maxBytes (show only the first N bytes), maxFont / minFont (px).
|
||||
*/
|
||||
export function hexView(bytes, { rows: maxRows = Infinity } = {}) {
|
||||
export function hexView(bytes, { maxBytes = Infinity, maxFont = 15, minFont = 9.5 } = {}) {
|
||||
const inspector = h('div', { class: 'hx-inspector', 'aria-live': 'polite' }, 'HOVER OR CLICK A BYTE TO DECODE IT');
|
||||
const body = h('div', { class: 'hx-body', tabindex: '0', role: 'group', 'aria-label': 'Hex dump' });
|
||||
const total = Math.min(bytes.length, maxBytes);
|
||||
let pinned = null;
|
||||
const cells = new Map();
|
||||
let cells = new Map();
|
||||
let perRow = 0;
|
||||
let ratio = 0; // width of one character as a fraction of the font size
|
||||
|
||||
const rowCount = Math.min(Math.ceil(bytes.length / 16), maxRows);
|
||||
for (let r = 0; r < rowCount; r++) {
|
||||
const start = r * 16;
|
||||
const hexCells = [];
|
||||
const ascCells = [];
|
||||
for (let k = 0; k < 16; k++) {
|
||||
const i = start + k;
|
||||
if (i >= bytes.length) {
|
||||
hexCells.push(h('span', { class: 'hx-b pad' }, ' '));
|
||||
continue;
|
||||
function build(n) {
|
||||
perRow = n;
|
||||
cells = new Map();
|
||||
const rows = [];
|
||||
for (let start = 0; start < total; start += n) {
|
||||
const hexCells = [];
|
||||
const ascCells = [];
|
||||
for (let k = 0; k < n; k++) {
|
||||
const i = start + k;
|
||||
if (i >= total) {
|
||||
hexCells.push(h('span', { class: 'hx-b pad' }, ' '));
|
||||
continue;
|
||||
}
|
||||
const v = bytes[i];
|
||||
const cls = v === 0 ? 'hx-b z' : 'hx-b';
|
||||
const a = h('span', { class: `${cls} a`, 'data-i': i }, printable(v));
|
||||
const x = h('span', { class: cls, 'data-i': i }, hex2(v));
|
||||
cells.set(i, [x, a]);
|
||||
hexCells.push(x);
|
||||
ascCells.push(a);
|
||||
if (n === 16 && k === 7) hexCells.push(h('span', { class: 'hx-gap' }, ' '));
|
||||
}
|
||||
const v = bytes[i];
|
||||
const cls = v === 0 ? 'hx-b z' : 'hx-b';
|
||||
const a = h('span', { class: `${cls} a`, 'data-i': i }, printable(v));
|
||||
const x = h('span', { class: cls, 'data-i': i }, hex2(v));
|
||||
cells.set(i, [x, a]);
|
||||
hexCells.push(x);
|
||||
ascCells.push(a);
|
||||
if (k === 7) hexCells.push(h('span', { class: 'hx-gap' }, ' '));
|
||||
rows.push(
|
||||
h('div', { class: 'hx-row' },
|
||||
h('span', { class: 'hx-off' }, pad(start.toString(16).toUpperCase(), 4)),
|
||||
h('span', { class: 'hx-hex' }, hexCells),
|
||||
h('span', { class: 'hx-asc' }, ascCells),
|
||||
),
|
||||
);
|
||||
}
|
||||
body.append(
|
||||
h('div', { class: 'hx-row' },
|
||||
h('span', { class: 'hx-off' }, pad(start.toString(16).toUpperCase(), 4)),
|
||||
h('span', { class: 'hx-hex' }, hexCells),
|
||||
h('span', { class: 'hx-asc' }, ascCells),
|
||||
),
|
||||
);
|
||||
body.replaceChildren(...rows);
|
||||
if (pinned !== null) cells.get(pinned)?.forEach((c) => c.classList.add('sel'));
|
||||
}
|
||||
|
||||
function measure() {
|
||||
if (ratio) return ratio;
|
||||
const probe = h('span', { class: 'hx-probe' }, 'M'.repeat(20));
|
||||
body.append(probe);
|
||||
ratio = probe.getBoundingClientRect().width / 20 / 100;
|
||||
probe.remove();
|
||||
return ratio || 0.6;
|
||||
}
|
||||
|
||||
function layout() {
|
||||
const cs = getComputedStyle(body);
|
||||
const avail = body.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
|
||||
if (!(avail > 0)) return; // not laid out yet
|
||||
const r = measure();
|
||||
const fontFor = (n) => avail / (rowChars(n) * r);
|
||||
// The widest layout that still gives a readable font; keep the current one a
|
||||
// little longer than that, so a scrollbar appearing cannot make it flap.
|
||||
let n = BYTES_PER_ROW.find((c) => fontFor(c) >= minFont - (c === perRow ? 0.6 : 0)) ?? 4;
|
||||
body.style.fontSize = `${Math.min(maxFont, fontFor(n)).toFixed(2)}px`;
|
||||
if (n !== perRow) build(n);
|
||||
}
|
||||
|
||||
const select = (i, pin) => {
|
||||
if (pin) {
|
||||
if (pinned !== null) cells.get(pinned)?.forEach((c) => c.classList.remove('sel'));
|
||||
pinned = pinned === i ? null : i;
|
||||
if (pinned !== null) cells.get(pinned).forEach((c) => c.classList.add('sel'));
|
||||
if (pinned !== null) cells.get(pinned)?.forEach((c) => c.classList.add('sel'));
|
||||
}
|
||||
const shown = pinned ?? i;
|
||||
inspector.textContent = shown === null ? 'HOVER OR CLICK A BYTE TO DECODE IT' : describe(bytes, shown);
|
||||
@@ -87,6 +126,10 @@ export function hexView(bytes, { rows: maxRows = Infinity } = {}) {
|
||||
body.addEventListener('mouseleave', () => { if (pinned === null) select(null, false); });
|
||||
body.addEventListener('click', (e) => { const i = target(e); if (i !== null) select(i, true); });
|
||||
|
||||
build(16);
|
||||
if (typeof ResizeObserver !== 'undefined') new ResizeObserver(layout).observe(body); // fires once attached, before first paint
|
||||
else window.addEventListener('resize', layout);
|
||||
|
||||
return { el: h('div', { class: 'hex' }, body, inspector), inspector };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user