@intentic/fileq 1.317.0
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 +21 -0
- package/README.md +46 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +43 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/derive.command.d.ts +5 -0
- package/dist/commands/derive.command.d.ts.map +1 -0
- package/dist/commands/derive.command.js +53 -0
- package/dist/commands/derive.command.js.map +1 -0
- package/dist/commands/git-attributes.command.d.ts +3 -0
- package/dist/commands/git-attributes.command.d.ts.map +1 -0
- package/dist/commands/git-attributes.command.js +12 -0
- package/dist/commands/git-attributes.command.js.map +1 -0
- package/dist/commands/read.command.d.ts +3 -0
- package/dist/commands/read.command.d.ts.map +1 -0
- package/dist/commands/read.command.js +126 -0
- package/dist/commands/read.command.js.map +1 -0
- package/dist/commands/sweep.command.d.ts +3 -0
- package/dist/commands/sweep.command.d.ts.map +1 -0
- package/dist/commands/sweep.command.js +41 -0
- package/dist/commands/sweep.command.js.map +1 -0
- package/dist/lib/archives.d.ts +18 -0
- package/dist/lib/archives.d.ts.map +1 -0
- package/dist/lib/archives.js +133 -0
- package/dist/lib/archives.js.map +1 -0
- package/dist/lib/derive.d.ts +32 -0
- package/dist/lib/derive.d.ts.map +1 -0
- package/dist/lib/derive.js +99 -0
- package/dist/lib/derive.js.map +1 -0
- package/dist/lib/derivers/archive.d.ts +3 -0
- package/dist/lib/derivers/archive.d.ts.map +1 -0
- package/dist/lib/derivers/archive.js +132 -0
- package/dist/lib/derivers/archive.js.map +1 -0
- package/dist/lib/derivers/deriver.d.ts +13 -0
- package/dist/lib/derivers/deriver.d.ts.map +1 -0
- package/dist/lib/derivers/deriver.js +8 -0
- package/dist/lib/derivers/deriver.js.map +1 -0
- package/dist/lib/derivers/docx.d.ts +6 -0
- package/dist/lib/derivers/docx.d.ts.map +1 -0
- package/dist/lib/derivers/docx.js +29 -0
- package/dist/lib/derivers/docx.js.map +1 -0
- package/dist/lib/derivers/epub.d.ts +3 -0
- package/dist/lib/derivers/epub.d.ts.map +1 -0
- package/dist/lib/derivers/epub.js +102 -0
- package/dist/lib/derivers/epub.js.map +1 -0
- package/dist/lib/derivers/html.d.ts +3 -0
- package/dist/lib/derivers/html.d.ts.map +1 -0
- package/dist/lib/derivers/html.js +18 -0
- package/dist/lib/derivers/html.js.map +1 -0
- package/dist/lib/derivers/image.d.ts +3 -0
- package/dist/lib/derivers/image.d.ts.map +1 -0
- package/dist/lib/derivers/image.js +42 -0
- package/dist/lib/derivers/image.js.map +1 -0
- package/dist/lib/derivers/ipynb.d.ts +3 -0
- package/dist/lib/derivers/ipynb.d.ts.map +1 -0
- package/dist/lib/derivers/ipynb.js +75 -0
- package/dist/lib/derivers/ipynb.js.map +1 -0
- package/dist/lib/derivers/media.d.ts +3 -0
- package/dist/lib/derivers/media.d.ts.map +1 -0
- package/dist/lib/derivers/media.js +45 -0
- package/dist/lib/derivers/media.js.map +1 -0
- package/dist/lib/derivers/odp.d.ts +3 -0
- package/dist/lib/derivers/odp.d.ts.map +1 -0
- package/dist/lib/derivers/odp.js +41 -0
- package/dist/lib/derivers/odp.js.map +1 -0
- package/dist/lib/derivers/ods.d.ts +3 -0
- package/dist/lib/derivers/ods.d.ts.map +1 -0
- package/dist/lib/derivers/ods.js +86 -0
- package/dist/lib/derivers/ods.js.map +1 -0
- package/dist/lib/derivers/odt.d.ts +4 -0
- package/dist/lib/derivers/odt.d.ts.map +1 -0
- package/dist/lib/derivers/odt.js +77 -0
- package/dist/lib/derivers/odt.js.map +1 -0
- package/dist/lib/derivers/pdf-furniture.d.ts +7 -0
- package/dist/lib/derivers/pdf-furniture.d.ts.map +1 -0
- package/dist/lib/derivers/pdf-furniture.js +76 -0
- package/dist/lib/derivers/pdf-furniture.js.map +1 -0
- package/dist/lib/derivers/pdf.d.ts +4 -0
- package/dist/lib/derivers/pdf.d.ts.map +1 -0
- package/dist/lib/derivers/pdf.js +79 -0
- package/dist/lib/derivers/pdf.js.map +1 -0
- package/dist/lib/derivers/pptx.d.ts +4 -0
- package/dist/lib/derivers/pptx.d.ts.map +1 -0
- package/dist/lib/derivers/pptx.js +51 -0
- package/dist/lib/derivers/pptx.js.map +1 -0
- package/dist/lib/derivers/profile.d.ts +3 -0
- package/dist/lib/derivers/profile.d.ts.map +1 -0
- package/dist/lib/derivers/profile.js +115 -0
- package/dist/lib/derivers/profile.js.map +1 -0
- package/dist/lib/derivers/rtf.d.ts +4 -0
- package/dist/lib/derivers/rtf.d.ts.map +1 -0
- package/dist/lib/derivers/rtf.js +187 -0
- package/dist/lib/derivers/rtf.js.map +1 -0
- package/dist/lib/derivers/xlsx.d.ts +3 -0
- package/dist/lib/derivers/xlsx.d.ts.map +1 -0
- package/dist/lib/derivers/xlsx.js +68 -0
- package/dist/lib/derivers/xlsx.js.map +1 -0
- package/dist/lib/env.d.ts +2 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +5 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/flags.d.ts +2 -0
- package/dist/lib/flags.d.ts.map +1 -0
- package/dist/lib/flags.js +8 -0
- package/dist/lib/flags.js.map +1 -0
- package/dist/lib/formats.d.ts +6 -0
- package/dist/lib/formats.d.ts.map +1 -0
- package/dist/lib/formats.js +110 -0
- package/dist/lib/formats.js.map +1 -0
- package/dist/lib/sidecar.d.ts +30 -0
- package/dist/lib/sidecar.d.ts.map +1 -0
- package/dist/lib/sidecar.js +108 -0
- package/dist/lib/sidecar.js.map +1 -0
- package/dist/lib/sweep.d.ts +7 -0
- package/dist/lib/sweep.d.ts.map +1 -0
- package/dist/lib/sweep.js +56 -0
- package/dist/lib/sweep.js.map +1 -0
- package/dist/lib/tools.d.ts +2 -0
- package/dist/lib/tools.d.ts.map +1 -0
- package/dist/lib/tools.js +15 -0
- package/dist/lib/tools.js.map +1 -0
- package/dist/lib/xml.d.ts +3 -0
- package/dist/lib/xml.d.ts.map +1 -0
- package/dist/lib/xml.js +16 -0
- package/dist/lib/xml.js.map +1 -0
- package/dist/skill.d.ts +5 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +75 -0
- package/dist/skill.js.map +1 -0
- package/dist/testing.d.ts +25 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +220 -0
- package/dist/testing.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +89 -0
- package/src/app.ts +48 -0
- package/src/cli.integration.test.ts +200 -0
- package/src/cli.ts +6 -0
- package/src/commands/derive.command.ts +59 -0
- package/src/commands/git-attributes.command.ts +13 -0
- package/src/commands/read.command.ts +158 -0
- package/src/commands/sweep.command.ts +48 -0
- package/src/derivers.integration.test.ts +415 -0
- package/src/lib/archives.test.ts +72 -0
- package/src/lib/archives.ts +209 -0
- package/src/lib/derive.ts +132 -0
- package/src/lib/derivers/archive.ts +168 -0
- package/src/lib/derivers/deriver.ts +33 -0
- package/src/lib/derivers/docx.test.ts +38 -0
- package/src/lib/derivers/docx.ts +44 -0
- package/src/lib/derivers/epub.ts +130 -0
- package/src/lib/derivers/html.ts +21 -0
- package/src/lib/derivers/image.ts +54 -0
- package/src/lib/derivers/ipynb.ts +113 -0
- package/src/lib/derivers/load-cost.test.ts +44 -0
- package/src/lib/derivers/media.ts +51 -0
- package/src/lib/derivers/odp.ts +52 -0
- package/src/lib/derivers/ods.ts +104 -0
- package/src/lib/derivers/odt.ts +101 -0
- package/src/lib/derivers/pdf-furniture.test.ts +60 -0
- package/src/lib/derivers/pdf-furniture.ts +121 -0
- package/src/lib/derivers/pdf.ts +102 -0
- package/src/lib/derivers/pptx.ts +61 -0
- package/src/lib/derivers/profile.ts +172 -0
- package/src/lib/derivers/rtf.ts +233 -0
- package/src/lib/derivers/xlsx.ts +82 -0
- package/src/lib/env.ts +7 -0
- package/src/lib/formats.test.ts +50 -0
- package/src/lib/formats.ts +141 -0
- package/src/lib/sidecar.test.ts +60 -0
- package/src/lib/sidecar.ts +156 -0
- package/src/lib/sweep.ts +72 -0
- package/src/lib/tools.ts +16 -0
- package/src/lib/xml.ts +20 -0
- package/src/skill.ts +90 -0
- package/src/testing.ts +333 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/* Every deriver against a real file its real parser accepts, in a temp tree. */
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { requires } from "@intentic/testing/requires";
|
|
7
|
+
import { archiveDeriver } from "./lib/derivers/archive.js";
|
|
8
|
+
import { docxDeriver } from "./lib/derivers/docx.js";
|
|
9
|
+
import { epubDeriver } from "./lib/derivers/epub.js";
|
|
10
|
+
import { htmlDeriver } from "./lib/derivers/html.js";
|
|
11
|
+
import { imageDeriver } from "./lib/derivers/image.js";
|
|
12
|
+
import { ipynbDeriver } from "./lib/derivers/ipynb.js";
|
|
13
|
+
import { mediaDeriver } from "./lib/derivers/media.js";
|
|
14
|
+
import { odpDeriver } from "./lib/derivers/odp.js";
|
|
15
|
+
import { odsDeriver } from "./lib/derivers/ods.js";
|
|
16
|
+
import { odfToHtml, odtDeriver } from "./lib/derivers/odt.js";
|
|
17
|
+
import { ocrAvailable, pdfDeriver } from "./lib/derivers/pdf.js";
|
|
18
|
+
import { pptxDeriver } from "./lib/derivers/pptx.js";
|
|
19
|
+
import { profileDeriver } from "./lib/derivers/profile.js";
|
|
20
|
+
import { rtfDeriver, rtfParagraphs } from "./lib/derivers/rtf.js";
|
|
21
|
+
import { xlsxDeriver } from "./lib/derivers/xlsx.js";
|
|
22
|
+
import { detectFormat } from "./lib/formats.js";
|
|
23
|
+
import {
|
|
24
|
+
docxBytes,
|
|
25
|
+
epubBytes,
|
|
26
|
+
gzipBytes,
|
|
27
|
+
ipynbText,
|
|
28
|
+
odpBytes,
|
|
29
|
+
odsBytes,
|
|
30
|
+
odtBytes,
|
|
31
|
+
pdfBytes,
|
|
32
|
+
pngBytes,
|
|
33
|
+
pptxBytes,
|
|
34
|
+
rtfBytes,
|
|
35
|
+
tarBytes,
|
|
36
|
+
wavBytes,
|
|
37
|
+
zipBytes,
|
|
38
|
+
} from "./testing.js";
|
|
39
|
+
|
|
40
|
+
let root: string;
|
|
41
|
+
const fixture = (name: string, bytes: Uint8Array | string): string => {
|
|
42
|
+
const path = join(root, name);
|
|
43
|
+
writeFileSync(path, bytes);
|
|
44
|
+
return path;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
beforeAll(() => {
|
|
48
|
+
root = mkdtempSync(join(tmpdir(), "fileq-derivers-"));
|
|
49
|
+
});
|
|
50
|
+
afterAll(() => {
|
|
51
|
+
rmSync(root, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("docx", () => {
|
|
55
|
+
test("headings and paragraphs come through as markdown", async () => {
|
|
56
|
+
const path = fixture("plan.docx", docxBytes("Quarterly plan", ["Ship the derivers.", "Then the sweep."]));
|
|
57
|
+
const doc = await docxDeriver.derive(path);
|
|
58
|
+
expect(doc.markdown).toContain("# Quarterly plan");
|
|
59
|
+
expect(doc.markdown).toContain("Ship the derivers.");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("magic bytes recognize the container", async () => {
|
|
63
|
+
const path = fixture("plan2.docx", docxBytes("T", ["x"]));
|
|
64
|
+
expect(await detectFormat(path)).toBe("docx");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("pptx", () => {
|
|
69
|
+
test("one section per slide, in slide order", async () => {
|
|
70
|
+
const path = fixture("deck.pptx", pptxBytes([["Title slide", "A subtitle"], ["Second slide"]]));
|
|
71
|
+
const doc = await pptxDeriver.derive(path);
|
|
72
|
+
expect(doc.markdown).toContain("## Slide 1");
|
|
73
|
+
expect(doc.markdown).toContain("Title slide");
|
|
74
|
+
expect(doc.markdown.indexOf("Second slide")).toBeGreaterThan(doc.markdown.indexOf("A subtitle"));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("xlsx", () => {
|
|
79
|
+
test("sheets become capped markdown tables", async () => {
|
|
80
|
+
const ExcelJS = (await import("exceljs")).default;
|
|
81
|
+
const workbook = new ExcelJS.Workbook();
|
|
82
|
+
const sheet = workbook.addWorksheet("Costs");
|
|
83
|
+
sheet.addRow(["item", "price"]);
|
|
84
|
+
sheet.addRow(["widget", 4]);
|
|
85
|
+
sheet.addRow(["gadget", 7]);
|
|
86
|
+
const path = join(root, "costs.xlsx");
|
|
87
|
+
await workbook.xlsx.writeFile(path);
|
|
88
|
+
const doc = await xlsxDeriver.derive(path);
|
|
89
|
+
expect(doc.markdown).toContain("## Costs");
|
|
90
|
+
expect(doc.markdown).toContain("| widget | 4 |");
|
|
91
|
+
expect(doc.notes).toEqual([]); // nothing was cut, so nothing claims to be
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("pdf", () => {
|
|
96
|
+
test("the text layer is the markdown", async () => {
|
|
97
|
+
const path = fixture("hello.pdf", pdfBytes("Hello from the text layer of this fixture document"));
|
|
98
|
+
const doc = await pdfDeriver.derive(path);
|
|
99
|
+
expect(doc.markdown).toContain("Hello from the text layer");
|
|
100
|
+
expect(doc.notes).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a page with no usable text says scan, not silence (no tesseract on PATH)", async () => {
|
|
104
|
+
const path = fixture("scan.pdf", pdfBytes("x"));
|
|
105
|
+
const previousPath = process.env["PATH"];
|
|
106
|
+
process.env["PATH"] = "/nonexistent";
|
|
107
|
+
try {
|
|
108
|
+
expect(pdfDeriver.name).toBe("pdf");
|
|
109
|
+
const doc = await pdfDeriver.derive(path);
|
|
110
|
+
expect(doc.markdown).toBe("");
|
|
111
|
+
expect(doc.notes.join(" ")).toContain("OCR is not part of this tier");
|
|
112
|
+
} finally {
|
|
113
|
+
process.env["PATH"] = previousPath;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/* The OCR tier, exercised for real where the image carries tesseract + poppler (an extension's layer, and ci-base). */
|
|
118
|
+
const DEJAVU = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf";
|
|
119
|
+
const ocr = requires(ocrAvailable() && existsSync(DEJAVU), `tesseract and pdftoppm on PATH, and the DejaVu font at ${DEJAVU}`);
|
|
120
|
+
test.skipIf(!ocr.runs)(ocr.title("a scan is recognised by tesseract when the image carries it, and says so"), async () => {
|
|
121
|
+
const path = join(root, "receipt.pdf");
|
|
122
|
+
execFileSync("python3", [
|
|
123
|
+
"-c",
|
|
124
|
+
[
|
|
125
|
+
"import sys",
|
|
126
|
+
"from PIL import Image, ImageDraw, ImageFont",
|
|
127
|
+
"img = Image.new('RGB', (1400, 300), 'white')",
|
|
128
|
+
`ImageDraw.Draw(img).text((40, 100), 'TOTAL DUE 1845 DOLLARS', fill='black', font=ImageFont.truetype(sys.argv[2], 72))`,
|
|
129
|
+
"img.save(sys.argv[1], resolution=200.0)",
|
|
130
|
+
].join("\n"),
|
|
131
|
+
path,
|
|
132
|
+
DEJAVU,
|
|
133
|
+
]);
|
|
134
|
+
expect(pdfDeriver.name).toBe("pdf+ocr");
|
|
135
|
+
const doc = await pdfDeriver.derive(path);
|
|
136
|
+
expect(doc.markdown).toMatch(/TOTAL DUE 1845/);
|
|
137
|
+
expect(doc.notes.join(" ")).toContain("recognised, not exact");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("ipynb", () => {
|
|
142
|
+
test("markdown cells pass through, code is fenced in the kernel language, text outputs follow, rich ones are counted", async () => {
|
|
143
|
+
const path = fixture("analysis.ipynb", ipynbText("Churn analysis", "print(df.shape)", ["(1200, 14)"]));
|
|
144
|
+
const doc = await ipynbDeriver.derive(path);
|
|
145
|
+
expect(doc.title).toBe("Churn analysis");
|
|
146
|
+
expect(doc.markdown).toContain("# Churn analysis");
|
|
147
|
+
expect(doc.markdown).toContain("```python\nprint(df.shape)\n```");
|
|
148
|
+
expect(doc.markdown).toContain("(1200, 14)");
|
|
149
|
+
expect(doc.markdown).toContain("[image/png output omitted]");
|
|
150
|
+
expect(doc.notes.join(" ")).toContain("1 rich output");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a runaway output is cut at the cap and the cut is announced", async () => {
|
|
154
|
+
const path = fixture(
|
|
155
|
+
"loop.ipynb",
|
|
156
|
+
ipynbText(
|
|
157
|
+
"Loop",
|
|
158
|
+
"for i in range(100): print(i)",
|
|
159
|
+
Array.from({ length: 100 }, (_, i) => String(i)),
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
const doc = await ipynbDeriver.derive(path);
|
|
163
|
+
// 100 stream lines plus the fixture's one rich-output marker: 101 lines, 40 kept.
|
|
164
|
+
expect(doc.markdown).toContain("… 61 more output lines");
|
|
165
|
+
expect(doc.notes.join(" ")).toContain("output cut at 40 lines");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("the extension is the recognition: JSON has no magic", async () => {
|
|
169
|
+
expect(await detectFormat(fixture("nb.ipynb", ipynbText("T", "x", [])))).toBe("ipynb");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("profile", () => {
|
|
174
|
+
const frame = (functionName: string, lineNumber = 0) => ({ functionName, url: "file:///work/app/server.js", lineNumber, columnNumber: 0 });
|
|
175
|
+
|
|
176
|
+
test("a CPU profile ranks functions by self and total time, one row per function however many call paths it has", async () => {
|
|
177
|
+
const cpu = {
|
|
178
|
+
nodes: [
|
|
179
|
+
{ id: 1, callFrame: { functionName: "(root)", url: "" }, children: [2] },
|
|
180
|
+
{ id: 2, callFrame: frame("handle", 9), children: [3] },
|
|
181
|
+
{ id: 3, callFrame: frame("hashPassword", 41), children: [] },
|
|
182
|
+
],
|
|
183
|
+
startTime: 0,
|
|
184
|
+
endTime: 400,
|
|
185
|
+
samples: [3, 3, 2, 3],
|
|
186
|
+
timeDeltas: [0, 100, 100, 100],
|
|
187
|
+
};
|
|
188
|
+
const path = fixture("CPU.20260924.cpuprofile", JSON.stringify(cpu));
|
|
189
|
+
expect(await detectFormat(path)).toBe("profile");
|
|
190
|
+
const doc = await profileDeriver.derive(path);
|
|
191
|
+
expect(doc.title).toBe("CPU profile: 0.4 ms over 4 samples");
|
|
192
|
+
// hashPassword owns three samples (the last one timed at the mean gap); handle owns one and sits under all four.
|
|
193
|
+
expect(doc.markdown).toContain("| 0.3 ms | 75.0% | 0.3 ms | 75.0% | `hashPassword /work/app/server.js:42` |");
|
|
194
|
+
expect(doc.markdown).toContain("| 0.1 ms | 25.0% | 0.4 ms | 100.0% | `handle /work/app/server.js:10` |");
|
|
195
|
+
expect(doc.notes).toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("a heap profile ranks allocation sites by bytes here and below", async () => {
|
|
199
|
+
const heap = {
|
|
200
|
+
head: {
|
|
201
|
+
callFrame: { functionName: "(root)", url: "" },
|
|
202
|
+
selfSize: 0,
|
|
203
|
+
children: [{ callFrame: frame("load"), selfSize: 1_024, children: [{ callFrame: frame("parse", 7), selfSize: 3_072, children: [] }] }],
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
const doc = await profileDeriver.derive(fixture("Heap.heapprofile", JSON.stringify(heap)));
|
|
207
|
+
expect(doc.title).toBe("Heap profile: 4.0 KB sampled as allocated");
|
|
208
|
+
expect(doc.markdown).toContain("| 3.0 KB | 75.0% | 3.0 KB | 75.0% | `parse /work/app/server.js:8` |");
|
|
209
|
+
expect(doc.markdown).toContain("| 1.0 KB | 25.0% | 4.0 KB | 100.0% | `load /work/app/server.js:1` |");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("JSON that is neither profile shape says so instead of rendering an empty table", async () => {
|
|
213
|
+
const doc = await profileDeriver.derive(fixture("other.cpuprofile", JSON.stringify({ hello: "world" })));
|
|
214
|
+
expect(doc.markdown).toBe("");
|
|
215
|
+
expect(doc.notes).toEqual(["not a V8 CPU or heap profile: neither `nodes` with start and end times, nor a `head` node"]);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("odt", () => {
|
|
220
|
+
test("headings, paragraphs, lists and tables come through webq's writer; comments do not", async () => {
|
|
221
|
+
const path = fixture("letter.odt", odtBytes("Notice of intent", ["We will ship the derivers.", "Then the sweep."]));
|
|
222
|
+
expect(await detectFormat(path)).toBe("odt");
|
|
223
|
+
const doc = await odtDeriver.derive(path);
|
|
224
|
+
expect(doc.title).toBe("Notice of intent");
|
|
225
|
+
expect(doc.markdown).toContain("# Notice of intent");
|
|
226
|
+
expect(doc.markdown).toContain("We will ship the derivers.");
|
|
227
|
+
expect(doc.markdown).toMatch(/^- first item$/m);
|
|
228
|
+
expect(doc.markdown).toMatch(/^- second item$/m); // <text:s text:c="2"/> became spaces, which the writer collapses like any HTML
|
|
229
|
+
expect(doc.markdown).toMatch(/\| cell a \| cell b \|/);
|
|
230
|
+
expect(doc.markdown).not.toContain("reviewer's comment");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("the ODF → HTML rewrite unwraps what it does not name rather than swallowing it", () => {
|
|
234
|
+
const html = odfToHtml(
|
|
235
|
+
'<office:text><text:section text:name="s"><text:p>kept <text:span text:style-name="T1">inline</text:span></text:p></text:section><text:soft-page-break/><text:h text:outline-level="2">Two</text:h></office:text>',
|
|
236
|
+
);
|
|
237
|
+
expect(html).toBe("<p>kept <span>inline</span></p><h2>Two</h2>");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("ods", () => {
|
|
242
|
+
test("each sheet becomes a markdown table, without the padding ODF fills a sheet out with", async () => {
|
|
243
|
+
const path = fixture(
|
|
244
|
+
"budget.ods",
|
|
245
|
+
odsBytes([
|
|
246
|
+
{
|
|
247
|
+
name: "Q1",
|
|
248
|
+
rows: [
|
|
249
|
+
["Item", "Cost"],
|
|
250
|
+
["Paper", "12"],
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
{ name: "Q2", rows: [["Item", "Cost"]] },
|
|
254
|
+
]),
|
|
255
|
+
);
|
|
256
|
+
expect(await detectFormat(path)).toBe("ods");
|
|
257
|
+
const doc = await odsDeriver.derive(path);
|
|
258
|
+
expect(doc.markdown).toContain("## Q1");
|
|
259
|
+
expect(doc.markdown).toMatch(/\| Item \| Cost \|/);
|
|
260
|
+
expect(doc.markdown).toMatch(/\| Paper \| 12 \|/);
|
|
261
|
+
expect(doc.markdown).toContain("## Q2");
|
|
262
|
+
// A real sheet carries a thousand repeated empty cells and a million empty rows after its data.
|
|
263
|
+
expect(doc.markdown).not.toMatch(/\|\s+\|\s+\|\s+\|/);
|
|
264
|
+
expect(doc.notes).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("odp", () => {
|
|
269
|
+
test("each page becomes a slide section, with the speaker's notes quoted under it", async () => {
|
|
270
|
+
const path = fixture(
|
|
271
|
+
"pitch.odp",
|
|
272
|
+
odpBytes([
|
|
273
|
+
{ name: "Title", lines: ["Our plan", "In three parts"], note: "Smile here." },
|
|
274
|
+
{ name: "Detail", lines: ["Part one"] },
|
|
275
|
+
]),
|
|
276
|
+
);
|
|
277
|
+
expect(await detectFormat(path)).toBe("odp");
|
|
278
|
+
const doc = await odpDeriver.derive(path);
|
|
279
|
+
expect(doc.markdown).toContain("## Slide 1: Title");
|
|
280
|
+
expect(doc.markdown).toContain("In three parts");
|
|
281
|
+
expect(doc.markdown).toContain("> Smile here.");
|
|
282
|
+
expect(doc.markdown).toContain("## Slide 2: Detail");
|
|
283
|
+
// The note belongs to the slide that carries it, not to the next one.
|
|
284
|
+
expect(doc.markdown.indexOf("> Smile here.")).toBeLessThan(doc.markdown.indexOf("## Slide 2"));
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("rtf", () => {
|
|
289
|
+
test("paragraphs and table cells come through; the font table and an ignorable group do not", async () => {
|
|
290
|
+
const path = fixture("memo.rtf", rtfBytes(["A plain paragraph.", "Another one."]));
|
|
291
|
+
expect(await detectFormat(path)).toBe("rtf");
|
|
292
|
+
const doc = await rtfDeriver.derive(path);
|
|
293
|
+
expect(doc.markdown).toContain("A plain paragraph.");
|
|
294
|
+
expect(doc.markdown).toContain("Another one.");
|
|
295
|
+
expect(doc.markdown).toContain("left | right");
|
|
296
|
+
expect(doc.markdown).not.toContain("Times New Roman");
|
|
297
|
+
expect(doc.markdown).not.toContain("Not text");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("reads a byte through the code page and a \\u escape without its fallback character", () => {
|
|
301
|
+
expect(rtfParagraphs(`{\\rtf1\\ansi caf\\'e9 \\u233?\\par}`)).toEqual(["café é"]);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("epub", () => {
|
|
306
|
+
test("chapters land in spine order under their own headings; the nav document is not a chapter", async () => {
|
|
307
|
+
const path = fixture(
|
|
308
|
+
"novel.epub",
|
|
309
|
+
epubBytes("A Fixture Novel", [
|
|
310
|
+
{ title: "Chapter One", body: "It was a dark and stormy fixture." },
|
|
311
|
+
{ title: "Chapter Two", body: "The derivers were tested." },
|
|
312
|
+
]),
|
|
313
|
+
);
|
|
314
|
+
expect(await detectFormat(path)).toBe("epub");
|
|
315
|
+
const doc = await epubDeriver.derive(path);
|
|
316
|
+
expect(doc.title).toBe("A Fixture Novel");
|
|
317
|
+
expect(doc.markdown).toContain("# Chapter One");
|
|
318
|
+
expect(doc.markdown.indexOf("The derivers were tested.")).toBeGreaterThan(doc.markdown.indexOf("stormy fixture"));
|
|
319
|
+
expect(doc.markdown).not.toContain("Contents");
|
|
320
|
+
expect(doc.notes).toEqual([]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("a zip that is not really an EPUB says what is missing", async () => {
|
|
324
|
+
const { strToU8, zipSync } = await import("fflate");
|
|
325
|
+
const path = fixture("hollow.epub", zipSync({ mimetype: [strToU8("application/epub+zip"), { level: 0 }], "a.txt": strToU8("x") }));
|
|
326
|
+
const doc = await epubDeriver.derive(path);
|
|
327
|
+
expect(doc.markdown).toBe("");
|
|
328
|
+
expect(doc.notes.join(" ")).toContain("container.xml");
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("image", () => {
|
|
333
|
+
test("dimensions land, and the missing caption announces itself as ungenerated", async () => {
|
|
334
|
+
const path = fixture("pixel.png", pngBytes());
|
|
335
|
+
const doc = await imageDeriver.derive(path);
|
|
336
|
+
expect(doc.markdown).toContain("1×1");
|
|
337
|
+
expect(doc.notes.join(" ")).toContain("captioning");
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("media", () => {
|
|
342
|
+
test("duration lands, and the missing transcript announces itself as ungenerated", async () => {
|
|
343
|
+
const path = fixture("silence.wav", wavBytes());
|
|
344
|
+
const doc = await mediaDeriver.derive(path);
|
|
345
|
+
expect(doc.markdown).toContain("Duration: 1s");
|
|
346
|
+
expect(doc.notes.join(" ")).toContain("transcription");
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe("html", () => {
|
|
351
|
+
test("webq's writer renders it, title and all", async () => {
|
|
352
|
+
const path = fixture("page.html", "<html><head><title>Local report</title></head><body><h1>Findings</h1><p>All good.</p></body></html>");
|
|
353
|
+
const doc = await htmlDeriver.derive(path);
|
|
354
|
+
expect(doc.title).toBe("Local report");
|
|
355
|
+
expect(doc.markdown).toContain("# Findings");
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("archive", () => {
|
|
360
|
+
test("a zip's shadow is its manifest, and says the members' contents are not in it", async () => {
|
|
361
|
+
const path = fixture("bundle.zip", zipBytes({ "README.md": "# Bundle", "src/app.ts": "export const go = () => 1;\n".repeat(30) }));
|
|
362
|
+
const doc = await archiveDeriver.derive(path);
|
|
363
|
+
expect(doc.markdown).toContain("- Archive: zip");
|
|
364
|
+
expect(doc.markdown).toContain("- Members: 2 files");
|
|
365
|
+
expect(doc.markdown).toContain("| src/app.ts |");
|
|
366
|
+
expect(doc.markdown).not.toContain("export const go");
|
|
367
|
+
expect(doc.notes.join(" ")).toContain("member listing only");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("magic routes a zip to the archive deriver while a docx stays a document", async () => {
|
|
371
|
+
expect(await detectFormat(fixture("bundle2.zip", zipBytes({ "a.txt": "a" })))).toBe("archive");
|
|
372
|
+
expect(await detectFormat(fixture("report.docx", docxBytes("T", ["x"])))).toBe("docx");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("a renamed archive is recognized by its bytes, not its name", async () => {
|
|
376
|
+
expect(await detectFormat(fixture("mystery.bin", zipBytes({ "a.txt": "a" })))).toBe("archive");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("a tar lists members in archive order", async () => {
|
|
380
|
+
const path = fixture("backup.tar", tarBytes({ "notes.txt": "hello", "data/rows.csv": "a,b\n1,2\n" }));
|
|
381
|
+
const doc = await archiveDeriver.derive(path);
|
|
382
|
+
expect(doc.markdown).toContain("- Archive: tar");
|
|
383
|
+
expect(doc.markdown.indexOf("notes.txt")).toBeLessThan(doc.markdown.indexOf("data/rows.csv"));
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("a gzipped tar is listed as the tar inside it", async () => {
|
|
387
|
+
const path = fixture("release.tgz", gzipBytes("release.tar", tarBytes({ "bin/tool": "#!/bin/sh\n" })));
|
|
388
|
+
const doc = await archiveDeriver.derive(path);
|
|
389
|
+
expect(doc.markdown).toContain("| bin/tool |");
|
|
390
|
+
expect(doc.notes.join(" ")).toContain("gzip-compressed tar");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("a single compressed file carries its text, since there the archive is the document", async () => {
|
|
394
|
+
const path = fixture("server.log.gz", gzipBytes("server.log", "GET /健康 200\nGET /orders 500\n"));
|
|
395
|
+
const doc = await archiveDeriver.derive(path);
|
|
396
|
+
expect(doc.markdown).toContain("- Member: server.log");
|
|
397
|
+
expect(doc.markdown).toContain("GET /orders 500");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("a compressed binary says so instead of printing bytes", async () => {
|
|
401
|
+
const path = fixture("pixel.png.gz", gzipBytes("pixel.png", pngBytes()));
|
|
402
|
+
const doc = await archiveDeriver.derive(path);
|
|
403
|
+
expect(doc.markdown).not.toContain("PNG");
|
|
404
|
+
expect(doc.notes.join(" ")).toContain("not text");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("an empty archive says it holds nothing rather than reading as one", async () => {
|
|
408
|
+
const doc = await archiveDeriver.derive(fixture("empty.zip", zipBytes({})));
|
|
409
|
+
expect(doc.notes.join(" ")).toContain("holds nothing");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("a file that is no archive at all fails loudly", async () => {
|
|
413
|
+
await expect(archiveDeriver.derive(fixture("notes.zip", "plain text wearing a zip extension"))).rejects.toThrow(/not an archive/);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { gzipHead, looksLikeTar, parseTarListing, tarEntries, zipEntries } from "./archives.js";
|
|
2
|
+
import { gzipBytes, tarBytes, zipBytes } from "../testing.js";
|
|
3
|
+
|
|
4
|
+
// The readers, against archives built in code. What each test pins is the shape an archive's index has to arrive in;
|
|
5
|
+
// the external lister is pinned through its parser, since whether the box has GNU tar is not this suite's business.
|
|
6
|
+
|
|
7
|
+
describe("zip", () => {
|
|
8
|
+
test("every member is enumerated with both sizes, nothing inflated", () => {
|
|
9
|
+
const entries = zipEntries(zipBytes({ "README.md": "# hello", "src/index.ts": "export const x = 1;\n".repeat(40) }));
|
|
10
|
+
expect(entries.map((entry) => entry.path)).toEqual(["README.md", "src/index.ts"]);
|
|
11
|
+
const index = entries[1];
|
|
12
|
+
expect(index?.size).toBe(800);
|
|
13
|
+
// Deflate earns its keep on repeated text: the packed side has to be the smaller of the two.
|
|
14
|
+
expect(index?.packed).toBeLessThan(index?.size ?? 0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("a folder entry is marked as one", () => {
|
|
18
|
+
expect(zipEntries(zipBytes({ "docs/": "", "docs/a.txt": "a" })).map((entry) => entry.directory)).toEqual([true, false]);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("tar", () => {
|
|
23
|
+
test("members come back in archive order, with their sizes", () => {
|
|
24
|
+
const entries = tarEntries(tarBytes({ "notes.txt": "hello", "data/rows.csv": "a,b\n1,2\n" }));
|
|
25
|
+
expect(entries.map((entry) => [entry.path, entry.size])).toEqual([
|
|
26
|
+
["notes.txt", 5],
|
|
27
|
+
["data/rows.csv", 8],
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a path too long for the name field survives the block that carries it", () => {
|
|
32
|
+
const long = `deep/${"nested/".repeat(20)}file.txt`;
|
|
33
|
+
expect(tarEntries(tarBytes({ [long]: "x" })).map((entry) => entry.path)).toEqual([long]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a file that is not a tar is refused rather than read as empty", () => {
|
|
37
|
+
expect(() => tarEntries(new TextEncoder().encode("just some text, not an archive at all"))).toThrow(/not a tar/);
|
|
38
|
+
expect(looksLikeTar(new Uint8Array(600))).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("gzip", () => {
|
|
43
|
+
test("the header names the member and the trailer sizes it, without inflating", () => {
|
|
44
|
+
const head = gzipHead(gzipBytes("server.log", "x".repeat(1000)));
|
|
45
|
+
expect(head?.name).toBe("server.log");
|
|
46
|
+
expect(head?.unpackedBytes).toBe(1000);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("bytes that are not gzip answer nothing", () => {
|
|
50
|
+
expect(gzipHead(new Uint8Array([1, 2, 3]))).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("the external listing parser", () => {
|
|
55
|
+
test("mode, size and path are read off each row, spaces in names included", () => {
|
|
56
|
+
const stdout = [
|
|
57
|
+
"drwxr-xr-x root/root 0 2024-03-01 09:15 site/",
|
|
58
|
+
"-rw-r--r-- root/root 2048 2024-03-01 09:15 site/my notes.txt",
|
|
59
|
+
"lrwxrwxrwx root/root 0 2024-03-01 09:15 site/link -> site/my notes.txt",
|
|
60
|
+
"",
|
|
61
|
+
].join("\n");
|
|
62
|
+
expect(parseTarListing(stdout)).toEqual([
|
|
63
|
+
{ path: "site/", size: undefined, packed: undefined, directory: true },
|
|
64
|
+
{ path: "site/my notes.txt", size: 2048, packed: undefined, directory: false },
|
|
65
|
+
{ path: "site/link", size: 0, packed: undefined, directory: false },
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("anything that is not a listing row is skipped, so a warning on stderr's twin cannot become a member", () => {
|
|
70
|
+
expect(parseTarListing("tar: Removing leading `/' from member names\n")).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|