@pptx-studio/validate 0.2.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @pptx-studio/validate
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ae2e566: A table's style is the one PowerPoint draws. `BUILTIN_TABLE_STYLES` is PowerPoint's own Table
8
+ Styles gallery: 74 styles, each with its GUID, its name and the `a:tblStyle` PowerPoint writes for
9
+ it. `tableStyleOf(table)` returns the built-in a table's GUID names, in any case, or `null` for
10
+ PowerPoint's default 1-pt black grid; nothing in `ppt/tableStyles.xml` changes which. `builtinTableStyle`
11
+ and `parseTableStyle` read a `CT_TableStyle` into its thirteen parts. An inline `TableStyleRef` now
12
+ carries its `id`, a style GUID without braces or with padding throws `MODEL_TABLE_ATTR`, and
13
+ `MODEL_ERROR_CODES` lists every code, including the new `MODEL_TABLE_STYLE`.
14
+
15
+ validate adds `V032`, a warning on a table that names no built-in style, and `V033`, refusing the
16
+ table-style forms PowerPoint repairs.
17
+
3
18
  ## 0.2.1
4
19
 
5
20
  ### Patch Changes
package/NOTICE CHANGED
@@ -27,14 +27,24 @@ Entries are added here at the moment the material lands, not at release time.
27
27
  presetShapeDefinitions.xml, 538970 bytes
28
28
  sha256 4a762444d8d85876881c02a5b1dedf6f73006fcd8acb7b4e393435615b37c780
29
29
 
30
+ * Microsoft PowerPoint output
31
+ Written by Microsoft PowerPoint, driven by this project's own scripts.
32
+
33
+ @pptx-studio/model contains PowerPoint's 74 built-in table styles: each is
34
+ the a:tblStyle element PowerPoint writes into ppt/tableStyles.xml, byte for
35
+ byte, enumerated through PowerPoint's Table Styles gallery and saved by
36
+ PowerPoint (LEGAL.md, ADR 0063). corpus/ground-truth/table-styles.json holds
37
+ the same 74.
38
+
39
+ The decks in corpus/authored are PowerPoint Blank Presentations, so each
40
+ carries the Microsoft-generated ppt/theme/theme1.xml and default layouts
41
+ PowerPoint writes into a new deck (ADR 0009).
42
+
30
43
  --------------------------------------------------------------------------------
31
44
  Attributions the plan commits us to, listed so the obligation is visible before
32
45
  the code arrives
33
46
  --------------------------------------------------------------------------------
34
47
 
35
- * Pattern fill tiles (sub-phase 2.7) are generated from Mono libgdiplus.
36
- libgdiplus - Copyright Novell, Inc. and contributors - MIT
37
-
38
48
  * Metric-compatible substitute fonts ship in a separate package,
39
49
  @pptx-studio/fonts-metric-compat, under SIL OFL 1.1 with its own
40
50
  OFL.txt and reserved-font-name handling. They are deliberately kept out
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @pptx-studio/validate
2
2
 
3
- **The repair firewall.** Thirty-one rules a `.pptx` must not break, checked
3
+ **The repair firewall.** Thirty-three rules a `.pptx` must not break, checked
4
4
  before any bytes are handed over, each finding carrying the part and the XPath.
5
5
 
6
6
  Apache-2.0 · browser and Web Worker only, no Node · part of
@@ -28,7 +28,7 @@ quietly lost something.
28
28
  So this is not a schema validator with a nice report. It is the only feedback
29
29
  loop that exists, and it runs on every export in development and in production.
30
30
 
31
- ## Thirty-one rules, in two halves
31
+ ## Thirty-three rules, in two halves
32
32
 
33
33
  Roughly half come from ECMA-376: content-type coverage, `xsd:sequence` child
34
34
  order, `minOccurs`, the four identifier ranges. Those are derivable, and one of
@@ -38,19 +38,23 @@ schemas and checked against 194 148 elements PowerPoint wrote.
38
38
  The other half cannot be derived from anything. Each is the record of a package
39
39
  built with **one** change in it, opened in PowerPoint 16.0.20326, and declined:
40
40
 
41
- | | |
42
- | ------ | ---------------------------------------------------------------------------------------------------- |
43
- | `V022` | a `p:ph type="hdr"` on a slide. The other seven content types all open. |
44
- | `V023` | a geometry guide referenced but never defined. `ST_GeomGuideName` is an unconstrained token. |
45
- | `V024` | a `c:strLit` inside a series `c:tx`, two elements away from where the literal forms are legal. |
46
- | `V025` | a `p:control`, in all eight forms tried. An empty `p:controls` is accepted. |
47
- | `V026` | a `cs:chartStyle` with thirty of its thirty-one entries. No chart style at all is fine. |
48
- | `V019` | a master id colliding with a layout id. They are one number space and no schema says so. |
49
- | `V020` | a `p:cNvPr/@id` between 2147483648 and 4294967294. 4294967295 opens; it is minus one. |
50
- | `V009` | a slide without exactly one layout relationship; a `cx:chartSpace` with no `.rels`. |
51
- | `V004` | a percent-escape of an _unreserved_ character in a part name. `0x808D1005`. |
52
- | `V012` | an `a:ahXY` directly under `a:custGeom`, without its `a:ahLst` wrapper. |
53
- | `V031` | an `a:gridCol` without `@w`, an `a:tr` without `@h`, `gridSpan="2.0"`, `hMerge="on"`: a repair each. |
41
+ | | |
42
+ | ------ | ------------------------------------------------------------------------------------------------------- |
43
+ | `V022` | a `p:ph type="hdr"` on a slide. The other seven content types all open. |
44
+ | `V023` | a geometry guide referenced but never defined. `ST_GeomGuideName` is an unconstrained token. |
45
+ | `V024` | a `c:strLit` inside a series `c:tx`, two elements away from where the literal forms are legal. |
46
+ | `V025` | a `p:control`, in all eight forms tried. An empty `p:controls` is accepted. |
47
+ | `V026` | a `cs:chartStyle` with thirty of its thirty-one entries. No chart style at all is fine. |
48
+ | `V019` | a master id colliding with a layout id. They are one number space and no schema says so. |
49
+ | `V020` | a `p:cNvPr/@id` between 2147483648 and 4294967294. 4294967295 opens; it is minus one. |
50
+ | `V009` | a slide without exactly one layout relationship; a `cx:chartSpace` with no `.rels`. |
51
+ | `V004` | a percent-escape of an _unreserved_ character in a part name. `0x808D1005`. |
52
+ | `V012` | an `a:ahXY` directly under `a:custGeom`, without its `a:ahLst` wrapper. |
53
+ | `V031` | an `a:gridCol` without `@w`, an `a:tr` without `@h`, `gridSpan="2.0"`, `hMerge="on"`: a repair each. |
54
+ | `V033` | a table style id without braces or padded, a table-style part without `@def`, `b="yes"`: a repair each. |
55
+
56
+ And one warning measured the same way: `V032`, a table naming none of PowerPoint's 74 built-in
57
+ table styles, which PowerPoint draws as a 1-pt black grid whatever the package defines.
54
58
 
55
59
  Every rule carries a `why` recording what was tried and what opened, so whoever
56
60
  eventually contradicts one knows what they are contradicting.
@@ -86,7 +90,7 @@ the same reason.
86
90
 
87
91
  Two suites that check opposite things, and neither substitutes for the other.
88
92
 
89
- **It fires.** `validate.test.ts` breaks a minimal deck thirty-one ways, one per
93
+ **It fires.** `validate.test.ts` breaks a minimal deck once per
90
94
  rule, and asserts each rule reports the right part and the right XPath.
91
95
 
92
96
  **It is quiet.** `tools/corpus/suites/validate.test.ts` runs all fifty-two committed
package/dist/index.d.ts CHANGED
@@ -49,46 +49,9 @@ export declare function isValidateError(value: unknown): value is ValidateError;
49
49
  //#endregion
50
50
  //#region src/rules/rules.d.ts
51
51
  /**
52
- * The thirty-one rules, and the evidence for each.
53
- *
54
- * ## Why a table and not thirty-one functions
55
- *
56
- * The functions are in the files beside this one. What lives here is the part a
57
- * person reads: what each rule claims, how sure we are, and *how we know*. A
58
- * validator whose rules exist only as code is a validator nobody can audit, and
59
- * this one has to be audited, because roughly half of it enforces things no
60
- * schema says.
61
- *
62
- * That is the fact that shapes the whole package. ECMA-376 is a description of
63
- * a file format; PowerPoint is an implementation that refuses files the
64
- * description permits. Sub-phase 0.7 and the corpus bisections in
65
- * `tools/corpus/ROSTER.md` found nineteen such refusals by building a package
66
- * with one change in it and watching PowerPoint decline to open it - no
67
- * diagnostic, no log, no part named, just "PowerPoint could not open the file"
68
- * or `0x80070570`. Every rule below whose `evidence` says `measured` came from
69
- * that loop and from nowhere else.
70
- *
71
- * So each rule carries three things beyond its check:
72
- *
73
- * - `evidence` - `schema` (ECMA-376 says so), `measured` (we watched PowerPoint
74
- * refuse it), or `both`.
75
- * - `why` - the sentence that justifies the rule to somebody who is about to
76
- * delete it because it fired on their file.
77
- * - `severity` - `fatal` refuses an export; `warning` is reported and does not.
78
- *
79
- * ## On being wrong in the safe direction
80
- *
81
- * A false positive here blocks an export a user wanted. A false negative hands
82
- * them a file PowerPoint will not open, with no way to find out why. Those are
83
- * not symmetric, but the first is not free either - a validator that fires on
84
- * good files gets turned off, and then it catches nothing.
85
- *
86
- * The resolution is `origin`, which lives on the finding rather than here: a
87
- * fatal a caller *introduced* refuses the export, and the identical fatal that
88
- * was already in the file when it was opened is reported and does not. That is
89
- * the same split `PartStore.write` already makes for dangling relationships,
90
- * and for the same reason - refusing to re-export a file we did not break makes
91
- * the file unopenable in this editor and does not fix anything.
52
+ * The rules, and the evidence for each: `schema`, `measured` (PowerPoint refused or repaired it),
53
+ * or `both`. A fatal a caller introduced refuses an export; the same fatal inherited from the
54
+ * opened file is reported and does not. The README lists every rule.
92
55
  */
93
56
  type RuleCategory = 'package' | 'relationships' | 'order' | 'required' | 'ids' | 'refused' | 'preservation';
94
57
  type Severity = 'fatal' | 'warning';
@@ -104,13 +67,8 @@ interface Rule {
104
67
  /** Why the rule exists, for whoever is about to argue with it. */
105
68
  readonly why: string;
106
69
  /**
107
- * True when the rule needs the package as it was opened as well as the
108
- * package about to be written.
109
- *
110
- * Six rules do. They are skipped rather than passed when no baseline is
111
- * given, and the report says so - a preservation rule that silently reports
112
- * nothing because it had nothing to compare against is worse than one that
113
- * did not run, because it looks like a pass.
70
+ * True when the rule needs the package as it was opened; without a baseline it is reported as
71
+ * skipped, never as passed.
114
72
  */
115
73
  readonly needsBaseline?: true;
116
74
  }
@@ -334,6 +292,20 @@ export declare const RULES: readonly [{
334
292
  readonly evidence: "both";
335
293
  readonly title: "`a:gridCol/@w`, `a:tr/@h`, and typed `gridSpan`, `rowSpan`, `hMerge`, `vMerge`";
336
294
  readonly why: string;
295
+ }, {
296
+ readonly id: "V032";
297
+ readonly category: "ids";
298
+ readonly severity: "warning";
299
+ readonly evidence: "measured";
300
+ readonly title: "a table names one of PowerPoint's 74 built-in table styles, or none";
301
+ readonly why: string;
302
+ }, {
303
+ readonly id: "V033";
304
+ readonly category: "required";
305
+ readonly severity: "fatal";
306
+ readonly evidence: "both";
307
+ readonly title: "a table style id is a braced GUID, and the table-style part is one PowerPoint keeps";
308
+ readonly why: string;
337
309
  }];
338
310
  type RuleId = (typeof RULES)[number]['id'];
339
311
  export declare function ruleById(id: string): Rule | undefined;
@@ -440,33 +412,9 @@ export declare function rootName(document: XDocument): string;
440
412
  //#endregion
441
413
  //#region src/report/report.d.ts
442
414
  /**
443
- * What validation produces.
444
- *
445
- * ## Origin, and why it is computed rather than guessed
446
- *
447
- * The hardest question this package has to answer is not "is this file valid".
448
- * It is **"did we break it"**, and those are different questions with different
449
- * consequences. A fatal finding in a deck the user imported ten seconds ago is
450
- * information; the same finding in a deck they just edited is a bug in us, and
451
- * handing over the bytes would give them a repair prompt with no explanation.
452
- *
453
- * Refusing both would be the strict-looking choice and it is the wrong one. It
454
- * would mean that a deck with one pre-existing defect - a dangling image
455
- * relationship, say, which PowerPoint tolerates and which is common in files
456
- * that have been through three other tools - could be opened in this editor and
457
- * never saved again. The editor would be refusing to give the user back their
458
- * own file over a problem it did not cause and cannot fix. `PartStore.write`
459
- * already made exactly this call for dangling relationships, and this is the
460
- * same call generalised to all thirty-one rules.
461
- *
462
- * So `origin` is not a per-rule judgement call. It is computed by running the
463
- * rules a second time against the package **as it was opened** and differencing
464
- * the two reports: a finding that is in both is `inherited`, one that is only in
465
- * the new report is `introduced`. That is exact, it needs no rule to reason
466
- * about history, and it cannot drift from what the rules actually do.
467
- *
468
- * The second pass only happens when the first found something fatal, so a clean
469
- * export - the overwhelmingly common case - pays nothing for it.
415
+ * A finding is `inherited` when the package as it was opened already had it, `introduced` when it
416
+ * did not, computed by running the rules on both and differencing. Only an introduced fatal refuses
417
+ * an export, so a deck with a defect it arrived with can still be saved.
470
418
  */
471
419
  type Origin =
472
420
  /** The finding is in the package we are about to write and was not in the original. */
@@ -555,28 +503,9 @@ export declare function formatReport(report: Report, options?: FormatOptions): s
555
503
  //#endregion
556
504
  //#region src/context.d.ts
557
505
  /**
558
- * The state a rule reads, and the one method it writes.
559
- *
560
- * Every rule is `(ctx: Context) => void`. That is a deliberately small
561
- * interface, and it is the same shape `checkLayering({manifests, catalog})` and
562
- * `checkCorpus({manifests, files, ...})` already have in this repository, for
563
- * the same reason: a rule that takes a context and returns nothing is a rule
564
- * that can be run against a package assembled in a test, with no temporary
565
- * directory, no archive on disk and no other twenty-eight rules running beside
566
- * it obscuring which one fired.
567
- *
568
- * ## Everything is lazy, and everything is cached
569
- *
570
- * Twelve of the rules want the parsed tree of every XML part. Parsing each part
571
- * once per rule would be twelve passes over a deck that can be two hundred
572
- * megabytes. Parsing all of them up front would be one pass too many for the
573
- * package-level rules, which need no trees at all.
574
- *
575
- * So `document()` parses on first ask and remembers. A part that is not XML
576
- * returns `null` and is not asked again. A part that will not *parse* also
577
- * returns `null` - and records a `ReadProblem`, because a rule that silently
578
- * saw nothing is indistinguishable from a rule that found nothing, and those
579
- * are opposite answers.
506
+ * The state a rule reads, and the one method it writes. Every rule is `(ctx: Context) => void`,
507
+ * so one runs alone against a package built in a test. Trees parse on first ask and are cached;
508
+ * a part that will not parse returns `null` and records a `ReadProblem`.
580
509
  */
581
510
  interface Context {
582
511
  /** The package about to be handed over, or the one being inspected. */
@@ -688,11 +617,11 @@ interface ValidateOptions {
688
617
  * those are skipped and the report says so.
689
618
  */
690
619
  readonly bytes?: Uint8Array;
691
- /** The package as it was opened. The six preservation rules need it. */
620
+ /** The package as it was opened. The preservation rules need it. */
692
621
  readonly baseline?: PartStore;
693
622
  /** The baseline's archive, so an inherited `V003` can be recognised as inherited. */
694
623
  readonly baselineBytes?: Uint8Array;
695
- /** A subset to run. Defaults to all twenty-nine. */
624
+ /** A subset to run. Defaults to every rule. */
696
625
  readonly rules?: readonly RuleId[];
697
626
  /** Passed to `readZip` when `bytes` are given. */
698
627
  readonly zip?: ReadZipOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/rules/rules.ts","../src/report/location.ts","../src/report/report.ts","../src/context.ts","../src/validate.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/rules/rules.ts","../src/report/location.ts","../src/report/report.ts","../src/context.ts","../src/validate.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -295,6 +295,22 @@ const RULES = [
295
295
  evidence: "both",
296
296
  title: "`a:gridCol/@w`, `a:tr/@h`, and typed `gridSpan`, `rowSpan`, `hMerge`, `vMerge`",
297
297
  why: "`CT_TableCol/@w` and `CT_TableRow/@h` are required `ST_Coordinate`s, the spans are `xsd:int` and the flags `xsd:boolean`. C7 measured each: a `gridCol` without `@w`, a `tr` without `@h`, `gridSpan=\"2.0\"` and `hMerge=\"on\"` are a repair prompt apiece; PowerPoint saves the column at its minimum width, the row at zero, the span gone and the flag as \"1\". `w=\"108pt\"`, the universal-measure spelling, opens clean and is not this rule."
298
+ },
299
+ {
300
+ id: "V032",
301
+ category: "ids",
302
+ severity: "warning",
303
+ evidence: "measured",
304
+ title: "a table names one of PowerPoint's 74 built-in table styles, or none",
305
+ why: "C8 drew 84 probe packages in three themes. A table whose `a:tableStyleId`, or inline `a:tableStyle`, names one of the 74 built-ins draws that built-in, in any case, whatever `ppt/tableStyles.xml` says about it; any other id - defined in the part or not - draws a 1-pt black grid with no fill, and an id nothing defines is dropped on the next save. The deck opens without a word, which is why this is a warning: the file names one style and PowerPoint draws another."
306
+ },
307
+ {
308
+ id: "V033",
309
+ category: "required",
310
+ severity: "fatal",
311
+ evidence: "both",
312
+ title: "a table style id is a braced GUID, and the table-style part is one PowerPoint keeps",
313
+ why: "`ST_Guid` is a GUID in braces, `CT_TableStyleList/@def` and `CT_TableStyle/@styleId` are required, `b` and `i` are `ST_OnOffStyleType` and a border or fill must hold one. C8 measured each as a repair prompt: an id without braces or with padding, a `tblPr` naming its style twice, a part whose root is not `a:tblStyleLst`, a list without `@def`, a style without `@styleId`, `b=\"yes\"`, an edge with neither `a:ln` nor `a:lnRef`, and an empty `a:fill`. PowerPoint writes the all-zero GUID for the id and empties the part."
298
314
  }
299
315
  ];
300
316
  const BY_ID = new Map(RULES.map((rule) => [rule.id, rule]));
@@ -645,14 +661,7 @@ function readRelsParts(ctx) {
645
661
  return parts;
646
662
  }
647
663
  /**
648
- * Memoised above, because five of the thirty-one rules want this list and each
649
- * would otherwise re-walk every `.rels` part in the package. The parsed trees
650
- * are already cached on the context; what is saved here is the element scan and
651
- * the allocation, which on a three-hundred-slide deck is five passes over a
652
- * thousand relationship parts for one answer that cannot have changed.
653
- *
654
- * Keyed by context rather than by store, so nothing survives one validation run
655
- * into the next.
664
+ * Memoised per context: several rules want every relationship part, and this walks them once.
656
665
  */
657
666
  function scanRelsParts(ctx) {
658
667
  const out = [];
@@ -1169,16 +1178,8 @@ function forEachElement$1(ctx, visit) {
1169
1178
  //#endregion
1170
1179
  //#region src/rules/required.ts
1171
1180
  /**
1172
- * `V013` … `V017`, `V030` and `V031`: children and attributes that are not optional.
1173
- *
1174
- * Every one of these is a `minOccurs` the schema states plainly, and every one
1175
- * is broken the same way: by an editor deleting the last of something. The last
1176
- * paragraph goes and a `p:txBody` is left with no `a:p`; a shape is dragged out
1177
- * of a group and the group's `p:grpSpPr` is dropped with it. That is why these
1178
- * are separate rules from the ordering ones even though a missing child and a
1179
- * misplaced child are both "the sequence is wrong": the messages have to say
1180
- * *add this*, not *move this*, and they have to fire on the parent rather than
1181
- * on a child that is not there to point at.
1181
+ * `V013` … `V017`, `V030`, `V031` and `V033`: children and attributes that are not optional.
1182
+ * Each is a `minOccurs` or a type the schema states, reported on the element that lacks it.
1182
1183
  */
1183
1184
  /** The twelve attributes of `CT_ColorMapping`. All required, no defaults. */
1184
1185
  const CLR_MAP_ATTRIBUTES = [
@@ -1288,6 +1289,82 @@ function v031TableAttributes(ctx) {
1288
1289
  }
1289
1290
  });
1290
1291
  }
1292
+ /** `ST_Guid`, in any case: C8 measured lower case opening clean. */
1293
+ const TABLE_STYLE_GUID$1 = /^\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}$/i;
1294
+ const ON_OFF_STYLES = /* @__PURE__ */ new Set([
1295
+ "on",
1296
+ "off",
1297
+ "def"
1298
+ ]);
1299
+ const TABLE_STYLE_EDGES = /* @__PURE__ */ new Set([
1300
+ "left",
1301
+ "right",
1302
+ "top",
1303
+ "bottom",
1304
+ "insideH",
1305
+ "insideV",
1306
+ "tl2br",
1307
+ "tr2bl"
1308
+ ]);
1309
+ const FILL_PROPERTIES = /* @__PURE__ */ new Set([
1310
+ "noFill",
1311
+ "solidFill",
1312
+ "gradFill",
1313
+ "blipFill",
1314
+ "pattFill",
1315
+ "grpFill"
1316
+ ]);
1317
+ const drawingChildren = (element, local) => childElements(element).filter((child) => child.local === local && namespaceOf(child) === NS.a);
1318
+ /** A table-style id is a braced GUID, and a table style has the parts C8 measured as required. */
1319
+ function v033TableStyles(ctx) {
1320
+ for (const part of ctx.parts()) {
1321
+ if (ctx.contentType(part) !== CONTENT_TYPE.tableStyles) continue;
1322
+ const root = ctx.document(part)?.root;
1323
+ if (root === void 0 || root.local === "tblStyleLst" && namespaceOf(root) === NS.a) continue;
1324
+ ctx.add("V033", elementLocation(part, root), `The table-style part's root is <${root.qname}>, not <a:tblStyleLst>. C8 measured this as a repair prompt.`);
1325
+ }
1326
+ forEachElement(ctx, (part, element) => {
1327
+ if (namespaceOf(element) !== NS.a) return;
1328
+ const report = (at, what) => {
1329
+ ctx.add("V033", elementLocation(part, at), what + " C8 measured this as a repair prompt.");
1330
+ };
1331
+ if (element.local === "tblPr" && drawingChildren(element, "tableStyleId").length > 0 && drawingChildren(element, "tableStyle").length > 0) report(element, "<a:tblPr> names its style twice, by <a:tableStyleId> and <a:tableStyle>.");
1332
+ const guid = (raw, what) => {
1333
+ if (!TABLE_STYLE_GUID$1.test(raw)) report(element, `${what} is "${raw}", which is not a GUID in braces.`);
1334
+ };
1335
+ switch (element.local) {
1336
+ case "tableStyleId":
1337
+ guid(textContent(element), "<a:tableStyleId>");
1338
+ return;
1339
+ case "tableStyle":
1340
+ case "tblStyle": {
1341
+ const id = attributeValue(element, "styleId");
1342
+ if (id === void 0) report(element, `<a:${element.local}> has no @styleId.`);
1343
+ else guid(id, `<a:${element.local}>/@styleId`);
1344
+ return;
1345
+ }
1346
+ case "tblStyleLst":
1347
+ if (attributeValue(element, "def") === void 0) report(element, "<a:tblStyleLst> has no @def.");
1348
+ return;
1349
+ case "tcTxStyle":
1350
+ for (const name of ["b", "i"]) {
1351
+ const raw = attributeValue(element, name);
1352
+ if (raw !== void 0 && !ON_OFF_STYLES.has(raw)) report(element, `<a:tcTxStyle>/@${name} is "${raw}", not on, off or def.`);
1353
+ }
1354
+ return;
1355
+ case "tcBdr":
1356
+ for (const edge of childElements(element)) {
1357
+ if (!TABLE_STYLE_EDGES.has(edge.local) || namespaceOf(edge) !== NS.a) continue;
1358
+ if (drawingChildren(edge, "ln").length + drawingChildren(edge, "lnRef").length === 0) report(edge, `<a:${edge.local}> has neither <a:ln> nor <a:lnRef>.`);
1359
+ }
1360
+ return;
1361
+ case "tcStyle":
1362
+ case "tblBg":
1363
+ for (const fill of drawingChildren(element, "fill")) if (!childElements(fill).some((child) => FILL_PROPERTIES.has(child.local))) report(fill, "<a:fill> holds no fill.");
1364
+ return;
1365
+ }
1366
+ });
1367
+ }
1291
1368
  /** Zero is one and a negative runs to the edge: C7's span-zero and span-negative. */
1292
1369
  function spanOf(raw) {
1293
1370
  if (raw === void 0) return 1;
@@ -1404,22 +1481,93 @@ function forEachElement(ctx, visit) {
1404
1481
  }
1405
1482
  }
1406
1483
  //#endregion
1484
+ //#region src/rules/table-style-ids.ts
1485
+ /**
1486
+ * The GUIDs of PowerPoint's 74 built-in table styles, in its gallery's order.
1487
+ * GENERATED by tools/ground-truth/model/tables/styles/write-tables.ts from
1488
+ * corpus/ground-truth/table-styles.json. Do not edit: rules.test.ts re-derives it.
1489
+ */
1490
+ const BUILTIN_TABLE_STYLE_IDS = [
1491
+ "{2D5ABB26-0587-4C30-8999-92F81FD0307C}",
1492
+ "{3C2FFA5D-87B4-456A-9821-1D502468CF0F}",
1493
+ "{284E427A-3D55-4303-BF80-6455036E1DE7}",
1494
+ "{69C7853C-536D-4A76-A0AE-DD22124D55A5}",
1495
+ "{775DCB02-9BB8-47FD-8907-85C794F793BA}",
1496
+ "{35758FB7-9AC5-4552-8A53-C91805E547FA}",
1497
+ "{08FB837D-C827-4EFA-A057-4D05807E0F7C}",
1498
+ "{5940675A-B579-460E-94D1-54222C63F5DA}",
1499
+ "{D113A9D2-9D6B-4929-AA2D-F23B5EE8CBE7}",
1500
+ "{18603FDC-E32A-4AB5-989C-0864C3EAD2B8}",
1501
+ "{306799F8-075E-4A3A-A7F6-7FBC6576F1A4}",
1502
+ "{E269D01E-BC32-4049-B463-5C60D7B0CCD2}",
1503
+ "{327F97BB-C833-4FB7-BDE5-3F7075034690}",
1504
+ "{638B1855-1B75-4FBE-930C-398BA8C253C6}",
1505
+ "{9D7B26C5-4107-4FEC-AEDC-1716B250A1EF}",
1506
+ "{3B4B98B0-60AC-42C2-AFA5-B58CD77FA1E5}",
1507
+ "{0E3FDE45-AF77-4B5C-9715-49D594BDF05E}",
1508
+ "{C083E6E3-FA7D-4D7B-A595-EF9225AFEA82}",
1509
+ "{D27102A9-8310-4765-A935-A1911B00CA55}",
1510
+ "{5FD0F851-EC5A-4D38-B0AD-8093EC10F338}",
1511
+ "{68D230F3-CF80-4859-8CE7-A43EE81993B5}",
1512
+ "{7E9639D4-E3E2-4D34-9284-5A2195B3D0D7}",
1513
+ "{69012ECD-51FC-41F1-AA8D-1B2483CD663E}",
1514
+ "{72833802-FEF1-4C79-8D5D-14CF1EAF98D9}",
1515
+ "{F2DE63D5-997A-4646-A377-4702673A728D}",
1516
+ "{17292A2E-F333-43FB-9621-5CBBE7FDCDCB}",
1517
+ "{5A111915-BE36-4E01-A7E5-04B1672EAD32}",
1518
+ "{912C8C85-51F0-491E-9774-3900AFEF0FD7}",
1519
+ "{616DA210-FB5B-4158-B5E0-FEB733F419BA}",
1520
+ "{BC89EF96-8CEA-46FF-86C4-4CE0E7609802}",
1521
+ "{5DA37D80-6434-44D0-A028-1B22A696006F}",
1522
+ "{8799B23B-EC83-4686-B30A-512413B5E67A}",
1523
+ "{ED083AE6-46FA-4A59-8FB0-9F97EB10719F}",
1524
+ "{BDBED569-4797-4DF1-A0F4-6AAB3CD982D8}",
1525
+ "{E8B1032C-EA38-4F05-BA0D-38AFFFC7BED3}",
1526
+ "{793D81CF-94F2-401A-BA57-92F5A7B2D0C5}",
1527
+ "{B301B821-A1FF-4177-AEE7-76D212191A09}",
1528
+ "{9DCAF9ED-07DC-4A11-8D7F-57B35C25682E}",
1529
+ "{1FECB4D8-DB02-4DC6-A0A2-4F2EBAE1DC90}",
1530
+ "{1E171933-4619-4E11-9A3F-F7608DF75F80}",
1531
+ "{FABFCF23-3B69-468F-B69F-88F6DE6A72F2}",
1532
+ "{10A1B5D5-9B99-4C35-A422-299274C87663}",
1533
+ "{073A0DAA-6AF3-43AB-8588-CEC1D06C72B9}",
1534
+ "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}",
1535
+ "{21E4AEA4-8DFA-4A89-87EB-49C32662AFE0}",
1536
+ "{F5AB1C69-6EDB-4FF4-983F-18BD219EF322}",
1537
+ "{00A15C55-8517-42AA-B614-E9B94910E393}",
1538
+ "{7DF18680-E054-41AD-8BC1-D1AEF772440D}",
1539
+ "{93296810-A885-4BE3-A3E7-6D5BEEA58F35}",
1540
+ "{8EC20E35-A176-4012-BC5E-935CFFF8708E}",
1541
+ "{6E25E649-3F16-4E02-A733-19D2CDBF48F0}",
1542
+ "{85BE263C-DBD7-4A20-BB59-AAB30ACAA65A}",
1543
+ "{EB344D84-9AFB-497E-A393-DC336BA19D2E}",
1544
+ "{EB9631B5-78F2-41C9-869B-9F39066F8104}",
1545
+ "{74C1A8A3-306A-4EB7-A6B1-4F7E0EB9C5D6}",
1546
+ "{2A488322-F2BA-4B5B-9748-0D474271808F}",
1547
+ "{D7AC3CCA-C797-4891-BE02-D94E43425B78}",
1548
+ "{69CF1AB2-1976-4502-BF36-3FF5EA218861}",
1549
+ "{8A107856-5554-42FB-B03E-39F5DBC370BA}",
1550
+ "{0505E3EF-67EA-436B-97B2-0124C06EBD24}",
1551
+ "{C4B1156A-380E-4F78-BDF5-A606A8083BF9}",
1552
+ "{22838BEF-8BB2-4498-84A7-C5851F593DF1}",
1553
+ "{16D9F66E-5EB9-4882-86FB-DCBF35E3C3E4}",
1554
+ "{E8034E78-7F5D-4C2E-B375-FC64B27BC917}",
1555
+ "{125E5076-3810-47DD-B79F-674D7AD40C01}",
1556
+ "{37CE84F3-28C3-443E-9E96-99CF82512B78}",
1557
+ "{D03447BB-5D67-496B-8E87-E561075AD55C}",
1558
+ "{E929F9F4-4A8F-4326-A1B4-22849713DDAB}",
1559
+ "{8FD4443E-F989-4FC4-A0C8-D5A2AF1F390B}",
1560
+ "{AF606853-7671-496A-8E4F-DF71F8EC918B}",
1561
+ "{5202B0CA-FC54-4496-8BCA-5EF66A818D29}",
1562
+ "{0660B408-B3CF-4A94-85FC-2B1E0A45F4A2}",
1563
+ "{91EBBBCC-DAD2-459C-BE2E-F6DE35CF9A28}",
1564
+ "{46F890A9-2807-4EBB-B81D-B2AA78EC7F39}"
1565
+ ];
1566
+ //#endregion
1407
1567
  //#region src/rules/id.ts
1408
1568
  /**
1409
- * `V018` … `V021`: four identifier spaces, and never one allocator.
1410
- *
1411
- * A presentation has four kinds of id and they do not share a rule between
1412
- * them. Slide ids start at 256 and stop at 2147483647. Master and layout ids
1413
- * start at 2147483648, and - the part no schema says - come out of **one**
1414
- * counter shared between them. Shape ids are unique inside a part and free to
1415
- * repeat across parts. Placeholder indices are not identifiers at all; they are
1416
- * a join key against another part.
1417
- *
1418
- * Every one of those is a way to break a file that looks like a way to be
1419
- * tidy. Renumbering shape ids to be unique across the deck is the obvious
1420
- * example: it is more consistent, it is what a database would do, and it
1421
- * detaches every `p:custDataLst`, VML `@spid` and animation target that names
1422
- * the old number.
1569
+ * `V018` … `V021` and `V032`: identifier spaces and the ids that name into them. Slide, sheet and
1570
+ * shape ids never share an allocator, and a table style id names one of PowerPoint's built-ins.
1423
1571
  */
1424
1572
  const SLIDE_ID_MIN = 256;
1425
1573
  const SLIDE_ID_MAX = 2147483647;
@@ -1641,6 +1789,19 @@ function v021PlaceholderIndices(ctx) {
1641
1789
  }
1642
1790
  }
1643
1791
  }
1792
+ const BUILTIN_TABLE_STYLES = new Set(BUILTIN_TABLE_STYLE_IDS);
1793
+ /** A well-formed id; V033 reports the rest. */
1794
+ const TABLE_STYLE_GUID = /^\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}$/i;
1795
+ /** A table's `a:tableStyleId`, or its inline `a:tableStyle/@styleId`, names a built-in (C8). */
1796
+ function v032TableStyleIds(ctx) {
1797
+ forEachElement(ctx, (part, element) => {
1798
+ if (namespaceOf(element) !== NS.a) return;
1799
+ const named = element.local === "tableStyleId" ? textContent(element) : element.local === "tableStyle" ? attributeValue(element, "styleId") : void 0;
1800
+ if (named === void 0 || !TABLE_STYLE_GUID.test(named)) return;
1801
+ if (BUILTIN_TABLE_STYLES.has(named.toUpperCase())) return;
1802
+ ctx.add("V032", elementLocation(part, element), `<a:${element.local}> names ${named}, none of PowerPoint's 74 built-in table styles. PowerPoint draws a 1-pt black grid instead, whatever ppt/tableStyles.xml defines (C8).`);
1803
+ });
1804
+ }
1644
1805
  //#endregion
1645
1806
  //#region src/rules/refusal.ts
1646
1807
  /**
@@ -2109,12 +2270,7 @@ function v029TextAndFieldIdentity(ctx) {
2109
2270
  //#endregion
2110
2271
  //#region src/validate.ts
2111
2272
  /**
2112
- * The thirty-one, wired to their implementations.
2113
- *
2114
- * A plain table, so that "is every rule reachable" is a thing a test can ask
2115
- * rather than a thing a reader has to believe. `rules.test.ts` asserts that the
2116
- * keys here are exactly `RULE_IDS` - a rule with a definition and no function
2117
- * would otherwise sit in the table looking enforced.
2273
+ * Every rule wired to its implementation; `rules.test.ts` holds the keys to `RULE_IDS`.
2118
2274
  */
2119
2275
  const IMPLEMENTATIONS = {
2120
2276
  V001: v001ContentTypeCoverage,
@@ -2147,7 +2303,9 @@ const IMPLEMENTATIONS = {
2147
2303
  V028: v028OpaqueContainersUnchanged,
2148
2304
  V029: v029TextAndFieldIdentity,
2149
2305
  V030: v030TableGrid,
2150
- V031: v031TableAttributes
2306
+ V031: v031TableAttributes,
2307
+ V032: v032TableStyleIds,
2308
+ V033: v033TableStyles
2151
2309
  };
2152
2310
  /** Rules that cannot run without the archive bytes. See `Context.archive`. */
2153
2311
  const ARCHIVE_RULES = ["V003"];
@@ -2220,20 +2378,9 @@ function run(ctx, rules) {
2220
2378
  for (const id of rules) IMPLEMENTATIONS[id](ctx);
2221
2379
  }
2222
2380
  /**
2223
- * Decide, for each finding, whether we introduced it.
2224
- *
2225
- * The second pass is the whole of it: run the same rules against the package as
2226
- * it was opened and difference the two sets. A finding in both was already
2227
- * there. See `report.ts` for why that question is the one that decides whether
2228
- * an export is refused, and why answering it by differencing beats answering it
2229
- * per rule.
2230
- *
2231
- * Three properties make it cheap enough to be unconditional in the only case
2232
- * that matters. It runs **only when something fatal was found**, so a clean
2233
- * export - which is nearly all of them - pays nothing. It runs **only the rules
2234
- * that fired**, not all twenty-nine. And it skips the preservation rules, which
2235
- * would be comparing the baseline against itself and would find nothing by
2236
- * construction.
2381
+ * Which findings were already in the package as it was opened: the fired rules run again on
2382
+ * the baseline and the two sets are differenced. Only when something fatal was found, only the
2383
+ * rules that fired, and never the preservation rules. `report.ts` has why.
2237
2384
  */
2238
2385
  function attributeOrigins(ctx, options, running) {
2239
2386
  const baseline = options.baseline;