@pptx-studio/validate 0.2.1 → 0.4.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 +32 -0
- package/NOTICE +13 -3
- package/README.md +20 -16
- package/dist/index.d.ts +34 -98
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +307 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -295,6 +295,30 @@ 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."
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: "V034",
|
|
317
|
+
category: "required",
|
|
318
|
+
severity: "warning",
|
|
319
|
+
evidence: "measured",
|
|
320
|
+
title: "a border a cell writes is the one PowerPoint draws on that edge",
|
|
321
|
+
why: "A grid edge belongs to two cells, and each may write a line for it. C9 drew direct borders, on plain and merged cells, under seven built-ins and the default grid: PowerPoint draws an edge from the cell above or to the left - its `a:lnB` or `a:lnR`, or the style where it writes none - where that cell, merged or not, has its anchor level with the edge. Otherwise the cell below or to the right draws its own `a:lnT` or `a:lnL` where its anchor is level with the edge, and elsewhere the cell above or to the left keeps it. When PowerPoint sets a border itself it writes every side alike. The deck opens without a word, which is why this is a warning: the file says one border and PowerPoint draws another."
|
|
298
322
|
}
|
|
299
323
|
];
|
|
300
324
|
const BY_ID = new Map(RULES.map((rule) => [rule.id, rule]));
|
|
@@ -645,14 +669,7 @@ function readRelsParts(ctx) {
|
|
|
645
669
|
return parts;
|
|
646
670
|
}
|
|
647
671
|
/**
|
|
648
|
-
* Memoised
|
|
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.
|
|
672
|
+
* Memoised per context: several rules want every relationship part, and this walks them once.
|
|
656
673
|
*/
|
|
657
674
|
function scanRelsParts(ctx) {
|
|
658
675
|
const out = [];
|
|
@@ -1169,16 +1186,8 @@ function forEachElement$1(ctx, visit) {
|
|
|
1169
1186
|
//#endregion
|
|
1170
1187
|
//#region src/rules/required.ts
|
|
1171
1188
|
/**
|
|
1172
|
-
* `V013` … `V017`, `V030` and `
|
|
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.
|
|
1189
|
+
* `V013` … `V017`, `V030`, `V031`, `V033` and `V034`: children and attributes that are not optional,
|
|
1190
|
+
* and tables PowerPoint reads otherwise than they say.
|
|
1182
1191
|
*/
|
|
1183
1192
|
/** The twelve attributes of `CT_ColorMapping`. All required, no defaults. */
|
|
1184
1193
|
const CLR_MAP_ATTRIBUTES = [
|
|
@@ -1288,6 +1297,82 @@ function v031TableAttributes(ctx) {
|
|
|
1288
1297
|
}
|
|
1289
1298
|
});
|
|
1290
1299
|
}
|
|
1300
|
+
/** `ST_Guid`, in any case: C8 measured lower case opening clean. */
|
|
1301
|
+
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;
|
|
1302
|
+
const ON_OFF_STYLES = /* @__PURE__ */ new Set([
|
|
1303
|
+
"on",
|
|
1304
|
+
"off",
|
|
1305
|
+
"def"
|
|
1306
|
+
]);
|
|
1307
|
+
const TABLE_STYLE_EDGES = /* @__PURE__ */ new Set([
|
|
1308
|
+
"left",
|
|
1309
|
+
"right",
|
|
1310
|
+
"top",
|
|
1311
|
+
"bottom",
|
|
1312
|
+
"insideH",
|
|
1313
|
+
"insideV",
|
|
1314
|
+
"tl2br",
|
|
1315
|
+
"tr2bl"
|
|
1316
|
+
]);
|
|
1317
|
+
const FILL_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1318
|
+
"noFill",
|
|
1319
|
+
"solidFill",
|
|
1320
|
+
"gradFill",
|
|
1321
|
+
"blipFill",
|
|
1322
|
+
"pattFill",
|
|
1323
|
+
"grpFill"
|
|
1324
|
+
]);
|
|
1325
|
+
const drawingChildren = (element, local) => childElements(element).filter((child) => child.local === local && namespaceOf(child) === NS.a);
|
|
1326
|
+
/** A table-style id is a braced GUID, and a table style has the parts C8 measured as required. */
|
|
1327
|
+
function v033TableStyles(ctx) {
|
|
1328
|
+
for (const part of ctx.parts()) {
|
|
1329
|
+
if (ctx.contentType(part) !== CONTENT_TYPE.tableStyles) continue;
|
|
1330
|
+
const root = ctx.document(part)?.root;
|
|
1331
|
+
if (root === void 0 || root.local === "tblStyleLst" && namespaceOf(root) === NS.a) continue;
|
|
1332
|
+
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.`);
|
|
1333
|
+
}
|
|
1334
|
+
forEachElement(ctx, (part, element) => {
|
|
1335
|
+
if (namespaceOf(element) !== NS.a) return;
|
|
1336
|
+
const report = (at, what) => {
|
|
1337
|
+
ctx.add("V033", elementLocation(part, at), what + " C8 measured this as a repair prompt.");
|
|
1338
|
+
};
|
|
1339
|
+
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>.");
|
|
1340
|
+
const guid = (raw, what) => {
|
|
1341
|
+
if (!TABLE_STYLE_GUID$1.test(raw)) report(element, `${what} is "${raw}", which is not a GUID in braces.`);
|
|
1342
|
+
};
|
|
1343
|
+
switch (element.local) {
|
|
1344
|
+
case "tableStyleId":
|
|
1345
|
+
guid(textContent(element), "<a:tableStyleId>");
|
|
1346
|
+
return;
|
|
1347
|
+
case "tableStyle":
|
|
1348
|
+
case "tblStyle": {
|
|
1349
|
+
const id = attributeValue(element, "styleId");
|
|
1350
|
+
if (id === void 0) report(element, `<a:${element.local}> has no @styleId.`);
|
|
1351
|
+
else guid(id, `<a:${element.local}>/@styleId`);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
case "tblStyleLst":
|
|
1355
|
+
if (attributeValue(element, "def") === void 0) report(element, "<a:tblStyleLst> has no @def.");
|
|
1356
|
+
return;
|
|
1357
|
+
case "tcTxStyle":
|
|
1358
|
+
for (const name of ["b", "i"]) {
|
|
1359
|
+
const raw = attributeValue(element, name);
|
|
1360
|
+
if (raw !== void 0 && !ON_OFF_STYLES.has(raw)) report(element, `<a:tcTxStyle>/@${name} is "${raw}", not on, off or def.`);
|
|
1361
|
+
}
|
|
1362
|
+
return;
|
|
1363
|
+
case "tcBdr":
|
|
1364
|
+
for (const edge of childElements(element)) {
|
|
1365
|
+
if (!TABLE_STYLE_EDGES.has(edge.local) || namespaceOf(edge) !== NS.a) continue;
|
|
1366
|
+
if (drawingChildren(edge, "ln").length + drawingChildren(edge, "lnRef").length === 0) report(edge, `<a:${edge.local}> has neither <a:ln> nor <a:lnRef>.`);
|
|
1367
|
+
}
|
|
1368
|
+
return;
|
|
1369
|
+
case "tcStyle":
|
|
1370
|
+
case "tblBg":
|
|
1371
|
+
for (const fill of drawingChildren(element, "fill")) if (!childElements(fill).some((child) => FILL_PROPERTIES.has(child.local))) report(fill, "<a:fill> holds no fill.");
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1291
1376
|
/** Zero is one and a negative runs to the edge: C7's span-zero and span-negative. */
|
|
1292
1377
|
function spanOf(raw) {
|
|
1293
1378
|
if (raw === void 0) return 1;
|
|
@@ -1376,6 +1461,104 @@ function v030TableGrid(ctx) {
|
|
|
1376
1461
|
});
|
|
1377
1462
|
});
|
|
1378
1463
|
}
|
|
1464
|
+
/** Two elements alike but for their own names: the same attributes and the same children. */
|
|
1465
|
+
function sameMarkup(a, b) {
|
|
1466
|
+
const attrs = (e) => [...e.attributes].map((x) => `${x.qname}=${x.value}`).sort().join(" ");
|
|
1467
|
+
const ka = childElements(a);
|
|
1468
|
+
const kb = childElements(b);
|
|
1469
|
+
return attrs(a) === attrs(b) && textContent(a).trim() === textContent(b).trim() && ka.length === kb.length && ka.every((child, i) => child.qname === kb[i]?.qname && sameMarkup(child, kb[i]));
|
|
1470
|
+
}
|
|
1471
|
+
/** Whether a line draws nothing: an `a:noFill`, or no fill at all (C9). */
|
|
1472
|
+
function drawsNothing(line) {
|
|
1473
|
+
const fills = childElements(line).filter((c) => [
|
|
1474
|
+
"noFill",
|
|
1475
|
+
"solidFill",
|
|
1476
|
+
"gradFill",
|
|
1477
|
+
"pattFill"
|
|
1478
|
+
].includes(c.local));
|
|
1479
|
+
return fills.length === 0 || fills.some((c) => c.local === "noFill");
|
|
1480
|
+
}
|
|
1481
|
+
/** Two lines that draw alike: both draw nothing, or they are the same markup. */
|
|
1482
|
+
function alike(a, b) {
|
|
1483
|
+
return drawsNothing(a) && drawsNothing(b) || sameMarkup(a, b);
|
|
1484
|
+
}
|
|
1485
|
+
/** A cell's own `a:tcPr` line, by its local name. */
|
|
1486
|
+
function lineOf(cell, local) {
|
|
1487
|
+
const tcPr = childElements(cell).find((child) => child.local === "tcPr");
|
|
1488
|
+
return tcPr === void 0 ? void 0 : childElements(tcPr).find((child) => child.local === local);
|
|
1489
|
+
}
|
|
1490
|
+
/** The grid position across segment `k` of a cell's `side`, the cell anchored at (r, c). */
|
|
1491
|
+
function acrossOf(side, r, c, owner, k) {
|
|
1492
|
+
if (side === "lnT") return [r - 1, c + k];
|
|
1493
|
+
if (side === "lnL") return [r + k, c - 1];
|
|
1494
|
+
if (side === "lnB") return [r + owner.rows, c + k];
|
|
1495
|
+
return [r + k, c + owner.cols];
|
|
1496
|
+
}
|
|
1497
|
+
/** Each side a cell writes, the side facing it across the edge, and how a segment of it is named. */
|
|
1498
|
+
const SIDES = [
|
|
1499
|
+
{
|
|
1500
|
+
side: "lnT",
|
|
1501
|
+
facing: "lnB",
|
|
1502
|
+
across: "cols",
|
|
1503
|
+
after: true,
|
|
1504
|
+
where: "above column"
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
side: "lnL",
|
|
1508
|
+
facing: "lnR",
|
|
1509
|
+
across: "rows",
|
|
1510
|
+
after: true,
|
|
1511
|
+
where: "left of row"
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
side: "lnB",
|
|
1515
|
+
facing: "lnT",
|
|
1516
|
+
across: "cols",
|
|
1517
|
+
after: false,
|
|
1518
|
+
where: "below column"
|
|
1519
|
+
},
|
|
1520
|
+
{
|
|
1521
|
+
side: "lnR",
|
|
1522
|
+
facing: "lnL",
|
|
1523
|
+
across: "rows",
|
|
1524
|
+
after: false,
|
|
1525
|
+
where: "right of row"
|
|
1526
|
+
}
|
|
1527
|
+
];
|
|
1528
|
+
/**
|
|
1529
|
+
* A border a cell writes that PowerPoint draws from the cell across the edge: the cell before owns a
|
|
1530
|
+
* segment where its anchor is level with it, else the cell after where its is, else the cell before
|
|
1531
|
+
* (C9, ADR 0064). PowerPoint's own writer writes every side alike, so this never fires on its output.
|
|
1532
|
+
*/
|
|
1533
|
+
function v034TableBorders(ctx) {
|
|
1534
|
+
forEachElement(ctx, (part, element) => {
|
|
1535
|
+
if (element.local !== "tbl" || namespaceOf(element) !== NS.a) return;
|
|
1536
|
+
const tblGrid = childElements(element).find((child) => child.local === "tblGrid");
|
|
1537
|
+
const cols = tblGrid === void 0 ? 0 : childElements(tblGrid).filter((c) => c.local === "gridCol").length;
|
|
1538
|
+
const cells = childElements(element).filter((child) => child.local === "tr").map((row) => childElements(row).filter((child) => child.local === "tc"));
|
|
1539
|
+
const grid = owners(cells, cols);
|
|
1540
|
+
for (const [r, line] of cells.entries()) for (const [c, cell] of line.entries()) {
|
|
1541
|
+
const owner = grid[r]?.[c];
|
|
1542
|
+
if (owner?.row !== r || owner.col !== c) continue;
|
|
1543
|
+
for (const s of SIDES) {
|
|
1544
|
+
const written = lineOf(cell, s.side);
|
|
1545
|
+
if (written === void 0) continue;
|
|
1546
|
+
const reach = s.across === "cols" ? owner.cols : owner.rows;
|
|
1547
|
+
for (let k = 0; k < reach; k++) {
|
|
1548
|
+
const [nr, nc] = acrossOf(s.side, r, c, owner, k);
|
|
1549
|
+
const other = grid[nr]?.[nc];
|
|
1550
|
+
if (other === null || other === void 0) continue;
|
|
1551
|
+
const otherLevel = s.across === "cols" ? other.col === nc : other.row === nr;
|
|
1552
|
+
if (!(s.after ? otherLevel || k > 0 : k > 0 && otherLevel)) continue;
|
|
1553
|
+
const drawn = lineOf(cells[other.row][other.col], s.facing);
|
|
1554
|
+
if (drawn !== void 0 && alike(drawn, written)) continue;
|
|
1555
|
+
ctx.add("V034", elementLocation(part, written), `cell (${String(r + 1)}, ${String(c + 1)}) writes <a:${s.side}>, but ${s.where} ${String((s.across === "cols" ? nc : nr) + 1)} PowerPoint draws the <a:${s.facing}> of cell (${String(other.row + 1)}, ${String(other.col + 1)}), which writes ${drawn === void 0 ? "none" : "a different one"} - measured in C9.`);
|
|
1556
|
+
break;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1379
1562
|
/**
|
|
1380
1563
|
* Presentation-family parts only.
|
|
1381
1564
|
*
|
|
@@ -1404,22 +1587,93 @@ function forEachElement(ctx, visit) {
|
|
|
1404
1587
|
}
|
|
1405
1588
|
}
|
|
1406
1589
|
//#endregion
|
|
1590
|
+
//#region src/rules/table-style-ids.ts
|
|
1591
|
+
/**
|
|
1592
|
+
* The GUIDs of PowerPoint's 74 built-in table styles, in its gallery's order.
|
|
1593
|
+
* GENERATED by tools/ground-truth/model/tables/styles/write-tables.ts from
|
|
1594
|
+
* corpus/ground-truth/table-styles.json. Do not edit: rules.test.ts re-derives it.
|
|
1595
|
+
*/
|
|
1596
|
+
const BUILTIN_TABLE_STYLE_IDS = [
|
|
1597
|
+
"{2D5ABB26-0587-4C30-8999-92F81FD0307C}",
|
|
1598
|
+
"{3C2FFA5D-87B4-456A-9821-1D502468CF0F}",
|
|
1599
|
+
"{284E427A-3D55-4303-BF80-6455036E1DE7}",
|
|
1600
|
+
"{69C7853C-536D-4A76-A0AE-DD22124D55A5}",
|
|
1601
|
+
"{775DCB02-9BB8-47FD-8907-85C794F793BA}",
|
|
1602
|
+
"{35758FB7-9AC5-4552-8A53-C91805E547FA}",
|
|
1603
|
+
"{08FB837D-C827-4EFA-A057-4D05807E0F7C}",
|
|
1604
|
+
"{5940675A-B579-460E-94D1-54222C63F5DA}",
|
|
1605
|
+
"{D113A9D2-9D6B-4929-AA2D-F23B5EE8CBE7}",
|
|
1606
|
+
"{18603FDC-E32A-4AB5-989C-0864C3EAD2B8}",
|
|
1607
|
+
"{306799F8-075E-4A3A-A7F6-7FBC6576F1A4}",
|
|
1608
|
+
"{E269D01E-BC32-4049-B463-5C60D7B0CCD2}",
|
|
1609
|
+
"{327F97BB-C833-4FB7-BDE5-3F7075034690}",
|
|
1610
|
+
"{638B1855-1B75-4FBE-930C-398BA8C253C6}",
|
|
1611
|
+
"{9D7B26C5-4107-4FEC-AEDC-1716B250A1EF}",
|
|
1612
|
+
"{3B4B98B0-60AC-42C2-AFA5-B58CD77FA1E5}",
|
|
1613
|
+
"{0E3FDE45-AF77-4B5C-9715-49D594BDF05E}",
|
|
1614
|
+
"{C083E6E3-FA7D-4D7B-A595-EF9225AFEA82}",
|
|
1615
|
+
"{D27102A9-8310-4765-A935-A1911B00CA55}",
|
|
1616
|
+
"{5FD0F851-EC5A-4D38-B0AD-8093EC10F338}",
|
|
1617
|
+
"{68D230F3-CF80-4859-8CE7-A43EE81993B5}",
|
|
1618
|
+
"{7E9639D4-E3E2-4D34-9284-5A2195B3D0D7}",
|
|
1619
|
+
"{69012ECD-51FC-41F1-AA8D-1B2483CD663E}",
|
|
1620
|
+
"{72833802-FEF1-4C79-8D5D-14CF1EAF98D9}",
|
|
1621
|
+
"{F2DE63D5-997A-4646-A377-4702673A728D}",
|
|
1622
|
+
"{17292A2E-F333-43FB-9621-5CBBE7FDCDCB}",
|
|
1623
|
+
"{5A111915-BE36-4E01-A7E5-04B1672EAD32}",
|
|
1624
|
+
"{912C8C85-51F0-491E-9774-3900AFEF0FD7}",
|
|
1625
|
+
"{616DA210-FB5B-4158-B5E0-FEB733F419BA}",
|
|
1626
|
+
"{BC89EF96-8CEA-46FF-86C4-4CE0E7609802}",
|
|
1627
|
+
"{5DA37D80-6434-44D0-A028-1B22A696006F}",
|
|
1628
|
+
"{8799B23B-EC83-4686-B30A-512413B5E67A}",
|
|
1629
|
+
"{ED083AE6-46FA-4A59-8FB0-9F97EB10719F}",
|
|
1630
|
+
"{BDBED569-4797-4DF1-A0F4-6AAB3CD982D8}",
|
|
1631
|
+
"{E8B1032C-EA38-4F05-BA0D-38AFFFC7BED3}",
|
|
1632
|
+
"{793D81CF-94F2-401A-BA57-92F5A7B2D0C5}",
|
|
1633
|
+
"{B301B821-A1FF-4177-AEE7-76D212191A09}",
|
|
1634
|
+
"{9DCAF9ED-07DC-4A11-8D7F-57B35C25682E}",
|
|
1635
|
+
"{1FECB4D8-DB02-4DC6-A0A2-4F2EBAE1DC90}",
|
|
1636
|
+
"{1E171933-4619-4E11-9A3F-F7608DF75F80}",
|
|
1637
|
+
"{FABFCF23-3B69-468F-B69F-88F6DE6A72F2}",
|
|
1638
|
+
"{10A1B5D5-9B99-4C35-A422-299274C87663}",
|
|
1639
|
+
"{073A0DAA-6AF3-43AB-8588-CEC1D06C72B9}",
|
|
1640
|
+
"{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}",
|
|
1641
|
+
"{21E4AEA4-8DFA-4A89-87EB-49C32662AFE0}",
|
|
1642
|
+
"{F5AB1C69-6EDB-4FF4-983F-18BD219EF322}",
|
|
1643
|
+
"{00A15C55-8517-42AA-B614-E9B94910E393}",
|
|
1644
|
+
"{7DF18680-E054-41AD-8BC1-D1AEF772440D}",
|
|
1645
|
+
"{93296810-A885-4BE3-A3E7-6D5BEEA58F35}",
|
|
1646
|
+
"{8EC20E35-A176-4012-BC5E-935CFFF8708E}",
|
|
1647
|
+
"{6E25E649-3F16-4E02-A733-19D2CDBF48F0}",
|
|
1648
|
+
"{85BE263C-DBD7-4A20-BB59-AAB30ACAA65A}",
|
|
1649
|
+
"{EB344D84-9AFB-497E-A393-DC336BA19D2E}",
|
|
1650
|
+
"{EB9631B5-78F2-41C9-869B-9F39066F8104}",
|
|
1651
|
+
"{74C1A8A3-306A-4EB7-A6B1-4F7E0EB9C5D6}",
|
|
1652
|
+
"{2A488322-F2BA-4B5B-9748-0D474271808F}",
|
|
1653
|
+
"{D7AC3CCA-C797-4891-BE02-D94E43425B78}",
|
|
1654
|
+
"{69CF1AB2-1976-4502-BF36-3FF5EA218861}",
|
|
1655
|
+
"{8A107856-5554-42FB-B03E-39F5DBC370BA}",
|
|
1656
|
+
"{0505E3EF-67EA-436B-97B2-0124C06EBD24}",
|
|
1657
|
+
"{C4B1156A-380E-4F78-BDF5-A606A8083BF9}",
|
|
1658
|
+
"{22838BEF-8BB2-4498-84A7-C5851F593DF1}",
|
|
1659
|
+
"{16D9F66E-5EB9-4882-86FB-DCBF35E3C3E4}",
|
|
1660
|
+
"{E8034E78-7F5D-4C2E-B375-FC64B27BC917}",
|
|
1661
|
+
"{125E5076-3810-47DD-B79F-674D7AD40C01}",
|
|
1662
|
+
"{37CE84F3-28C3-443E-9E96-99CF82512B78}",
|
|
1663
|
+
"{D03447BB-5D67-496B-8E87-E561075AD55C}",
|
|
1664
|
+
"{E929F9F4-4A8F-4326-A1B4-22849713DDAB}",
|
|
1665
|
+
"{8FD4443E-F989-4FC4-A0C8-D5A2AF1F390B}",
|
|
1666
|
+
"{AF606853-7671-496A-8E4F-DF71F8EC918B}",
|
|
1667
|
+
"{5202B0CA-FC54-4496-8BCA-5EF66A818D29}",
|
|
1668
|
+
"{0660B408-B3CF-4A94-85FC-2B1E0A45F4A2}",
|
|
1669
|
+
"{91EBBBCC-DAD2-459C-BE2E-F6DE35CF9A28}",
|
|
1670
|
+
"{46F890A9-2807-4EBB-B81D-B2AA78EC7F39}"
|
|
1671
|
+
];
|
|
1672
|
+
//#endregion
|
|
1407
1673
|
//#region src/rules/id.ts
|
|
1408
1674
|
/**
|
|
1409
|
-
* `V018` … `V021`:
|
|
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.
|
|
1675
|
+
* `V018` … `V021` and `V032`: identifier spaces and the ids that name into them. Slide, sheet and
|
|
1676
|
+
* shape ids never share an allocator, and a table style id names one of PowerPoint's built-ins.
|
|
1423
1677
|
*/
|
|
1424
1678
|
const SLIDE_ID_MIN = 256;
|
|
1425
1679
|
const SLIDE_ID_MAX = 2147483647;
|
|
@@ -1641,6 +1895,19 @@ function v021PlaceholderIndices(ctx) {
|
|
|
1641
1895
|
}
|
|
1642
1896
|
}
|
|
1643
1897
|
}
|
|
1898
|
+
const BUILTIN_TABLE_STYLES = new Set(BUILTIN_TABLE_STYLE_IDS);
|
|
1899
|
+
/** A well-formed id; V033 reports the rest. */
|
|
1900
|
+
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;
|
|
1901
|
+
/** A table's `a:tableStyleId`, or its inline `a:tableStyle/@styleId`, names a built-in (C8). */
|
|
1902
|
+
function v032TableStyleIds(ctx) {
|
|
1903
|
+
forEachElement(ctx, (part, element) => {
|
|
1904
|
+
if (namespaceOf(element) !== NS.a) return;
|
|
1905
|
+
const named = element.local === "tableStyleId" ? textContent(element) : element.local === "tableStyle" ? attributeValue(element, "styleId") : void 0;
|
|
1906
|
+
if (named === void 0 || !TABLE_STYLE_GUID.test(named)) return;
|
|
1907
|
+
if (BUILTIN_TABLE_STYLES.has(named.toUpperCase())) return;
|
|
1908
|
+
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).`);
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1644
1911
|
//#endregion
|
|
1645
1912
|
//#region src/rules/refusal.ts
|
|
1646
1913
|
/**
|
|
@@ -2109,12 +2376,7 @@ function v029TextAndFieldIdentity(ctx) {
|
|
|
2109
2376
|
//#endregion
|
|
2110
2377
|
//#region src/validate.ts
|
|
2111
2378
|
/**
|
|
2112
|
-
*
|
|
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.
|
|
2379
|
+
* Every rule wired to its implementation; `rules.test.ts` holds the keys to `RULE_IDS`.
|
|
2118
2380
|
*/
|
|
2119
2381
|
const IMPLEMENTATIONS = {
|
|
2120
2382
|
V001: v001ContentTypeCoverage,
|
|
@@ -2147,7 +2409,10 @@ const IMPLEMENTATIONS = {
|
|
|
2147
2409
|
V028: v028OpaqueContainersUnchanged,
|
|
2148
2410
|
V029: v029TextAndFieldIdentity,
|
|
2149
2411
|
V030: v030TableGrid,
|
|
2150
|
-
V031: v031TableAttributes
|
|
2412
|
+
V031: v031TableAttributes,
|
|
2413
|
+
V032: v032TableStyleIds,
|
|
2414
|
+
V033: v033TableStyles,
|
|
2415
|
+
V034: v034TableBorders
|
|
2151
2416
|
};
|
|
2152
2417
|
/** Rules that cannot run without the archive bytes. See `Context.archive`. */
|
|
2153
2418
|
const ARCHIVE_RULES = ["V003"];
|
|
@@ -2220,20 +2485,9 @@ function run(ctx, rules) {
|
|
|
2220
2485
|
for (const id of rules) IMPLEMENTATIONS[id](ctx);
|
|
2221
2486
|
}
|
|
2222
2487
|
/**
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
*
|
|
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.
|
|
2488
|
+
* Which findings were already in the package as it was opened: the fired rules run again on
|
|
2489
|
+
* the baseline and the two sets are differenced. Only when something fatal was found, only the
|
|
2490
|
+
* rules that fired, and never the preservation rules. `report.ts` has why.
|
|
2237
2491
|
*/
|
|
2238
2492
|
function attributeOrigins(ctx, options, running) {
|
|
2239
2493
|
const baseline = options.baseline;
|