@pptx-studio/validate 0.1.2 → 0.2.1
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 +37 -0
- package/README.md +16 -15
- package/dist/index.d.ts +42 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +158 -6
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @pptx-studio/validate
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b58dd19: Built with tsdown 0.23. The declarations take a new shape, and the API is unchanged.
|
|
8
|
+
|
|
9
|
+
- Every value is exported where it is declared (`export declare function …`), not in a trailing
|
|
10
|
+
`export { … }`.
|
|
11
|
+
- The types are exported in one `export type { … }`.
|
|
12
|
+
- The JavaScript is byte-for-byte what 0.22 emitted.
|
|
13
|
+
- In geometry, opc, validate and xml, the source maps name fewer symbols.
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [b58dd19]
|
|
16
|
+
- @pptx-studio/opc@0.1.3
|
|
17
|
+
- @pptx-studio/xml@0.1.2
|
|
18
|
+
|
|
19
|
+
## 0.2.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- e8ba475: A table is parsed, and its grid is the one PowerPoint draws.
|
|
24
|
+
|
|
25
|
+
`Shape.table` carries the `a:tbl` of a `p:graphicFrame`: the `a:tblPr` flags and style source, every
|
|
26
|
+
`a:gridCol`, every `a:tr` with its cells, and each cell's spans, body and `a:tcPr` as written.
|
|
27
|
+
`tableGrid` resolves the merges by the rule C7 measured (`tables.json`, 76 of 76 tables PowerPoint
|
|
28
|
+
opened as written and 21 it authored itself): each `a:tc` takes the next column, the spans on the
|
|
29
|
+
anchor are the whole story, a covered position is covered whatever its own attributes say, a span
|
|
30
|
+
stops at the edge or at the first claimed position, and `hMerge`/`vMerge` change nothing. `a:tr/@h`
|
|
31
|
+
is a minimum, a column is never narrower than its cells' side margins plus 2 pt, and the frame's
|
|
32
|
+
`a:ext` says nothing about the drawn size. A table attribute of the wrong type throws
|
|
33
|
+
`MODEL_TABLE_ATTR`.
|
|
34
|
+
|
|
35
|
+
`@pptx-studio/validate` gains `V030`, a warning on a table whose rows, spans and flags disagree —
|
|
36
|
+
PowerPoint opens it silently and rewrites it on the next save — and `V031`, fatal on the missing or
|
|
37
|
+
mistyped table attributes PowerPoint repairs. The downstream packages carry the new rules and the
|
|
38
|
+
new field; nothing they draw changes.
|
|
39
|
+
|
|
3
40
|
## 0.1.2
|
|
4
41
|
|
|
5
42
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @pptx-studio/validate
|
|
2
2
|
|
|
3
|
-
**The repair firewall.**
|
|
3
|
+
**The repair firewall.** Thirty-one 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
|
-
##
|
|
31
|
+
## Thirty-one 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,18 +38,19 @@ 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.
|
|
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. |
|
|
53
54
|
|
|
54
55
|
Every rule carries a `why` recording what was tried and what opened, so whoever
|
|
55
56
|
eventually contradicts one knows what they are contradicting.
|
|
@@ -85,7 +86,7 @@ the same reason.
|
|
|
85
86
|
|
|
86
87
|
Two suites that check opposite things, and neither substitutes for the other.
|
|
87
88
|
|
|
88
|
-
**It fires.** `validate.test.ts` breaks a minimal deck
|
|
89
|
+
**It fires.** `validate.test.ts` breaks a minimal deck thirty-one ways, one per
|
|
89
90
|
rule, and asserts each rule reports the right part and the right XPath.
|
|
90
91
|
|
|
91
92
|
**It is quiet.** `tools/corpus/suites/validate.test.ts` runs all fifty-two committed
|
package/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { PartName, PartStore, ReadZipOptions, ZipArchive } from "@pptx-studio/op
|
|
|
24
24
|
*
|
|
25
25
|
* Deliberately few. Almost everything this package has to say is a finding.
|
|
26
26
|
*/
|
|
27
|
-
declare const VALIDATE_ERROR_CODES: readonly ["ERR_VALIDATION_FAILED", "ERR_UNVALIDATABLE", "ERR_UNKNOWN_RULE"];
|
|
27
|
+
export declare const VALIDATE_ERROR_CODES: readonly ["ERR_VALIDATION_FAILED", "ERR_UNVALIDATABLE", "ERR_UNKNOWN_RULE"];
|
|
28
28
|
type ValidateErrorCode = (typeof VALIDATE_ERROR_CODES)[number];
|
|
29
29
|
interface ValidateErrorDetail {
|
|
30
30
|
/** The part the failure is about, if it is about one. */
|
|
@@ -40,18 +40,18 @@ interface ValidateErrorDetail {
|
|
|
40
40
|
*/
|
|
41
41
|
readonly report?: unknown;
|
|
42
42
|
}
|
|
43
|
-
declare class ValidateError extends Error {
|
|
43
|
+
export declare class ValidateError extends Error {
|
|
44
44
|
readonly code: ValidateErrorCode;
|
|
45
45
|
readonly detail: ValidateErrorDetail;
|
|
46
46
|
constructor(code: ValidateErrorCode, message: string, detail?: ValidateErrorDetail);
|
|
47
47
|
}
|
|
48
|
-
declare function isValidateError(value: unknown): value is ValidateError;
|
|
48
|
+
export declare function isValidateError(value: unknown): value is ValidateError;
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/rules/rules.d.ts
|
|
51
51
|
/**
|
|
52
|
-
* The
|
|
52
|
+
* The thirty-one rules, and the evidence for each.
|
|
53
53
|
*
|
|
54
|
-
* ## Why a table and not
|
|
54
|
+
* ## Why a table and not thirty-one functions
|
|
55
55
|
*
|
|
56
56
|
* The functions are in the files beside this one. What lives here is the part a
|
|
57
57
|
* person reads: what each rule claims, how sure we are, and *how we know*. A
|
|
@@ -114,7 +114,7 @@ interface Rule {
|
|
|
114
114
|
*/
|
|
115
115
|
readonly needsBaseline?: true;
|
|
116
116
|
}
|
|
117
|
-
declare const RULES: readonly [{
|
|
117
|
+
export declare const RULES: readonly [{
|
|
118
118
|
readonly id: "V001";
|
|
119
119
|
readonly category: "package";
|
|
120
120
|
readonly severity: "fatal";
|
|
@@ -223,7 +223,7 @@ declare const RULES: readonly [{
|
|
|
223
223
|
readonly id: "V016";
|
|
224
224
|
readonly category: "required";
|
|
225
225
|
readonly severity: "fatal";
|
|
226
|
-
readonly evidence: "
|
|
226
|
+
readonly evidence: "both";
|
|
227
227
|
readonly title: "every text body has `a:bodyPr` and at least one `a:p`";
|
|
228
228
|
readonly why: string;
|
|
229
229
|
}, {
|
|
@@ -320,12 +320,26 @@ declare const RULES: readonly [{
|
|
|
320
320
|
readonly needsBaseline: true;
|
|
321
321
|
readonly title: "text and field identity survive: `xml:space`, `a:fld/@id`, cached field text";
|
|
322
322
|
readonly why: string;
|
|
323
|
+
}, {
|
|
324
|
+
readonly id: "V030";
|
|
325
|
+
readonly category: "required";
|
|
326
|
+
readonly severity: "warning";
|
|
327
|
+
readonly evidence: "measured";
|
|
328
|
+
readonly title: "a table is the one PowerPoint reads: a cell per column in every row, a flag where a span covers";
|
|
329
|
+
readonly why: string;
|
|
330
|
+
}, {
|
|
331
|
+
readonly id: "V031";
|
|
332
|
+
readonly category: "required";
|
|
333
|
+
readonly severity: "fatal";
|
|
334
|
+
readonly evidence: "both";
|
|
335
|
+
readonly title: "`a:gridCol/@w`, `a:tr/@h`, and typed `gridSpan`, `rowSpan`, `hMerge`, `vMerge`";
|
|
336
|
+
readonly why: string;
|
|
323
337
|
}];
|
|
324
338
|
type RuleId = (typeof RULES)[number]['id'];
|
|
325
|
-
declare function ruleById(id: string): Rule | undefined;
|
|
326
|
-
declare const RULE_IDS: readonly RuleId[];
|
|
339
|
+
export declare function ruleById(id: string): Rule | undefined;
|
|
340
|
+
export declare const RULE_IDS: readonly RuleId[];
|
|
327
341
|
/** Rules that need the package as it was opened. See `Rule.needsBaseline`. */
|
|
328
|
-
declare const BASELINE_RULES: readonly RuleId[];
|
|
342
|
+
export declare const BASELINE_RULES: readonly RuleId[];
|
|
329
343
|
//#endregion
|
|
330
344
|
//#region src/report/location.d.ts
|
|
331
345
|
/**
|
|
@@ -383,9 +397,9 @@ interface Location {
|
|
|
383
397
|
readonly offset: number | null;
|
|
384
398
|
}
|
|
385
399
|
/** A location that names a part and nothing inside it. */
|
|
386
|
-
declare function partLocation(part: string): Location;
|
|
400
|
+
export declare function partLocation(part: string): Location;
|
|
387
401
|
/** The location of the package itself: the archive, the content types, the graph. */
|
|
388
|
-
declare const PACKAGE_LOCATION: Location;
|
|
402
|
+
export declare const PACKAGE_LOCATION: Location;
|
|
389
403
|
/**
|
|
390
404
|
* The XPath of an element, from the document root.
|
|
391
405
|
*
|
|
@@ -394,11 +408,11 @@ declare const PACKAGE_LOCATION: Location;
|
|
|
394
408
|
* finding built from a node the caller synthesised is degraded rather than
|
|
395
409
|
* lost.
|
|
396
410
|
*/
|
|
397
|
-
declare function xpathOf(element: XElement): string;
|
|
411
|
+
export declare function xpathOf(element: XElement): string;
|
|
398
412
|
/** The XPath of an attribute: its owner's path, then `/@qname`. */
|
|
399
|
-
declare function xpathOfAttribute(owner: XElement, attr: XAttribute | string): string;
|
|
413
|
+
export declare function xpathOfAttribute(owner: XElement, attr: XAttribute | string): string;
|
|
400
414
|
/** A location for an element inside a named part. */
|
|
401
|
-
declare function elementLocation(part: string, element: XElement): Location;
|
|
415
|
+
export declare function elementLocation(part: string, element: XElement): Location;
|
|
402
416
|
/**
|
|
403
417
|
* A location for one attribute of an element.
|
|
404
418
|
*
|
|
@@ -406,7 +420,7 @@ declare function elementLocation(part: string, element: XElement): Location;
|
|
|
406
420
|
* element's start when it does not - which is the case that matters, because a
|
|
407
421
|
* *missing* required attribute is a finding and it has to point somewhere.
|
|
408
422
|
*/
|
|
409
|
-
declare function attributeLocation(part: string, element: XElement, qname: string): Location;
|
|
423
|
+
export declare function attributeLocation(part: string, element: XElement, qname: string): Location;
|
|
410
424
|
/**
|
|
411
425
|
* `line:column` for an offset, 1-based, or `null`.
|
|
412
426
|
*
|
|
@@ -415,14 +429,14 @@ declare function attributeLocation(part: string, element: XElement, qname: strin
|
|
|
415
429
|
* hundred times. Callers that render a report for a human ask for it once, at
|
|
416
430
|
* the point of rendering, for the findings they are about to show.
|
|
417
431
|
*/
|
|
418
|
-
declare function lineColumn(source: string, offset: number): {
|
|
432
|
+
export declare function lineColumn(source: string, offset: number): {
|
|
419
433
|
line: number;
|
|
420
434
|
column: number;
|
|
421
435
|
};
|
|
422
436
|
/** Document order, for sorting findings within a part. */
|
|
423
|
-
declare function inDocumentOrder(a: XNode, b: XNode): number;
|
|
437
|
+
export declare function inDocumentOrder(a: XNode, b: XNode): number;
|
|
424
438
|
/** The root element's qname, for the handful of rules that dispatch on it. */
|
|
425
|
-
declare function rootName(document: XDocument): string;
|
|
439
|
+
export declare function rootName(document: XDocument): string;
|
|
426
440
|
//#endregion
|
|
427
441
|
//#region src/report/report.d.ts
|
|
428
442
|
/**
|
|
@@ -443,7 +457,7 @@ declare function rootName(document: XDocument): string;
|
|
|
443
457
|
* never saved again. The editor would be refusing to give the user back their
|
|
444
458
|
* own file over a problem it did not cause and cannot fix. `PartStore.write`
|
|
445
459
|
* already made exactly this call for dangling relationships, and this is the
|
|
446
|
-
* same call generalised to all
|
|
460
|
+
* same call generalised to all thirty-one rules.
|
|
447
461
|
*
|
|
448
462
|
* So `origin` is not a per-rule judgement call. It is computed by running the
|
|
449
463
|
* rules a second time against the package **as it was opened** and differencing
|
|
@@ -514,14 +528,14 @@ interface Report {
|
|
|
514
528
|
* the same place with different values are different findings, and treating
|
|
515
529
|
* them as one would let an introduced defect hide behind an inherited one.
|
|
516
530
|
*/
|
|
517
|
-
declare function findingKey(finding: Finding): string;
|
|
518
|
-
declare function buildReport(input: {
|
|
531
|
+
export declare function findingKey(finding: Finding): string;
|
|
532
|
+
export declare function buildReport(input: {
|
|
519
533
|
readonly findings: readonly Finding[];
|
|
520
534
|
readonly checked: readonly RuleId[];
|
|
521
535
|
readonly skipped: readonly SkippedRule[];
|
|
522
536
|
readonly problems: readonly ReadProblem[];
|
|
523
537
|
}): Report;
|
|
524
|
-
declare function isReport(value: unknown): value is Report;
|
|
538
|
+
export declare function isReport(value: unknown): value is Report;
|
|
525
539
|
interface FormatOptions {
|
|
526
540
|
/** Include `inherited` findings. Default true. */
|
|
527
541
|
readonly inherited?: boolean;
|
|
@@ -537,7 +551,7 @@ interface FormatOptions {
|
|
|
537
551
|
* "what is wrong with this file", and a file is a set of parts. Grouping by
|
|
538
552
|
* rule is the right shape for the corpus gate and the wrong one here.
|
|
539
553
|
*/
|
|
540
|
-
declare function formatReport(report: Report, options?: FormatOptions): string;
|
|
554
|
+
export declare function formatReport(report: Report, options?: FormatOptions): string;
|
|
541
555
|
//#endregion
|
|
542
556
|
//#region src/context.d.ts
|
|
543
557
|
/**
|
|
@@ -651,7 +665,7 @@ declare class RuntimeContext implements Context {
|
|
|
651
665
|
add(rule: RuleId, where: Location, message: string): void;
|
|
652
666
|
problem(part: string, message: string): void;
|
|
653
667
|
}
|
|
654
|
-
declare function createContext(input: ContextInput): RuntimeContext;
|
|
668
|
+
export declare function createContext(input: ContextInput): RuntimeContext;
|
|
655
669
|
//#endregion
|
|
656
670
|
//#region src/validate.d.ts
|
|
657
671
|
interface ValidateOptions {
|
|
@@ -690,7 +704,7 @@ interface ValidateOptions {
|
|
|
690
704
|
* findings in it, which is the shape a caller can render. It throws only when
|
|
691
705
|
* it was handed something it cannot open at all.
|
|
692
706
|
*/
|
|
693
|
-
declare function validatePackage(options: ValidateOptions): Report;
|
|
707
|
+
export declare function validatePackage(options: ValidateOptions): Report;
|
|
694
708
|
/**
|
|
695
709
|
* Validate, and refuse to go on if we broke something.
|
|
696
710
|
*
|
|
@@ -701,7 +715,7 @@ declare function validatePackage(options: ValidateOptions): Report;
|
|
|
701
715
|
* feedback loop that exists, and a caller who forgot to check a return value
|
|
702
716
|
* would have removed it.
|
|
703
717
|
*/
|
|
704
|
-
declare function assertValid(options: ValidateOptions): Report;
|
|
718
|
+
export declare function assertValid(options: ValidateOptions): Report;
|
|
705
719
|
//#endregion
|
|
706
|
-
export {
|
|
720
|
+
export type { Context, ContextInput, Evidence, Finding, FormatOptions, Location, Origin, ReadProblem, Report, Rule, RuleCategory, RuleId, Severity, SkippedRule, ValidateErrorCode, ValidateErrorDetail, ValidateOptions };
|
|
707
721
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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
|
@@ -169,9 +169,9 @@ const RULES = [
|
|
|
169
169
|
id: "V016",
|
|
170
170
|
category: "required",
|
|
171
171
|
severity: "fatal",
|
|
172
|
-
evidence: "
|
|
172
|
+
evidence: "both",
|
|
173
173
|
title: "every text body has `a:bodyPr` and at least one `a:p`",
|
|
174
|
-
why: "`CT_TextBody` is `bodyPr` then optional `lstStyle` then `a:p` at `[1..unbounded]`. A shape whose text was deleted down to nothing is the way this gets broken in an editor: the last paragraph goes, and what is left is a `p:txBody` with no `a:p` in it. An empty paragraph is not the same as no paragraph, and `a:endParaRPr` is where its height comes from."
|
|
174
|
+
why: "`CT_TextBody` is `bodyPr` then optional `lstStyle` then `a:p` at `[1..unbounded]`. A shape whose text was deleted down to nothing is the way this gets broken in an editor: the last paragraph goes, and what is left is a `p:txBody` with no `a:p` in it. An empty paragraph is not the same as no paragraph, and `a:endParaRPr` is where its height comes from. C7 measured it on a table cell: a repair prompt, and PowerPoint writes the paragraph in."
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
177
|
id: "V017",
|
|
@@ -279,6 +279,22 @@ const RULES = [
|
|
|
279
279
|
needsBaseline: true,
|
|
280
280
|
title: "text and field identity survive: `xml:space`, `a:fld/@id`, cached field text",
|
|
281
281
|
why: "Three small things that are each silently destructive. An `a:t` with leading or trailing whitespace needs `xml:space=\"preserve\"` if it was written with it, because dropping the attribute changes what the text *is*. `a:fld/@id` is a required `ST_Guid` and regenerating one can make PowerPoint repair the file. And a field’s cached `a:t` is the only thing that renders when we cannot evaluate the field ourselves, so discarding it turns a date placeholder into an empty box on every consumer that is not PowerPoint."
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "V030",
|
|
285
|
+
category: "required",
|
|
286
|
+
severity: "warning",
|
|
287
|
+
evidence: "measured",
|
|
288
|
+
title: "a table is the one PowerPoint reads: a cell per column in every row, a flag where a span covers",
|
|
289
|
+
why: "Nothing in the schema ties an `a:tr` to the number of `a:gridCol`, or a `gridSpan` on one cell to the `hMerge` on the next. C7 built 82 tables, 52 of them to disagree with themselves, and asked PowerPoint which cell every grid position belongs to: the spans on the anchor are the whole answer (76 of 76, against 52 for the flags), a short row is padded, a fifth cell is dropped, a colliding or oversized span is clamped and a covered cell’s own span ignored - and PowerPoint writes all of that back on save. The deck opens without a word, which is why this is a warning: the file says one table and PowerPoint draws another, and the next save is the one that rewrites the part."
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: "V031",
|
|
293
|
+
category: "required",
|
|
294
|
+
severity: "fatal",
|
|
295
|
+
evidence: "both",
|
|
296
|
+
title: "`a:gridCol/@w`, `a:tr/@h`, and typed `gridSpan`, `rowSpan`, `hMerge`, `vMerge`",
|
|
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."
|
|
282
298
|
}
|
|
283
299
|
];
|
|
284
300
|
const BY_ID = new Map(RULES.map((rule) => [rule.id, rule]));
|
|
@@ -629,7 +645,7 @@ function readRelsParts(ctx) {
|
|
|
629
645
|
return parts;
|
|
630
646
|
}
|
|
631
647
|
/**
|
|
632
|
-
* Memoised above, because five of the
|
|
648
|
+
* Memoised above, because five of the thirty-one rules want this list and each
|
|
633
649
|
* would otherwise re-walk every `.rels` part in the package. The parsed trees
|
|
634
650
|
* are already cached on the context; what is saved here is the element scan and
|
|
635
651
|
* the allocation, which on a three-hundred-slide deck is five passes over a
|
|
@@ -1153,7 +1169,7 @@ function forEachElement$1(ctx, visit) {
|
|
|
1153
1169
|
//#endregion
|
|
1154
1170
|
//#region src/rules/required.ts
|
|
1155
1171
|
/**
|
|
1156
|
-
* `V013` … `V017`: children and attributes that are not optional.
|
|
1172
|
+
* `V013` … `V017`, `V030` and `V031`: children and attributes that are not optional.
|
|
1157
1173
|
*
|
|
1158
1174
|
* Every one of these is a `minOccurs` the schema states plainly, and every one
|
|
1159
1175
|
* is broken the same way: by an editor deleting the last of something. The last
|
|
@@ -1226,6 +1242,140 @@ function v017GraphicFrame(ctx) {
|
|
|
1226
1242
|
if (childElements(element).find((child) => child.local === "graphic" && namespaceOf(child) === NS.a) === void 0) ctx.add("V017", elementLocation(part, element), "no <a:graphic>. The frame is the wrapper; the graphic is the thing in it, and a frame without one names no table, chart, diagram or OLE object at all.");
|
|
1227
1243
|
});
|
|
1228
1244
|
}
|
|
1245
|
+
/** The lexical forms of `xsd:boolean`. */
|
|
1246
|
+
const BOOLEANS = /* @__PURE__ */ new Set([
|
|
1247
|
+
"1",
|
|
1248
|
+
"0",
|
|
1249
|
+
"true",
|
|
1250
|
+
"false"
|
|
1251
|
+
]);
|
|
1252
|
+
const INT = /^[+-]?\d+$/;
|
|
1253
|
+
/** `a:gridCol` carries `@w`, `a:tr` carries `@h`, and a cell's four merge attributes are typed. */
|
|
1254
|
+
function v031TableAttributes(ctx) {
|
|
1255
|
+
forEachElement(ctx, (part, element) => {
|
|
1256
|
+
if (namespaceOf(element) !== NS.a) return;
|
|
1257
|
+
const missing = (name) => {
|
|
1258
|
+
ctx.add("V031", elementLocation(part, element), "<" + element.qname + "> has no @" + name + ". CT_TableCol and CT_TableRow require it, and C7 measured the repair prompt: PowerPoint opens the deck only after rewriting the part.");
|
|
1259
|
+
};
|
|
1260
|
+
if (element.local === "gridCol" && attribute(element, "w") === void 0) missing("w");
|
|
1261
|
+
if (element.local === "tr" && attribute(element, "h") === void 0) missing("h");
|
|
1262
|
+
if (element.local !== "tc") return;
|
|
1263
|
+
for (const [name, ok, type] of [
|
|
1264
|
+
[
|
|
1265
|
+
"gridSpan",
|
|
1266
|
+
INT,
|
|
1267
|
+
"xsd:int"
|
|
1268
|
+
],
|
|
1269
|
+
[
|
|
1270
|
+
"rowSpan",
|
|
1271
|
+
INT,
|
|
1272
|
+
"xsd:int"
|
|
1273
|
+
],
|
|
1274
|
+
[
|
|
1275
|
+
"hMerge",
|
|
1276
|
+
BOOLEANS,
|
|
1277
|
+
"xsd:boolean"
|
|
1278
|
+
],
|
|
1279
|
+
[
|
|
1280
|
+
"vMerge",
|
|
1281
|
+
BOOLEANS,
|
|
1282
|
+
"xsd:boolean"
|
|
1283
|
+
]
|
|
1284
|
+
]) {
|
|
1285
|
+
const raw = attribute(element, name)?.value;
|
|
1286
|
+
if (raw === void 0 || (ok instanceof RegExp ? ok.test(raw) : ok.has(raw))) continue;
|
|
1287
|
+
ctx.add("V031", elementLocation(part, element), "<a:tc>/@" + name + " is \"" + raw + "\", which is not an " + type + ". C7 measured gridSpan=\"2.0\" and hMerge=\"on\": a repair prompt each; the span is dropped with the merge it named and the flag comes back as \"1\" under its span.");
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
/** Zero is one and a negative runs to the edge: C7's span-zero and span-negative. */
|
|
1292
|
+
function spanOf(raw) {
|
|
1293
|
+
if (raw === void 0) return 1;
|
|
1294
|
+
const n = Number(raw);
|
|
1295
|
+
return n < 0 ? Number.POSITIVE_INFINITY : Math.max(1, n);
|
|
1296
|
+
}
|
|
1297
|
+
function flagged(cell, name) {
|
|
1298
|
+
const raw = attribute(cell, name)?.value;
|
|
1299
|
+
return raw === "1" || raw === "true";
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* The occupancy `@pptx-studio/model` ships, over attributes: each cell takes the next column, a
|
|
1303
|
+
* claimed position is covered whatever it says, a span stops at the edge or at a claimed position.
|
|
1304
|
+
*/
|
|
1305
|
+
function owners(cells, cols) {
|
|
1306
|
+
const grid = cells.map(() => Array.from({ length: cols }).fill(null));
|
|
1307
|
+
cells.forEach((line, r) => {
|
|
1308
|
+
line.forEach((cell, c) => {
|
|
1309
|
+
if (c >= cols || grid[r][c] !== null) return;
|
|
1310
|
+
const wide = spanOf(attribute(cell, "gridSpan")?.value);
|
|
1311
|
+
const tall = spanOf(attribute(cell, "rowSpan")?.value);
|
|
1312
|
+
const owner = {
|
|
1313
|
+
row: r,
|
|
1314
|
+
col: c,
|
|
1315
|
+
rows: 1,
|
|
1316
|
+
cols: 1
|
|
1317
|
+
};
|
|
1318
|
+
while (c + owner.cols < cols && owner.cols < wide && grid[r][c + owner.cols] === null) owner.cols += 1;
|
|
1319
|
+
owner.rows = Math.min(tall, cells.length - r);
|
|
1320
|
+
for (let rr = r; rr < r + owner.rows; rr++) for (let cc = c; cc < c + owner.cols; cc++) grid[rr][cc] = owner;
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
return grid;
|
|
1324
|
+
}
|
|
1325
|
+
/** The table PowerPoint reads: one cell per column in every row, a flag exactly where a span covers. */
|
|
1326
|
+
function v030TableGrid(ctx) {
|
|
1327
|
+
forEachElement(ctx, (part, element) => {
|
|
1328
|
+
if (element.local !== "tbl" || namespaceOf(element) !== NS.a) return;
|
|
1329
|
+
const add = (where, message) => {
|
|
1330
|
+
ctx.add("V030", elementLocation(part, where), message);
|
|
1331
|
+
};
|
|
1332
|
+
const grid = childElements(element).find((child) => child.local === "tblGrid");
|
|
1333
|
+
const cols = grid === void 0 ? 0 : childElements(grid).filter((c) => c.local === "gridCol").length;
|
|
1334
|
+
const rows = childElements(element).filter((child) => child.local === "tr");
|
|
1335
|
+
if (cols === 0 || rows.length === 0) {
|
|
1336
|
+
add(element, "the table has " + String(cols) + " column(s) and " + String(rows.length) + " row(s). PowerPoint invents one of whichever is missing, empty, and writes it back - measured in C7.");
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const cells = rows.map((row) => childElements(row).filter((child) => child.local === "tc"));
|
|
1340
|
+
cells.forEach((line, r) => {
|
|
1341
|
+
if (line.length === cols) return;
|
|
1342
|
+
add(rows[r], "row " + String(r + 1) + " holds " + String(line.length) + " cell(s) for " + String(cols) + " column(s). PowerPoint pads a short row with an empty cell, drops the cells past the last column, and writes the result back - measured in C7.");
|
|
1343
|
+
});
|
|
1344
|
+
const grid2 = owners(cells, cols);
|
|
1345
|
+
cells.forEach((line, r) => {
|
|
1346
|
+
line.forEach((cell, c) => {
|
|
1347
|
+
const owner = grid2[r]?.[c];
|
|
1348
|
+
if (owner === null || owner === void 0) return;
|
|
1349
|
+
const at = "cell (" + String(r + 1) + ", " + String(c + 1) + ")";
|
|
1350
|
+
if (owner.row === r && owner.col === c) {
|
|
1351
|
+
for (const [name, span] of [["gridSpan", owner.cols], ["rowSpan", owner.rows]]) {
|
|
1352
|
+
const written = attribute(cell, name)?.value;
|
|
1353
|
+
if (written === void 0 || Number(written) === span) continue;
|
|
1354
|
+
add(cell, at + " says " + name + "=\"" + written + "\" and covers " + String(span) + ". A span stops at the grid edge or at a position an earlier span claimed, zero is one and a negative runs to the edge; PowerPoint writes the span it drew - measured in C7.");
|
|
1355
|
+
}
|
|
1356
|
+
for (const name of ["hMerge", "vMerge"]) {
|
|
1357
|
+
if (!flagged(cell, name)) continue;
|
|
1358
|
+
add(cell, at + " says " + name + "=\"1\" and no span covers it. The flag changes nothing: PowerPoint reads the spans alone, 76 of 76 in C7, and drops the flag on save.");
|
|
1359
|
+
}
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
for (const [name, covered] of [["hMerge", owner.col < c], ["vMerge", owner.row < r]]) {
|
|
1363
|
+
if (covered === flagged(cell, name)) continue;
|
|
1364
|
+
if (!covered) {
|
|
1365
|
+
add(cell, at + " says " + name + "=\"1\" and the span of cell (" + String(owner.row + 1) + ", " + String(owner.col + 1) + ") covers it the other way. PowerPoint drops the flag on save - measured in C7.");
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
add(cell, at + " is covered by the span of cell (" + String(owner.row + 1) + ", " + String(owner.col + 1) + ") and does not say " + name + "=\"1\". PowerPoint reads it as covered anyway and adds the flag on save; a reader that trusts the flag draws a cell PowerPoint does not - measured in C7.");
|
|
1369
|
+
}
|
|
1370
|
+
for (const [name, allowed] of [["gridSpan", c === owner.col ? owner.cols : null], ["rowSpan", r === owner.row ? owner.rows : null]]) {
|
|
1371
|
+
const written = attribute(cell, name)?.value;
|
|
1372
|
+
if (written === void 0 || Number(written) === allowed) continue;
|
|
1373
|
+
add(cell, at + " is covered and says " + name + "=\"" + written + "\". A covered cell’s own span is ignored and PowerPoint deletes it on save unless it repeats the anchor’s other span, as PowerPoint’s own form does - measured in C7.");
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1229
1379
|
/**
|
|
1230
1380
|
* Presentation-family parts only.
|
|
1231
1381
|
*
|
|
@@ -1959,7 +2109,7 @@ function v029TextAndFieldIdentity(ctx) {
|
|
|
1959
2109
|
//#endregion
|
|
1960
2110
|
//#region src/validate.ts
|
|
1961
2111
|
/**
|
|
1962
|
-
* The
|
|
2112
|
+
* The thirty-one, wired to their implementations.
|
|
1963
2113
|
*
|
|
1964
2114
|
* A plain table, so that "is every rule reachable" is a thing a test can ask
|
|
1965
2115
|
* rather than a thing a reader has to believe. `rules.test.ts` asserts that the
|
|
@@ -1995,7 +2145,9 @@ const IMPLEMENTATIONS = {
|
|
|
1995
2145
|
V026: v026ChartStyleComplete,
|
|
1996
2146
|
V027: v027UneditedPartsUnchanged,
|
|
1997
2147
|
V028: v028OpaqueContainersUnchanged,
|
|
1998
|
-
V029: v029TextAndFieldIdentity
|
|
2148
|
+
V029: v029TextAndFieldIdentity,
|
|
2149
|
+
V030: v030TableGrid,
|
|
2150
|
+
V031: v031TableAttributes
|
|
1999
2151
|
};
|
|
2000
2152
|
/** Rules that cannot run without the archive bytes. See `Context.archive`. */
|
|
2001
2153
|
const ARCHIVE_RULES = ["V003"];
|