@json-to-office/core-docx 3.3.0 → 4.0.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
@@ -12817,6 +12823,93 @@ function walkActive(node, path4, page, visit) {
12817
12823
  (child, index) => walkActive(child, `${path4}/children/${index}`, page, visit)
12818
12824
  );
12819
12825
  }
12826
+ function textSizeFact(node, props, path4, typography, role) {
12827
+ const size = effectiveFontSize(node, props, typography);
12828
+ if (size === void 0) return void 0;
12829
+ return {
12830
+ id: `docx:text-size:${path4}`,
12831
+ kind: "docx/text-size",
12832
+ path: path4,
12833
+ role: role ?? styleKey(node, props),
12834
+ fontSizePt: size.fontSizePt,
12835
+ authored: size.authored,
12836
+ ...size.authored && { sizePath: `${path4}/props/font/size` }
12837
+ };
12838
+ }
12839
+ function tableTextSizeFacts(props, authored, path4, typography, page) {
12840
+ const facts = [];
12841
+ const sizeOf = (holder) => finiteNumber(asRecord(asRecord(holder)?.font)?.size);
12842
+ const authoredSize = (holder, pointer, role) => {
12843
+ const size = sizeOf(holder);
12844
+ if (size === void 0) return;
12845
+ facts.push({
12846
+ id: `docx:text-size:${pointer}`,
12847
+ kind: "docx/text-size",
12848
+ path: pointer,
12849
+ role,
12850
+ fontSizePt: size,
12851
+ authored: true,
12852
+ sizePath: `${pointer}/font/size`
12853
+ });
12854
+ };
12855
+ const cellContent = (holder, pointer, role) => {
12856
+ const content = asRecord(holder)?.content;
12857
+ if (asRecord(content) === void 0) return;
12858
+ walkActive(content, `${pointer}/content`, page, (node, nodePath) => {
12859
+ if (node.name !== "paragraph" && node.name !== "heading") return;
12860
+ const nodeProps = asRecord(node.props) ?? {};
12861
+ if (typeof nodeProps.text !== "string" || nodeProps.text.trim() === "")
12862
+ return;
12863
+ const fact = textSizeFact(node, nodeProps, nodePath, typography, role);
12864
+ if (fact) facts.push(fact);
12865
+ });
12866
+ };
12867
+ if (authored) {
12868
+ authoredSize(
12869
+ authored.cellDefaults,
12870
+ `${path4}/props/cellDefaults`,
12871
+ "tableCell"
12872
+ );
12873
+ authoredSize(
12874
+ authored.headerCellDefaults,
12875
+ `${path4}/props/headerCellDefaults`,
12876
+ "tableHeader"
12877
+ );
12878
+ const columns = Array.isArray(authored.columns) ? authored.columns : [];
12879
+ columns.forEach((column, index) => {
12880
+ const record = asRecord(column) ?? {};
12881
+ const columnPath = `${path4}/props/columns/${index}`;
12882
+ authoredSize(
12883
+ record.cellDefaults,
12884
+ `${columnPath}/cellDefaults`,
12885
+ "tableCell"
12886
+ );
12887
+ authoredSize(record.header, `${columnPath}/header`, "tableHeader");
12888
+ cellContent(record.header, `${columnPath}/header`, "tableHeader");
12889
+ const cells = Array.isArray(record.cells) ? record.cells : [];
12890
+ cells.forEach((cell, cellIndex) => {
12891
+ authoredSize(cell, `${columnPath}/cells/${cellIndex}`, "tableCell");
12892
+ cellContent(cell, `${columnPath}/cells/${cellIndex}`, "tableCell");
12893
+ });
12894
+ });
12895
+ }
12896
+ for (const [key, role] of [
12897
+ ["cellDefaults", "tableCell"],
12898
+ ["headerCellDefaults", "tableHeader"]
12899
+ ]) {
12900
+ const size = sizeOf(props[key]);
12901
+ if (size === void 0) continue;
12902
+ facts.push({
12903
+ id: `docx:text-size:${path4}:${role}`,
12904
+ kind: "docx/text-size",
12905
+ path: path4,
12906
+ role,
12907
+ fontSizePt: size,
12908
+ authored: false
12909
+ });
12910
+ }
12911
+ return facts;
12912
+ }
12820
12913
  function prepareDocxQualityDocument(document, options = {}) {
12821
12914
  const themed = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
12822
12915
  customThemes: options.customThemes,
@@ -12886,6 +12979,43 @@ function prepareDocxQualityDocument(document, options = {}) {
12886
12979
  const header = part("header");
12887
12980
  const footer = part("footer");
12888
12981
  inherited = { header, footer };
12982
+ for (const kind of ["header", "footer"]) {
12983
+ if (!Array.isArray(props[kind])) continue;
12984
+ const drawnByBlock = !Array.isArray(
12985
+ asRecord(
12986
+ asRecord(
12987
+ (Array.isArray(themed.document.children) ? themed.document.children : [])[index]
12988
+ )?.props
12989
+ )?.[kind]
12990
+ );
12991
+ const part2 = props[kind];
12992
+ const visitChrome = (child, childPath) => {
12993
+ if (child.name !== "paragraph" && child.name !== "heading") return;
12994
+ const childProps = asRecord(child.props) ?? {};
12995
+ if (typeof childProps.text !== "string" || childProps.text.trim() === "")
12996
+ return;
12997
+ const fact = textSizeFact(
12998
+ child,
12999
+ childProps,
13000
+ childPath,
13001
+ typography,
13002
+ kind
13003
+ );
13004
+ if (fact)
13005
+ addFact({
13006
+ ...fact,
13007
+ generated: drawnByBlock || authoredPath(childPath) !== childPath
13008
+ });
13009
+ };
13010
+ part2.forEach(
13011
+ (child, childIndex) => walkActive(
13012
+ child,
13013
+ `/children/${index}/props/${kind}/${childIndex}`,
13014
+ basePage,
13015
+ visitChrome
13016
+ )
13017
+ );
13018
+ }
12889
13019
  addFact({
12890
13020
  id: `docx:section-chrome:${index}`,
12891
13021
  kind: "docx/section-chrome",
@@ -12919,12 +13049,34 @@ function prepareDocxQualityDocument(document, options = {}) {
12919
13049
  (value) => `#${resolveDesignColor2(value, visualColors)}`
12920
13050
  ) ?? SERIES_COLOR_TOKENS.filter((token) => paletteHexes[token] !== void 0);
12921
13051
  const authoredPropsAt = (pointer) => asRecord(asRecord(nodeAtPointer(context.document, pointer))?.props);
13052
+ const roleSizesPt = {};
13053
+ for (const key of Object.keys(typography.styles)) {
13054
+ const size = effectiveFontSize(
13055
+ { name: "paragraph" },
13056
+ { themeStyle: key },
13057
+ typography
13058
+ );
13059
+ if (size) roleSizesPt[key] = size.fontSizePt;
13060
+ }
13061
+ const scale = resolved.theme.typography?.scale?.[designCanvas2("docx", resolved.theme.page?.size)];
13062
+ const typeScalePt = [
13063
+ .../* @__PURE__ */ new Set([
13064
+ ...Object.values(roleSizesPt),
13065
+ ...["heading", "body", "mono", "light"].flatMap((role) => {
13066
+ const size = resolveFontSize(resolved.theme, role);
13067
+ return size === void 0 ? [] : [size];
13068
+ }),
13069
+ ...scale ? typeScaleSizes(scale) : []
13070
+ ])
13071
+ ].sort((a, b) => a - b);
12922
13072
  addFact({
12923
13073
  id: "docx:theme",
12924
13074
  kind: "docx/theme",
12925
13075
  path: "/props",
12926
13076
  themeName: context.themeName,
12927
13077
  paletteHexes,
13078
+ typeScalePt,
13079
+ roleSizesPt,
12928
13080
  // `heading` and `body` only. A theme also names `mono` and `light`, but
12929
13081
  // those paint nothing until a component asks for them — counting an
12930
13082
  // unused `Courier New` against a document's family budget would flag a
@@ -12977,6 +13129,15 @@ function prepareDocxQualityDocument(document, options = {}) {
12977
13129
  if (node.name === "table") {
12978
13130
  const fact = tableFact(props, path4, page.availableWidthTwips);
12979
13131
  if (fact) addFact(fact);
13132
+ for (const size of tableTextSizeFacts(
13133
+ props,
13134
+ authoredPropsAt(path4),
13135
+ path4,
13136
+ typography,
13137
+ page
13138
+ )) {
13139
+ addFact({ ...size, generated: authoredPath(size.path) !== size.path });
13140
+ }
12980
13141
  const design = tableDesignFact(
12981
13142
  props,
12982
13143
  path4,
@@ -13035,6 +13196,10 @@ function prepareDocxQualityDocument(document, options = {}) {
13035
13196
  if (node.name === "paragraph" || node.name === "heading") {
13036
13197
  const fact = lineBoxFact(node, props, path4, typography, context.document);
13037
13198
  if (fact) addFact(fact);
13199
+ if (typeof props.text === "string" && props.text.trim() !== "") {
13200
+ const fact2 = textSizeFact(node, props, path4, typography);
13201
+ if (fact2) addFact({ ...fact2, generated: authoredPath(path4) !== path4 });
13202
+ }
13038
13203
  }
13039
13204
  if (node.name === "image" || node.name === "visual") {
13040
13205
  for (const fact of svgTextFacts(props, path4)) addFact(fact);
@@ -13607,6 +13772,7 @@ function numberParameter(parameters, name, fallback) {
13607
13772
  }
13608
13773
  var docxTableWidthRule = {
13609
13774
  id: "docx/table-width",
13775
+ description: "Explicit column widths that sum past the usable width of their section.",
13610
13776
  code: QUALITY_CODES.TABLE_WIDTH_OVERFLOW,
13611
13777
  category: "integrity",
13612
13778
  defaultSeverity: "warning",
@@ -13654,6 +13820,7 @@ var docxTableWidthRule = {
13654
13820
  };
13655
13821
  var docxHeadingHierarchyRule = {
13656
13822
  id: "docx/heading-hierarchy",
13823
+ description: "A heading that skips a level and breaks the outline.",
13657
13824
  code: QUALITY_CODES.HEADING_SKIP,
13658
13825
  category: "hierarchy",
13659
13826
  defaultSeverity: "info",
@@ -13697,6 +13864,7 @@ function frameTextFacts(facts) {
13697
13864
  }
13698
13865
  var docxTextFitRule = {
13699
13866
  id: "docx/text-fit",
13867
+ description: "A word too wide for its floating frame, or a frame whose wrapped text runs off the sheet.",
13700
13868
  code: QUALITY_CODES.TEXT_OVERFLOW,
13701
13869
  category: "integrity",
13702
13870
  defaultSeverity: "warning",
@@ -13770,6 +13938,7 @@ var docxTextFitRule = {
13770
13938
  var FRAME_COLLISION_MIN_WIDTH_TWIPS = 240;
13771
13939
  var docxFrameCollisionRule = {
13772
13940
  id: "docx/frame-collision",
13941
+ description: "Two page-anchored frames whose estimated text lands on the same region of a page.",
13773
13942
  code: QUALITY_CODES.FRAME_COLLISION,
13774
13943
  category: "integrity",
13775
13944
  defaultSeverity: "warning",
@@ -13866,6 +14035,7 @@ var docxFrameCollisionRule = {
13866
14035
  };
13867
14036
  var docxSvgTextBoundsRule = {
13868
14037
  id: "docx/svg-text-bounds",
14038
+ description: "A text baseline outside an inline SVG\u2019s viewBox, so the words are never painted.",
13869
14039
  code: QUALITY_CODES.SVG_TEXT_CLIPPED,
13870
14040
  category: "integrity",
13871
14041
  defaultSeverity: "warning",
@@ -13901,6 +14071,7 @@ function tenths(value) {
13901
14071
  }
13902
14072
  var docxLineBoxRule = {
13903
14073
  id: "docx/line-box",
14074
+ description: "An `exactly` line box shorter than the capitals it has to hold.",
13904
14075
  code: QUALITY_CODES.LINE_BOX_COLLAPSE,
13905
14076
  category: "legibility",
13906
14077
  defaultSeverity: "warning",
@@ -13954,6 +14125,7 @@ var docxLineBoxRule = {
13954
14125
  };
13955
14126
  var docxPlaceholderRule = {
13956
14127
  id: "docx/placeholder-text",
14128
+ description: "An unfilled scaffold slot, or leftover filler copy.",
13957
14129
  code: QUALITY_CODES.PLACEHOLDER_TEXT,
13958
14130
  category: "integrity",
13959
14131
  defaultSeverity: "warning",
@@ -13972,6 +14144,7 @@ var docxPlaceholderRule = {
13972
14144
  };
13973
14145
  var docxSlotBudgetRule = {
13974
14146
  id: "docx/slot-budget",
14147
+ description: "A block slot holding more words than its budget allows \u2014 a takeaway past the word count the block sets.",
13975
14148
  code: QUALITY_CODES.SLOT_BUDGET,
13976
14149
  category: "composition",
13977
14150
  defaultSeverity: "warning",
@@ -13995,6 +14168,7 @@ var docxSlotBudgetRule = {
13995
14168
  var DEFAULT_MAX_FONT_FAMILIES = 3;
13996
14169
  var docxFontCountRule = {
13997
14170
  id: "docx/font-count",
14171
+ description: "Distinct font families the document can paint.",
13998
14172
  code: QUALITY_CODES.FONT_COUNT,
13999
14173
  category: "brand",
14000
14174
  defaultSeverity: "warning",
@@ -14031,6 +14205,7 @@ var docxFontCountRule = {
14031
14205
  };
14032
14206
  var docxPaletteRule = {
14033
14207
  id: "docx/palette-adherence",
14208
+ description: "A literal colour the resolved theme does not define.",
14034
14209
  code: QUALITY_CODES.OFF_PALETTE,
14035
14210
  category: "brand",
14036
14211
  defaultSeverity: "info",
@@ -14056,6 +14231,7 @@ var docxPaletteRule = {
14056
14231
  var DEFAULT_MAX_TABLE_ROWS_PER_PAGE = 25;
14057
14232
  var docxChartRule = {
14058
14233
  id: "docx/chart-design",
14234
+ description: "What a chart claims about its numbers: the comparison, the palette, the unit and the caption.",
14059
14235
  code: QUALITY_CODES.CHART_OVERLOADED,
14060
14236
  category: "information-design",
14061
14237
  defaultSeverity: "warning",
@@ -14097,6 +14273,7 @@ function seriesColorFix(fact) {
14097
14273
  }
14098
14274
  var docxTableDesignRule = {
14099
14275
  id: "docx/table-design",
14276
+ description: "How a table lays its numbers out: alignment, rounding, rules and length.",
14100
14277
  code: QUALITY_CODES.TABLE_NUMERIC_ALIGN,
14101
14278
  category: "information-design",
14102
14279
  defaultSeverity: "warning",
@@ -14154,6 +14331,7 @@ function stringListParameter(parameters, name) {
14154
14331
  }
14155
14332
  var docxRequiredChromeRule = {
14156
14333
  id: "docx/required-chrome",
14334
+ 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
14335
  code: QUALITY_CODES.CHROME_MISSING,
14158
14336
  category: "consistency",
14159
14337
  defaultSeverity: "warning",
@@ -14177,6 +14355,7 @@ var docxRequiredChromeRule = {
14177
14355
  var SECTION_CHROME_PARTS = ["header", "footer", "pageNumber"];
14178
14356
  var docxRunningHeadRule = {
14179
14357
  id: "docx/running-head",
14358
+ description: "A body section without the running head a profile or policy expects. Off until one names parts.",
14180
14359
  code: QUALITY_CODES.CHROME_MISSING,
14181
14360
  category: "consistency",
14182
14361
  defaultSeverity: "warning",
@@ -14209,6 +14388,170 @@ var docxRunningHeadRule = {
14209
14388
  });
14210
14389
  }
14211
14390
  };
14391
+ var SIZE_TOLERANCE_PT = 0.25;
14392
+ function themeFact(facts) {
14393
+ return facts.find(
14394
+ (fact) => fact.kind === "docx/theme"
14395
+ );
14396
+ }
14397
+ function textSizeFacts(facts) {
14398
+ return facts.filter(
14399
+ (fact) => fact.kind === "docx/text-size"
14400
+ );
14401
+ }
14402
+ function roleDriftFacts(facts, theme) {
14403
+ const byRole = /* @__PURE__ */ new Map();
14404
+ for (const fact of textSizeFacts(facts)) {
14405
+ byRole.set(fact.role, [...byRole.get(fact.role) ?? [], fact]);
14406
+ }
14407
+ const drifting = /* @__PURE__ */ new Map();
14408
+ for (const [role, members] of byRole) {
14409
+ const sizes = [...new Set(members.map((fact) => fact.fontSizePt))].sort(
14410
+ (a, b) => a - b
14411
+ );
14412
+ if (sizes.length < 2) continue;
14413
+ const expected = theme?.roleSizesPt[role];
14414
+ if (expected === void 0) continue;
14415
+ for (const fact of members) {
14416
+ if (fact.sizePath !== void 0 && !fact.generated && Math.abs(fact.fontSizePt - expected) > SIZE_TOLERANCE_PT)
14417
+ drifting.set(fact, { expected, sizes });
14418
+ }
14419
+ }
14420
+ return drifting;
14421
+ }
14422
+ function nearestSize(size, scale) {
14423
+ let best;
14424
+ for (const candidate of scale) {
14425
+ if (best === void 0 || Math.abs(candidate - size) < Math.abs(best - size))
14426
+ best = candidate;
14427
+ }
14428
+ return best;
14429
+ }
14430
+ var docxTypeScaleRule = {
14431
+ id: "docx/type-scale",
14432
+ 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.",
14433
+ code: QUALITY_CODES.TYPE_OFF_SCALE,
14434
+ category: "consistency",
14435
+ defaultSeverity: "warning",
14436
+ defaultCertainty: "deterministic",
14437
+ formats: ["docx"],
14438
+ defaultEnabled: false,
14439
+ evaluate: ({ facts }) => {
14440
+ const theme = themeFact(facts);
14441
+ const scale = theme?.typeScalePt ?? [];
14442
+ if (scale.length === 0) return [];
14443
+ const drifting = roleDriftFacts(facts, theme);
14444
+ const offScale = textSizeFacts(facts).filter(
14445
+ (fact) => fact.sizePath !== void 0 && !fact.generated && !drifting.has(fact) && !scale.some(
14446
+ (size) => Math.abs(size - fact.fontSizePt) <= SIZE_TOLERANCE_PT
14447
+ )
14448
+ );
14449
+ const groups = /* @__PURE__ */ new Map();
14450
+ for (const fact of offScale) {
14451
+ const key = `${fact.role}@${fact.fontSizePt}`;
14452
+ groups.set(key, [...groups.get(key) ?? [], fact]);
14453
+ }
14454
+ return [...groups.values()].map((members) => {
14455
+ const [first] = members;
14456
+ const nearest = nearestSize(first.fontSizePt, scale);
14457
+ const sizePaths = members.map((fact) => fact.sizePath);
14458
+ const count = members.length === 1 ? "" : ` (${members.length} places, patched together)`;
14459
+ return {
14460
+ path: sizePaths[0],
14461
+ ...sizePaths.length > 1 && { relatedPaths: sizePaths.slice(1) },
14462
+ message: `${first.fontSizePt}pt is not a size the ${theme.themeName} theme paints; the nearest on its scale is ${nearest}pt${count}.`,
14463
+ suggestion: `Use ${nearest}pt, or drop the size and let the "${first.role}" style set it.`,
14464
+ context: { role: first.role, scale, paths: sizePaths },
14465
+ evidence: {
14466
+ actual: first.fontSizePt,
14467
+ expected: nearest,
14468
+ unit: "pt",
14469
+ values: { source: "theme" }
14470
+ },
14471
+ fixes: sizePaths.map((path4) => ({
14472
+ op: "replace",
14473
+ path: path4,
14474
+ value: nearest
14475
+ }))
14476
+ };
14477
+ });
14478
+ }
14479
+ };
14480
+ var docxSizeCountRule = {
14481
+ id: "docx/size-count",
14482
+ description: "More distinct text sizes than maximumSizes allows, blocks included. Off until a profile or policy enables it.",
14483
+ code: QUALITY_CODES.TYPE_SIZE_COUNT,
14484
+ category: "consistency",
14485
+ defaultSeverity: "warning",
14486
+ defaultCertainty: "deterministic",
14487
+ formats: ["docx"],
14488
+ defaultEnabled: false,
14489
+ defaultParameters: { maximumSizes: 8 },
14490
+ evaluate: ({ facts, configuration, profile }) => {
14491
+ const maximum = numberParameter(
14492
+ configuration.parameters,
14493
+ "maximumSizes",
14494
+ 8
14495
+ );
14496
+ const firstPathBySize = /* @__PURE__ */ new Map();
14497
+ for (const fact of textSizeFacts(facts)) {
14498
+ const size = Math.round(fact.fontSizePt * 4) / 4;
14499
+ if (!firstPathBySize.has(size)) firstPathBySize.set(size, fact.path);
14500
+ }
14501
+ if (firstPathBySize.size <= maximum) return [];
14502
+ const sizes = [...firstPathBySize.keys()].sort((a, b) => a - b);
14503
+ return [
14504
+ {
14505
+ path: themeFact(facts)?.path ?? "/props",
14506
+ relatedPaths: sizes.map((size) => firstPathBySize.get(size)),
14507
+ message: `The document paints ${sizes.length} distinct text sizes (${sizes.join(", ")}pt); the ${profile?.id ?? "selected"} profile allows ${maximum}.`,
14508
+ suggestion: "Keep to the theme styles \u2014 title, headings, body, label, source \u2014 and drop the ad-hoc sizes.",
14509
+ context: { sizes, maximum },
14510
+ evidence: {
14511
+ actual: sizes.length,
14512
+ expected: maximum,
14513
+ values: { source: "profile" }
14514
+ }
14515
+ }
14516
+ ];
14517
+ }
14518
+ };
14519
+ var docxRoleDriftRule = {
14520
+ id: "docx/role-drift",
14521
+ 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.",
14522
+ code: QUALITY_CODES.TYPE_ROLE_DRIFT,
14523
+ category: "consistency",
14524
+ defaultSeverity: "warning",
14525
+ defaultCertainty: "deterministic",
14526
+ formats: ["docx"],
14527
+ defaultEnabled: false,
14528
+ evaluate: ({ facts }) => {
14529
+ const theme = themeFact(facts);
14530
+ const findings = [];
14531
+ for (const [fact, { expected, sizes }] of roleDriftFacts(facts, theme)) {
14532
+ const keeper = textSizeFacts(facts).find(
14533
+ (member) => member.role === fact.role && Math.abs(member.fontSizePt - expected) <= SIZE_TOLERANCE_PT
14534
+ );
14535
+ const others = sizes.filter((size) => size !== fact.fontSizePt);
14536
+ const sizePath = fact.sizePath;
14537
+ findings.push({
14538
+ path: sizePath,
14539
+ ...keeper && { relatedPaths: [keeper.path] },
14540
+ message: `"${fact.role}" is painted at ${fact.fontSizePt}pt here and at ${others.join("pt, ")}pt elsewhere; the theme sets it at ${expected}pt.`,
14541
+ suggestion: `Drop the size so "${fact.role}" paints at the theme's ${expected}pt.`,
14542
+ context: { role: fact.role, sizes },
14543
+ evidence: {
14544
+ actual: fact.fontSizePt,
14545
+ expected,
14546
+ unit: "pt",
14547
+ values: { role: fact.role, source: "theme" }
14548
+ },
14549
+ fixes: [{ op: "replace", path: sizePath, value: expected }]
14550
+ });
14551
+ }
14552
+ return findings;
14553
+ }
14554
+ };
14212
14555
  var DOCX_QUALITY_RULES = {
14213
14556
  id: "docx/default",
14214
14557
  rules: [
@@ -14225,14 +14568,17 @@ var DOCX_QUALITY_RULES = {
14225
14568
  docxFontCountRule,
14226
14569
  docxPaletteRule,
14227
14570
  docxRequiredChromeRule,
14228
- docxRunningHeadRule
14571
+ docxRunningHeadRule,
14572
+ docxTypeScaleRule,
14573
+ docxSizeCountRule,
14574
+ docxRoleDriftRule
14229
14575
  ]
14230
14576
  };
14231
14577
  var DOCX_QUALITY_PROFILES = {
14232
14578
  "client-report": {
14233
14579
  id: "client-report",
14234
14580
  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.",
14581
+ 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
14582
  rules: {
14237
14583
  "docx/required-chrome": {
14238
14584
  parameters: { required: ["takeaway", "source"] }
@@ -14243,7 +14589,10 @@ var DOCX_QUALITY_PROFILES = {
14243
14589
  fromSection: 1
14244
14590
  }
14245
14591
  },
14246
- "docx/heading-hierarchy": { severity: "warning" }
14592
+ "docx/heading-hierarchy": { severity: "warning" },
14593
+ "docx/type-scale": { enabled: true },
14594
+ "docx/size-count": { enabled: true, parameters: { maximumSizes: 8 } },
14595
+ "docx/role-drift": { enabled: true }
14247
14596
  }
14248
14597
  },
14249
14598
  "executive-report": {