Skip to content

Commit 9beb28d

Browse files
committed
Json component render byte arrays
1 parent 80f2b75 commit 9beb28d

6 files changed

Lines changed: 81 additions & 5 deletions

File tree

src/components/ImageView/ImageView.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('ImageView Component', () => {
2323
assert(source?.kind === 'file')
2424

2525
const { findByRole, findByText } = render(
26-
<ImageView source={source} setError={console.error} />
26+
<ImageView source={source} setError={vi.fn()} />
2727
)
2828

2929
// wait for asynchronous image loading

src/components/Json/Json.module.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@
7070
.comment {
7171
color: #ccd8;
7272
}
73+
.hexDump {
74+
margin: 4px 0 4px 20px;
75+
font-family: monospace;
76+
line-height: 1.4;
77+
}
7378
.showMore {
7479
cursor: pointer;
7580
color: #ccd8;

src/components/Json/Json.stories.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,19 @@ export const MessagesList: Story = {
144144
},
145145
render,
146146
}
147+
148+
export const ByteArrays: Story = {
149+
args: {
150+
json: {
151+
small: new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]),
152+
withAscii: new Uint8Array(
153+
Array.from('Hello, World! This is a test of the hex dump viewer.', c => c.charCodeAt(0))
154+
),
155+
binary: new Uint8Array(Array.from({ length: 256 }, (_, i) => i)),
156+
empty: new Uint8Array(0),
157+
fromBuffer: new Uint8Array(new ArrayBuffer(32)).map((_, i) => i * 8),
158+
},
159+
label: 'json',
160+
},
161+
render,
162+
}

src/components/Json/Json.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ describe('Json Component', () => {
194194
await user.click(treeItem) // expand again
195195
expect(queryByText('...')).toBeNull()
196196
})
197+
198+
it('renders Uint8Array as hex dump', () => {
199+
const bytes = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])
200+
const { getByText } = render(<Json json={bytes} />)
201+
getByText('Uint8Array(5)')
202+
getByText(/48 65 6c 6c 6f/)
203+
getByText('Hello')
204+
})
205+
206+
it('renders ArrayBuffer as hex dump', () => {
207+
const { buffer } = new Uint8Array([0xff, 0x00, 0x7e])
208+
const { getByText } = render(<Json json={buffer} />)
209+
getByText(/ff 00 7e/)
210+
})
197211
})
198212

199213
describe('isPrimitive', () => {

src/components/Json/Json.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ function JsonContent({ json, label, expandRoot, pageLimit }: JsonProps): ReactNo
3030
} else if (json instanceof Date) {
3131
const key = label ? <span className={styles.key}>{label}: </span> : ''
3232
div = <>{key}<span className={styles.string}>{`"${json.toISOString()}"`}</span></>
33+
} else if (json instanceof ArrayBuffer || json instanceof Uint8Array) {
34+
const bytes = json instanceof ArrayBuffer ? new Uint8Array(json) : json
35+
div = <ByteArray bytes={bytes} label={label} expandRoot={expandRoot} />
3336
} else if (typeof json === 'object' && json !== null) {
3437
div = <JsonObject label={label} obj={json} expandRoot={expandRoot} pageLimit={pageLimit} />
3538
} else {
@@ -54,6 +57,44 @@ function JsonContent({ json, label, expandRoot, pageLimit }: JsonProps): ReactNo
5457
return div
5558
}
5659

60+
function formatHexDump(bytes: Uint8Array): { hex: string, ascii: string }[] {
61+
const lines: { hex: string, ascii: string }[] = []
62+
for (let i = 0; i < bytes.length; i += 16) {
63+
const slice = bytes.slice(i, i + 16)
64+
const hex = Array.from(slice).map(b => b.toString(16).padStart(2, '0')).join(' ')
65+
const ascii = Array.from(slice).map(b => b >= 0x20 && b <= 0x7e ? String.fromCharCode(b) : '.').join('')
66+
lines.push({ hex: hex.padEnd(47), ascii })
67+
}
68+
return lines
69+
}
70+
71+
function ByteArray({ bytes, label, expandRoot }: { bytes: Uint8Array, label?: string, expandRoot?: boolean }): ReactNode {
72+
const [collapsed, setCollapsed] = useState(!expandRoot)
73+
const key = label ? <span className={styles.key}>{label}: </span> : ''
74+
const summary = `${bytes.constructor.name}(${bytes.length})`
75+
76+
if (collapsed) {
77+
return <div role="treeitem" className={styles.clickable} aria-expanded="false" onClick={() => { setCollapsed(false) }}>
78+
{key}
79+
<span className={styles.comment}>{summary}</span>
80+
</div>
81+
}
82+
83+
const lines = formatHexDump(bytes)
84+
return <>
85+
<div role="treeitem" className={styles.clickable} aria-expanded="true" onClick={() => { setCollapsed(true) }}>
86+
{key}
87+
<span className={styles.comment}>{summary}</span>
88+
</div>
89+
<pre className={styles.hexDump}>
90+
{lines.map((line, i) => {
91+
const offset = (i * 16).toString(16).padStart(8, '0')
92+
return <div key={i}><span className={styles.comment}>{offset}</span> <span className={styles.number}>{line.hex}</span> <span className={styles.string}>{line.ascii}</span></div>
93+
})}
94+
</pre>
95+
</>
96+
}
97+
5798
function CollapsedArray({ array }: {array: unknown[]}): ReactNode {
5899
const { elementRef, width } = useWidth<HTMLSpanElement>()
59100
const maxCharacterCount = Math.max(20, Math.floor(width / 8))

src/components/SlidePanel/SlidePanel.module.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@
1414

1515
/* resizer separator */
1616
& > [role="separator"] {
17-
border-right: 1px solid #ddd;
18-
border-left: 1px solid #ddd;
17+
border-right: 1px solid var(--color-border, #ddd);
18+
border-left: 1px solid var(--color-border, #ddd);
1919
width: 6px;
2020
cursor: col-resize;
21-
background-color: #eee;
21+
background-color: var(--color-background-alt, #eee);
2222
transition: background-color 0.2s;
2323
user-select: none;
2424

2525
&:hover {
26-
background-color: #9f9999;
26+
background-color: var(--color-neutral-500, #9f9999);
2727
}
2828
}
2929

0 commit comments

Comments
 (0)