@json-to-office/core-docx 3.3.0 → 4.1.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/dist/index.js CHANGED
@@ -246,6 +246,7 @@ var init_minimal_docx_theme = __esm({
246
246
  name: "minimal",
247
247
  displayName: "Minimal Clean",
248
248
  description: "A quiet, warm minimal theme \u2014 Calibri throughout, sage-green ink on ivory neutrals, a large tracked-tight bold title, wide margins and hairline rules",
249
+ whenToUse: "Quiet documents where the type carries the tone: notes, briefs, drafts and internal papers that want warmth without decoration.",
249
250
  version: "4.0.0",
250
251
  colors: {
251
252
  primary: "#2B302B",
@@ -435,6 +436,7 @@ var init_devportal_docx_theme = __esm({
435
436
  name: "devportal",
436
437
  displayName: "Field Editorial",
437
438
  description: "A compact editorial theme \u2014 Helvetica in near-black ink with a burnt-orange accent, tight condensed titles, letterspaced labels, warm tinted fills and hairline rules",
439
+ whenToUse: "Developer and product documentation, API guides and technical handbooks: dense text, code and tables that need compact, letterspaced structure.",
438
440
  version: "4.0.0",
439
441
  colors: {
440
442
  primary: "#12191F",
@@ -633,6 +635,7 @@ var init_vermilion_docx_theme = __esm({
633
635
  name: "vermilion",
634
636
  displayName: "Vermilion Editorial",
635
637
  description: "Poster-red editorial system \u2014 vermilion display headings, ink text, warm creams and hairline rules",
638
+ whenToUse: "Editorial and annual-report work that wants a strong red display voice: covers, feature-style sections, reports built around a few large headings.",
636
639
  version: "1.0.0",
637
640
  colors: {
638
641
  primary: "#282829",
@@ -851,6 +854,7 @@ var init_consulting_docx_theme = __esm({
851
854
  name: "consulting",
852
855
  displayName: "Consulting House",
853
856
  description: "The house style for client and technical reports \u2014 near-black ink, three greys and one deep-blue accent, Calibri body under Arial headings, hairline rules, no fills behind body text, safe fonts only",
857
+ whenToUse: "Client and technical reports, memos and anything a client will read as the house's own work; the default choice for a report unless the brief names a brand.",
854
858
  version: "1.0.0",
855
859
  colors: {
856
860
  primary: "#1A1F26",
@@ -11876,8 +11880,10 @@ import {
11876
11880
  } from "@json-to-office/quality";
11877
11881
  import { DEFAULT_DOCX_RENDERER_ID as DEFAULT_DOCX_RENDERER_ID2 } from "@json-to-office/shared-docx";
11878
11882
  import {
11883
+ designCanvas as designCanvas2,
11879
11884
  designColors as designColors2,
11880
- resolveDesignColor as resolveDesignColor2
11885
+ resolveDesignColor as resolveDesignColor2,
11886
+ typeScaleSizes
11881
11887
  } from "@json-to-office/shared";
11882
11888
 
11883
11889
  // src/core/generationContext.ts
@@ -12390,9 +12396,176 @@ async function expandBlocksWithPlugins(document, theme, plugins, render, preserv
12390
12396
  init_styleHelpers();
12391
12397
  init_defaults();
12392
12398
  init_widthUtils();
12399
+
12400
+ // src/quality/text-inventory.ts
12393
12401
  function asRecord(value) {
12394
12402
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
12395
12403
  }
12404
+ function twipsToPt(value) {
12405
+ return typeof value === "number" && Number.isFinite(value) ? value / 20 : void 0;
12406
+ }
12407
+ function frameOf(props) {
12408
+ const widthPt = twipsToPt(asRecord(props.floating)?.width);
12409
+ return widthPt === void 0 ? void 0 : { widthPt };
12410
+ }
12411
+ function collectDocxTextInventory(children, basePath = "/children") {
12412
+ const entries = [];
12413
+ const add = (path4, text, role, extra = {}) => {
12414
+ if (typeof text !== "string" || text.trim() === "") return;
12415
+ entries.push({
12416
+ id: `docx:text:${path4}`,
12417
+ kind: "docx/text",
12418
+ path: path4,
12419
+ text,
12420
+ role,
12421
+ order: entries.length,
12422
+ ...extra
12423
+ });
12424
+ };
12425
+ const visitCell = (cell, path4, role, inherited) => {
12426
+ const cellRole = inherited.repeats ? "chrome" : role;
12427
+ const extra = inherited.repeats ? { repeats: true } : {};
12428
+ if (typeof cell === "string") {
12429
+ add(path4, cell, cellRole, extra);
12430
+ return;
12431
+ }
12432
+ const rec = asRecord(cell);
12433
+ if (!rec) return;
12434
+ if ("content" in rec) {
12435
+ if (typeof rec.content === "string")
12436
+ add(`${path4}/content`, rec.content, cellRole, extra);
12437
+ else {
12438
+ const inner = asRecord(rec.content);
12439
+ if (inner) visitNode(inner, `${path4}/content`, inherited);
12440
+ }
12441
+ return;
12442
+ }
12443
+ if (typeof rec.name === "string") visitNode(rec, path4, inherited);
12444
+ };
12445
+ const visitNode = (node, path4, inherited) => {
12446
+ if (node.enabled === false) return;
12447
+ const props = asRecord(node.props) ?? {};
12448
+ const name = typeof node.name === "string" ? node.name : "";
12449
+ const extra = {
12450
+ ...inherited.repeats && { repeats: true }
12451
+ };
12452
+ switch (name) {
12453
+ case "heading": {
12454
+ if (inherited.repeats) {
12455
+ add(`${path4}/props/text`, props.text, "chrome", extra);
12456
+ break;
12457
+ }
12458
+ const level = typeof props.level === "number" && Number.isFinite(props.level) ? props.level : 1;
12459
+ add(`${path4}/props/text`, props.text, "heading", { ...extra, level });
12460
+ break;
12461
+ }
12462
+ case "paragraph": {
12463
+ const frame = frameOf(props);
12464
+ add(
12465
+ `${path4}/props/text`,
12466
+ props.text,
12467
+ inherited.repeats ? "chrome" : "body",
12468
+ {
12469
+ ...extra,
12470
+ ...frame && { frame }
12471
+ }
12472
+ );
12473
+ break;
12474
+ }
12475
+ case "list": {
12476
+ if (Array.isArray(props.items)) {
12477
+ props.items.forEach((item, index) => {
12478
+ const itemPath = `${path4}/props/items/${index}`;
12479
+ if (typeof item === "string")
12480
+ add(itemPath, item, "list-item", extra);
12481
+ else {
12482
+ const rec = asRecord(item);
12483
+ if (rec) add(`${itemPath}/text`, rec.text, "list-item", extra);
12484
+ }
12485
+ });
12486
+ }
12487
+ break;
12488
+ }
12489
+ case "table": {
12490
+ const columns = Array.isArray(props.columns) ? props.columns.map(asRecord) : [];
12491
+ columns.forEach((column, columnIndex) => {
12492
+ if (!column) return;
12493
+ visitCell(
12494
+ column.header,
12495
+ `${path4}/props/columns/${columnIndex}/header`,
12496
+ "table-header",
12497
+ inherited
12498
+ );
12499
+ });
12500
+ const rows = Math.max(
12501
+ 0,
12502
+ ...columns.map(
12503
+ (column) => Array.isArray(column?.cells) ? column.cells.length : 0
12504
+ )
12505
+ );
12506
+ for (let row = 0; row < rows; row++) {
12507
+ columns.forEach((column, columnIndex) => {
12508
+ if (!column || !Array.isArray(column.cells)) return;
12509
+ if (row >= column.cells.length) return;
12510
+ visitCell(
12511
+ column.cells[row],
12512
+ `${path4}/props/columns/${columnIndex}/cells/${row}`,
12513
+ "table-cell",
12514
+ inherited
12515
+ );
12516
+ });
12517
+ }
12518
+ break;
12519
+ }
12520
+ case "statistic": {
12521
+ add(`${path4}/props/number`, props.number, "statistic", extra);
12522
+ add(`${path4}/props/description`, props.description, "statistic", extra);
12523
+ break;
12524
+ }
12525
+ case "image":
12526
+ case "chart":
12527
+ case "highcharts":
12528
+ case "visual":
12529
+ case "visual-native": {
12530
+ add(`${path4}/props/caption`, props.caption, "caption", extra);
12531
+ break;
12532
+ }
12533
+ case "section": {
12534
+ for (const part of ["header", "footer"]) {
12535
+ const components = props[part];
12536
+ if (!Array.isArray(components)) continue;
12537
+ components.forEach((component, index) => {
12538
+ const rec = asRecord(component);
12539
+ if (rec) {
12540
+ visitNode(rec, `${path4}/props/${part}/${index}`, {
12541
+ repeats: true
12542
+ });
12543
+ }
12544
+ });
12545
+ }
12546
+ break;
12547
+ }
12548
+ default:
12549
+ break;
12550
+ }
12551
+ if (Array.isArray(node.children)) {
12552
+ node.children.forEach((child, index) => {
12553
+ const rec = asRecord(child);
12554
+ if (rec) visitNode(rec, `${path4}/children/${index}`, inherited);
12555
+ });
12556
+ }
12557
+ };
12558
+ children.forEach((child, index) => {
12559
+ const rec = asRecord(child);
12560
+ if (rec) visitNode(rec, `${basePath}/${index}`, {});
12561
+ });
12562
+ return entries;
12563
+ }
12564
+
12565
+ // src/quality/facts.ts
12566
+ function asRecord2(value) {
12567
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
12568
+ }
12396
12569
  function pageBox(theme, themeName, pageOverride) {
12397
12570
  const page = createSectionProperties(
12398
12571
  getColumnSettings("single"),
@@ -12418,7 +12591,7 @@ function tableFact(props, path4, availableWidthTwips) {
12418
12591
  let percentSum = 0;
12419
12592
  const explicitWidths = [];
12420
12593
  columns.forEach((column, index) => {
12421
- const width = asRecord(column)?.width;
12594
+ const width = asRecord2(column)?.width;
12422
12595
  if (typeof width === "number" && Number.isFinite(width)) {
12423
12596
  hasExplicitWidth = true;
12424
12597
  explicitWidths.push({ index, width });
@@ -12499,7 +12672,7 @@ function chartFact(node, props, path4, paletteTokens) {
12499
12672
  const categoryCount = Math.max(
12500
12673
  0,
12501
12674
  ...series.map((entry) => {
12502
- const record = asRecord(entry);
12675
+ const record = asRecord2(entry);
12503
12676
  const labels = Array.isArray(record?.labels) ? record.labels.length : 0;
12504
12677
  const values = Array.isArray(record?.values) ? record.values.length : 0;
12505
12678
  return Math.max(labels, values);
@@ -12541,8 +12714,8 @@ function statesOwnBorders(authored) {
12541
12714
  return true;
12542
12715
  }
12543
12716
  const layers = [
12544
- asRecord(authored.cellDefaults),
12545
- asRecord(authored.headerCellDefaults)
12717
+ asRecord2(authored.cellDefaults),
12718
+ asRecord2(authored.headerCellDefaults)
12546
12719
  ];
12547
12720
  if (layers.some(
12548
12721
  (layer) => layer?.borderSize !== void 0 || layer?.borderColor !== void 0
@@ -12551,8 +12724,8 @@ function statesOwnBorders(authored) {
12551
12724
  }
12552
12725
  const columns = Array.isArray(authored.columns) ? authored.columns : [];
12553
12726
  return columns.some((column) => {
12554
- const record = asRecord(column);
12555
- const defaults = asRecord(record?.cellDefaults);
12727
+ const record = asRecord2(column);
12728
+ const defaults = asRecord2(record?.cellDefaults);
12556
12729
  return defaults?.borderSize !== void 0 || defaults?.borderColor !== void 0;
12557
12730
  });
12558
12731
  }
@@ -12576,7 +12749,7 @@ function tableDesignFact(props, path4, theme, themeName, authored) {
12576
12749
  );
12577
12750
  const columns = authoredColumns.map(
12578
12751
  (column, index) => {
12579
- const authoredColumn = asRecord(column) ?? {};
12752
+ const authoredColumn = asRecord2(column) ?? {};
12580
12753
  const cells = Array.isArray(authoredColumn.cells) ? authoredColumn.cells : [];
12581
12754
  const alignments = /* @__PURE__ */ new Set();
12582
12755
  const values = [];
@@ -12595,11 +12768,11 @@ function tableDesignFact(props, path4, theme, themeName, authored) {
12595
12768
  },
12596
12769
  values,
12597
12770
  alignment: alignments.size === 1 ? [...alignments][0] : alignments.size === 0 ? "left" : "mixed",
12598
- hasCellDefaults: asRecord(authoredColumn.cellDefaults) !== void 0,
12599
- hasHeader: asRecord(authoredColumn.header) !== void 0,
12771
+ hasCellDefaults: asRecord2(authoredColumn.cellDefaults) !== void 0,
12772
+ hasHeader: asRecord2(authoredColumn.header) !== void 0,
12600
12773
  generated: false,
12601
12774
  cellsWithOwnAlignment: cells.flatMap((cell, cellIndex) => {
12602
- const alignment2 = asRecord(cell)?.horizontalAlignment;
12775
+ const alignment2 = asRecord2(cell)?.horizontalAlignment;
12603
12776
  return typeof alignment2 === "string" && DOCX_ALIGNMENTS.has(alignment2) && alignment2 !== "right" ? [cellIndex] : [];
12604
12777
  })
12605
12778
  };
@@ -12620,7 +12793,7 @@ function finiteNumber(value) {
12620
12793
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
12621
12794
  }
12622
12795
  function lineHeightPt(fontSizePt, spacing) {
12623
- const rule = asRecord(spacing);
12796
+ const rule = asRecord2(spacing);
12624
12797
  const value = finiteNumber(rule?.value);
12625
12798
  switch (rule?.type) {
12626
12799
  // `exactly` is an absolute line box, and display type routinely sets it
@@ -12637,7 +12810,7 @@ function lineHeightPt(fontSizePt, spacing) {
12637
12810
  }
12638
12811
  }
12639
12812
  function characterSpacingPt(spacing) {
12640
- const rule = asRecord(spacing);
12813
+ const rule = asRecord2(spacing);
12641
12814
  const value = finiteNumber(rule?.value);
12642
12815
  if (rule === void 0 || value === void 0) return 0;
12643
12816
  return rule.type === "condensed" ? -value / TWIPS_PER_POINT3 : value / TWIPS_PER_POINT3;
@@ -12651,7 +12824,7 @@ function nodeAtPointer(root, pointer) {
12651
12824
  current = current[index];
12652
12825
  continue;
12653
12826
  }
12654
- const record = asRecord(current);
12827
+ const record = asRecord2(current);
12655
12828
  if (!record) return void 0;
12656
12829
  current = record[token];
12657
12830
  }
@@ -12667,10 +12840,10 @@ function styleKey(node, props) {
12667
12840
  return typeof props.themeStyle === "string" && props.themeStyle !== "" ? props.themeStyle : "normal";
12668
12841
  }
12669
12842
  function effectiveFontSize(node, props, typography) {
12670
- const authored = finiteNumber(asRecord(props.font)?.size);
12843
+ const authored = finiteNumber(asRecord2(props.font)?.size);
12671
12844
  if (authored !== void 0) return { fontSizePt: authored, authored: true };
12672
12845
  const { styles, theme } = typography;
12673
- const style = asRecord(styles[styleKey(node, props)]) ?? asRecord(styles.normal);
12846
+ const style = asRecord2(styles[styleKey(node, props)]) ?? asRecord2(styles.normal);
12674
12847
  const stated = finiteNumber(style?.size);
12675
12848
  if (stated !== void 0) return { fontSizePt: stated, authored: false };
12676
12849
  const reference = style?.font === "heading" || style?.font === "body" || style?.font === "mono" || style?.font === "light" ? style.font : void 0;
@@ -12680,11 +12853,11 @@ function effectiveFontSize(node, props, typography) {
12680
12853
  function pinnedLineBox(props, path4) {
12681
12854
  const candidates = [
12682
12855
  [props.lineSpacing, `${path4}/props/lineSpacing`],
12683
- [asRecord(props.font)?.lineSpacing, `${path4}/props/font/lineSpacing`]
12856
+ [asRecord2(props.font)?.lineSpacing, `${path4}/props/font/lineSpacing`]
12684
12857
  ];
12685
12858
  for (const [spacing, pointer] of candidates) {
12686
12859
  if (spacing === void 0) continue;
12687
- const rule = asRecord(spacing);
12860
+ const rule = asRecord2(spacing);
12688
12861
  if (rule?.type !== "exactly") return void 0;
12689
12862
  const lineBoxPt = finiteNumber(rule.value);
12690
12863
  return lineBoxPt === void 0 ? void 0 : { lineBoxPt, pointer };
@@ -12710,8 +12883,8 @@ function lineBoxFact(node, props, path4, typography, authored) {
12710
12883
  };
12711
12884
  }
12712
12885
  function frameSignature(floating) {
12713
- const horizontal = asRecord(floating.horizontalPosition);
12714
- const vertical = asRecord(floating.verticalPosition);
12886
+ const horizontal = asRecord2(floating.horizontalPosition);
12887
+ const vertical = asRecord2(floating.verticalPosition);
12715
12888
  return JSON.stringify([
12716
12889
  floating.width ?? null,
12717
12890
  floating.height ?? null,
@@ -12721,24 +12894,24 @@ function frameSignature(floating) {
12721
12894
  vertical?.offset ?? null,
12722
12895
  vertical?.relative ?? null,
12723
12896
  vertical?.align ?? null,
12724
- asRecord(floating.wrap)?.type ?? null
12897
+ asRecord2(floating.wrap)?.type ?? null
12725
12898
  ]);
12726
12899
  }
12727
12900
  function frameTextFact(props, path4, page, frameChainId, flowIndex) {
12728
- const floating = asRecord(props.floating);
12901
+ const floating = asRecord2(props.floating);
12729
12902
  const frameWidthTwips = finiteNumber(floating?.width);
12730
12903
  if (frameWidthTwips === void 0) return void 0;
12731
12904
  const text = typeof props.text === "string" ? props.text : "";
12732
12905
  if (text.trim() === "") return void 0;
12733
- const font = asRecord(props.font);
12906
+ const font = asRecord2(props.font);
12734
12907
  const fontSizePt = finiteNumber(font?.size);
12735
12908
  if (fontSizePt === void 0) return void 0;
12736
12909
  const longestWord = text.split(/\s+/).reduce(
12737
12910
  (longest, word) => word.length > longest.length ? word : longest,
12738
12911
  ""
12739
12912
  );
12740
- const horizontal = asRecord(floating?.horizontalPosition);
12741
- const vertical = asRecord(floating?.verticalPosition);
12913
+ const horizontal = asRecord2(floating?.horizontalPosition);
12914
+ const vertical = asRecord2(floating?.verticalPosition);
12742
12915
  const offsetX = finiteNumber(horizontal?.offset);
12743
12916
  const offsetY = finiteNumber(vertical?.offset);
12744
12917
  const absolutelyPinned = horizontal?.offset !== void 0 || vertical?.offset !== void 0;
@@ -12809,7 +12982,7 @@ function svgTextFacts(props, path4) {
12809
12982
  return facts;
12810
12983
  }
12811
12984
  function walkActive(node, path4, page, visit) {
12812
- const rec = asRecord(node);
12985
+ const rec = asRecord2(node);
12813
12986
  if (!rec || rec.enabled === false) return;
12814
12987
  visit(rec, path4, page);
12815
12988
  const children = Array.isArray(rec.children) ? rec.children : [];
@@ -12817,6 +12990,93 @@ function walkActive(node, path4, page, visit) {
12817
12990
  (child, index) => walkActive(child, `${path4}/children/${index}`, page, visit)
12818
12991
  );
12819
12992
  }
12993
+ function textSizeFact(node, props, path4, typography, role) {
12994
+ const size = effectiveFontSize(node, props, typography);
12995
+ if (size === void 0) return void 0;
12996
+ return {
12997
+ id: `docx:text-size:${path4}`,
12998
+ kind: "docx/text-size",
12999
+ path: path4,
13000
+ role: role ?? styleKey(node, props),
13001
+ fontSizePt: size.fontSizePt,
13002
+ authored: size.authored,
13003
+ ...size.authored && { sizePath: `${path4}/props/font/size` }
13004
+ };
13005
+ }
13006
+ function tableTextSizeFacts(props, authored, path4, typography, page) {
13007
+ const facts = [];
13008
+ const sizeOf = (holder) => finiteNumber(asRecord2(asRecord2(holder)?.font)?.size);
13009
+ const authoredSize = (holder, pointer, role) => {
13010
+ const size = sizeOf(holder);
13011
+ if (size === void 0) return;
13012
+ facts.push({
13013
+ id: `docx:text-size:${pointer}`,
13014
+ kind: "docx/text-size",
13015
+ path: pointer,
13016
+ role,
13017
+ fontSizePt: size,
13018
+ authored: true,
13019
+ sizePath: `${pointer}/font/size`
13020
+ });
13021
+ };
13022
+ const cellContent = (holder, pointer, role) => {
13023
+ const content = asRecord2(holder)?.content;
13024
+ if (asRecord2(content) === void 0) return;
13025
+ walkActive(content, `${pointer}/content`, page, (node, nodePath) => {
13026
+ if (node.name !== "paragraph" && node.name !== "heading") return;
13027
+ const nodeProps = asRecord2(node.props) ?? {};
13028
+ if (typeof nodeProps.text !== "string" || nodeProps.text.trim() === "")
13029
+ return;
13030
+ const fact = textSizeFact(node, nodeProps, nodePath, typography, role);
13031
+ if (fact) facts.push(fact);
13032
+ });
13033
+ };
13034
+ if (authored) {
13035
+ authoredSize(
13036
+ authored.cellDefaults,
13037
+ `${path4}/props/cellDefaults`,
13038
+ "tableCell"
13039
+ );
13040
+ authoredSize(
13041
+ authored.headerCellDefaults,
13042
+ `${path4}/props/headerCellDefaults`,
13043
+ "tableHeader"
13044
+ );
13045
+ const columns = Array.isArray(authored.columns) ? authored.columns : [];
13046
+ columns.forEach((column, index) => {
13047
+ const record = asRecord2(column) ?? {};
13048
+ const columnPath = `${path4}/props/columns/${index}`;
13049
+ authoredSize(
13050
+ record.cellDefaults,
13051
+ `${columnPath}/cellDefaults`,
13052
+ "tableCell"
13053
+ );
13054
+ authoredSize(record.header, `${columnPath}/header`, "tableHeader");
13055
+ cellContent(record.header, `${columnPath}/header`, "tableHeader");
13056
+ const cells = Array.isArray(record.cells) ? record.cells : [];
13057
+ cells.forEach((cell, cellIndex) => {
13058
+ authoredSize(cell, `${columnPath}/cells/${cellIndex}`, "tableCell");
13059
+ cellContent(cell, `${columnPath}/cells/${cellIndex}`, "tableCell");
13060
+ });
13061
+ });
13062
+ }
13063
+ for (const [key, role] of [
13064
+ ["cellDefaults", "tableCell"],
13065
+ ["headerCellDefaults", "tableHeader"]
13066
+ ]) {
13067
+ const size = sizeOf(props[key]);
13068
+ if (size === void 0) continue;
13069
+ facts.push({
13070
+ id: `docx:text-size:${path4}:${role}`,
13071
+ kind: "docx/text-size",
13072
+ path: path4,
13073
+ role,
13074
+ fontSizePt: size,
13075
+ authored: false
13076
+ });
13077
+ }
13078
+ return facts;
13079
+ }
12820
13080
  function prepareDocxQualityDocument(document, options = {}) {
12821
13081
  const themed = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
12822
13082
  customThemes: options.customThemes,
@@ -12881,11 +13141,48 @@ function prepareDocxQualityDocument(document, options = {}) {
12881
13141
  let inherited = {};
12882
13142
  topLevel.forEach((node, index) => {
12883
13143
  if (node?.name !== "section") return;
12884
- const props = asRecord(node.props) ?? {};
13144
+ const props = asRecord2(node.props) ?? {};
12885
13145
  const part = (kind) => props[kind] === "linkToPrevious" ? inherited[kind] : props[kind];
12886
13146
  const header = part("header");
12887
13147
  const footer = part("footer");
12888
13148
  inherited = { header, footer };
13149
+ for (const kind of ["header", "footer"]) {
13150
+ if (!Array.isArray(props[kind])) continue;
13151
+ const drawnByBlock = !Array.isArray(
13152
+ asRecord2(
13153
+ asRecord2(
13154
+ (Array.isArray(themed.document.children) ? themed.document.children : [])[index]
13155
+ )?.props
13156
+ )?.[kind]
13157
+ );
13158
+ const part2 = props[kind];
13159
+ const visitChrome = (child, childPath) => {
13160
+ if (child.name !== "paragraph" && child.name !== "heading") return;
13161
+ const childProps = asRecord2(child.props) ?? {};
13162
+ if (typeof childProps.text !== "string" || childProps.text.trim() === "")
13163
+ return;
13164
+ const fact = textSizeFact(
13165
+ child,
13166
+ childProps,
13167
+ childPath,
13168
+ typography,
13169
+ kind
13170
+ );
13171
+ if (fact)
13172
+ addFact({
13173
+ ...fact,
13174
+ generated: drawnByBlock || authoredPath(childPath) !== childPath
13175
+ });
13176
+ };
13177
+ part2.forEach(
13178
+ (child, childIndex) => walkActive(
13179
+ child,
13180
+ `/children/${index}/props/${kind}/${childIndex}`,
13181
+ basePage,
13182
+ visitChrome
13183
+ )
13184
+ );
13185
+ }
12889
13186
  addFact({
12890
13187
  id: `docx:section-chrome:${index}`,
12891
13188
  kind: "docx/section-chrome",
@@ -12918,13 +13215,35 @@ function prepareDocxQualityDocument(document, options = {}) {
12918
13215
  const paletteTokens = resolved.theme.palette?.chart?.map(
12919
13216
  (value) => `#${resolveDesignColor2(value, visualColors)}`
12920
13217
  ) ?? SERIES_COLOR_TOKENS.filter((token) => paletteHexes[token] !== void 0);
12921
- const authoredPropsAt = (pointer) => asRecord(asRecord(nodeAtPointer(context.document, pointer))?.props);
13218
+ const authoredPropsAt = (pointer) => asRecord2(asRecord2(nodeAtPointer(context.document, pointer))?.props);
13219
+ const roleSizesPt = {};
13220
+ for (const key of Object.keys(typography.styles)) {
13221
+ const size = effectiveFontSize(
13222
+ { name: "paragraph" },
13223
+ { themeStyle: key },
13224
+ typography
13225
+ );
13226
+ if (size) roleSizesPt[key] = size.fontSizePt;
13227
+ }
13228
+ const scale = resolved.theme.typography?.scale?.[designCanvas2("docx", resolved.theme.page?.size)];
13229
+ const typeScalePt = [
13230
+ .../* @__PURE__ */ new Set([
13231
+ ...Object.values(roleSizesPt),
13232
+ ...["heading", "body", "mono", "light"].flatMap((role) => {
13233
+ const size = resolveFontSize(resolved.theme, role);
13234
+ return size === void 0 ? [] : [size];
13235
+ }),
13236
+ ...scale ? typeScaleSizes(scale) : []
13237
+ ])
13238
+ ].sort((a, b) => a - b);
12922
13239
  addFact({
12923
13240
  id: "docx:theme",
12924
13241
  kind: "docx/theme",
12925
13242
  path: "/props",
12926
13243
  themeName: context.themeName,
12927
13244
  paletteHexes,
13245
+ typeScalePt,
13246
+ roleSizesPt,
12928
13247
  // `heading` and `body` only. A theme also names `mono` and `light`, but
12929
13248
  // those paint nothing until a component asks for them — counting an
12930
13249
  // unused `Courier New` against a document's family budget would flag a
@@ -12932,7 +13251,7 @@ function prepareDocxQualityDocument(document, options = {}) {
12932
13251
  fontFamilies: [
12933
13252
  ...new Set(
12934
13253
  ["heading", "body"].flatMap((role) => {
12935
- const family = asRecord(
13254
+ const family = asRecord2(
12936
13255
  resolved.theme.fonts?.[role]
12937
13256
  )?.family;
12938
13257
  return typeof family === "string" && family.trim() !== "" ? [family] : [];
@@ -12968,15 +13287,27 @@ function prepareDocxQualityDocument(document, options = {}) {
12968
13287
  excerpt: occurrence.match.excerpt
12969
13288
  });
12970
13289
  });
13290
+ for (const entry of collectDocxTextInventory(resolved.children)) {
13291
+ addFact({ ...entry, generated: authoredPath(entry.path) !== entry.path });
13292
+ }
12971
13293
  let previousVisitPath;
12972
13294
  let lastFrame;
12973
13295
  let flowIndex = 0;
12974
13296
  const parentOf = (path4) => path4.slice(0, path4.lastIndexOf("/"));
12975
13297
  const visit = (node, path4, page) => {
12976
- const props = asRecord(node.props) ?? {};
13298
+ const props = asRecord2(node.props) ?? {};
12977
13299
  if (node.name === "table") {
12978
13300
  const fact = tableFact(props, path4, page.availableWidthTwips);
12979
13301
  if (fact) addFact(fact);
13302
+ for (const size of tableTextSizeFacts(
13303
+ props,
13304
+ authoredPropsAt(path4),
13305
+ path4,
13306
+ typography,
13307
+ page
13308
+ )) {
13309
+ addFact({ ...size, generated: authoredPath(size.path) !== size.path });
13310
+ }
12980
13311
  const design = tableDesignFact(
12981
13312
  props,
12982
13313
  path4,
@@ -12993,7 +13324,7 @@ function prepareDocxQualityDocument(document, options = {}) {
12993
13324
  return {
12994
13325
  ...column,
12995
13326
  path: authored,
12996
- generated: table2 === void 0 || asRecord(nodeAtPointer(themed.document, table2))?.name !== "table"
13327
+ generated: table2 === void 0 || asRecord2(nodeAtPointer(themed.document, table2))?.name !== "table"
12997
13328
  };
12998
13329
  })
12999
13330
  });
@@ -13009,7 +13340,7 @@ function prepareDocxQualityDocument(document, options = {}) {
13009
13340
  addFact({
13010
13341
  ...fact,
13011
13342
  generated: !["chart", "highcharts"].includes(
13012
- String(asRecord(nodeAtPointer(themed.document, authored))?.name)
13343
+ String(asRecord2(nodeAtPointer(themed.document, authored))?.name)
13013
13344
  ),
13014
13345
  seriesColorsPath: authoredPath(fact.seriesColorsPath),
13015
13346
  ...fact.annotation && {
@@ -13023,7 +13354,7 @@ function prepareDocxQualityDocument(document, options = {}) {
13023
13354
  }
13024
13355
  }
13025
13356
  if (node.name === "paragraph" || node.name === "text-box") {
13026
- const floating = asRecord(props.floating);
13357
+ const floating = asRecord2(props.floating);
13027
13358
  if (floating) {
13028
13359
  const signature = frameSignature(floating);
13029
13360
  const chainId = lastFrame !== void 0 && previousVisitPath === lastFrame.path && parentOf(lastFrame.path) === parentOf(path4) && lastFrame.signature === signature ? lastFrame.chainId : `docx:frame-chain:${path4}`;
@@ -13035,6 +13366,10 @@ function prepareDocxQualityDocument(document, options = {}) {
13035
13366
  if (node.name === "paragraph" || node.name === "heading") {
13036
13367
  const fact = lineBoxFact(node, props, path4, typography, context.document);
13037
13368
  if (fact) addFact(fact);
13369
+ if (typeof props.text === "string" && props.text.trim() !== "") {
13370
+ const fact2 = textSizeFact(node, props, path4, typography);
13371
+ if (fact2) addFact({ ...fact2, generated: authoredPath(path4) !== path4 });
13372
+ }
13038
13373
  }
13039
13374
  if (node.name === "image" || node.name === "visual") {
13040
13375
  for (const fact of svgTextFacts(props, path4)) addFact(fact);
@@ -13607,6 +13942,7 @@ function numberParameter(parameters, name, fallback) {
13607
13942
  }
13608
13943
  var docxTableWidthRule = {
13609
13944
  id: "docx/table-width",
13945
+ description: "Explicit column widths that sum past the usable width of their section.",
13610
13946
  code: QUALITY_CODES.TABLE_WIDTH_OVERFLOW,
13611
13947
  category: "integrity",
13612
13948
  defaultSeverity: "warning",
@@ -13654,6 +13990,7 @@ var docxTableWidthRule = {
13654
13990
  };
13655
13991
  var docxHeadingHierarchyRule = {
13656
13992
  id: "docx/heading-hierarchy",
13993
+ description: "A heading that skips a level and breaks the outline.",
13657
13994
  code: QUALITY_CODES.HEADING_SKIP,
13658
13995
  category: "hierarchy",
13659
13996
  defaultSeverity: "info",
@@ -13697,6 +14034,7 @@ function frameTextFacts(facts) {
13697
14034
  }
13698
14035
  var docxTextFitRule = {
13699
14036
  id: "docx/text-fit",
14037
+ description: "A word too wide for its floating frame, or a frame whose wrapped text runs off the sheet.",
13700
14038
  code: QUALITY_CODES.TEXT_OVERFLOW,
13701
14039
  category: "integrity",
13702
14040
  defaultSeverity: "warning",
@@ -13770,6 +14108,7 @@ var docxTextFitRule = {
13770
14108
  var FRAME_COLLISION_MIN_WIDTH_TWIPS = 240;
13771
14109
  var docxFrameCollisionRule = {
13772
14110
  id: "docx/frame-collision",
14111
+ description: "Two page-anchored frames whose estimated text lands on the same region of a page.",
13773
14112
  code: QUALITY_CODES.FRAME_COLLISION,
13774
14113
  category: "integrity",
13775
14114
  defaultSeverity: "warning",
@@ -13866,6 +14205,7 @@ var docxFrameCollisionRule = {
13866
14205
  };
13867
14206
  var docxSvgTextBoundsRule = {
13868
14207
  id: "docx/svg-text-bounds",
14208
+ description: "A text baseline outside an inline SVG\u2019s viewBox, so the words are never painted.",
13869
14209
  code: QUALITY_CODES.SVG_TEXT_CLIPPED,
13870
14210
  category: "integrity",
13871
14211
  defaultSeverity: "warning",
@@ -13901,6 +14241,7 @@ function tenths(value) {
13901
14241
  }
13902
14242
  var docxLineBoxRule = {
13903
14243
  id: "docx/line-box",
14244
+ description: "An `exactly` line box shorter than the capitals it has to hold.",
13904
14245
  code: QUALITY_CODES.LINE_BOX_COLLAPSE,
13905
14246
  category: "legibility",
13906
14247
  defaultSeverity: "warning",
@@ -13954,6 +14295,7 @@ var docxLineBoxRule = {
13954
14295
  };
13955
14296
  var docxPlaceholderRule = {
13956
14297
  id: "docx/placeholder-text",
14298
+ description: "An unfilled scaffold slot, or leftover filler copy.",
13957
14299
  code: QUALITY_CODES.PLACEHOLDER_TEXT,
13958
14300
  category: "integrity",
13959
14301
  defaultSeverity: "warning",
@@ -13972,6 +14314,7 @@ var docxPlaceholderRule = {
13972
14314
  };
13973
14315
  var docxSlotBudgetRule = {
13974
14316
  id: "docx/slot-budget",
14317
+ description: "A block slot holding more words than its budget allows \u2014 a takeaway past the word count the block sets.",
13975
14318
  code: QUALITY_CODES.SLOT_BUDGET,
13976
14319
  category: "composition",
13977
14320
  defaultSeverity: "warning",
@@ -13995,6 +14338,7 @@ var docxSlotBudgetRule = {
13995
14338
  var DEFAULT_MAX_FONT_FAMILIES = 3;
13996
14339
  var docxFontCountRule = {
13997
14340
  id: "docx/font-count",
14341
+ description: "Distinct font families the document can paint.",
13998
14342
  code: QUALITY_CODES.FONT_COUNT,
13999
14343
  category: "brand",
14000
14344
  defaultSeverity: "warning",
@@ -14031,6 +14375,7 @@ var docxFontCountRule = {
14031
14375
  };
14032
14376
  var docxPaletteRule = {
14033
14377
  id: "docx/palette-adherence",
14378
+ description: "A literal colour the resolved theme does not define.",
14034
14379
  code: QUALITY_CODES.OFF_PALETTE,
14035
14380
  category: "brand",
14036
14381
  defaultSeverity: "info",
@@ -14056,6 +14401,7 @@ var docxPaletteRule = {
14056
14401
  var DEFAULT_MAX_TABLE_ROWS_PER_PAGE = 25;
14057
14402
  var docxChartRule = {
14058
14403
  id: "docx/chart-design",
14404
+ description: "What a chart claims about its numbers: the comparison, the palette, the unit and the caption.",
14059
14405
  code: QUALITY_CODES.CHART_OVERLOADED,
14060
14406
  category: "information-design",
14061
14407
  defaultSeverity: "warning",
@@ -14097,6 +14443,7 @@ function seriesColorFix(fact) {
14097
14443
  }
14098
14444
  var docxTableDesignRule = {
14099
14445
  id: "docx/table-design",
14446
+ description: "How a table lays its numbers out: alignment, rounding, rules and length.",
14100
14447
  code: QUALITY_CODES.TABLE_NUMERIC_ALIGN,
14101
14448
  category: "information-design",
14102
14449
  defaultSeverity: "warning",
@@ -14154,6 +14501,7 @@ function stringListParameter(parameters, name) {
14154
14501
  }
14155
14502
  var docxRequiredChromeRule = {
14156
14503
  id: "docx/required-chrome",
14504
+ description: "A block slot with a role a profile or policy requires \u2014 a takeaway, a source \u2014 left empty. Off until one names roles.",
14157
14505
  code: QUALITY_CODES.CHROME_MISSING,
14158
14506
  category: "consistency",
14159
14507
  defaultSeverity: "warning",
@@ -14177,6 +14525,7 @@ var docxRequiredChromeRule = {
14177
14525
  var SECTION_CHROME_PARTS = ["header", "footer", "pageNumber"];
14178
14526
  var docxRunningHeadRule = {
14179
14527
  id: "docx/running-head",
14528
+ description: "A body section without the running head a profile or policy expects. Off until one names parts.",
14180
14529
  code: QUALITY_CODES.CHROME_MISSING,
14181
14530
  category: "consistency",
14182
14531
  defaultSeverity: "warning",
@@ -14209,6 +14558,170 @@ var docxRunningHeadRule = {
14209
14558
  });
14210
14559
  }
14211
14560
  };
14561
+ var SIZE_TOLERANCE_PT = 0.25;
14562
+ function themeFact(facts) {
14563
+ return facts.find(
14564
+ (fact) => fact.kind === "docx/theme"
14565
+ );
14566
+ }
14567
+ function textSizeFacts(facts) {
14568
+ return facts.filter(
14569
+ (fact) => fact.kind === "docx/text-size"
14570
+ );
14571
+ }
14572
+ function roleDriftFacts(facts, theme) {
14573
+ const byRole = /* @__PURE__ */ new Map();
14574
+ for (const fact of textSizeFacts(facts)) {
14575
+ byRole.set(fact.role, [...byRole.get(fact.role) ?? [], fact]);
14576
+ }
14577
+ const drifting = /* @__PURE__ */ new Map();
14578
+ for (const [role, members] of byRole) {
14579
+ const sizes = [...new Set(members.map((fact) => fact.fontSizePt))].sort(
14580
+ (a, b) => a - b
14581
+ );
14582
+ if (sizes.length < 2) continue;
14583
+ const expected = theme?.roleSizesPt[role];
14584
+ if (expected === void 0) continue;
14585
+ for (const fact of members) {
14586
+ if (fact.sizePath !== void 0 && !fact.generated && Math.abs(fact.fontSizePt - expected) > SIZE_TOLERANCE_PT)
14587
+ drifting.set(fact, { expected, sizes });
14588
+ }
14589
+ }
14590
+ return drifting;
14591
+ }
14592
+ function nearestSize(size, scale) {
14593
+ let best;
14594
+ for (const candidate of scale) {
14595
+ if (best === void 0 || Math.abs(candidate - size) < Math.abs(best - size))
14596
+ best = candidate;
14597
+ }
14598
+ return best;
14599
+ }
14600
+ var docxTypeScaleRule = {
14601
+ id: "docx/type-scale",
14602
+ description: "An authored size the theme never paints: not a style, not a font role, not a step of its scale. Off until a profile or policy enables it.",
14603
+ code: QUALITY_CODES.TYPE_OFF_SCALE,
14604
+ category: "consistency",
14605
+ defaultSeverity: "warning",
14606
+ defaultCertainty: "deterministic",
14607
+ formats: ["docx"],
14608
+ defaultEnabled: false,
14609
+ evaluate: ({ facts }) => {
14610
+ const theme = themeFact(facts);
14611
+ const scale = theme?.typeScalePt ?? [];
14612
+ if (scale.length === 0) return [];
14613
+ const drifting = roleDriftFacts(facts, theme);
14614
+ const offScale = textSizeFacts(facts).filter(
14615
+ (fact) => fact.sizePath !== void 0 && !fact.generated && !drifting.has(fact) && !scale.some(
14616
+ (size) => Math.abs(size - fact.fontSizePt) <= SIZE_TOLERANCE_PT
14617
+ )
14618
+ );
14619
+ const groups = /* @__PURE__ */ new Map();
14620
+ for (const fact of offScale) {
14621
+ const key = `${fact.role}@${fact.fontSizePt}`;
14622
+ groups.set(key, [...groups.get(key) ?? [], fact]);
14623
+ }
14624
+ return [...groups.values()].map((members) => {
14625
+ const [first] = members;
14626
+ const nearest = nearestSize(first.fontSizePt, scale);
14627
+ const sizePaths = members.map((fact) => fact.sizePath);
14628
+ const count = members.length === 1 ? "" : ` (${members.length} places, patched together)`;
14629
+ return {
14630
+ path: sizePaths[0],
14631
+ ...sizePaths.length > 1 && { relatedPaths: sizePaths.slice(1) },
14632
+ message: `${first.fontSizePt}pt is not a size the ${theme.themeName} theme paints; the nearest on its scale is ${nearest}pt${count}.`,
14633
+ suggestion: `Use ${nearest}pt, or drop the size and let the "${first.role}" style set it.`,
14634
+ context: { role: first.role, scale, paths: sizePaths },
14635
+ evidence: {
14636
+ actual: first.fontSizePt,
14637
+ expected: nearest,
14638
+ unit: "pt",
14639
+ values: { source: "theme" }
14640
+ },
14641
+ fixes: sizePaths.map((path4) => ({
14642
+ op: "replace",
14643
+ path: path4,
14644
+ value: nearest
14645
+ }))
14646
+ };
14647
+ });
14648
+ }
14649
+ };
14650
+ var docxSizeCountRule = {
14651
+ id: "docx/size-count",
14652
+ description: "More distinct text sizes than maximumSizes allows, blocks included. Off until a profile or policy enables it.",
14653
+ code: QUALITY_CODES.TYPE_SIZE_COUNT,
14654
+ category: "consistency",
14655
+ defaultSeverity: "warning",
14656
+ defaultCertainty: "deterministic",
14657
+ formats: ["docx"],
14658
+ defaultEnabled: false,
14659
+ defaultParameters: { maximumSizes: 8 },
14660
+ evaluate: ({ facts, configuration, profile }) => {
14661
+ const maximum = numberParameter(
14662
+ configuration.parameters,
14663
+ "maximumSizes",
14664
+ 8
14665
+ );
14666
+ const firstPathBySize = /* @__PURE__ */ new Map();
14667
+ for (const fact of textSizeFacts(facts)) {
14668
+ const size = Math.round(fact.fontSizePt * 4) / 4;
14669
+ if (!firstPathBySize.has(size)) firstPathBySize.set(size, fact.path);
14670
+ }
14671
+ if (firstPathBySize.size <= maximum) return [];
14672
+ const sizes = [...firstPathBySize.keys()].sort((a, b) => a - b);
14673
+ return [
14674
+ {
14675
+ path: themeFact(facts)?.path ?? "/props",
14676
+ relatedPaths: sizes.map((size) => firstPathBySize.get(size)),
14677
+ message: `The document paints ${sizes.length} distinct text sizes (${sizes.join(", ")}pt); the ${profile?.id ?? "selected"} profile allows ${maximum}.`,
14678
+ suggestion: "Keep to the theme styles \u2014 title, headings, body, label, source \u2014 and drop the ad-hoc sizes.",
14679
+ context: { sizes, maximum },
14680
+ evidence: {
14681
+ actual: sizes.length,
14682
+ expected: maximum,
14683
+ values: { source: "profile" }
14684
+ }
14685
+ }
14686
+ ];
14687
+ }
14688
+ };
14689
+ var docxRoleDriftRule = {
14690
+ id: "docx/role-drift",
14691
+ description: "One heading level, table role or paragraph style painted at two sizes; the theme size is the fix. Off until a profile or policy enables it.",
14692
+ code: QUALITY_CODES.TYPE_ROLE_DRIFT,
14693
+ category: "consistency",
14694
+ defaultSeverity: "warning",
14695
+ defaultCertainty: "deterministic",
14696
+ formats: ["docx"],
14697
+ defaultEnabled: false,
14698
+ evaluate: ({ facts }) => {
14699
+ const theme = themeFact(facts);
14700
+ const findings = [];
14701
+ for (const [fact, { expected, sizes }] of roleDriftFacts(facts, theme)) {
14702
+ const keeper = textSizeFacts(facts).find(
14703
+ (member) => member.role === fact.role && Math.abs(member.fontSizePt - expected) <= SIZE_TOLERANCE_PT
14704
+ );
14705
+ const others = sizes.filter((size) => size !== fact.fontSizePt);
14706
+ const sizePath = fact.sizePath;
14707
+ findings.push({
14708
+ path: sizePath,
14709
+ ...keeper && { relatedPaths: [keeper.path] },
14710
+ message: `"${fact.role}" is painted at ${fact.fontSizePt}pt here and at ${others.join("pt, ")}pt elsewhere; the theme sets it at ${expected}pt.`,
14711
+ suggestion: `Drop the size so "${fact.role}" paints at the theme's ${expected}pt.`,
14712
+ context: { role: fact.role, sizes },
14713
+ evidence: {
14714
+ actual: fact.fontSizePt,
14715
+ expected,
14716
+ unit: "pt",
14717
+ values: { role: fact.role, source: "theme" }
14718
+ },
14719
+ fixes: [{ op: "replace", path: sizePath, value: expected }]
14720
+ });
14721
+ }
14722
+ return findings;
14723
+ }
14724
+ };
14212
14725
  var DOCX_QUALITY_RULES = {
14213
14726
  id: "docx/default",
14214
14727
  rules: [
@@ -14225,14 +14738,17 @@ var DOCX_QUALITY_RULES = {
14225
14738
  docxFontCountRule,
14226
14739
  docxPaletteRule,
14227
14740
  docxRequiredChromeRule,
14228
- docxRunningHeadRule
14741
+ docxRunningHeadRule,
14742
+ docxTypeScaleRule,
14743
+ docxSizeCountRule,
14744
+ docxRoleDriftRule
14229
14745
  ]
14230
14746
  };
14231
14747
  var DOCX_QUALITY_PROFILES = {
14232
14748
  "client-report": {
14233
14749
  id: "client-report",
14234
14750
  formats: ["docx"],
14235
- description: "Client or public-administration report: a running head with page numbers on every section after the cover, a takeaway and a source wherever a block declares them, no heading skipped.",
14751
+ description: "Client or public-administration report: a running head with page numbers on every section after the cover, a takeaway and a source wherever a block declares them, no heading skipped, and every size on the theme scale with at most eight in play.",
14236
14752
  rules: {
14237
14753
  "docx/required-chrome": {
14238
14754
  parameters: { required: ["takeaway", "source"] }
@@ -14243,7 +14759,10 @@ var DOCX_QUALITY_PROFILES = {
14243
14759
  fromSection: 1
14244
14760
  }
14245
14761
  },
14246
- "docx/heading-hierarchy": { severity: "warning" }
14762
+ "docx/heading-hierarchy": { severity: "warning" },
14763
+ "docx/type-scale": { enabled: true },
14764
+ "docx/size-count": { enabled: true, parameters: { maximumSizes: 8 } },
14765
+ "docx/role-drift": { enabled: true }
14247
14766
  }
14248
14767
  },
14249
14768
  "executive-report": {
@@ -15975,6 +16494,7 @@ export {
15975
16494
  analyzeDocxQuality,
15976
16495
  blockSlotBudgets,
15977
16496
  cleanComponentProps,
16497
+ collectDocxTextInventory,
15978
16498
  consultingTheme,
15979
16499
  createComponent,
15980
16500
  createDocumentGenerator,