@pptx-studio/validate 0.3.0 → 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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @pptx-studio/validate
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7d2c88e: A table cell's fill, borders and text now come from its style the way PowerPoint draws them.
8
+ `tablePartsAt` names the parts that reach a grid position, and `TABLE_PART_ORDER` is the order they
9
+ compose in. `tableCellFill`, `tableBackground` and `tableEdgeLine` resolve one property each and
10
+ name where it came from: a part, `tblBg`, `a:tcPr`, `a:tblPr` or the default grid. Merged cells and
11
+ the owner of every edge segment follow what PowerPoint drew. `tableTextLayer` gives the style's
12
+ 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
13
+ built-in style draws. `TextContext.cell` puts a cell's list style and its style's text layer into
14
+ the text cascade, and `Origin` gains `cell` and `tableStyle`. A position or edge a table does not
15
+ have throws the new `MODEL_TABLE_POSITION`.
16
+
17
+ validate adds `V034`, a warning on a table cell that writes a border PowerPoint draws from the cell
18
+ across the edge instead, where that cell writes none or a different one.
19
+
3
20
  ## 0.3.0
4
21
 
5
22
  ### 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. 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,104 @@ 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
+ /** 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
+ }
1456
1562
  /**
1457
1563
  * Presentation-family parts only.
1458
1564
  *
@@ -2305,7 +2411,8 @@ const IMPLEMENTATIONS = {
2305
2411
  V030: v030TableGrid,
2306
2412
  V031: v031TableAttributes,
2307
2413
  V032: v032TableStyleIds,
2308
- V033: v033TableStyles
2414
+ V033: v033TableStyles,
2415
+ V034: v034TableBorders
2309
2416
  };
2310
2417
  /** Rules that cannot run without the archive bytes. See `Context.archive`. */
2311
2418
  const ARCHIVE_RULES = ["V003"];