@intentic/fileq 1.319.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.
Files changed (39) hide show
  1. package/NOTICE +4 -4
  2. package/dist/lib/check/{docx.d.ts → check-docx.d.ts} +1 -1
  3. package/dist/lib/check/check-docx.d.ts.map +1 -0
  4. package/dist/lib/check/{docx.js → check-docx.js} +46 -14
  5. package/dist/lib/check/check-docx.js.map +1 -0
  6. package/dist/lib/check/{pdf.d.ts → check-pdf.d.ts} +1 -1
  7. package/dist/lib/check/check-pdf.d.ts.map +1 -0
  8. package/dist/lib/check/{pdf.js → check-pdf.js} +4 -2
  9. package/dist/lib/check/check-pdf.js.map +1 -0
  10. package/dist/lib/check/{pptx.d.ts → check-pptx.d.ts} +1 -1
  11. package/dist/lib/check/check-pptx.d.ts.map +1 -0
  12. package/dist/lib/check/{pptx.js → check-pptx.js} +18 -5
  13. package/dist/lib/check/check-pptx.js.map +1 -0
  14. package/dist/lib/check/{xlsx.d.ts → check-xlsx.d.ts} +1 -1
  15. package/dist/lib/check/check-xlsx.d.ts.map +1 -0
  16. package/dist/lib/check/{xlsx.js → check-xlsx.js} +17 -6
  17. package/dist/lib/check/check-xlsx.js.map +1 -0
  18. package/dist/lib/check/check.js +4 -4
  19. package/dist/lib/check/check.js.map +1 -1
  20. package/dist/lib/derive.d.ts.map +1 -1
  21. package/dist/lib/derive.js +4 -1
  22. package/dist/lib/derive.js.map +1 -1
  23. package/package.json +6 -6
  24. package/src/lib/check/{docx.ts → check-docx.ts} +112 -27
  25. package/src/lib/check/{pdf.ts → check-pdf.ts} +23 -4
  26. package/src/lib/check/{pptx.ts → check-pptx.ts} +48 -11
  27. package/src/lib/check/{xlsx.ts → check-xlsx.ts} +20 -6
  28. package/src/lib/check/check.test.ts +99 -27
  29. package/src/lib/check/check.ts +4 -4
  30. package/src/lib/derive.ts +4 -1
  31. package/src/render.integration.test.ts +80 -56
  32. package/dist/lib/check/docx.d.ts.map +0 -1
  33. package/dist/lib/check/docx.js.map +0 -1
  34. package/dist/lib/check/pdf.d.ts.map +0 -1
  35. package/dist/lib/check/pdf.js.map +0 -1
  36. package/dist/lib/check/pptx.d.ts.map +0 -1
  37. package/dist/lib/check/pptx.js.map +0 -1
  38. package/dist/lib/check/xlsx.d.ts.map +0 -1
  39. package/dist/lib/check/xlsx.js.map +0 -1
@@ -73,10 +73,12 @@ const headingLevels = (styles: XmlElement | undefined): ReadonlyMap<string, numb
73
73
  const basedOn = childNamed(style, "w:basedOn")?.attrs["w:val"];
74
74
  return basedOn === undefined ? undefined : levelOf(basedOn, depth + 1);
75
75
  };
76
- return new Map([...byId.keys()].flatMap((id) => {
77
- const level = levelOf(id, 0);
78
- return level === undefined ? [] : [[id, level] as const];
79
- }));
76
+ return new Map(
77
+ [...byId.keys()].flatMap((id) => {
78
+ const level = levelOf(id, 0);
79
+ return level === undefined ? [] : [[id, level] as const];
80
+ }),
81
+ );
80
82
  };
81
83
 
82
84
  const headingOf = (paragraph: XmlElement, levels: ReadonlyMap<string, number>): number | undefined => {
@@ -106,7 +108,12 @@ const locate = (paragraphs: readonly XmlElement[], levels: ReadonlyMap<string, n
106
108
  const text = paragraphText(paragraph);
107
109
  const heading = headingOf(paragraph, levels);
108
110
  const number = index + 1;
109
- const label = text.trim() !== "" ? `paragraph ${number} · ${excerpt(text, 40)}` : lastHeading === undefined ? `paragraph ${number}` : `paragraph ${number} · under ${lastHeading}`;
111
+ const label =
112
+ text.trim() !== ""
113
+ ? `paragraph ${number} · ${excerpt(text, 40)}`
114
+ : lastHeading === undefined
115
+ ? `paragraph ${number}`
116
+ : `paragraph ${number} · under ${lastHeading}`;
110
117
  located.push({ paragraph, number, text, heading });
111
118
  whereOf.set(paragraph, label);
112
119
  if (heading !== undefined && text.trim() !== "") {
@@ -151,7 +158,12 @@ const stepField = (node: XmlElement, open: OpenField[], fields: Field[], where:
151
158
  current.separated = true;
152
159
  } else if (type === "end") {
153
160
  open.pop();
154
- fields.push({ instruction: current.instruction.trim(), result: current.result, hasResult: current.separated && current.result.trim() !== "", where: current.where });
161
+ fields.push({
162
+ instruction: current.instruction.trim(),
163
+ result: current.result,
164
+ hasResult: current.separated && current.result.trim() !== "",
165
+ where: current.where,
166
+ });
155
167
  }
156
168
  };
157
169
 
@@ -195,19 +207,33 @@ const bookmarkArgument = (instruction: string): string | undefined => {
195
207
  const fieldFindings = (fields: readonly Field[], bookmarks: ReadonlySet<string>, updatesOnOpen: boolean): Finding[] =>
196
208
  fields.flatMap((field): Finding[] => {
197
209
  const keyword = (field.instruction.split(/\s+/)[0] ?? "").toUpperCase();
198
- if (field.result.trim().startsWith('Error!')) {
199
- return [error("field-error", field.where, `the ${keyword} field shows ${excerpt(field.result)}: fix what it points at, then update fields`)];
210
+ if (field.result.trim().startsWith("Error!")) {
211
+ return [
212
+ error("field-error", field.where, `the ${keyword} field shows ${excerpt(field.result)}: fix what it points at, then update fields`),
213
+ ];
200
214
  }
201
215
  const bookmark = bookmarkArgument(field.instruction);
202
216
  if (bookmark !== undefined && !bookmarks.has(bookmark) && !BUILT_IN_ANCHORS.has(bookmark)) {
203
- return [error("broken-reference", field.where, `the ${keyword} field points to bookmark "${bookmark}", which is not in the document: Word shows "Error! Reference source not found."`)];
217
+ return [
218
+ error(
219
+ "broken-reference",
220
+ field.where,
221
+ `the ${keyword} field points to bookmark "${bookmark}", which is not in the document: Word shows "Error! Reference source not found."`,
222
+ ),
223
+ ];
204
224
  }
205
225
  if (keyword === "MERGEFIELD") {
206
- return [warning("merge-field", field.where, `a mail-merge field is left in (${field.instruction}): it prints its placeholder, not a value`)];
226
+ return [
227
+ warning("merge-field", field.where, `a mail-merge field is left in (${field.instruction}): it prints its placeholder, not a value`),
228
+ ];
207
229
  }
208
230
  if (keyword === "TOC" && !field.hasResult) {
209
- const when = updatesOnOpen ? "Word offers to update fields when the file opens" : "nothing fills it until someone updates fields (F9 in Word)";
210
- return [warning("empty-toc", field.where, `the table of contents has no entries saved: ${when}, and previews and LibreOffice show it empty`)];
231
+ const when = updatesOnOpen
232
+ ? "Word offers to update fields when the file opens"
233
+ : "nothing fills it until someone updates fields (F9 in Word)";
234
+ return [
235
+ warning("empty-toc", field.where, `the table of contents has no entries saved: ${when}, and previews and LibreOffice show it empty`),
236
+ ];
211
237
  }
212
238
  if (!field.hasResult && keyword !== "" && !COMPUTED_FIELDS.has(keyword) && keyword !== "TOC") {
213
239
  return [warning("empty-field", field.where, `the ${keyword} field has no saved result, so it shows blank until fields are updated`)];
@@ -217,7 +243,13 @@ const fieldFindings = (fields: readonly Field[], bookmarks: ReadonlySet<string>,
217
243
 
218
244
  // --- references and images -------------------------------------------------------------------------------------
219
245
 
220
- const referenceFindings = (context: DocContext, part: string, tree: XmlElement, whereOfNode: (node: XmlElement) => string, referenced: Set<string>): Finding[] => {
246
+ const referenceFindings = (
247
+ context: DocContext,
248
+ part: string,
249
+ tree: XmlElement,
250
+ whereOfNode: (node: XmlElement) => string,
251
+ referenced: Set<string>,
252
+ ): Finding[] => {
221
253
  const rels = context.pkg.rels(part);
222
254
  const findings: Finding[] = [];
223
255
  for (const node of everyElement(tree)) {
@@ -244,10 +276,20 @@ const referenceFinding = (
244
276
  ): Finding | undefined => {
245
277
  const isImage = node.name === "a:blip" || node.name === "v:imagedata";
246
278
  if (relationship === undefined) {
247
- return error(isImage ? "missing-image" : "missing-part", where, `${isImage ? "an image" : node.name} refers to ${id}, which the document does not define: it shows as missing`);
279
+ return error(
280
+ isImage ? "missing-image" : "missing-part",
281
+ where,
282
+ `${isImage ? "an image" : node.name} refers to ${id}, which the document does not define: it shows as missing`,
283
+ );
248
284
  }
249
285
  if (relationship.external) {
250
- return isImage ? warning("linked-image", where, `an image is linked from outside the file (${relationship.target}), so it will not show on anyone else's machine; embed it`) : undefined;
286
+ return isImage
287
+ ? warning(
288
+ "linked-image",
289
+ where,
290
+ `an image is linked from outside the file (${relationship.target}), so it will not show on anyone else's machine; embed it`,
291
+ )
292
+ : undefined;
251
293
  }
252
294
  if (pkg.has(relationship.target)) {
253
295
  return undefined;
@@ -290,7 +332,13 @@ const wideImageFindings = (context: DocContext, body: XmlElement, owner: Readonl
290
332
  return [];
291
333
  }
292
334
  const where = paragraph === undefined ? "" : (context.whereOf.get(paragraph) ?? "");
293
- return [warning("wide-image", where, `a picture is ${inches(cx)} wide in a ${inches(width)} text column, so it runs into the margin or is cut off; scale it to fit`)];
335
+ return [
336
+ warning(
337
+ "wide-image",
338
+ where,
339
+ `a picture is ${inches(cx)} wide in a ${inches(width)} text column, so it runs into the margin or is cut off; scale it to fit`,
340
+ ),
341
+ ];
294
342
  });
295
343
  };
296
344
 
@@ -298,11 +346,17 @@ const wideImageFindings = (context: DocContext, body: XmlElement, owner: Readonl
298
346
 
299
347
  const draftFindings = (context: DocContext, body: XmlElement): Finding[] => {
300
348
  const findings: Finding[] = [];
301
- const changes = everyElement(body).filter((node) => ["w:ins", "w:del", "w:moveFrom", "w:moveTo"].includes(node.name) && node.children.some(isElement)).length;
349
+ const changes = everyElement(body).filter(
350
+ (node) => ["w:ins", "w:del", "w:moveFrom", "w:moveTo"].includes(node.name) && node.children.some(isElement),
351
+ ).length;
302
352
  if (changes > 0) {
303
- findings.push(warning("tracked-changes", "", `${plural(changes, "tracked change")} still in the document: accept or reject them before it goes out`));
353
+ findings.push(
354
+ warning("tracked-changes", "", `${plural(changes, "tracked change")} still in the document: accept or reject them before it goes out`),
355
+ );
304
356
  }
305
- const commentsPart = [...context.pkg.rels(context.main).values()].find((relationship) => relationship.kind === "comments" && !relationship.external);
357
+ const commentsPart = [...context.pkg.rels(context.main).values()].find(
358
+ (relationship) => relationship.kind === "comments" && !relationship.external,
359
+ );
306
360
  const comments = commentsPart === undefined ? undefined : context.pkg.xml(commentsPart.target);
307
361
  const count = comments === undefined ? 0 : childrenNamed(comments, "w:comment").length;
308
362
  if (count > 0) {
@@ -321,7 +375,13 @@ const paragraphFindings = (context: DocContext): Finding[] => {
321
375
  const hasPicture = descendantsNamed(located.paragraph, "w:drawing").length > 0 || descendantsNamed(located.paragraph, "w:pict").length > 0;
322
376
  if (located.heading !== undefined && located.text.trim() === "" && !hasPicture) {
323
377
  const level = located.heading === 0 ? "title" : `heading ${located.heading}`;
324
- findings.push(warning("empty-heading", where, `empty ${level}: a blank line in the text and an empty entry in the navigation pane and any table of contents; delete it`));
378
+ findings.push(
379
+ warning(
380
+ "empty-heading",
381
+ where,
382
+ `empty ${level}: a blank line in the text and an empty entry in the navigation pane and any table of contents; delete it`,
383
+ ),
384
+ );
325
385
  }
326
386
  if (TYPED_BULLET.test(located.text) && childAt(located.paragraph, "w:pPr", "w:numPr") === undefined) {
327
387
  typedBullets += 1;
@@ -330,7 +390,11 @@ const paragraphFindings = (context: DocContext): Finding[] => {
330
390
  }
331
391
  if (firstBullet !== undefined) {
332
392
  findings.push(
333
- warning("typed-bullets", firstBullet, `${plural(typedBullets, "paragraph")} ${typedBullets === 1 ? "starts" : "start"} with a typed bullet character instead of a list style, so the list will not indent or renumber; use a list style`),
393
+ warning(
394
+ "typed-bullets",
395
+ firstBullet,
396
+ `${plural(typedBullets, "paragraph")} ${typedBullets === 1 ? "starts" : "start"} with a typed bullet character instead of a list style, so the list will not indent or renumber; use a list style`,
397
+ ),
334
398
  );
335
399
  }
336
400
  return findings;
@@ -343,7 +407,11 @@ const controlFindings = (body: XmlElement, whereOfNode: (node: XmlElement) => st
343
407
  const prompt = textIn(childNamed(control, "w:sdtContent") ?? control, "w:t");
344
408
  // A block-level control holds whole paragraphs; it is found by the first of them.
345
409
  const where = whereOfNode(descendantsNamed(control, "w:p")[0] ?? control);
346
- return error("placeholder-control", where, `a content control still shows its prompt ${excerpt(prompt)}: enter the value or remove the control`);
410
+ return error(
411
+ "placeholder-control",
412
+ where,
413
+ `a content control still shows its prompt ${excerpt(prompt)}: enter the value or remove the control`,
414
+ );
347
415
  });
348
416
 
349
417
  const linkFindings = (body: XmlElement, bookmarks: ReadonlySet<string>, whereOfNode: (node: XmlElement) => string): Finding[] =>
@@ -352,7 +420,13 @@ const linkFindings = (body: XmlElement, bookmarks: ReadonlySet<string>, whereOfN
352
420
  if (anchor === undefined || bookmarks.has(anchor) || BUILT_IN_ANCHORS.has(anchor)) {
353
421
  return [];
354
422
  }
355
- return [error("broken-link", whereOfNode(link), `the link ${excerpt(textIn(link, "w:t"))} points to bookmark "${anchor}", which is not in the document, so clicking it goes nowhere`)];
423
+ return [
424
+ error(
425
+ "broken-link",
426
+ whereOfNode(link),
427
+ `the link ${excerpt(textIn(link, "w:t"))} points to bookmark "${anchor}", which is not in the document, so clicking it goes nowhere`,
428
+ ),
429
+ ];
356
430
  });
357
431
 
358
432
  // Headers, footers and notes: each part's own images and relationships, located by the part's name.
@@ -360,7 +434,10 @@ const storyFindings = (context: DocContext, stories: readonly { part: string; tr
360
434
  stories.flatMap((story) => {
361
435
  const label = story.part.replace(/^word\//, "").replace(/\.xml$/, "");
362
436
  const referenced = new Set<string>();
363
- return [...referenceFindings(context, story.part, story.tree, () => label, referenced), ...missingTargetFindings(context.pkg, story.part, label, referenced)];
437
+ return [
438
+ ...referenceFindings(context, story.part, story.tree, () => label, referenced),
439
+ ...missingTargetFindings(context.pkg, story.part, label, referenced),
440
+ ];
364
441
  });
365
442
 
366
443
  const openDocx = (bytes: Uint8Array): OoxmlPackage | CheckReport => {
@@ -368,7 +445,11 @@ const openDocx = (bytes: Uint8Array): OoxmlPackage | CheckReport => {
368
445
  return openPackage(bytes);
369
446
  } catch (cause) {
370
447
  if (cause instanceof NotAPackage) {
371
- return { format: "docx", findings: [error("unreadable", "", `${cause.message}: this is not a Word file (or it is password-protected)`)], notes: [] };
448
+ return {
449
+ format: "docx",
450
+ findings: [error("unreadable", "", `${cause.message}: this is not a Word file (or it is password-protected)`)],
451
+ notes: [],
452
+ };
372
453
  }
373
454
  throw cause;
374
455
  }
@@ -403,7 +484,9 @@ export const checkDocx = (bytes: Uint8Array): CheckReport => {
403
484
  return tree === undefined ? [] : [{ part: relationship.target, tree }];
404
485
  });
405
486
  const bookmarks = new Set(
406
- [body, ...stories.map((story) => story.tree)].flatMap((tree) => descendantsNamed(tree, "w:bookmarkStart").flatMap((mark) => mark.attrs["w:name"] ?? [])),
487
+ [body, ...stories.map((story) => story.tree)].flatMap((tree) =>
488
+ descendantsNamed(tree, "w:bookmarkStart").flatMap((mark) => mark.attrs["w:name"] ?? []),
489
+ ),
407
490
  );
408
491
  const context: DocContext = { pkg, main, paragraphs: located, whereOf, bookmarks };
409
492
  if (located.every((item) => item.text.trim() === "") && descendantsNamed(body, "w:drawing").length === 0) {
@@ -424,7 +507,9 @@ export const checkDocx = (bytes: Uint8Array): CheckReport => {
424
507
  );
425
508
  // A field showing "Error!" or a control showing its prompt is already named for what it is; the same words read as
426
509
  // leftover text would say it twice.
427
- const explained = new Set(findings.filter((finding) => finding.rule === "field-error" || finding.rule === "placeholder-control").map((finding) => finding.where));
510
+ const explained = new Set(
511
+ findings.filter((finding) => finding.rule === "field-error" || finding.rule === "placeholder-control").map((finding) => finding.where),
512
+ );
428
513
  return {
429
514
  format: "docx",
430
515
  extent: plural(located.length, "paragraph"),
@@ -157,7 +157,13 @@ const linkFindings = async (pdf: PdfDocument, page: PdfPage, number: number): Pr
157
157
  // allow(silent-catch): pdf.js throws where the file's destination tree is damaged, and then the link goes nowhere too.
158
158
  const target = await pdf.getDestination(named).catch(() => null);
159
159
  if (target === null) {
160
- findings.push(error("broken-link", `page ${number}`, `a link goes to "${named}", a destination the file does not define, so clicking it does nothing`));
160
+ findings.push(
161
+ error(
162
+ "broken-link",
163
+ `page ${number}`,
164
+ `a link goes to "${named}", a destination the file does not define, so clicking it does nothing`,
165
+ ),
166
+ );
161
167
  }
162
168
  }
163
169
  return findings;
@@ -173,7 +179,10 @@ const openPdf = async (bytes: Uint8Array): Promise<PdfDocument | CheckReport> =>
173
179
  // Errors only: pdf.js otherwise narrates its recovery of a damaged file on the console, into the report.
174
180
  return await getDocumentProxy(bytes, { verbosity: 0 });
175
181
  } catch (cause) {
176
- const reason = cause instanceof Error && cause.name === "PasswordException" ? "it is password-protected, so nothing past the password can be checked" : `it is not a readable PDF (${cause instanceof Error ? cause.message : String(cause)})`;
182
+ const reason =
183
+ cause instanceof Error && cause.name === "PasswordException"
184
+ ? "it is password-protected, so nothing past the password can be checked"
185
+ : `it is not a readable PDF (${cause instanceof Error ? cause.message : String(cause)})`;
177
186
  return { format: "pdf", findings: [error("unreadable", "", reason)], notes: [] };
178
187
  }
179
188
  };
@@ -211,14 +220,24 @@ export const checkPdf = async (bytes: Uint8Array): Promise<CheckReport> => {
211
220
  }
212
221
  for (const [font, pages] of unembedded) {
213
222
  findings.push(
214
- warning("font-not-embedded", `page ${pageList(pages)}`, `text is set in ${font}, which the file does not carry: a reader without that font substitutes another, and lines can break differently; embed it`),
223
+ warning(
224
+ "font-not-embedded",
225
+ `page ${pageList(pages)}`,
226
+ `text is set in ${font}, which the file does not carry: a reader without that font substitutes another, and lines can break differently; embed it`,
227
+ ),
215
228
  );
216
229
  }
217
230
  if (sizes.size > 1) {
218
231
  const [common] = [...sizes.entries()].toSorted((a, b) => b[1].length - a[1].length);
219
232
  const odd = [...sizes.entries()].filter(([label]) => label !== common?.[0]);
220
233
  for (const [label, pages] of odd) {
221
- findings.push(warning("page-size", `page ${pageList(pages)}`, `${plural(pages.length, "page is", "pages are")} ${label}, the rest ${common?.[0] ?? "another size"}`));
234
+ findings.push(
235
+ warning(
236
+ "page-size",
237
+ `page ${pageList(pages)}`,
238
+ `${plural(pages.length, "page is", "pages are")} ${label}, the rest ${common?.[0] ?? "another size"}`,
239
+ ),
240
+ );
222
241
  }
223
242
  }
224
243
  if (characters === 0 && total > 0) {
@@ -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 => intAttr(childAt(element, ...path), attribute);
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 = runSizes.length > 0 ? Math.max(...runSizes) : (intAttr(childNamed(p, "a:endParaRPr"), "sz") ?? inherited("sz") ?? DEFAULT_SIZE_HUNDREDTHS);
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
- ? [warning("text-overflow", where, `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}`)]
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(paragraphsOf(txBody, { listStyles: [childNamed(txBody, "a:lstStyle")] }, 1, 0), (cellWidth - marginX) / EMU_PER_PT, true);
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("linked-image", where, `image is linked from outside the file (${relationship.target}), so it will not show on anyone else's machine; embed it`)
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 (box !== undefined && relationship !== undefined && !relationship.external && context.pkg.has(relationship.target) && box.cx > 0 && box.cy > 0) {
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 = layoutRel === undefined ? undefined : [...pkg.rels(layoutRel.target).values()].find((relationship) => relationship.kind === "slideMaster");
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(warning("hidden-slide", slideWhere, "the slide is hidden, so the slide show skips it; delete it if it is not meant to be delivered"));
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 { format: "pptx", findings: [error("unreadable", "", `${cause.message}: this is not a PowerPoint file (or it is password-protected)`)], notes: [] };
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(error("missing-part", `slide ${number}`, "the slide list names a slide that is not in the file; PowerPoint will offer to repair the deck"));
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 => (/^[A-Za-z_][\w.]*$/.test(sheet) ? `${sheet}!${ref}` : `'${sheet.replaceAll("'", "''")}'!${ref}`);
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.slice(0, 5).map((cell) => cell.slice(cell.lastIndexOf("!") + 1)).join(", ");
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 ? "the workbook asks to be recalculated on open, so spreadsheet applications fill them in" : "Excel, LibreOffice and Google Sheets compute them on open";
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 { format: "xlsx", findings: [error("unreadable", "", `${cause.message}: this is not an Excel file (or it is password-protected)`)], notes: [] };
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!") ? [error("broken-name", "", `the defined name "${name.attrs["name"] ?? "?"}" refers to ${excerpt(target)}, a deleted range`)] : [];
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(error("missing-part", name, "the sheet list names a sheet that is not in the file; Excel will offer to repair the workbook"));
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.