@kikuchan/hexdump 0.1.0-alpha.3 → 0.1.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 kikuchan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,8 +1,53 @@
1
- # hexdump-js
1
+ # @kikuchan/hexdump
2
+
3
+ Tiny, configurable hex dump utility for Node.js and browsers.
2
4
 
3
5
  ## Install
4
6
 
5
7
  ```bash
6
- $ npm install -D @kikuchan/hexdump
8
+ npm install -D @kikuchan/hexdump
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { hexdump } from '@kikuchan/hexdump';
15
+
16
+ const bytes = Uint8Array.from([
17
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
18
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
19
+ ]);
20
+
21
+ hexdump.log(bytes);
22
+ ```
23
+
24
+ output:
25
+
7
26
  ```
27
+ 00000000: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
28
+ 00000010: | |
29
+ ```
30
+
31
+ ## API
32
+
33
+ ### Basic Functions
34
+
35
+ - `hexdump.log|warn|error(data, options?)`: outputs to `console.log|warn|error` respectively.
36
+ - `hexdump(data, options?)`: returns the full string instead of printing.
37
+ - `hexdump.create(printer)`: returns a hexdump function that prints to `printer` (a function that takes a string).
38
+
39
+ ### `data`
40
+ - Binary-like data: `Uint8Array`, `Uint8ClampedArray`, `ArrayBufferLike` or `DataView`;
8
41
 
42
+ ### `options?`
43
+ - `addrOffset: number` — Starting address for the first byte. Default `0`.
44
+ - `addrLength: number` — Hex digits shown for the address. Default `8 - prefix.length` (clamped to `>= 0`).
45
+ - `prefix: string` — Printed before the address (e.g. `"0x"`). Default `""`.
46
+ - `foldSize: number` — Bytes per row. Default `16`.
47
+ - `printChars: boolean` — Show the ASCII gutter at right. Default `true`.
48
+ - `footer: boolean` — Print a trailing line showing the next address. Default `true`.
49
+ - `color: boolean | 'simple' | 'html' | ((s: string, ctx: Context) => ColorizerOperation | undefined)` —
50
+ - `true` or `'simple'`: add ANSI colors in terminals.
51
+ - `'html'`: wrap parts with semantic spans like `hexdump-address`, `hexdump-ascii`, etc., and escape content.
52
+ - `printer?: (line: string) => void | null` — Provide a per-line sink directly in options. Set to `null` to disable printing even if a default printer is configured.
53
+ - `formatter: (s: string, ctx: Context) => string | undefined` — Transform each fragment before it is appended. Return `undefined` to drop that fragment.
package/package.json CHANGED
@@ -1,31 +1,16 @@
1
1
  {
2
2
  "name": "@kikuchan/hexdump",
3
- "version": "0.1.0-alpha.3",
4
3
  "description": "hexdump",
5
- "type": "module",
6
- "main": "dist/hexdump.js",
7
- "types": "dist/hexdump.d.ts",
8
- "files": [
9
- "dist/"
10
- ],
11
- "scripts": {
12
- "build:types": "tsc",
13
- "build": "pnpm run build:esm && pnpm run build:types",
14
- "build:esm": "esbuild src/hexdump.ts --minify --outfile=dist/hexdump.js",
15
- "lint": "biome lint ./src",
16
- "lint:fix": "biome lint ./src --write",
17
- "format": "biome format ./src",
18
- "format:fix": "biome format ./src --write"
19
- },
20
4
  "keywords": [
21
5
  "hexdump"
22
6
  ],
7
+ "version": "0.1.0-alpha.5",
8
+ "type": "module",
9
+ "main": "./src/index.ts",
23
10
  "author": "kikuchan <kikuchan98@gmail.com>",
24
- "homepage": "https://github.com/kikuchan/hexdump-js#readme",
11
+ "homepage": "https://github.com/kikuchan/utils-on-npm#readme",
25
12
  "license": "MIT",
26
- "devDependencies": {
27
- "@biomejs/biome": "^1.9.4",
28
- "esbuild": "^0.24.2",
29
- "typescript": "^5.7.3"
13
+ "scripts": {
14
+ "build": "tsdown"
30
15
  }
31
- }
16
+ }
package/src/index.ts ADDED
@@ -0,0 +1,278 @@
1
+ export type BinaryLike = Uint8Array | Uint8ClampedArray | ArrayBufferLike | DataView;
2
+
3
+ function hex(v: number, c: number) {
4
+ return Number(v).toString(16).padStart(c, '0');
5
+ }
6
+
7
+ type Context =
8
+ | {
9
+ type: 'hex-value' | 'hex-value-prefix' | 'hex-value-suffix' | 'character-value';
10
+ address: number;
11
+ value: number;
12
+ }
13
+ | {
14
+ type: 'address';
15
+ address: number;
16
+ }
17
+ | {
18
+ type:
19
+ | 'line-prefix'
20
+ | 'address-prefix'
21
+ | 'address-suffix'
22
+ | 'hex-dump-prefix'
23
+ | 'hex-group-prefix'
24
+ | 'hex-value-no-data'
25
+ | 'character-value-no-data'
26
+ | 'hex-gap'
27
+ | 'hex-group-gap'
28
+ | 'hex-group-suffix'
29
+ | 'hex-dump-suffix'
30
+ | 'character-prefix'
31
+ | 'character-suffix'
32
+ | 'line-suffix'
33
+ | 'flush';
34
+ };
35
+
36
+ type ColorizerOperation = { enter: string; leave: string; escape?: (s: string) => string } | null;
37
+
38
+ type Colorizer =
39
+ | boolean
40
+ | undefined
41
+ | 'simple'
42
+ | 'html'
43
+ | ((s: string, ctx: Context) => ColorizerOperation | undefined);
44
+ type Formatter = undefined | ((s: string, ctx: Context) => string | undefined);
45
+
46
+ type Options = {
47
+ addrOffset?: number;
48
+ addrLength?: number;
49
+ printer?: null | ((s: string) => void) /* for each line */;
50
+ formatter?: Formatter /* for each item */;
51
+ color?: Colorizer;
52
+ prefix?: string;
53
+ printChars?: boolean;
54
+ foldSize?: number;
55
+ footer?: boolean;
56
+ };
57
+
58
+ interface Hexdumper {
59
+ (buf: BinaryLike, options?: Options): string;
60
+ (buf: BinaryLike, len: number, options?: Options): string;
61
+ }
62
+
63
+ interface Hexdump extends Hexdumper {
64
+ log: Hexdumper;
65
+ warn: Hexdumper;
66
+ error: Hexdumper;
67
+ string: Hexdumper;
68
+
69
+ create: (printer: (s: string) => void) => Hexdumper;
70
+ }
71
+
72
+ const identity = <T>(s: T) => s;
73
+
74
+ const htmlEscaper = (s: string) =>
75
+ s.replace(
76
+ /[&<>]/g,
77
+ (x) =>
78
+ ({
79
+ '&': '&amp;',
80
+ '<': '&lt;',
81
+ '>': '&gt;',
82
+ })[x]!,
83
+ );
84
+
85
+ const simpleColorizer = {
86
+ simple: {
87
+ address: { enter: '\x1b[38;5;238m', leave: '\x1b[m' },
88
+ separator: { enter: '\x1b[38;5;238m', leave: '\x1b[m' },
89
+ control: { enter: '\x1b[38;5;178m', leave: '\x1b[m' },
90
+ ascii: { enter: '\x1b[m', leave: '\x1b[m' },
91
+ exascii: { enter: '\x1b[38;5;209m', leave: '\x1b[m' },
92
+ null: { enter: '\x1b[38;5;244m', leave: '\x1b[m' },
93
+ normal: null,
94
+ },
95
+ html: {
96
+ address: { enter: '<span class="hexdump-address">', leave: '</span>', escape: htmlEscaper },
97
+ separator: { enter: '<span class="hexdump-separator">', leave: '</span>', escape: htmlEscaper },
98
+ control: { enter: '<span class="hexdump-control">', leave: '</span>', escape: htmlEscaper },
99
+ ascii: { enter: '<span class="hexdump-ascii">', leave: '</span>', escape: htmlEscaper },
100
+ exascii: { enter: '<span class="hexdump-exascii">', leave: '</span>', escape: htmlEscaper },
101
+ null: { enter: '<span class="hexdump-null">', leave: '</span>', escape: htmlEscaper },
102
+ normal: { enter: '', leave: '', escape: htmlEscaper },
103
+ },
104
+ } as Record<
105
+ string,
106
+ {
107
+ address?: ColorizerOperation;
108
+ separator?: ColorizerOperation;
109
+ control?: ColorizerOperation;
110
+ ascii?: ColorizerOperation;
111
+ exascii?: ColorizerOperation;
112
+ null?: ColorizerOperation;
113
+ normal: ColorizerOperation;
114
+ }
115
+ >;
116
+
117
+ const create_colorizer = (colorizer: Colorizer) => {
118
+ let lastColor: ColorizerOperation | undefined = undefined;
119
+
120
+ if (!colorizer) return identity;
121
+ if (colorizer === true) colorizer = 'simple';
122
+
123
+ if (typeof colorizer === 'string') {
124
+ const defs = simpleColorizer[colorizer];
125
+ const separators = ['address-prefix', 'address-suffix', 'character-prefix', 'character-suffix'];
126
+
127
+ colorizer = function (s, ctx) {
128
+ if (!s.trim()) return undefined; // keep the last color context on empty
129
+
130
+ if (defs.address && ctx.type === 'address') return defs.address;
131
+ if (defs.separator && separators.includes(ctx.type)) return defs.separator;
132
+
133
+ if ((ctx.type === 'hex-value' || ctx.type === 'character-value') && typeof ctx.value === 'number') {
134
+ if (defs.null !== undefined && ctx.value === 0) return defs.null;
135
+ if (defs.control !== undefined && ctx.value < 0x20) return defs.control;
136
+ if (defs.ascii !== undefined && 0x20 <= ctx.value && ctx.value < 0x7f) return defs.ascii;
137
+ if (defs.exascii !== undefined && 0x80 <= ctx.value && ctx.value <= 0xff) return defs.exascii;
138
+ }
139
+
140
+ return defs.normal;
141
+ };
142
+ }
143
+
144
+ return (s: string, ctx: Context) => {
145
+ const color = ctx.type === 'flush' ? null : colorizer(s, ctx);
146
+
147
+ // If color changes, emit leave/enter tokens and escape content.
148
+ if (
149
+ color !== undefined &&
150
+ (lastColor?.enter !== color?.enter || lastColor?.leave !== color?.leave || lastColor?.escape !== color?.escape)
151
+ ) {
152
+ s = (lastColor?.leave || '') + (color?.enter || '') + (color?.escape || identity)(s);
153
+ lastColor = color;
154
+ return s;
155
+ }
156
+
157
+ // If color stays the same and provides an escaper, still escape content.
158
+ if (color && color.escape) {
159
+ return color.escape(s);
160
+ }
161
+
162
+ return s;
163
+ };
164
+ };
165
+
166
+ const create_formatter = (formatter: Formatter) => {
167
+ if (!formatter) formatter = identity;
168
+
169
+ return (s: string, ctx: Context) => {
170
+ return formatter(s, ctx) || '';
171
+ };
172
+ };
173
+
174
+ function create_hexdumper(printer: ((s: string) => void) | null): Hexdumper {
175
+ return (buf: BinaryLike, len?: number | Options, options?: Options) => {
176
+ const u8 = ArrayBuffer.isView(buf)
177
+ ? new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
178
+ : new Uint8Array(buf);
179
+ if (typeof len !== 'number') {
180
+ options = len;
181
+ len = u8.length;
182
+ }
183
+ options = { ...(options ?? {}) };
184
+ if (len === undefined || len < 0) len = u8.length;
185
+
186
+ printer = options.printer === null ? null : (options.printer ?? printer);
187
+ const formatter = create_formatter(options.formatter);
188
+ const colorize = create_colorizer(options.color);
189
+
190
+ const foldSize = options.foldSize || 16;
191
+ const printChars = options.printChars !== false;
192
+
193
+ let address = options.addrOffset || 0;
194
+ const offset = address % foldSize;
195
+ const rows = (len ? Math.ceil(((offset % foldSize) + len) / foldSize) : 0) + (options.footer !== false ? 1 : 0);
196
+
197
+ const result: string[] = [];
198
+ let line = '';
199
+ const print = function (s: string, ctx: Context) {
200
+ line += colorize(formatter(s, ctx), ctx);
201
+ };
202
+
203
+ const prefix = options?.prefix || '';
204
+ const addrLength = Math.max(options?.addrLength ?? 8 - prefix.length, 0);
205
+
206
+ for (let i = 0; i < rows; i++) {
207
+ const addressBase = address;
208
+
209
+ print('', { type: 'line-prefix' });
210
+ print(prefix, { type: 'address-prefix' });
211
+ if (addrLength >= 1) print(hex(address, addrLength), { type: 'address', address });
212
+ print(': ', { type: 'address-suffix' });
213
+
214
+ print(' ', { type: 'hex-dump-prefix' });
215
+ print('', { type: 'hex-group-prefix' });
216
+ for (let j = 0; j < foldSize; j++) {
217
+ const idx = i * foldSize + j - offset;
218
+ if (j && j % 8 == 0) {
219
+ print('', { type: 'hex-group-suffix' });
220
+ print(' ', { type: 'hex-group-gap' });
221
+ print('', { type: 'hex-group-prefix' });
222
+ } else if (j) {
223
+ print(' ', { type: 'hex-gap' });
224
+ }
225
+
226
+ if (0 <= idx && idx < len) {
227
+ print('', { type: 'hex-value-prefix', address, value: u8[idx] });
228
+ print(hex(u8[idx], 2), { type: 'hex-value', address, value: u8[idx] });
229
+ print('', { type: 'hex-value-suffix', address, value: u8[idx] });
230
+ address++;
231
+ } else {
232
+ print(' ', { type: 'hex-value-no-data' });
233
+ }
234
+ }
235
+ print('', { type: 'hex-group-suffix' });
236
+ print(' ', { type: 'hex-dump-suffix' });
237
+
238
+ if (printChars) {
239
+ let address = addressBase;
240
+ print(' |', { type: 'character-prefix' });
241
+ for (let j = 0; j < foldSize; j++) {
242
+ const idx = i * foldSize + j - offset;
243
+
244
+ if (0 <= idx && idx < len) {
245
+ print(u8[idx] >= 0x20 && u8[idx] < 0x7f ? String.fromCharCode(u8[idx]) : '.', {
246
+ type: 'character-value',
247
+ address,
248
+ value: u8[idx],
249
+ });
250
+ address++;
251
+ } else {
252
+ print(' ', { type: 'character-value-no-data' });
253
+ }
254
+ }
255
+ print('|', { type: 'character-suffix' });
256
+ }
257
+ print('', { type: 'line-suffix' });
258
+ print('', { type: 'flush' });
259
+
260
+ printer?.(line);
261
+ result.push(line);
262
+ line = '';
263
+ }
264
+ return result.join('\n');
265
+ };
266
+ }
267
+
268
+ export const hexdump: Hexdump = Object.assign(create_hexdumper(null), {
269
+ create: create_hexdumper,
270
+
271
+ log: create_hexdumper((s) => console.log(s)),
272
+ warn: create_hexdumper((s) => console.warn(s)),
273
+ error: create_hexdumper((s) => console.error(s)),
274
+
275
+ string: create_hexdumper(null),
276
+ });
277
+
278
+ export default hexdump;
@@ -0,0 +1,388 @@
1
+ import hexdumpDefault, { hexdump } from '@kikuchan/hexdump';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ const u8 = (arr: number[]) => new Uint8Array(arr);
5
+
6
+ describe('hexdump basics', () => {
7
+ it('prints a basic hexdump', () => {
8
+ const out = hexdump(u8([0x00, 0x20, 0x41, 0x7e, 0x7f, 0x80, 0xff]));
9
+ expect(out).toBe(
10
+ [
11
+ `00000000: 00 20 41 7e 7f 80 ff |. A~... |`,
12
+ `00000007: | |`,
13
+ ].join('\n'),
14
+ );
15
+ });
16
+
17
+ it('prints a single footer line for empty input', () => {
18
+ const out = hexdump(u8([]), {
19
+ foldSize: 8,
20
+ printChars: false,
21
+ addrLength: 4,
22
+ });
23
+ expect(out).toBe(['0000: '].join('\n'));
24
+ });
25
+
26
+ it('prints bytes and a footer line', () => {
27
+ const out = hexdump(u8([0x41, 0x42, 0x43]), {
28
+ foldSize: 4,
29
+ printChars: false,
30
+ addrLength: 4,
31
+ });
32
+ expect(out).toBe(['0000: 41 42 43 ', '0003: '].join('\n'));
33
+ });
34
+ });
35
+
36
+ describe('hexdump printer integration', () => {
37
+ it('invokes custom printer for each line', () => {
38
+ const lines: string[] = [];
39
+ const dump = hexdump.create((s) => lines.push(s));
40
+ const out = dump(new Uint8Array([0x41, 0x42, 0x43]), {
41
+ foldSize: 2,
42
+ printChars: false,
43
+ addrLength: 4,
44
+ });
45
+
46
+ const outLines = out.split('\n');
47
+ expect(lines.join('\n')).toBe(out);
48
+ expect(lines.length).toBe(outLines.length);
49
+ expect(lines[0]).toContain('0000:');
50
+ expect(lines[0]).toContain('41 42');
51
+ });
52
+ });
53
+
54
+ describe('hexdump HTML colorizer escaping', () => {
55
+ it('escapes <, >, & in ascii region', () => {
56
+ const out = hexdump(new TextEncoder().encode('<&>'), {
57
+ foldSize: 8,
58
+ printChars: true,
59
+ addrLength: 2,
60
+ color: 'html',
61
+ });
62
+ expect(out).toContain('&lt;');
63
+ expect(out).toContain('&gt;');
64
+ expect(out).toContain('&amp;');
65
+ });
66
+ });
67
+
68
+ describe('hexdump hex value prefix/suffix', () => {
69
+ it('wraps each hex byte with custom markers', () => {
70
+ const bytes = new Uint8Array([0x41, 0x42]);
71
+ const out = hexdump(bytes, {
72
+ foldSize: 2,
73
+ printChars: false,
74
+ addrLength: 2,
75
+ color: (_s, ctx) => {
76
+ if (ctx.type === 'hex-value-prefix') return { enter: '{', leave: '' };
77
+ if (ctx.type === 'hex-value-suffix') return { enter: '', leave: '}' };
78
+ return undefined;
79
+ },
80
+ });
81
+ const firstLine = out.split('\n')[0] || '';
82
+ const opens = (firstLine.match(/\{/g) || []).length;
83
+ const closes = (firstLine.match(/\}/g) || []).length;
84
+ expect(opens).toBe(2);
85
+ expect(closes).toBe(2);
86
+ expect(firstLine).toContain('{41');
87
+ expect(firstLine).toContain('{42');
88
+ });
89
+ });
90
+
91
+ describe('hexdump flush handling', () => {
92
+ it('appends leave token at end of line on flush', () => {
93
+ const out = hexdump(new Uint8Array([0x00]), {
94
+ foldSize: 1,
95
+ printChars: false,
96
+ addrLength: 2,
97
+ color: (_s, ctx) => {
98
+ if (ctx.type === 'line-prefix') return { enter: '<', leave: '>' };
99
+ return undefined;
100
+ },
101
+ });
102
+ const first = out.split('\n')[0] || '';
103
+ expect(first.endsWith('>')).toBe(true);
104
+ });
105
+ });
106
+
107
+ describe('hexdump options', () => {
108
+ it('respects addrOffset and computes footer address', () => {
109
+ const out = hexdump(u8([0xaa, 0xbb]), {
110
+ addrOffset: 1,
111
+ foldSize: 4,
112
+ printChars: false,
113
+ addrLength: 4,
114
+ });
115
+ expect(out).toBe(['0001: aa bb ', '0003: '].join('\n'));
116
+ });
117
+
118
+ it('applies a custom formatter to hex values', () => {
119
+ const out = hexdump(u8([0x00, 0x20, 0x41]), {
120
+ foldSize: 4,
121
+ printChars: false,
122
+ addrLength: 4,
123
+ formatter: (s, ctx) => (ctx.type === 'hex-value' ? `[${s}]` : s),
124
+ });
125
+ expect(out).toBe(['0000: [00] [20] [41] ', '0003: '].join('\n'));
126
+ });
127
+
128
+ it("adds ANSI codes when color is 'simple'", () => {
129
+ const out = hexdump(u8([0x00]), {
130
+ foldSize: 1,
131
+ printChars: false,
132
+ addrLength: 2,
133
+ color: 'simple',
134
+ });
135
+
136
+ expect(/\x1b\[[0-9;]*m/.test(out)).toBe(true);
137
+ });
138
+
139
+ it("applies 'control' color for <0x20 (simple)", () => {
140
+ const out = hexdump(u8([0x01]), {
141
+ foldSize: 1,
142
+ printChars: false,
143
+ addrLength: 2,
144
+ color: 'simple',
145
+ });
146
+ expect(/\x1b\[38;5;178m/.test(out)).toBe(true);
147
+ });
148
+
149
+ it("escapes ASCII with color:'html' and emits span", () => {
150
+ const out = hexdump(u8([0x3c, 0x20]), {
151
+ foldSize: 2,
152
+ printChars: true,
153
+ addrLength: 2,
154
+ color: 'html',
155
+ });
156
+ expect(out).toContain('hexdump-ascii');
157
+ expect(out).toContain('&lt;');
158
+ });
159
+
160
+ it("assigns exascii class for >=0x80 with color:'html'", () => {
161
+ const out = hexdump(u8([0x80]), {
162
+ foldSize: 1,
163
+ printChars: true,
164
+ addrLength: 2,
165
+ color: 'html',
166
+ });
167
+ expect(out).toContain('hexdump-exascii');
168
+ });
169
+
170
+ it("uses 'normal' branch for DEL (0x7f) with color:'html'", () => {
171
+ const out = hexdump(u8([0x7f]), {
172
+ foldSize: 1,
173
+ printChars: true,
174
+ addrLength: 2,
175
+ color: 'html',
176
+ });
177
+ expect(out).not.toContain('hexdump-ascii');
178
+ expect(out).not.toContain('hexdump-control');
179
+ expect(out).not.toContain('hexdump-exascii');
180
+ });
181
+
182
+ it('respects footer:false (no footer line)', () => {
183
+ const out = hexdump(u8([0x41, 0x42, 0x43]), {
184
+ foldSize: 8,
185
+ printChars: false,
186
+ addrLength: 4,
187
+ footer: false,
188
+ });
189
+ const lines = out.split(/\n/);
190
+ expect(lines.length).toBe(1);
191
+ });
192
+
193
+ it('derives addrLength from prefix when not set', () => {
194
+ const out = hexdump(u8([]), {
195
+ foldSize: 8,
196
+ printChars: false,
197
+ prefix: '0x',
198
+ });
199
+ expect(out).toBe(['0x000000: '].join('\n'));
200
+ });
201
+
202
+ it('clamps addrLength to 0 when prefix is long (address hidden)', () => {
203
+ const out = hexdump(u8([]), {
204
+ foldSize: 8,
205
+ printChars: false,
206
+ prefix: '012345678',
207
+ });
208
+ expect(out).toBe(['012345678: '].join('\n'));
209
+ });
210
+
211
+ it('color:true behaves like simple (ANSI on)', () => {
212
+ const out = hexdump(u8([0x41]), {
213
+ foldSize: 1,
214
+ printChars: false,
215
+ addrLength: 2,
216
+ color: true,
217
+ });
218
+ expect(/\x1b\[[0-9;]*m/.test(out)).toBe(true);
219
+ });
220
+
221
+ it('custom colorizer wraps line with markers and flush closes it', () => {
222
+ const out = hexdump(u8([0x41, 0x42]), {
223
+ foldSize: 2,
224
+ printChars: false,
225
+ addrLength: 2,
226
+ color: (s) => {
227
+ if (!s.trim()) return undefined;
228
+ return { enter: '[', leave: ']' };
229
+ },
230
+ });
231
+ expect(out).toBe(['[00: 41 42 ]', '[02: ]'].join('\n'));
232
+ });
233
+ });
234
+
235
+ describe('printer callbacks', () => {
236
+ it('calls the printer once per rendered line', () => {
237
+ const lines: string[] = [];
238
+ const dump = hexdump.create((s) => lines.push(s));
239
+
240
+ const out = dump(u8([1, 2, 3, 4, 5]), {
241
+ foldSize: 4,
242
+ printChars: false,
243
+ addrLength: 4,
244
+ });
245
+
246
+ const split = out.split(/\n/);
247
+ expect(lines.length).toBe(split.length);
248
+ expect(lines[0]).toBe(split[0]);
249
+ });
250
+
251
+ it('disables printer when options.printer is null', () => {
252
+ const dump = hexdump.create((s) => {
253
+ throw new Error('should not be called');
254
+ });
255
+ const out = dump(u8([0x41]), { foldSize: 1, printChars: false, addrLength: 2, printer: null });
256
+ expect(out.includes('41')).toBe(true);
257
+ });
258
+ });
259
+
260
+ describe('characters view', () => {
261
+ it('shows ASCII and dots for non-printables', () => {
262
+ const out = hexdump(u8([0x00, 0x41, 0x7f, 0x80]), {
263
+ foldSize: 4,
264
+ printChars: true,
265
+ addrLength: 2,
266
+ });
267
+ expect(out).toBe(['00: 00 41 7f 80 |.A..|', '04: | |'].join('\n'));
268
+ });
269
+ });
270
+
271
+ describe('overloads and inputs', () => {
272
+ it('accepts len overload to limit bytes', () => {
273
+ const out = hexdump(u8([0, 1, 2, 3]), 2, {
274
+ foldSize: 8,
275
+ printChars: false,
276
+ addrLength: 4,
277
+ });
278
+ expect(out).toBe(['0000: 00 01 ', '0002: '].join('\n'));
279
+ });
280
+
281
+ it('treats negative len as full length', () => {
282
+ const out = hexdump(u8([1, 2, 3]), -1, {
283
+ foldSize: 8,
284
+ printChars: false,
285
+ addrLength: 4,
286
+ });
287
+ expect(out).toBe(['0000: 01 02 03 ', '0003: '].join('\n'));
288
+ });
289
+
290
+ it('accepts DataView input', () => {
291
+ const arr = u8([0xde, 0xad, 0xbe, 0xef]);
292
+ const view = new DataView(arr.buffer);
293
+ const out = hexdump(view, {
294
+ foldSize: 8,
295
+ printChars: false,
296
+ addrLength: 4,
297
+ });
298
+ expect(out).toBe(['0000: de ad be ef ', '0004: '].join('\n'));
299
+ });
300
+
301
+ it('accepts ArrayBuffer input', () => {
302
+ const arr = u8([0xde, 0xad]);
303
+ const out = hexdump(arr.buffer, {
304
+ foldSize: 8,
305
+ printChars: false,
306
+ addrLength: 4,
307
+ });
308
+ expect(out).toBe(['0000: de ad ', '0002: '].join('\n'));
309
+ });
310
+ });
311
+
312
+ describe('grouping and gaps', () => {
313
+ it('inserts a double-space between groups of 8', () => {
314
+ const bytes = Array.from({ length: 10 }, (_, i) => i);
315
+ const out = hexdump(u8(bytes), {
316
+ foldSize: 10,
317
+ printChars: false,
318
+ addrLength: 4,
319
+ });
320
+ expect(out).toBe(['0000: 00 01 02 03 04 05 06 07 08 09 ', '000a: '].join('\n'));
321
+ });
322
+ });
323
+
324
+ describe('predefined printers', () => {
325
+ it('warn printer emits to console and returns string', () => {
326
+ const orig = console.warn;
327
+ const captured: string[] = [];
328
+ console.warn = (s: string) => void captured.push(s);
329
+ try {
330
+ const out = hexdump.warn(u8([0x11, 0x22]), {
331
+ foldSize: 8,
332
+ printChars: false,
333
+ addrLength: 4,
334
+ });
335
+ expect(out).toBe(['0000: 11 22 ', '0002: '].join('\n'));
336
+ expect(captured.length).toBeGreaterThan(0);
337
+ expect(captured[0]).toBe(out.split(/\n/)[0]);
338
+ } finally {
339
+ console.warn = orig;
340
+ }
341
+ });
342
+
343
+ it('log printer emits to console.log', () => {
344
+ const orig = console.log;
345
+ const captured: string[] = [];
346
+ console.log = (s: string) => void captured.push(s);
347
+ try {
348
+ const out = hexdump.log(u8([0xaa]), { foldSize: 8, printChars: false, addrLength: 4 });
349
+ expect(out).toBe(['0000: aa ', '0001: '].join('\n'));
350
+ expect(captured.length).toBeGreaterThan(0);
351
+ expect(captured[0]).toBe(out.split(/\n/)[0]);
352
+ } finally {
353
+ console.log = orig;
354
+ }
355
+ });
356
+
357
+ it('error printer emits to console.error', () => {
358
+ const orig = console.error;
359
+ const captured: string[] = [];
360
+ console.error = (s: string) => void captured.push(s);
361
+ try {
362
+ const out = hexdump.error(u8([0xbb]), { foldSize: 8, printChars: false, addrLength: 4 });
363
+ expect(out).toBe(['0000: bb ', '0001: '].join('\n'));
364
+ expect(captured.length).toBeGreaterThan(0);
365
+ expect(captured[0]).toBe(out.split(/\n/)[0]);
366
+ } finally {
367
+ console.error = orig;
368
+ }
369
+ });
370
+
371
+ it('string and default produce identical output', () => {
372
+ const buf = u8([0, 1, 2]);
373
+ const opts = { foldSize: 8, printChars: false, addrLength: 4 } as const;
374
+ const a = hexdump(buf, opts);
375
+ const b = hexdump.string(buf, opts);
376
+ expect(a).toBe(b);
377
+ });
378
+ });
379
+
380
+ describe('default export', () => {
381
+ it('exposes hexdump equivalent to named export', () => {
382
+ const buf = u8([0x01, 0x02]);
383
+ const opts = { foldSize: 8, printChars: false, addrLength: 4 } as const;
384
+ const a = hexdump(buf, opts);
385
+ const b = hexdumpDefault(buf, opts);
386
+ expect(a).toBe(b);
387
+ });
388
+ });
package/tsconfig.json ADDED
@@ -0,0 +1 @@
1
+ { "extends": "../../tsconfig.common.json" }
package/dist/hexdump.d.ts DELETED
@@ -1,44 +0,0 @@
1
- type Context = {
2
- type: 'hex-value' | 'hex-value-prefix' | 'hex-value-suffix' | 'character-value';
3
- address: number;
4
- value: number;
5
- } | {
6
- type: 'address';
7
- address: number;
8
- } | {
9
- type: 'line-prefix' | 'address-prefix' | 'address-suffix' | 'hex-dump-prefix' | 'hex-group-prefix' | 'hex-value-no-data' | 'character-value-no-data' | 'hex-gap' | 'hex-group-gap' | 'hex-group-suffix' | 'hex-dump-suffix' | 'character-prefix' | 'character-suffix' | 'line-suffix' | 'flush';
10
- };
11
- type ColorizerOperation = {
12
- enter: string;
13
- leave: string;
14
- escape?: (s: string) => string;
15
- } | null;
16
- type Colorizer = boolean | undefined | 'simple' | 'html' | ((s: string, ctx: Context) => ColorizerOperation | undefined);
17
- type Formatter = undefined | ((s: string, ctx: Context) => string | undefined);
18
- type Options = {
19
- addrOffset?: number;
20
- addrLength?: number;
21
- printer?: false | ((s: string) => void);
22
- formatter?: Formatter;
23
- color?: Colorizer;
24
- prefix?: string;
25
- printChars?: boolean;
26
- foldSize?: number;
27
- footer?: boolean;
28
- };
29
- interface Hexdumper {
30
- (buf: ArrayLike<number> | ArrayBuffer | DataView, options?: Options): string;
31
- (buf: ArrayLike<number> | ArrayBuffer | DataView, len: number, options?: Options): string;
32
- }
33
- interface Hexdump extends Hexdumper {
34
- log: Hexdumper;
35
- warn: Hexdumper;
36
- error: Hexdumper;
37
- string: Hexdumper;
38
- create: (printer: (s: string) => void) => Hexdumper;
39
- }
40
- export declare const hexdump: Hexdump;
41
- declare const _default: {
42
- hexdump: Hexdump;
43
- };
44
- export default _default;
package/dist/hexdump.js DELETED
@@ -1,2 +0,0 @@
1
- "use strict";function C(t,n){return Number(t).toString(16).padStart(n,"0")}const g=t=>t,f=t=>t.replace(/[&<>]/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;"})[n]),A={simple:{address:{enter:"\x1B[38;5;238m",leave:"\x1B[m"},separator:{enter:"\x1B[38;5;238m",leave:"\x1B[m"},control:{enter:"\x1B[38;5;178m",leave:"\x1B[m"},ascii:{enter:"\x1B[m",leave:"\x1B[m"},exascii:{enter:"\x1B[38;5;209m",leave:"\x1B[m"},null:{enter:"\x1B[38;5;244m",leave:"\x1B[m"},normal:null},html:{address:{enter:'<span class="hexdump-address">',leave:"</span>",escape:f},separator:{enter:'<span class="hexdump-separator">',leave:"</span>",escape:f},control:{enter:'<span class="hexdump-control">',leave:"</span>",escape:f},ascii:{enter:'<span class="hexdump-ascii">',leave:"</span>",escape:f},exascii:{enter:'<span class="hexdump-exascii">',leave:"</span>",escape:f},null:{enter:'<span class="hexdump-null">',leave:"</span>",escape:f},normal:{enter:"",leave:"",escape:f}}},L=t=>{let n;if(!t)return g;if(t===!0&&(t="simple"),typeof t=="string"){const e=A[t],s=["address-prefix","address-suffix","character-prefix","character-suffix"];t=function(a,i){if(a.trim())return e.address&&i.type==="address"?e.address:e.separator&&s.includes(i.type)?e.separator:(i.type==="hex-value"||i.type==="character-value")&&typeof i.value=="number"?e.null!==void 0&&i.value===0?e.null:e.control!==void 0&&i.value<32?e.control:e.ascii!==void 0&&32<=i.value&&i.value<127?e.ascii:e.exascii!==void 0&&128<=i.value&&i.value<=255?e.exascii:e.normal:e.normal}}return(e,s)=>{const a=s.type==="flush"?null:t(e,s);return a!==void 0&&(n?.enter!==a?.enter||n?.leave!==a?.leave||n?.escape!==a?.escape)&&(e=(n?.leave||"")+(a?.enter||"")+(a?.escape||g)(e),n=a),e}},S=t=>(t||(t=g),(n,e)=>t(n,e)||"");function c(t){return(n,e,s)=>{const a=ArrayBuffer.isView(n)?new Uint8Array(n.buffer,n.byteOffset,n.byteLength):new Uint8Array(n);typeof e!="number"&&(s=e,e=a.length),s={...s??{}},(e===void 0||e<0)&&(e=a.length),t=s.printer||t;const i=S(s.formatter),O=L(s.color),l=s.foldSize||16,z=s.printChars!==!1;let p=s.addrOffset||0;const y=p%l,w=(e?Math.ceil((y%l+e)/l):0)+(s.footer!==!1?1:0),v=[];let m="";const r=function(d,h){m+=O(i(d,h),h)},b=s?.prefix||"",H=s?.addrLength??8-b.length;for(let d=0;d<w;d++){const h=p;r("",{type:"line-prefix"}),r(b,{type:"address-prefix"}),r(C(p,H),{type:"address",address:p}),r(": ",{type:"address-suffix"}),r(" ",{type:"hex-dump-prefix"}),r("",{type:"hex-group-prefix"});for(let o=0;o<l;o++){const u=d*l+o-y;o&&o%8==0?(r("",{type:"hex-group-suffix"}),r(" ",{type:"hex-group-gap"}),r("",{type:"hex-group-prefix"})):o&&r(" ",{type:"hex-gap"}),0<=u&&u<e?(r("",{type:"hex-value-prefix",address:p,value:a[u]}),r(C(a[u],2),{type:"hex-value",address:p,value:a[u]}),r("",{type:"hex-value-suffix",address:p,value:a[u]}),p++):r(" ",{type:"hex-value-no-data"})}if(r("",{type:"hex-group-suffix"}),r(" ",{type:"hex-dump-suffix"}),z){let o=h;r(" |",{type:"character-prefix"});for(let u=0;u<l;u++){const x=d*l+u-y;0<=x&&x<e?(r(a[x]>=32&&a[x]<127?String.fromCharCode(a[x]):".",{type:"character-value",address:o,value:a[x]}),o++):r(" ",{type:"character-value-no-data"})}r("|",{type:"character-suffix"})}r("",{type:"line-suffix"}),r("",{type:"flush"}),t?.(m),v.push(m),m=""}return v.join(`
2
- `)}}export const hexdump=Object.assign(c(null),{create:c,log:c(console.log),warn:c(console.warn),error:c(console.error),string:c(null)});export default{hexdump};