@intentic/fileq 1.318.0 → 1.320.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/NOTICE +4 -4
- package/README.md +16 -10
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +4 -6
- package/dist/app.js.map +1 -1
- package/dist/commands/derive.command.js +1 -1
- package/dist/commands/derive.command.js.map +1 -1
- package/dist/commands/read.command.d.ts.map +1 -1
- package/dist/commands/read.command.js +26 -45
- package/dist/commands/read.command.js.map +1 -1
- package/dist/lib/check/{docx.d.ts → check-docx.d.ts} +1 -1
- package/dist/lib/check/check-docx.d.ts.map +1 -0
- package/dist/lib/check/{docx.js → check-docx.js} +46 -14
- package/dist/lib/check/check-docx.js.map +1 -0
- package/dist/lib/check/{pdf.d.ts → check-pdf.d.ts} +1 -1
- package/dist/lib/check/check-pdf.d.ts.map +1 -0
- package/dist/lib/check/{pdf.js → check-pdf.js} +4 -2
- package/dist/lib/check/check-pdf.js.map +1 -0
- package/dist/lib/check/{pptx.d.ts → check-pptx.d.ts} +1 -1
- package/dist/lib/check/check-pptx.d.ts.map +1 -0
- package/dist/lib/check/{pptx.js → check-pptx.js} +18 -5
- package/dist/lib/check/check-pptx.js.map +1 -0
- package/dist/lib/check/{xlsx.d.ts → check-xlsx.d.ts} +1 -1
- package/dist/lib/check/check-xlsx.d.ts.map +1 -0
- package/dist/lib/check/{xlsx.js → check-xlsx.js} +17 -6
- package/dist/lib/check/check-xlsx.js.map +1 -0
- package/dist/lib/check/check.js +4 -4
- package/dist/lib/check/check.js.map +1 -1
- package/dist/lib/content-cache.d.ts +12 -0
- package/dist/lib/content-cache.d.ts.map +1 -0
- package/dist/lib/content-cache.js +33 -0
- package/dist/lib/content-cache.js.map +1 -0
- package/dist/lib/derive.d.ts +10 -0
- package/dist/lib/derive.d.ts.map +1 -1
- package/dist/lib/derive.js +40 -4
- package/dist/lib/derive.js.map +1 -1
- package/dist/lib/sidecar.d.ts +11 -2
- package/dist/lib/sidecar.d.ts.map +1 -1
- package/dist/lib/sidecar.js +28 -16
- package/dist/lib/sidecar.js.map +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +8 -7
- package/dist/skill.js.map +1 -1
- package/package.json +7 -7
- package/src/app.ts +4 -6
- package/src/cli.integration.test.ts +52 -29
- package/src/commands/check.command.ts +1 -1
- package/src/commands/derive.command.ts +1 -1
- package/src/commands/read.command.ts +30 -52
- package/src/lib/archives.ts +1 -1
- package/src/lib/check/{docx.ts → check-docx.ts} +112 -27
- package/src/lib/check/{pdf.ts → check-pdf.ts} +23 -4
- package/src/lib/check/{pptx.ts → check-pptx.ts} +48 -11
- package/src/lib/check/{xlsx.ts → check-xlsx.ts} +20 -6
- package/src/lib/check/check.test.ts +99 -27
- package/src/lib/check/check.ts +4 -4
- package/src/lib/content-cache.ts +58 -0
- package/src/lib/derive.integration.test.ts +198 -0
- package/src/lib/derive.ts +69 -7
- package/src/lib/derivers/archive.ts +1 -1
- package/src/lib/derivers/load-cost.test.ts +1 -1
- package/src/lib/derivers/pdf.ts +1 -1
- package/src/lib/derivers/xlsx.ts +1 -1
- package/src/lib/sidecar.ts +42 -22
- package/src/render.integration.test.ts +80 -56
- package/src/skill.ts +8 -7
- package/dist/commands/sweep.command.d.ts +0 -3
- package/dist/commands/sweep.command.d.ts.map +0 -1
- package/dist/commands/sweep.command.js +0 -41
- package/dist/commands/sweep.command.js.map +0 -1
- package/dist/lib/check/docx.d.ts.map +0 -1
- package/dist/lib/check/docx.js.map +0 -1
- package/dist/lib/check/pdf.d.ts.map +0 -1
- package/dist/lib/check/pdf.js.map +0 -1
- package/dist/lib/check/pptx.d.ts.map +0 -1
- package/dist/lib/check/pptx.js.map +0 -1
- package/dist/lib/check/xlsx.d.ts.map +0 -1
- package/dist/lib/check/xlsx.js.map +0 -1
- package/dist/lib/sweep.d.ts +0 -7
- package/dist/lib/sweep.d.ts.map +0 -1
- package/dist/lib/sweep.js +0 -56
- package/dist/lib/sweep.js.map +0 -1
- package/src/commands/sweep.command.ts +0 -48
- package/src/lib/sweep.ts +0 -72
|
@@ -250,7 +250,8 @@ const textStyleOf = (drawing: XmlElement, chain: readonly XmlElement[], inherita
|
|
|
250
250
|
return { listStyles: [...own, ...tail] };
|
|
251
251
|
};
|
|
252
252
|
|
|
253
|
-
const hundredthsAt = (element: XmlElement | undefined, path: readonly string[], attribute: string): number | undefined =>
|
|
253
|
+
const hundredthsAt = (element: XmlElement | undefined, path: readonly string[], attribute: string): number | undefined =>
|
|
254
|
+
intAttr(childAt(element, ...path), attribute);
|
|
254
255
|
|
|
255
256
|
const paragraphsOf = (txBody: XmlElement, style: TextStyle, fontScale: number, spacingReduction: number): Paragraph[] =>
|
|
256
257
|
childrenNamed(txBody, "a:p").map((p) => {
|
|
@@ -274,7 +275,10 @@ const paragraphsOf = (txBody: XmlElement, style: TextStyle, fontScale: number, s
|
|
|
274
275
|
...style.listStyles.map((listStyle) => () => intAttr(childAt(listStyle, `a:lvl${level}pPr`, "a:defRPr"), attribute)),
|
|
275
276
|
]);
|
|
276
277
|
const runSizes = runs.flatMap((run) => intAttr(childNamed(run, "a:rPr"), "sz") ?? []);
|
|
277
|
-
const size =
|
|
278
|
+
const size =
|
|
279
|
+
runSizes.length > 0
|
|
280
|
+
? Math.max(...runSizes)
|
|
281
|
+
: (intAttr(childNamed(p, "a:endParaRPr"), "sz") ?? inherited("sz") ?? DEFAULT_SIZE_HUNDREDTHS);
|
|
278
282
|
const bold = runs.some((run) => childNamed(run, "a:rPr")?.attrs["b"] === "1") || (runs.length > 0 && inherited("b") === 1);
|
|
279
283
|
const spacing = (name: string): number | undefined =>
|
|
280
284
|
firstOf([
|
|
@@ -348,7 +352,13 @@ const overflowFindings = (drawing: XmlElement, chain: readonly XmlElement[], box
|
|
|
348
352
|
if (body.autofit === "grow") {
|
|
349
353
|
// The box grows to fit its text, so the text cannot overflow the box, only the slide.
|
|
350
354
|
return box.y + needed > context.height * 1.02
|
|
351
|
-
? [
|
|
355
|
+
? [
|
|
356
|
+
warning(
|
|
357
|
+
"text-overflow",
|
|
358
|
+
where,
|
|
359
|
+
`the box grows to fit its text, and about ${plural(estimate.lines, "line")} of ${estimate.sizePt} pt take it past the bottom of the slide ${look}`,
|
|
360
|
+
),
|
|
361
|
+
]
|
|
352
362
|
: [];
|
|
353
363
|
}
|
|
354
364
|
if (estimate.heightPt <= innerHeight * OVERFLOW_MARGIN) {
|
|
@@ -391,7 +401,11 @@ const tableFindings = (frame: XmlElement, box: Box, where: string, context: Slid
|
|
|
391
401
|
const tcPr = childNamed(cell, "a:tcPr");
|
|
392
402
|
const marginX = (intAttr(tcPr, "marL") ?? DEFAULT_INSET_X) + (intAttr(tcPr, "marR") ?? DEFAULT_INSET_X);
|
|
393
403
|
const marginY = (intAttr(tcPr, "marT") ?? DEFAULT_INSET_Y) + (intAttr(tcPr, "marB") ?? DEFAULT_INSET_Y);
|
|
394
|
-
const estimate = estimateText(
|
|
404
|
+
const estimate = estimateText(
|
|
405
|
+
paragraphsOf(txBody, { listStyles: [childNamed(txBody, "a:lstStyle")] }, 1, 0),
|
|
406
|
+
(cellWidth - marginX) / EMU_PER_PT,
|
|
407
|
+
true,
|
|
408
|
+
);
|
|
395
409
|
rowHeight = Math.max(rowHeight, estimate.heightPt * EMU_PER_PT + marginY);
|
|
396
410
|
}
|
|
397
411
|
total += rowHeight;
|
|
@@ -421,7 +435,11 @@ const referenceFinding = (node: XmlElement, id: string, where: string, context:
|
|
|
421
435
|
}
|
|
422
436
|
if (relationship.external) {
|
|
423
437
|
return isImage
|
|
424
|
-
? warning(
|
|
438
|
+
? warning(
|
|
439
|
+
"linked-image",
|
|
440
|
+
where,
|
|
441
|
+
`image is linked from outside the file (${relationship.target}), so it will not show on anyone else's machine; embed it`,
|
|
442
|
+
)
|
|
425
443
|
: undefined;
|
|
426
444
|
}
|
|
427
445
|
if (context.pkg.has(relationship.target)) {
|
|
@@ -452,7 +470,14 @@ const pictureFindings = (picture: XmlElement, box: Box | undefined, where: strin
|
|
|
452
470
|
}
|
|
453
471
|
const embed = childAt(blipFill, "a:blip")?.attrs["r:embed"];
|
|
454
472
|
const relationship = embed === undefined ? undefined : context.pkg.rels(context.part).get(embed);
|
|
455
|
-
if (
|
|
473
|
+
if (
|
|
474
|
+
box !== undefined &&
|
|
475
|
+
relationship !== undefined &&
|
|
476
|
+
!relationship.external &&
|
|
477
|
+
context.pkg.has(relationship.target) &&
|
|
478
|
+
box.cx > 0 &&
|
|
479
|
+
box.cy > 0
|
|
480
|
+
) {
|
|
456
481
|
context.pictures.push({ where, media: relationship.target, cx: box.cx, cy: box.cy, crop: sides });
|
|
457
482
|
}
|
|
458
483
|
return [];
|
|
@@ -553,7 +578,8 @@ const drawingFindings = (drawing: XmlElement, context: SlideContext, grouped: bo
|
|
|
553
578
|
const layoutsOf = (pkg: OoxmlPackage, slidePart: string): Pick<Inheritance, "layout" | "master"> => {
|
|
554
579
|
const layoutRel = [...pkg.rels(slidePart).values()].find((relationship) => relationship.kind === "slideLayout" && !relationship.external);
|
|
555
580
|
const layout = layoutRel === undefined ? undefined : pkg.xml(layoutRel.target);
|
|
556
|
-
const masterRel =
|
|
581
|
+
const masterRel =
|
|
582
|
+
layoutRel === undefined ? undefined : [...pkg.rels(layoutRel.target).values()].find((relationship) => relationship.kind === "slideMaster");
|
|
557
583
|
return { layout, master: masterRel === undefined ? undefined : pkg.xml(masterRel.target) };
|
|
558
584
|
};
|
|
559
585
|
|
|
@@ -561,7 +587,9 @@ const slideFindings = (context: SlideContext, tree: XmlElement): Finding[] => {
|
|
|
561
587
|
const findings: Finding[] = [];
|
|
562
588
|
const slideWhere = `slide ${context.number}`;
|
|
563
589
|
if (tree.attrs["show"] === "0") {
|
|
564
|
-
findings.push(
|
|
590
|
+
findings.push(
|
|
591
|
+
warning("hidden-slide", slideWhere, "the slide is hidden, so the slide show skips it; delete it if it is not meant to be delivered"),
|
|
592
|
+
);
|
|
565
593
|
}
|
|
566
594
|
const referenced = new Set<string>();
|
|
567
595
|
const background = childAt(tree, "p:cSld", "p:bg");
|
|
@@ -588,7 +616,11 @@ export const checkPptx = (bytes: Uint8Array): CheckReport => {
|
|
|
588
616
|
pkg = openPackage(bytes);
|
|
589
617
|
} catch (cause) {
|
|
590
618
|
if (cause instanceof NotAPackage) {
|
|
591
|
-
return {
|
|
619
|
+
return {
|
|
620
|
+
format: "pptx",
|
|
621
|
+
findings: [error("unreadable", "", `${cause.message}: this is not a PowerPoint file (or it is password-protected)`)],
|
|
622
|
+
notes: [],
|
|
623
|
+
};
|
|
592
624
|
}
|
|
593
625
|
throw cause;
|
|
594
626
|
}
|
|
@@ -617,7 +649,13 @@ export const checkPptx = (bytes: Uint8Array): CheckReport => {
|
|
|
617
649
|
const number = index + 1;
|
|
618
650
|
const relationship = rels.get(slideId.attrs["r:id"] ?? "");
|
|
619
651
|
if (relationship === undefined || relationship.external || !pkg.has(relationship.target)) {
|
|
620
|
-
findings.push(
|
|
652
|
+
findings.push(
|
|
653
|
+
error(
|
|
654
|
+
"missing-part",
|
|
655
|
+
`slide ${number}`,
|
|
656
|
+
"the slide list names a slide that is not in the file; PowerPoint will offer to repair the deck",
|
|
657
|
+
),
|
|
658
|
+
);
|
|
621
659
|
return;
|
|
622
660
|
}
|
|
623
661
|
const tree = pkg.xml(relationship.target);
|
|
@@ -638,4 +676,3 @@ export const checkPptx = (bytes: Uint8Array): CheckReport => {
|
|
|
638
676
|
findings.push(...stretchFindings(pkg, pictures));
|
|
639
677
|
return { format: "pptx", extent: plural(slideIds.length, "slide"), findings, notes };
|
|
640
678
|
};
|
|
641
|
-
|
|
@@ -23,7 +23,8 @@ const INLINE = /<((?:\w+:)?)is\b/;
|
|
|
23
23
|
const attributeIn = (attributes: string, name: string): string | undefined => new RegExp(`(?:^|\\s)${name}="([^"]*)"`).exec(attributes)?.[1];
|
|
24
24
|
|
|
25
25
|
// A sheet name as a formula would quote it: `Sheet1!B4`, `'Q3 plan'!B4`.
|
|
26
|
-
const cellRef = (sheet: string, ref: string): string =>
|
|
26
|
+
const cellRef = (sheet: string, ref: string): string =>
|
|
27
|
+
/^[A-Za-z_][\w.]*$/.test(sheet) ? `${sheet}!${ref}` : `'${sheet.replaceAll("'", "''")}'!${ref}`;
|
|
27
28
|
|
|
28
29
|
interface SheetScan {
|
|
29
30
|
readonly errors: Finding[];
|
|
@@ -71,9 +72,14 @@ const scanSheet = (sheet: string, xml: string): SheetScan => {
|
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
const uncachedFinding = (sheet: string, cells: readonly string[], recalculates: boolean): Finding => {
|
|
74
|
-
const shown = cells
|
|
75
|
+
const shown = cells
|
|
76
|
+
.slice(0, 5)
|
|
77
|
+
.map((cell) => cell.slice(cell.lastIndexOf("!") + 1))
|
|
78
|
+
.join(", ");
|
|
75
79
|
const more = cells.length > 5 ? ", …" : "";
|
|
76
|
-
const onOpen = recalculates
|
|
80
|
+
const onOpen = recalculates
|
|
81
|
+
? "the workbook asks to be recalculated on open, so spreadsheet applications fill them in"
|
|
82
|
+
: "Excel, LibreOffice and Google Sheets compute them on open";
|
|
77
83
|
return warning(
|
|
78
84
|
"formula-uncached",
|
|
79
85
|
sheet,
|
|
@@ -86,7 +92,11 @@ const openWorkbook = (bytes: Uint8Array): OoxmlPackage | CheckReport => {
|
|
|
86
92
|
return openPackage(bytes);
|
|
87
93
|
} catch (cause) {
|
|
88
94
|
if (cause instanceof NotAPackage) {
|
|
89
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
format: "xlsx",
|
|
97
|
+
findings: [error("unreadable", "", `${cause.message}: this is not an Excel file (or it is password-protected)`)],
|
|
98
|
+
notes: [],
|
|
99
|
+
};
|
|
90
100
|
}
|
|
91
101
|
throw cause;
|
|
92
102
|
}
|
|
@@ -96,7 +106,9 @@ const namesFindings = (workbook: XmlElement | undefined): Finding[] => {
|
|
|
96
106
|
const names = workbook === undefined ? undefined : childNamed(workbook, "x:definedNames");
|
|
97
107
|
return (names === undefined ? [] : childrenNamed(names, "x:definedName")).flatMap((name) => {
|
|
98
108
|
const target = name.children.filter((child): child is string => !isElement(child)).join("");
|
|
99
|
-
return target.includes("#REF!")
|
|
109
|
+
return target.includes("#REF!")
|
|
110
|
+
? [error("broken-name", "", `the defined name "${name.attrs["name"] ?? "?"}" refers to ${excerpt(target)}, a deleted range`)]
|
|
111
|
+
: [];
|
|
100
112
|
});
|
|
101
113
|
};
|
|
102
114
|
|
|
@@ -121,7 +133,9 @@ const sheetsFindings = (pkg: OoxmlPackage, main: string, sheets: readonly XmlEle
|
|
|
121
133
|
const name = sheet.attrs["name"] ?? "?";
|
|
122
134
|
const relationship = rels.get(sheet.attrs["r:id"] ?? "");
|
|
123
135
|
if (relationship === undefined || relationship.external || !pkg.has(relationship.target)) {
|
|
124
|
-
findings.push(
|
|
136
|
+
findings.push(
|
|
137
|
+
error("missing-part", name, "the sheet list names a sheet that is not in the file; Excel will offer to repair the workbook"),
|
|
138
|
+
);
|
|
125
139
|
continue;
|
|
126
140
|
}
|
|
127
141
|
// A chart sheet has no cells, and a sheet over the size cap is named in the notes.
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { strToU8 } from "fflate";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
deckBytes,
|
|
4
|
+
field,
|
|
5
|
+
inlinePicture,
|
|
6
|
+
para,
|
|
7
|
+
pdfBytes,
|
|
8
|
+
pdfPagesBytes,
|
|
9
|
+
pictureFrame,
|
|
10
|
+
pngOfSize,
|
|
11
|
+
tableFrame,
|
|
12
|
+
textBox,
|
|
13
|
+
wordBytes,
|
|
14
|
+
workbookBytes,
|
|
15
|
+
} from "../../testing.js";
|
|
16
|
+
import { checkDocx } from "./check-docx.js";
|
|
4
17
|
import { leftoverText, type CheckReport } from "./finding.js";
|
|
5
|
-
import { checkPdf } from "./pdf.js";
|
|
6
|
-
import { checkPptx } from "./pptx.js";
|
|
18
|
+
import { checkPdf } from "./check-pdf.js";
|
|
19
|
+
import { checkPptx } from "./check-pptx.js";
|
|
7
20
|
import { estimateText, wrappedLines, type Paragraph } from "./text-fit.js";
|
|
8
|
-
import { checkXlsx } from "./xlsx.js";
|
|
21
|
+
import { checkXlsx } from "./check-xlsx.js";
|
|
9
22
|
import { parseXml } from "./xml-tree.js";
|
|
10
23
|
|
|
11
24
|
// Every checker against a document built with one known defect, and against a clean one, which must come back with
|
|
@@ -52,7 +65,9 @@ describe("pptx", () => {
|
|
|
52
65
|
test("a drawing wholly off the slide is an error, one bleeding past an edge a warning with how far", () => {
|
|
53
66
|
const report = checkPptx(
|
|
54
67
|
deckBytes({
|
|
55
|
-
slides: [
|
|
68
|
+
slides: [
|
|
69
|
+
{ drawings: [textBox("Gone", { x: 14, y: 1, w: 2, h: 1 }, ["off"]), textBox("Bleed", { x: 12, y: 1, w: 3, h: 1 }, ["partly"])] },
|
|
70
|
+
],
|
|
56
71
|
}),
|
|
57
72
|
);
|
|
58
73
|
expect(found(report)).toEqual([
|
|
@@ -100,7 +115,10 @@ describe("pptx", () => {
|
|
|
100
115
|
deckBytes({
|
|
101
116
|
slides: [
|
|
102
117
|
{
|
|
103
|
-
drawings: [
|
|
118
|
+
drawings: [
|
|
119
|
+
pictureFrame("Gone", { x: 1, y: 1, w: 4, h: 3 }, "rId2"),
|
|
120
|
+
pictureFrame("Undefined", { x: 6, y: 1, w: 4, h: 3 }, "rId9"),
|
|
121
|
+
],
|
|
104
122
|
rels: { rId2: { type: "image", target: "../media/missing.png" } },
|
|
105
123
|
},
|
|
106
124
|
],
|
|
@@ -116,7 +134,12 @@ describe("pptx", () => {
|
|
|
116
134
|
test("an image linked from outside the file is a warning naming where it points", () => {
|
|
117
135
|
const report = checkPptx(
|
|
118
136
|
deckBytes({
|
|
119
|
-
slides: [
|
|
137
|
+
slides: [
|
|
138
|
+
{
|
|
139
|
+
drawings: [pictureFrame("Logo", { x: 1, y: 1, w: 4, h: 3 }, "rId2")],
|
|
140
|
+
rels: { rId2: { type: "image", target: "file:///C:/Users/me/logo.png", external: true } },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
120
143
|
}),
|
|
121
144
|
);
|
|
122
145
|
expect(found(report)).toEqual([["warning", "linked-image", 'slide 1 · "Logo"']]);
|
|
@@ -128,7 +151,10 @@ describe("pptx", () => {
|
|
|
128
151
|
deckBytes({
|
|
129
152
|
slides: [
|
|
130
153
|
{
|
|
131
|
-
drawings: [
|
|
154
|
+
drawings: [
|
|
155
|
+
pictureFrame("Wide", { x: 1, y: 1, w: 4, h: 1 }, "rId2"),
|
|
156
|
+
pictureFrame("Cropped", { x: 6, y: 1, w: 4, h: 3 }, "rId2", { l: 60_000, r: 40_000 }),
|
|
157
|
+
],
|
|
132
158
|
rels: { rId2: IMAGE },
|
|
133
159
|
},
|
|
134
160
|
],
|
|
@@ -147,7 +173,9 @@ describe("pptx", () => {
|
|
|
147
173
|
test("a picture cropped to its box's proportions is not stretched", () => {
|
|
148
174
|
const report = checkPptx(
|
|
149
175
|
deckBytes({
|
|
150
|
-
slides: [
|
|
176
|
+
slides: [
|
|
177
|
+
{ drawings: [pictureFrame("Banner", { x: 1, y: 1, w: 4, h: 1.5 }, "rId2", { t: 25_000, b: 25_000 })], rels: { rId2: IMAGE } },
|
|
178
|
+
],
|
|
151
179
|
media: { "image1.png": pngOfSize(400, 300) },
|
|
152
180
|
}),
|
|
153
181
|
);
|
|
@@ -155,7 +183,8 @@ describe("pptx", () => {
|
|
|
155
183
|
});
|
|
156
184
|
|
|
157
185
|
test("text that cannot fit its box is a warning with the estimate, pointing at render", () => {
|
|
158
|
-
const long =
|
|
186
|
+
const long =
|
|
187
|
+
"This is a very long sentence that will surely not fit in a small box of three inches at twenty four points, and it goes on and on.";
|
|
159
188
|
const report = checkPptx(
|
|
160
189
|
deckBytes({
|
|
161
190
|
slides: [
|
|
@@ -177,7 +206,10 @@ describe("pptx", () => {
|
|
|
177
206
|
{
|
|
178
207
|
drawings: [
|
|
179
208
|
textBox("Fits", { x: 1, y: 1, w: 6, h: 1.2 }, ["Two lines of", "comfortable text"], { size: 24 }),
|
|
180
|
-
textBox("Grows", { x: 1, y: 3, w: 3, h: 0.5 }, ["A sentence long enough to need three or four lines here."], {
|
|
209
|
+
textBox("Grows", { x: 1, y: 3, w: 3, h: 0.5 }, ["A sentence long enough to need three or four lines here."], {
|
|
210
|
+
size: 18,
|
|
211
|
+
autofit: "grow",
|
|
212
|
+
}),
|
|
181
213
|
],
|
|
182
214
|
},
|
|
183
215
|
],
|
|
@@ -187,9 +219,14 @@ describe("pptx", () => {
|
|
|
187
219
|
});
|
|
188
220
|
|
|
189
221
|
test("shrink-on-overflow says PowerPoint shows it overflowing until edited", () => {
|
|
190
|
-
const long =
|
|
191
|
-
|
|
192
|
-
|
|
222
|
+
const long =
|
|
223
|
+
"A paragraph much too long for its box, which PowerPoint only shrinks once somebody edits the text and LibreOffice shrinks at once.";
|
|
224
|
+
const report = checkPptx(
|
|
225
|
+
deckBytes({ slides: [{ drawings: [textBox("Body", { x: 1, y: 1, w: 3, h: 0.6 }, [long], { size: 24, autofit: "shrink" })] }] }),
|
|
226
|
+
);
|
|
227
|
+
expect(messageOf(report, "text-overflow")).toContain(
|
|
228
|
+
"shrink-on-overflow is on, so LibreOffice shrinks it but PowerPoint shows it overflowing until the text is edited",
|
|
229
|
+
);
|
|
193
230
|
});
|
|
194
231
|
|
|
195
232
|
test("a table whose rows run past the bottom of the slide is a warning", () => {
|
|
@@ -233,7 +270,14 @@ describe("docx", () => {
|
|
|
233
270
|
test("a well-made document has nothing to report", () => {
|
|
234
271
|
const report = checkDocx(
|
|
235
272
|
wordBytes({
|
|
236
|
-
body: [
|
|
273
|
+
body: [
|
|
274
|
+
para("Report", "berschrift1"),
|
|
275
|
+
para("The body of the report."),
|
|
276
|
+
'<w:p><w:bookmarkStart w:id="0" w:name="results"/><w:bookmarkEnd w:id="0"/></w:p>',
|
|
277
|
+
'<w:p><w:hyperlink w:anchor="results"><w:r><w:t>see results</w:t></w:r></w:hyperlink></w:p>',
|
|
278
|
+
inlinePicture("rId5", 6, 3),
|
|
279
|
+
field("PAGE", "1"),
|
|
280
|
+
],
|
|
237
281
|
rels: { rId5: DOC_IMAGE },
|
|
238
282
|
media: { "image1.png": pngOfSize(600, 300) },
|
|
239
283
|
}),
|
|
@@ -243,13 +287,21 @@ describe("docx", () => {
|
|
|
243
287
|
});
|
|
244
288
|
|
|
245
289
|
test("a link to a bookmark that is not there is an error, located by its paragraph", () => {
|
|
246
|
-
const report = checkDocx(
|
|
290
|
+
const report = checkDocx(
|
|
291
|
+
wordBytes({
|
|
292
|
+
body: [para("Intro", "berschrift1"), '<w:p><w:hyperlink w:anchor="nowhere"><w:r><w:t>see section</w:t></w:r></w:hyperlink></w:p>'],
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
247
295
|
expect(found(report)).toEqual([["error", "broken-link", 'paragraph 2 · "see section"']]);
|
|
248
|
-
expect(messageOf(report, "broken-link")).toBe(
|
|
296
|
+
expect(messageOf(report, "broken-link")).toBe(
|
|
297
|
+
'the link "see section" points to bookmark "nowhere", which is not in the document, so clicking it goes nowhere',
|
|
298
|
+
);
|
|
249
299
|
});
|
|
250
300
|
|
|
251
301
|
test("a cross-reference to a missing bookmark, and a field showing Word's error, are errors said once", () => {
|
|
252
|
-
const report = checkDocx(
|
|
302
|
+
const report = checkDocx(
|
|
303
|
+
wordBytes({ body: [field("PAGEREF _Ref999 \\h", "3"), field("REF _Ref123 \\h", "Error! Reference source not found.")] }),
|
|
304
|
+
);
|
|
253
305
|
expect(found(report)).toEqual([
|
|
254
306
|
["error", "broken-reference", 'paragraph 1 · "3"'],
|
|
255
307
|
["error", "field-error", 'paragraph 2 · "Error! Reference source not found."'],
|
|
@@ -281,7 +333,9 @@ describe("docx", () => {
|
|
|
281
333
|
["error", "missing-image", "paragraph 2"],
|
|
282
334
|
["warning", "wide-image", "paragraph 1"],
|
|
283
335
|
]);
|
|
284
|
-
expect(messageOf(report, "wide-image")).toBe(
|
|
336
|
+
expect(messageOf(report, "wide-image")).toBe(
|
|
337
|
+
"a picture is 9 in wide in a 6.5 in text column, so it runs into the margin or is cut off; scale it to fit",
|
|
338
|
+
);
|
|
285
339
|
});
|
|
286
340
|
|
|
287
341
|
test("what a draft leaves behind: tracked changes, comments, a control showing its prompt, filler text", () => {
|
|
@@ -289,7 +343,7 @@ describe("docx", () => {
|
|
|
289
343
|
wordBytes({
|
|
290
344
|
body: [
|
|
291
345
|
'<w:p><w:ins w:id="1" w:author="a"><w:r><w:t>new words</w:t></w:r></w:ins></w:p>',
|
|
292
|
-
|
|
346
|
+
"<w:sdt><w:sdtPr><w:showingPlcHdr/></w:sdtPr><w:sdtContent><w:p><w:r><w:t>Click or tap here to enter text.</w:t></w:r></w:p></w:sdtContent></w:sdt>",
|
|
293
347
|
para("Lorem ipsum dolor sit amet."),
|
|
294
348
|
],
|
|
295
349
|
comments: ["check this figure"],
|
|
@@ -316,7 +370,9 @@ const cell = (ref: string, inner: string, type?: string): string => `<c r="${ref
|
|
|
316
370
|
|
|
317
371
|
describe("xlsx", () => {
|
|
318
372
|
test("a workbook of values and computed formulas has nothing to report", () => {
|
|
319
|
-
const report = checkXlsx(
|
|
373
|
+
const report = checkXlsx(
|
|
374
|
+
workbookBytes([{ name: "Sheet1", rows: `<row r="1">${cell("A1", "<v>1</v>")}${cell("B1", "<f>A1*2</f><v>2</v>")}</row>` }]),
|
|
375
|
+
);
|
|
320
376
|
expect(report.extent).toBe("1 sheet");
|
|
321
377
|
expect(report.findings).toEqual([]);
|
|
322
378
|
});
|
|
@@ -336,7 +392,9 @@ describe("xlsx", () => {
|
|
|
336
392
|
const rows = `<row r="1">${cell("A1", "<v>1</v>")}${cell("B1", "<f>A1+1</f>")}${cell("C1", "<f>A1+2</f>")}</row>`;
|
|
337
393
|
const plain = checkXlsx(workbookBytes([{ name: "Data", rows }]));
|
|
338
394
|
expect(found(plain)).toEqual([["warning", "formula-uncached", "Data"]]);
|
|
339
|
-
expect(messageOf(plain, "formula-uncached")).toContain(
|
|
395
|
+
expect(messageOf(plain, "formula-uncached")).toContain(
|
|
396
|
+
"2 formulas saved with no value (B1, C1): Excel, LibreOffice and Google Sheets compute them on open",
|
|
397
|
+
);
|
|
340
398
|
const flagged = checkXlsx(workbookBytes([{ name: "Data", rows }], { calcPr: '<calcPr fullCalcOnLoad="1"/>' }));
|
|
341
399
|
expect(messageOf(flagged, "formula-uncached")).toContain("the workbook asks to be recalculated on open");
|
|
342
400
|
});
|
|
@@ -365,12 +423,19 @@ describe("pdf", () => {
|
|
|
365
423
|
});
|
|
366
424
|
|
|
367
425
|
test("a blank page is an error; a page with only a drawing is not blank", async () => {
|
|
368
|
-
const report = await checkPdf(
|
|
426
|
+
const report = await checkPdf(
|
|
427
|
+
pdfPagesBytes([{ content: "BT /F1 12 Tf 72 720 Td (Cover) Tj ET" }, { content: "" }, { content: "0 0 1 rg 10 10 100 100 re f" }]),
|
|
428
|
+
);
|
|
369
429
|
expect(found(report)).toEqual([["error", "blank-page", "page 2"]]);
|
|
370
430
|
});
|
|
371
431
|
|
|
372
432
|
test("text in a font the file does not carry is a warning; the fourteen standard fonts are not", async () => {
|
|
373
|
-
const report = await checkPdf(
|
|
433
|
+
const report = await checkPdf(
|
|
434
|
+
pdfPagesBytes([
|
|
435
|
+
{ content: "BT /F1 12 Tf 72 720 Td (Hello) Tj ET", font: "Calibri" },
|
|
436
|
+
{ content: "BT /F1 12 Tf 72 720 Td (Again) Tj ET", font: "Calibri" },
|
|
437
|
+
]),
|
|
438
|
+
);
|
|
374
439
|
expect(found(report)).toEqual([["warning", "font-not-embedded", "page 1, 2"]]);
|
|
375
440
|
expect(messageOf(report, "font-not-embedded")).toContain("text is set in Calibri, which the file does not carry");
|
|
376
441
|
});
|
|
@@ -384,7 +449,12 @@ describe("pdf", () => {
|
|
|
384
449
|
|
|
385
450
|
test("a link to a named destination the file does not define is an error", async () => {
|
|
386
451
|
const report = await checkPdf(
|
|
387
|
-
pdfPagesBytes([
|
|
452
|
+
pdfPagesBytes([
|
|
453
|
+
{
|
|
454
|
+
content: "BT /F1 12 Tf 72 720 Td (See results) Tj ET",
|
|
455
|
+
extra: "/Annots [<< /Type /Annot /Subtype /Link /Rect [72 700 200 730] /Dest (results) >>]",
|
|
456
|
+
},
|
|
457
|
+
]),
|
|
388
458
|
);
|
|
389
459
|
expect(found(report)).toEqual([["error", "broken-link", "page 1"]]);
|
|
390
460
|
});
|
|
@@ -422,7 +492,9 @@ describe("xml tree", () => {
|
|
|
422
492
|
});
|
|
423
493
|
|
|
424
494
|
test("entities decode, CDATA is text, whitespace is kept only inside a text element", () => {
|
|
425
|
-
const tree = parseXml(
|
|
495
|
+
const tree = parseXml(
|
|
496
|
+
'<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">\n <w:t> A & B </w:t><w:t><![CDATA[<raw>]]></w:t>\n</w:p>',
|
|
497
|
+
);
|
|
426
498
|
expect(tree.children).toEqual([
|
|
427
499
|
{ name: "w:t", attrs: {}, children: [" A & B "] },
|
|
428
500
|
{ name: "w:t", attrs: {}, children: ["<raw>"] },
|
package/src/lib/check/check.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { errorMessage, isMissing } from "@intentic/base/errors";
|
|
3
3
|
import { claimedFormat, type Format } from "../formats.js";
|
|
4
|
-
import { checkDocx } from "./docx.js";
|
|
4
|
+
import { checkDocx } from "./check-docx.js";
|
|
5
5
|
import type { CheckReport } from "./finding.js";
|
|
6
|
-
import { checkPdf } from "./pdf.js";
|
|
7
|
-
import { checkPptx } from "./pptx.js";
|
|
8
|
-
import { checkXlsx } from "./xlsx.js";
|
|
6
|
+
import { checkPdf } from "./check-pdf.js";
|
|
7
|
+
import { checkPptx } from "./check-pptx.js";
|
|
8
|
+
import { checkXlsx } from "./check-xlsx.js";
|
|
9
9
|
|
|
10
10
|
// `fileq check`'s routing: which formats have a checker, and the one read of the file every checker works from.
|
|
11
11
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { toolHome } from "@intentic/agent-cli/env";
|
|
3
|
+
import type { DerivedDoc } from "./derivers/deriver.js";
|
|
4
|
+
import { DERIVED_DIR, ignoreShadowsInGit, isFresh, parseSidecarFront, readSidecar, sidecarBody, writeDerived, type DerivedEntry } from "./sidecar.js";
|
|
5
|
+
|
|
6
|
+
// Renderings keyed by what they are a function of, the source's sha256 and the deriver stamp, never by its path: the
|
|
7
|
+
// same bytes read again (the same file, a copy or a move, a download, a git blob) answer from here without a deriver.
|
|
8
|
+
// Kept in the workspace's derived tree when there is a workspace, so its .gitignore covers them, else in the per-user
|
|
9
|
+
// cache. A cache that cannot be read or written costs the speed-up, never the read.
|
|
10
|
+
|
|
11
|
+
export const contentCacheDir = (workspaceRoot: string | undefined): string =>
|
|
12
|
+
workspaceRoot === undefined ? join(toolHome("fileq"), "by-hash") : join(workspaceRoot, DERIVED_DIR, ".by-hash");
|
|
13
|
+
|
|
14
|
+
// The stamp made safe for a file name (`docx v2` → `docx-v2`); a hit is checked against the entry's own front matter.
|
|
15
|
+
export const contentCachePathFor = (workspaceRoot: string | undefined, sourceSha: string, deriverStamp: string): string =>
|
|
16
|
+
join(contentCacheDir(workspaceRoot), `${sourceSha}.${deriverStamp.replaceAll(/[^A-Za-z0-9.+-]/g, "-")}.md`);
|
|
17
|
+
|
|
18
|
+
export interface CachedRendering {
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly doc: DerivedDoc;
|
|
21
|
+
readonly derivedAt: Date;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The stored rendering of these bytes by this deriver; undefined when there is none, or none that still matches. */
|
|
25
|
+
export const readCachedRendering = async (
|
|
26
|
+
workspaceRoot: string | undefined,
|
|
27
|
+
sourceSha: string,
|
|
28
|
+
deriverStamp: string,
|
|
29
|
+
): Promise<CachedRendering | undefined> => {
|
|
30
|
+
const path = contentCachePathFor(workspaceRoot, sourceSha, deriverStamp);
|
|
31
|
+
const content = await readSidecar(path);
|
|
32
|
+
if (content === undefined || !isFresh(content, sourceSha, deriverStamp)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const front = parseSidecarFront(content);
|
|
36
|
+
const derivedAt = new Date(front.derivedAt ?? Number.NaN);
|
|
37
|
+
if (Number.isNaN(derivedAt.getTime())) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const body = sidecarBody(content);
|
|
41
|
+
// writeDerived ends a non-empty body with one newline of its own.
|
|
42
|
+
const markdown = body.endsWith("\n") ? body.slice(0, -1) : body;
|
|
43
|
+
return { path, doc: { markdown, title: front.title, notes: [...front.notes] }, derivedAt };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Stores a rendering under its content key; answers the entry's path, or undefined when it could not be written. */
|
|
47
|
+
export const cacheRendering = async (workspaceRoot: string | undefined, entry: Omit<DerivedEntry, "source">): Promise<string | undefined> => {
|
|
48
|
+
try {
|
|
49
|
+
const written = await writeDerived(contentCachePathFor(workspaceRoot, entry.sourceSha, entry.deriverStamp), entry);
|
|
50
|
+
if (workspaceRoot !== undefined) {
|
|
51
|
+
await ignoreShadowsInGit(workspaceRoot);
|
|
52
|
+
}
|
|
53
|
+
return written.path;
|
|
54
|
+
} catch {
|
|
55
|
+
// allow(silent-catch): an unwritable cache costs the next read its speed-up, never this read its rendering.
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
};
|