@pptx-studio/validate 0.3.0 → 0.5.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,36 @@
1
1
  # @pptx-studio/validate
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - db00af0: `tableBackground` now returns the table background's two layers, `{ fill, effects }`, each taken
8
+ from `a:tblPr` where it writes one and from the style's `tblBg` otherwise. An empty `a:effectLst` in
9
+ `a:tblPr` removes the style's effect, as PowerPoint draws it, and `themedEffects` follows an
10
+ `effectRef` into the theme. `tableCellDiagonals` gives a cell's diagonals as drawn: the anchor's
11
+ own `a:lnTlToBr` and `a:lnBlToTr` across its whole span, a covered position's never, and swapped in a
12
+ right-to-left table.
13
+
14
+ validate's `V034` also warns on every line a covered cell writes, which PowerPoint keeps on save
15
+ and never draws, and on a border written under a position no `a:tc` reached.
16
+
17
+ ## 0.4.0
18
+
19
+ ### Minor Changes
20
+
21
+ - 7d2c88e: A table cell's fill, borders and text now come from its style the way PowerPoint draws them.
22
+ `tablePartsAt` names the parts that reach a grid position, and `TABLE_PART_ORDER` is the order they
23
+ compose in. `tableCellFill`, `tableBackground` and `tableEdgeLine` resolve one property each and
24
+ name where it came from: a part, `tblBg`, `a:tcPr`, `a:tblPr` or the default grid. Merged cells and
25
+ the owner of every edge segment follow what PowerPoint drew. `tableTextLayer` gives the style's
26
+ bold, italic, faces and colour as run properties for the text cascade. `DEFAULT_GRID_LINE` is the 1-pt black line a table with no
27
+ built-in style draws. `TextContext.cell` puts a cell's list style and its style's text layer into
28
+ the text cascade, and `Origin` gains `cell` and `tableStyle`. A position or edge a table does not
29
+ have throws the new `MODEL_TABLE_POSITION`.
30
+
31
+ validate adds `V034`, a warning on a table cell that writes a border PowerPoint draws from the cell
32
+ across the edge instead, where that cell writes none or a different one.
33
+
3
34
  ## 0.3.0
4
35
 
5
36
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -306,6 +306,13 @@ export declare const RULES: readonly [{
306
306
  readonly evidence: "both";
307
307
  readonly title: "a table style id is a braced GUID, and the table-style part is one PowerPoint keeps";
308
308
  readonly why: string;
309
+ }, {
310
+ readonly id: "V034";
311
+ readonly category: "required";
312
+ readonly severity: "warning";
313
+ readonly evidence: "measured";
314
+ readonly title: "a border a cell writes is the one PowerPoint draws on that edge";
315
+ readonly why: string;
309
316
  }];
310
317
  type RuleId = (typeof RULES)[number]['id'];
311
318
  export declare function ruleById(id: string): Rule | undefined;
@@ -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
@@ -311,6 +311,14 @@ const RULES = [
311
311
  evidence: "both",
312
312
  title: "a table style id is a braced GUID, and the table-style part is one PowerPoint keeps",
313
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. A position no `a:tc` reached owns its edges as a cell writing nothing does, and a covered cell’s own lines are never drawn, though PowerPoint keeps them on save. 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."
314
322
  }
315
323
  ];
316
324
  const BY_ID = new Map(RULES.map((rule) => [rule.id, rule]));
@@ -1178,8 +1186,8 @@ function forEachElement$1(ctx, visit) {
1178
1186
  //#endregion
1179
1187
  //#region src/rules/required.ts
1180
1188
  /**
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.
1189
+ * `V013` … `V017`, `V030`, `V031`, `V033` and `V034`: children and attributes that are not optional,
1190
+ * and tables PowerPoint reads otherwise than they say.
1183
1191
  */
1184
1192
  /** The twelve attributes of `CT_ColorMapping`. All required, no defaults. */
1185
1193
  const CLR_MAP_ATTRIBUTES = [
@@ -1453,6 +1461,129 @@ function v030TableGrid(ctx) {
1453
1461
  });
1454
1462
  });
1455
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
+ /** Each line a covered cell's own `a:tcPr` writes that would draw something: none is drawn (C9). */
1491
+ function reportCovered(ctx, part, cell, [r, c], owner) {
1492
+ const tcPr = childElements(cell).find((child) => child.local === "tcPr");
1493
+ const lines = tcPr === void 0 ? [] : childElements(tcPr).filter((child) => LINE_TAGS.has(child.local) && !drawsNothing(child));
1494
+ for (const written of lines) ctx.add("V034", elementLocation(part, written), `cell (${String(r + 1)}, ${String(c + 1)}) writes <a:${written.local}>, but it is covered by the span of cell (${String(owner.row + 1)}, ${String(owner.col + 1)}), and PowerPoint never draws a covered cell’s own lines - measured in C9.`);
1495
+ }
1496
+ const LINE_TAGS = /* @__PURE__ */ new Set([
1497
+ "lnL",
1498
+ "lnR",
1499
+ "lnT",
1500
+ "lnB",
1501
+ "lnTlToBr",
1502
+ "lnBlToTr"
1503
+ ]);
1504
+ /** The grid position across segment `k` of a cell's `side`, the cell anchored at (r, c). */
1505
+ function acrossOf(side, r, c, owner, k) {
1506
+ if (side === "lnT") return [r - 1, c + k];
1507
+ if (side === "lnL") return [r + k, c - 1];
1508
+ if (side === "lnB") return [r + owner.rows, c + k];
1509
+ return [r + k, c + owner.cols];
1510
+ }
1511
+ /** Each side a cell writes, the side facing it across the edge, and how a segment of it is named. */
1512
+ const SIDES = [
1513
+ {
1514
+ side: "lnT",
1515
+ facing: "lnB",
1516
+ across: "cols",
1517
+ after: true,
1518
+ where: "above column"
1519
+ },
1520
+ {
1521
+ side: "lnL",
1522
+ facing: "lnR",
1523
+ across: "rows",
1524
+ after: true,
1525
+ where: "left of row"
1526
+ },
1527
+ {
1528
+ side: "lnB",
1529
+ facing: "lnT",
1530
+ across: "cols",
1531
+ after: false,
1532
+ where: "below column"
1533
+ },
1534
+ {
1535
+ side: "lnR",
1536
+ facing: "lnL",
1537
+ across: "rows",
1538
+ after: false,
1539
+ where: "right of row"
1540
+ }
1541
+ ];
1542
+ /**
1543
+ * A border a cell writes that PowerPoint draws from the cell or empty position across the edge, and
1544
+ * any line a covered cell writes (C9, ADR 0064). PowerPoint's own writer writes every side alike and
1545
+ * leaves a covered cell's `a:tcPr` empty, so this never fires on its output.
1546
+ */
1547
+ function v034TableBorders(ctx) {
1548
+ forEachElement(ctx, (part, element) => {
1549
+ if (element.local !== "tbl" || namespaceOf(element) !== NS.a) return;
1550
+ const tblGrid = childElements(element).find((child) => child.local === "tblGrid");
1551
+ const cols = tblGrid === void 0 ? 0 : childElements(tblGrid).filter((c) => c.local === "gridCol").length;
1552
+ const cells = childElements(element).filter((child) => child.local === "tr").map((row) => childElements(row).filter((child) => child.local === "tc"));
1553
+ const grid = owners(cells, cols);
1554
+ for (const [r, line] of cells.entries()) for (const [c, cell] of line.entries()) {
1555
+ const owner = grid[r]?.[c];
1556
+ if (owner === null || owner === void 0) continue;
1557
+ if (owner.row !== r || owner.col !== c) {
1558
+ reportCovered(ctx, part, cell, [r, c], owner);
1559
+ continue;
1560
+ }
1561
+ for (const s of SIDES) {
1562
+ const written = lineOf(cell, s.side);
1563
+ if (written === void 0) continue;
1564
+ const reach = s.across === "cols" ? owner.cols : owner.rows;
1565
+ for (let k = 0; k < reach; k++) {
1566
+ const [nr, nc] = acrossOf(s.side, r, c, owner, k);
1567
+ const other = grid[nr]?.[nc];
1568
+ if (other === void 0) continue;
1569
+ const at = other ?? {
1570
+ row: nr,
1571
+ col: nc,
1572
+ rows: 1,
1573
+ cols: 1
1574
+ };
1575
+ const otherLevel = s.across === "cols" ? at.col === nc : at.row === nr;
1576
+ if (!(s.after ? otherLevel || k > 0 : k > 0 && otherLevel)) continue;
1577
+ const drawn = other === null ? void 0 : lineOf(cells[at.row][at.col], s.facing);
1578
+ if (drawn !== void 0 && alike(drawn, written)) continue;
1579
+ const whose = other === null ? `position (${String(nr + 1)}, ${String(nc + 1)}), which no <a:tc> reached` : `cell (${String(at.row + 1)}, ${String(at.col + 1)}), which writes ` + (drawn === void 0 ? "none" : "a different one");
1580
+ 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 ${whose} - measured in C9.`);
1581
+ break;
1582
+ }
1583
+ }
1584
+ }
1585
+ });
1586
+ }
1456
1587
  /**
1457
1588
  * Presentation-family parts only.
1458
1589
  *
@@ -2305,7 +2436,8 @@ const IMPLEMENTATIONS = {
2305
2436
  V030: v030TableGrid,
2306
2437
  V031: v031TableAttributes,
2307
2438
  V032: v032TableStyleIds,
2308
- V033: v033TableStyles
2439
+ V033: v033TableStyles,
2440
+ V034: v034TableBorders
2309
2441
  };
2310
2442
  /** Rules that cannot run without the archive bytes. See `Context.archive`. */
2311
2443
  const ARCHIVE_RULES = ["V003"];