This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
// Reads a ZIP the way a spreadsheet application does: from the central directory at the end. It is
|
||||
// the test's independent reader of what `buildWorkbook` wrote, so the writer is not checked by itself.
|
||||
|
||||
import { inflateRawSync } from "node:zlib"
|
||||
|
||||
export function unzip(bytes: Uint8Array): Map<string, string> {
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
||||
let eocd = bytes.length - 22
|
||||
while (eocd >= 0 && view.getUint32(eocd, true) !== 0x06054b50) eocd -= 1
|
||||
if (eocd < 0) throw new Error("no end-of-central-directory record")
|
||||
const entries = view.getUint16(eocd + 10, true)
|
||||
let at = view.getUint32(eocd + 16, true)
|
||||
const files = new Map<string, string>()
|
||||
for (let i = 0; i < entries; i += 1) {
|
||||
if (view.getUint32(at, true) !== 0x02014b50) throw new Error("a bad central directory entry")
|
||||
const method = view.getUint16(at + 10, true)
|
||||
const size = view.getUint32(at + 20, true)
|
||||
const nameLength = view.getUint16(at + 28, true)
|
||||
const extraLength = view.getUint16(at + 30, true)
|
||||
const commentLength = view.getUint16(at + 32, true)
|
||||
const local = view.getUint32(at + 42, true)
|
||||
const name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + nameLength))
|
||||
const dataAt = local + 30 + view.getUint16(local + 26, true) + view.getUint16(local + 28, true)
|
||||
const raw = bytes.subarray(dataAt, dataAt + size)
|
||||
files.set(name, new TextDecoder().decode(method === 8 ? inflateRawSync(raw) : raw))
|
||||
at += 46 + nameLength + extraLength + commentLength
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
/** The text and number cells of the first sheet, row by row. */
|
||||
export function sheetRows(bytes: Uint8Array): Array<Array<string | number>> {
|
||||
const sheet = unzip(bytes).get("xl/worksheets/sheet1.xml") ?? ""
|
||||
return [...sheet.matchAll(/<row [^>]*>(.*?)<\/row>/gs)].map(([, row]) =>
|
||||
[...(row as string).matchAll(/<c ([^>]*)>(.*?)<\/c>/gs)].map(([, attributes, body]) => {
|
||||
if (!(attributes as string).includes('t="inlineStr"')) return Number(/<v>(.*?)<\/v>/s.exec(body as string)?.[1])
|
||||
const text = /<t[^>]*>(.*?)<\/t>/s.exec(body as string)?.[1] ?? ""
|
||||
return text
|
||||
.replace(/"/g, '"')
|
||||
.replace(/>/g, ">")
|
||||
.replace(/</g, "<")
|
||||
.replace(/&/g, "&")
|
||||
}),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user