init
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// Hex dump with an inspector: hover or click a byte to decode it.
|
||||
|
||||
import { h, pad } from './dom.js';
|
||||
|
||||
const hex2 = (b) => pad(b.toString(16).toUpperCase(), 2);
|
||||
const printable = (b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : '.');
|
||||
|
||||
/** Decode the bytes at offset i as little-endian integers. */
|
||||
export function inspect(bytes, i) {
|
||||
const b = (k) => (i + k < bytes.length ? bytes[i + k] : null);
|
||||
const have = (n) => i + n <= bytes.length;
|
||||
const out = { offset: i, u8: bytes[i] };
|
||||
if (have(2)) {
|
||||
const v = b(0) | (b(1) << 8);
|
||||
out.u16 = v;
|
||||
out.i16 = (v << 16) >> 16;
|
||||
}
|
||||
if (have(4)) {
|
||||
const v = (b(0) | (b(1) << 8) | (b(2) << 16) | (b(3) << 24));
|
||||
out.i32 = v;
|
||||
out.u32 = v >>> 0;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function describe(bytes, i) {
|
||||
const d = inspect(bytes, i);
|
||||
const parts = [`OFFSET 0x${pad(d.offset.toString(16).toUpperCase(), 4)} (${d.offset})`, `U8 ${d.u8}`];
|
||||
if (d.i16 !== undefined) parts.push(`I16 ${d.i16}`);
|
||||
if (d.i32 !== undefined) parts.push(`I32 ${d.i32}`, `U32 ${d.u32}`);
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a hex dump. Returns { el, inspector } where inspector is the element
|
||||
* that shows the decoded value of the hovered/selected byte.
|
||||
*/
|
||||
export function hexView(bytes, { rows: maxRows = Infinity } = {}) {
|
||||
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' });
|
||||
let pinned = null;
|
||||
const cells = new Map();
|
||||
|
||||
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;
|
||||
}
|
||||
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' }, ' '));
|
||||
}
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
const shown = pinned ?? i;
|
||||
inspector.textContent = shown === null ? 'HOVER OR CLICK A BYTE TO DECODE IT' : describe(bytes, shown);
|
||||
};
|
||||
const target = (e) => {
|
||||
const t = e.target.closest('[data-i]');
|
||||
return t ? Number(t.dataset.i) : null;
|
||||
};
|
||||
body.addEventListener('mouseover', (e) => { const i = target(e); if (i !== null && pinned === null) select(i, false); });
|
||||
body.addEventListener('mouseleave', () => { if (pinned === null) select(null, false); });
|
||||
body.addEventListener('click', (e) => { const i = target(e); if (i !== null) select(i, true); });
|
||||
|
||||
return { el: h('div', { class: 'hex' }, body, inspector), inspector };
|
||||
}
|
||||
|
||||
/** Parse hex text ("de ad 0xBE, ef") into bytes; throws on bad input. */
|
||||
export function parseHex(text) {
|
||||
const cleaned = text.replace(/0x/gi, ' ').replace(/[,\s]+/g, ' ').trim();
|
||||
if (!cleaned) return new Uint8Array(0);
|
||||
const out = [];
|
||||
for (const tok of cleaned.split(' ')) {
|
||||
if (!/^[0-9a-fA-F]+$/.test(tok) || tok.length % 2) throw new Error(`"${tok}" is not whole bytes of hex`);
|
||||
for (let i = 0; i < tok.length; i += 2) out.push(parseInt(tok.slice(i, i + 2), 16));
|
||||
}
|
||||
return Uint8Array.from(out);
|
||||
}
|
||||
Reference in New Issue
Block a user