| 1 | /** |
| 2 | * Word documents and spreadsheets for `make_file` (files.ts), as Office |
| 3 | * Open XML: a .docx from Markdown and an .xlsx from rows, each a zip of a |
| 4 | * few XML parts. Pure and small: the zip stores its parts uncompressed, |
| 5 | * which every reader takes. |
| 6 | */ |
| 7 | import { type Block, type Span, parseBlocks } from "./file-markdown.ts"; |
| 8 | |
| 9 | // ── Zip ─────────────────────────────────────────────────────────────────── |
| 10 | |
| 11 | const CRC_TABLE = (() => { |
| 12 | const table = new Uint32Array(256); |
| 13 | for (let i = 0; i < 256; i++) { |
| 14 | let c = i; |
| 15 | for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; |
| 16 | table[i] = c >>> 0; |
| 17 | } |
| 18 | return table; |
| 19 | })(); |
| 20 | |
| 21 | export function crc32(bytes: Uint8Array): number { |
| 22 | let crc = 0xffffffff; |
| 23 | for (const byte of bytes) crc = CRC_TABLE[(crc ^ byte) & 0xff]! ^ (crc >>> 8); |
| 24 | return (crc ^ 0xffffffff) >>> 0; |
| 25 | } |
| 26 | |
| 27 | /** A zip of `files` (path to bytes or text), stored without compression. */ |
| 28 | export function zip(files: [string, Uint8Array | string][], at = new Date()): Uint8Array { |
| 29 | const encoder = new TextEncoder(); |
| 30 | const time = (at.getUTCHours() << 11) | (at.getUTCMinutes() << 5) | Math.floor(at.getUTCSeconds() / 2); |
| 31 | const date = ((Math.max(1980, at.getUTCFullYear()) - 1980) << 9) | ((at.getUTCMonth() + 1) << 5) | at.getUTCDate(); |
| 32 | const locals: Uint8Array[] = []; |
| 33 | const centrals: Uint8Array[] = []; |
| 34 | let offset = 0; |
| 35 | for (const [path, content] of files) { |
| 36 | const name = encoder.encode(path); |
| 37 | const data = typeof content === "string" ? encoder.encode(content) : content; |
| 38 | const crc = crc32(data); |
| 39 | const local = new Uint8Array(30 + name.length + data.length); |
| 40 | const lv = new DataView(local.buffer); |
| 41 | lv.setUint32(0, 0x04034b50, true); |
| 42 | lv.setUint16(4, 20, true); |
| 43 | lv.setUint16(6, 0x0800, true); |
| 44 | lv.setUint16(8, 0, true); |
| 45 | lv.setUint16(10, time, true); |
| 46 | lv.setUint16(12, date, true); |
| 47 | lv.setUint32(14, crc, true); |
| 48 | lv.setUint32(18, data.length, true); |
| 49 | lv.setUint32(22, data.length, true); |
| 50 | lv.setUint16(26, name.length, true); |
| 51 | lv.setUint16(28, 0, true); |
| 52 | local.set(name, 30); |
| 53 | local.set(data, 30 + name.length); |
| 54 | const central = new Uint8Array(46 + name.length); |
| 55 | const cv = new DataView(central.buffer); |
| 56 | cv.setUint32(0, 0x02014b50, true); |
| 57 | cv.setUint16(4, 20, true); |
| 58 | cv.setUint16(6, 20, true); |
| 59 | cv.setUint16(8, 0x0800, true); |
| 60 | cv.setUint16(10, 0, true); |
| 61 | cv.setUint16(12, time, true); |
| 62 | cv.setUint16(14, date, true); |
| 63 | cv.setUint32(16, crc, true); |
| 64 | cv.setUint32(20, data.length, true); |
| 65 | cv.setUint32(24, data.length, true); |
| 66 | cv.setUint16(28, name.length, true); |
| 67 | cv.setUint32(42, offset, true); |
| 68 | central.set(name, 46); |
| 69 | locals.push(local); |
| 70 | centrals.push(central); |
| 71 | offset += local.length; |
| 72 | } |
| 73 | const centralSize = centrals.reduce((sum, c) => sum + c.length, 0); |
| 74 | const end = new Uint8Array(22); |
| 75 | const ev = new DataView(end.buffer); |
| 76 | ev.setUint32(0, 0x06054b50, true); |
| 77 | ev.setUint16(8, files.length, true); |
| 78 | ev.setUint16(10, files.length, true); |
| 79 | ev.setUint32(12, centralSize, true); |
| 80 | ev.setUint32(16, offset, true); |
| 81 | const out = new Uint8Array(offset + centralSize + end.length); |
| 82 | let at2 = 0; |
| 83 | for (const part of [...locals, ...centrals, end]) { |
| 84 | out.set(part, at2); |
| 85 | at2 += part.length; |
| 86 | } |
| 87 | return out; |
| 88 | } |
| 89 | |
| 90 | /** Text for XML: escaped, without the control characters XML forbids. */ |
| 91 | export function xml(text: string): string { |
| 92 | return String(text) |
| 93 | // eslint-disable-next-line no-control-regex |
| 94 | .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, "") |
| 95 | .replace(/&/g, "&") |
| 96 | .replace(/</g, "<") |
| 97 | .replace(/>/g, ">") |
| 98 | .replace(/"/g, """); |
| 99 | } |
| 100 | |
| 101 | const XML_HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'; |
| 102 | |
| 103 | function coreProps(title: string, at: Date): string { |
| 104 | const when = at.toISOString().replace(/\.\d{3}Z$/, "Z"); |
| 105 | return `${XML_HEAD}<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>${xml(title)}</dc:title><dc:creator>g1t</dc:creator><dcterms:created xsi:type="dcterms:W3CDTF">${when}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${when}</dcterms:modified></cp:coreProperties>`; |
| 106 | } |
| 107 | |
| 108 | // ── Word ────────────────────────────────────────────────────────────────── |
| 109 | |
| 110 | const W = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"'; |
| 111 | |
| 112 | const DOCX_STYLES = `${XML_HEAD}<w:styles ${W}> |
| 113 | <w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:eastAsia="Calibri" w:cs="Calibri"/><w:sz w:val="22"/><w:szCs w:val="22"/><w:lang w:val="en-US"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="140" w:line="276" w:lineRule="auto"/></w:pPr></w:pPrDefault></w:docDefaults> |
| 114 | <w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style> |
| 115 | <w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="360" w:after="160"/><w:outlineLvl w:val="0"/></w:pPr><w:rPr><w:b/><w:sz w:val="36"/><w:szCs w:val="36"/></w:rPr></w:style> |
| 116 | <w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="280" w:after="120"/><w:outlineLvl w:val="1"/></w:pPr><w:rPr><w:b/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr></w:style> |
| 117 | <w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="heading 3"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="200" w:after="80"/><w:outlineLvl w:val="2"/></w:pPr><w:rPr><w:b/><w:sz w:val="24"/><w:szCs w:val="24"/></w:rPr></w:style> |
| 118 | <w:style w:type="paragraph" w:styleId="Quote"><w:name w:val="Quote"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:pBdr><w:left w:val="single" w:sz="18" w:space="8" w:color="BFBFBF"/></w:pBdr><w:ind w:left="240"/></w:pPr><w:rPr><w:color w:val="595959"/></w:rPr></w:style> |
| 119 | <w:style w:type="paragraph" w:styleId="Code"><w:name w:val="Code"/><w:basedOn w:val="Normal"/><w:pPr><w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/><w:spacing w:after="140" w:line="240" w:lineRule="auto"/></w:pPr><w:rPr><w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/><w:sz w:val="19"/><w:szCs w:val="19"/></w:rPr></w:style> |
| 120 | <w:style w:type="paragraph" w:styleId="ListParagraph"><w:name w:val="List Paragraph"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:after="60"/></w:pPr></w:style> |
| 121 | <w:style w:type="character" w:styleId="Hyperlink"><w:name w:val="Hyperlink"/><w:rPr><w:color w:val="2F5FD0"/><w:u w:val="single"/></w:rPr></w:style> |
| 122 | <w:style w:type="table" w:styleId="Table"><w:name w:val="Table"/><w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:color="BFBFBF"/><w:left w:val="single" w:sz="4" w:color="BFBFBF"/><w:bottom w:val="single" w:sz="4" w:color="BFBFBF"/><w:right w:val="single" w:sz="4" w:color="BFBFBF"/><w:insideH w:val="single" w:sz="4" w:color="BFBFBF"/><w:insideV w:val="single" w:sz="4" w:color="BFBFBF"/></w:tblBorders><w:tblCellMar><w:left w:w="100" w:type="dxa"/><w:right w:w="100" w:type="dxa"/></w:tblCellMar></w:tblPr></w:style> |
| 123 | </w:styles>`; |
| 124 | |
| 125 | function runs(spans: Span[], links: string[], base: { bold?: boolean } = {}): string { |
| 126 | return spans |
| 127 | .map((span) => { |
| 128 | const props = [ |
| 129 | span.href ? '<w:rStyle w:val="Hyperlink"/>' : "", |
| 130 | span.code ? '<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>' : "", |
| 131 | span.bold || base.bold ? "<w:b/>" : "", |
| 132 | span.italic ? "<w:i/>" : "", |
| 133 | span.code ? '<w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/>' : "", |
| 134 | ].join(""); |
| 135 | const run = `<w:r>${props ? `<w:rPr>${props}</w:rPr>` : ""}<w:t xml:space="preserve">${xml(span.text)}</w:t></w:r>`; |
| 136 | if (!span.href || !/^(https?:|mailto:)/i.test(span.href)) return run; |
| 137 | links.push(span.href); |
| 138 | return `<w:hyperlink r:id="rIdLink${links.length}">${run}</w:hyperlink>`; |
| 139 | }) |
| 140 | .join(""); |
| 141 | } |
| 142 | |
| 143 | function paragraph(style: string | null, body: string, extra = ""): string { |
| 144 | const props = `${style ? `<w:pStyle w:val="${style}"/>` : ""}${extra}`; |
| 145 | return `<w:p>${props ? `<w:pPr>${props}</w:pPr>` : ""}${body}</w:p>`; |
| 146 | } |
| 147 | |
| 148 | function wordBlock(block: Block, links: string[]): string { |
| 149 | switch (block.kind) { |
| 150 | case "heading": |
| 151 | return paragraph(`Heading${block.level}`, runs(block.spans, links)); |
| 152 | case "paragraph": |
| 153 | return paragraph(null, runs(block.spans, links)); |
| 154 | case "item": { |
| 155 | const left = 360 + block.depth * 360; |
| 156 | const marker = block.ordered ? `${block.number}.` : block.depth % 2 ? "–" : "•"; |
| 157 | return paragraph("ListParagraph", `<w:r><w:t xml:space="preserve">${xml(marker)}</w:t></w:r><w:r><w:tab/></w:r>${runs(block.spans, links)}`, `<w:tabs><w:tab w:val="left" w:pos="${left}"/></w:tabs><w:ind w:left="${left}" w:hanging="300"/>`); |
| 158 | } |
| 159 | case "quote": |
| 160 | return paragraph("Quote", runs(block.spans, links)); |
| 161 | case "code": |
| 162 | return paragraph( |
| 163 | "Code", |
| 164 | block.text |
| 165 | .split("\n") |
| 166 | .map((line, i) => `${i ? "<w:r><w:br/></w:r>" : ""}<w:r><w:t xml:space="preserve">${xml(line)}</w:t></w:r>`) |
| 167 | .join(""), |
| 168 | ); |
| 169 | case "rule": |
| 170 | return paragraph(null, "", '<w:pBdr><w:bottom w:val="single" w:sz="6" w:space="1" w:color="BFBFBF"/></w:pBdr>'); |
| 171 | case "table": { |
| 172 | const columns = Math.max(block.header.length, ...block.rows.map((row) => row.length), 1); |
| 173 | const row = (cells: Span[][], header: boolean) => |
| 174 | `<w:tr>${header ? "<w:trPr><w:tblHeader/></w:trPr>" : ""}${Array.from({ length: columns }, (_, c) => `<w:tc><w:tcPr>${header ? '<w:shd w:val="clear" w:color="auto" w:fill="EFEFEF"/>' : ""}</w:tcPr>${paragraph(null, runs(cells[c] ?? [], links, { bold: header }), '<w:spacing w:after="0"/>')}</w:tc>`).join("")}</w:tr>`; |
| 175 | return `<w:tbl><w:tblPr><w:tblStyle w:val="Table"/><w:tblW w:w="5000" w:type="pct"/></w:tblPr><w:tblGrid>${Array.from({ length: columns }, () => `<w:gridCol w:w="${Math.floor(9720 / columns)}"/>`).join("")}</w:tblGrid>${block.header.length ? row(block.header, true) : ""}${block.rows.map((r) => row(r, false)).join("")}</w:tbl>${paragraph(null, "")}`; |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | /** A Word document of `markdown`, titled `title` (as its first heading when the Markdown starts without one). */ |
| 181 | export function markdownDocx(title: string, markdown: string, at = new Date()): Uint8Array { |
| 182 | let blocks = parseBlocks(markdown); |
| 183 | if (title.trim() && !(blocks[0]?.kind === "heading" && blocks[0].level === 1)) blocks = [{ kind: "heading", level: 1, spans: [{ text: title.trim() }] }, ...blocks]; |
| 184 | const links: string[] = []; |
| 185 | const body = blocks.map((block) => wordBlock(block, links)).join("\n"); |
| 186 | const document = `${XML_HEAD}<w:document ${W}><w:body>${body}<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1260" w:right="1260" w:bottom="1260" w:left="1260" w:header="708" w:footer="708" w:gutter="0"/></w:sectPr></w:body></w:document>`; |
| 187 | const rels = `${XML_HEAD}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>${links |
| 188 | .map((href, i) => `<Relationship Id="rIdLink${i + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="${xml(href)}" TargetMode="External"/>`) |
| 189 | .join("")}</Relationships>`; |
| 190 | return zip( |
| 191 | [ |
| 192 | [ |
| 193 | "[Content_Types].xml", |
| 194 | `${XML_HEAD}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>`, |
| 195 | ], |
| 196 | [ |
| 197 | "_rels/.rels", |
| 198 | `${XML_HEAD}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>`, |
| 199 | ], |
| 200 | ["docProps/core.xml", coreProps(title, at)], |
| 201 | ["word/document.xml", document], |
| 202 | ["word/_rels/document.xml.rels", rels], |
| 203 | ["word/styles.xml", DOCX_STYLES], |
| 204 | ], |
| 205 | at, |
| 206 | ); |
| 207 | } |
| 208 | |
| 209 | // ── Excel ───────────────────────────────────────────────────────────────── |
| 210 | |
| 211 | export type Cell = string | number | boolean | null; |
| 212 | export type Sheet = { name: string; rows: Cell[][] }; |
| 213 | |
| 214 | /** A column's letters: 0 is A, 26 is AA. */ |
| 215 | export function columnName(index: number): string { |
| 216 | let name = ""; |
| 217 | for (let i = index + 1; i > 0; i = Math.floor((i - 1) / 26)) name = String.fromCharCode(65 + ((i - 1) % 26)) + name; |
| 218 | return name; |
| 219 | } |
| 220 | |
| 221 | /** A cell's value as Excel keeps it: numbers (and numeric text) as numbers, the rest as text. */ |
| 222 | export function cellValue(value: Cell): { kind: "number"; value: number } | { kind: "text"; value: string } | null { |
| 223 | if (value === null || value === undefined || value === "") return null; |
| 224 | if (typeof value === "number") return Number.isFinite(value) ? { kind: "number", value } : { kind: "text", value: String(value) }; |
| 225 | if (typeof value === "boolean") return { kind: "text", value: value ? "TRUE" : "FALSE" }; |
| 226 | const text = String(value); |
| 227 | // Numeric text is a number, unless a leading zero says it's a code (a ZIP, an id). |
| 228 | if (/^-?(0|[1-9]\d{0,14})(\.\d+)?$/.test(text.trim())) return { kind: "number", value: Number(text.trim()) }; |
| 229 | return { kind: "text", value: text }; |
| 230 | } |
| 231 | |
| 232 | /** A sheet name Excel takes: at most 31 characters, none of []:*?/\, unique in the workbook. */ |
| 233 | export function sheetNames(names: string[]): string[] { |
| 234 | const used = new Set<string>(); |
| 235 | return names.map((raw, i) => { |
| 236 | const base = (String(raw ?? "").replace(/[[\]:*?/\\]/g, " ").replace(/\s+/g, " ").trim() || `Sheet${i + 1}`).replace(/^'|'$/g, "").slice(0, 31) || `Sheet${i + 1}`; |
| 237 | let name = base; |
| 238 | for (let k = 2; used.has(name.toLowerCase()); k++) name = `${base.slice(0, 31 - String(k).length - 1)} ${k}`; |
| 239 | used.add(name.toLowerCase()); |
| 240 | return name; |
| 241 | }); |
| 242 | } |
| 243 | |
| 244 | function worksheet(sheet: Sheet): string { |
| 245 | const columns = Math.max(1, ...sheet.rows.map((row) => row.length)); |
| 246 | const widths = Array.from({ length: columns }, (_, c) => Math.min(60, Math.max(8, ...sheet.rows.slice(0, 200).map((row) => String(row[c] ?? "").length + 2)))); |
| 247 | const rows = sheet.rows |
| 248 | .map((row, r) => { |
| 249 | const cells = row |
| 250 | .map((raw, c) => { |
| 251 | const value = cellValue(raw); |
| 252 | if (!value) return ""; |
| 253 | const ref = `${columnName(c)}${r + 1}`; |
| 254 | const style = r === 0 ? ' s="1"' : ""; |
| 255 | return value.kind === "number" ? `<c r="${ref}"${style}><v>${value.value}</v></c>` : `<c r="${ref}"${style} t="inlineStr"><is><t xml:space="preserve">${xml(value.value)}</t></is></c>`; |
| 256 | }) |
| 257 | .join(""); |
| 258 | return `<row r="${r + 1}">${cells}</row>`; |
| 259 | }) |
| 260 | .join(""); |
| 261 | const frozen = sheet.rows.length > 1 ? '<pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/>' : ""; |
| 262 | return `${XML_HEAD}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetViews><sheetView workbookViewId="0">${frozen}</sheetView></sheetViews><cols>${widths.map((w, c) => `<col min="${c + 1}" max="${c + 1}" width="${w}" customWidth="1"/>`).join("")}</cols><sheetData>${rows}</sheetData></worksheet>`; |
| 263 | } |
| 264 | |
| 265 | const XLSX_STYLES = `${XML_HEAD}<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="2"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`; |
| 266 | |
| 267 | /** An Excel workbook of `sheets`, each one's first row its header (in bold, frozen). */ |
| 268 | export function workbook(title: string, sheets: Sheet[], at = new Date()): Uint8Array { |
| 269 | const names = sheetNames(sheets.map((sheet) => sheet.name)); |
| 270 | return zip( |
| 271 | [ |
| 272 | [ |
| 273 | "[Content_Types].xml", |
| 274 | `${XML_HEAD}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>${sheets |
| 275 | .map((_, i) => `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`) |
| 276 | .join("")}<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>`, |
| 277 | ], |
| 278 | [ |
| 279 | "_rels/.rels", |
| 280 | `${XML_HEAD}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>`, |
| 281 | ], |
| 282 | ["docProps/core.xml", coreProps(title, at)], |
| 283 | [ |
| 284 | "xl/workbook.xml", |
| 285 | `${XML_HEAD}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>${names |
| 286 | .map((name, i) => `<sheet name="${xml(name)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`) |
| 287 | .join("")}</sheets></workbook>`, |
| 288 | ], |
| 289 | [ |
| 290 | "xl/_rels/workbook.xml.rels", |
| 291 | `${XML_HEAD}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${sheets |
| 292 | .map((_, i) => `<Relationship Id="rId${i + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`) |
| 293 | .join("")}<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`, |
| 294 | ], |
| 295 | ["xl/styles.xml", XLSX_STYLES], |
| 296 | ...sheets.map((sheet, i): [string, string] => [`xl/worksheets/sheet${i + 1}.xml`, worksheet(sheet)]), |
| 297 | ], |
| 298 | at, |
| 299 | ); |
| 300 | } |