@json-to-office/core-docx 6.8.0 → 7.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
@@ -1389,7 +1389,8 @@ var init_consulting_docx_theme = __esm({
1389
1389
  },
1390
1390
  stat: {
1391
1391
  face: "heading",
1392
- weight: 700,
1392
+ size: 22,
1393
+ weight: 400,
1393
1394
  color: "accent",
1394
1395
  lineHeight: 1
1395
1396
  },
@@ -1696,7 +1697,7 @@ var init_themes = __esm({
1696
1697
  getTheme = (themeName) => {
1697
1698
  return themes[themeName];
1698
1699
  };
1699
- getThemeWithFallback = (themeName, fallbackTheme = "minimal") => {
1700
+ getThemeWithFallback = (themeName, fallbackTheme = "consulting") => {
1700
1701
  const theme = getTheme(themeName) || getTheme(fallbackTheme);
1701
1702
  if (!theme) {
1702
1703
  throw new Error(
@@ -1729,7 +1730,7 @@ function resolveTheme(theme, themeName) {
1729
1730
  if (themeName) {
1730
1731
  return getTheme(themeName);
1731
1732
  }
1732
- return getTheme("minimal");
1733
+ return getTheme("consulting");
1733
1734
  }
1734
1735
  function resolveFontFamily(theme, fontRef) {
1735
1736
  if (!theme?.fonts?.body?.family) {
@@ -3792,7 +3793,9 @@ function createDocxJsRenderer() {
3792
3793
  function buildDocument(ir, resources = /* @__PURE__ */ new Map()) {
3793
3794
  return new Document({
3794
3795
  styles: emitStyles(ir.styles),
3795
- sections: ir.sections.map((section2) => sectionOptions(section2, resources)),
3796
+ sections: ir.sections.map(
3797
+ (section2, index) => sectionOptions(section2, resources, index === ir.sections.length - 1)
3798
+ ),
3796
3799
  ...coreProperties(ir),
3797
3800
  features: {
3798
3801
  updateFields: ir.settings.updateFields,
@@ -3948,7 +3951,7 @@ function collectImagePlacements(ir) {
3948
3951
  }
3949
3952
  return placements;
3950
3953
  }
3951
- function sectionOptions(section2, resources) {
3954
+ function sectionOptions(section2, resources, closesDocument = false) {
3952
3955
  const { page, columns } = section2.properties;
3953
3956
  const options = {
3954
3957
  properties: {
@@ -3987,7 +3990,7 @@ function sectionOptions(section2, resources) {
3987
3990
  }
3988
3991
  } : {}
3989
3992
  },
3990
- children: sectionChildren(section2, resources)
3993
+ children: sectionChildren(section2, resources, closesDocument)
3991
3994
  };
3992
3995
  const headers = chromeSlots(section2.headers, Header, resources);
3993
3996
  if (headers) options.headers = headers;
@@ -4009,12 +4012,13 @@ function chromeSlots(set, Part, resources) {
4009
4012
  function partChildren(part, resources) {
4010
4013
  return part.children.map((block2) => emitBlock(block2, resources));
4011
4014
  }
4012
- function sectionChildren(section2, resources) {
4015
+ function sectionChildren(section2, resources, closesDocument = false) {
4013
4016
  const blocks = section2.children.map((block2) => emitBlock(block2, resources));
4014
4017
  const bookmark = section2.bookmark;
4015
- if (!bookmark) return blocks;
4018
+ const endsInFrame = closesDocument && lastBlockIsFrame(section2);
4019
+ if (!bookmark && !endsInFrame) return blocks;
4016
4020
  const out = [...blocks];
4017
- if (bookmark.opens) {
4021
+ if (bookmark?.opens) {
4018
4022
  const start = new BookmarkStart2(bookmark.name, bookmark.id);
4019
4023
  const first = out[0];
4020
4024
  if (first instanceof Paragraph2) {
@@ -4023,20 +4027,26 @@ function sectionChildren(section2, resources) {
4023
4027
  out.unshift(anchorParagraph(start));
4024
4028
  }
4025
4029
  }
4026
- if (bookmark.closes) {
4030
+ if (bookmark?.closes) {
4027
4031
  const end = new BookmarkEnd2(bookmark.id);
4028
4032
  const last = out[out.length - 1];
4029
- if (last instanceof Paragraph2) {
4033
+ if (last instanceof Paragraph2 && !endsInFrame) {
4030
4034
  last.addChildElement(end);
4031
4035
  } else {
4032
4036
  out.push(anchorParagraph(end));
4033
4037
  }
4038
+ } else if (endsInFrame) {
4039
+ out.push(anchorParagraph());
4034
4040
  }
4035
4041
  return out;
4036
4042
  }
4043
+ function lastBlockIsFrame(section2) {
4044
+ const last = section2.children[section2.children.length - 1];
4045
+ return last?.kind === "paragraph" && last.frame !== void 0;
4046
+ }
4037
4047
  function anchorParagraph(anchor) {
4038
4048
  return new Paragraph2({
4039
- children: [anchor],
4049
+ children: anchor ? [anchor] : [],
4040
4050
  spacing: { before: 0, after: 0, line: 20, lineRule: LineRuleType2.EXACT }
4041
4051
  });
4042
4052
  }
@@ -4855,7 +4865,7 @@ function borders2(value) {
4855
4865
  }
4856
4866
  return out;
4857
4867
  }
4858
- function section(value, ctx) {
4868
+ function section(value, ctx, closesDocument = false) {
4859
4869
  const { page, columns } = value.properties;
4860
4870
  return {
4861
4871
  properties: {
@@ -4907,7 +4917,7 @@ function section(value, ctx) {
4907
4917
  }
4908
4918
  } : {}
4909
4919
  },
4910
- children: sectionChildren2(value, ctx),
4920
+ children: sectionChildren2(value, ctx, closesDocument),
4911
4921
  ...value.headers ? { headers: headerFooterSet(value.headers, ctx) } : {},
4912
4922
  ...value.footers ? { footers: headerFooterSet(value.footers, ctx) } : {}
4913
4923
  };
@@ -4920,8 +4930,16 @@ function headerFooterSet(set, ctx) {
4920
4930
  }
4921
4931
  return out;
4922
4932
  }
4923
- function sectionChildren2(value, ctx) {
4933
+ function sectionChildren2(value, ctx, closesDocument = false) {
4924
4934
  const blocks = value.children.map((child) => block(child, ctx));
4935
+ const last = value.children[value.children.length - 1];
4936
+ if (closesDocument && last?.kind === "paragraph" && last.frame !== void 0)
4937
+ blocks.push({
4938
+ paragraph: {
4939
+ children: [],
4940
+ spacing: { before: 0, after: 0, line: 20, lineRule: "exact" }
4941
+ }
4942
+ });
4925
4943
  const bookmark = value.bookmark;
4926
4944
  if (!bookmark) return blocks;
4927
4945
  return [
@@ -5090,7 +5108,9 @@ async function buildDocumentOptions(ir, charts = [], svgRasterFallback) {
5090
5108
  };
5091
5109
  return {
5092
5110
  styles: emitStyles2(ir.styles),
5093
- sections: ir.sections.map((value) => section(value, ctx)),
5111
+ sections: ir.sections.map(
5112
+ (value, index) => section(value, ctx, index === ir.sections.length - 1)
5113
+ ),
5094
5114
  ...coreProperties2(ir),
5095
5115
  features: {
5096
5116
  updateFields: ir.settings.updateFields,
@@ -6824,7 +6844,12 @@ function resolveColors(entries, at, deps) {
6824
6844
  init_themes();
6825
6845
  init_colorUtils();
6826
6846
  init_styleHelpers();
6827
- import { capsFormatting, synthesizeFamilyName } from "@json-to-office/shared";
6847
+ import {
6848
+ capsFormatting,
6849
+ designCanvas,
6850
+ synthesizeFamilyName,
6851
+ typeScaleSizes
6852
+ } from "@json-to-office/shared";
6828
6853
  var RIGHT_MARGIN_TAB_TWIPS = 9026 * 2;
6829
6854
  var STATISTIC_NUMBER_STYLE_ID = "StatisticNumber";
6830
6855
  var STATISTIC_DESCRIPTION_STYLE_ID = "StatisticDescription";
@@ -6833,6 +6858,27 @@ var STATISTIC_SIZE_POINTS = {
6833
6858
  medium: 28,
6834
6859
  large: 40
6835
6860
  };
6861
+ function statisticSizes(theme, size) {
6862
+ const sizeOf = (style) => {
6863
+ const value = theme.styles?.[style]?.size;
6864
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
6865
+ };
6866
+ const role = sizeOf("stat");
6867
+ const medium = sizeOf(STATISTIC_NUMBER_STYLE_ID) ?? role ?? STATISTIC_SIZE_POINTS.medium;
6868
+ const scale = role === void 0 ? void 0 : theme.typography?.scale?.[designCanvas("docx", theme.page?.size)];
6869
+ const steps = scale ? typeScaleSizes(scale) : [];
6870
+ const below = [...steps].reverse().find((step) => step < medium - 0.01);
6871
+ const above = steps.find((step) => step > medium + 0.01);
6872
+ const number = size === "small" ? role !== void 0 && below !== void 0 ? below : STATISTIC_SIZE_POINTS.small : size === "large" ? role !== void 0 && above !== void 0 ? above : STATISTIC_SIZE_POINTS.large : medium;
6873
+ const label = sizeOf("label");
6874
+ return {
6875
+ number,
6876
+ medium,
6877
+ // Half-points in the document, floored at 6pt.
6878
+ suffix: label ?? Math.max(12, Math.round(number)) / 2,
6879
+ description: sizeOf(STATISTIC_DESCRIPTION_STYLE_ID) ?? label ?? 10
6880
+ };
6881
+ }
6836
6882
  function getStyleSafe(theme, styleName) {
6837
6883
  return theme.styles && Object.hasOwn(theme.styles, styleName) ? theme.styles[styleName] : void 0;
6838
6884
  }
@@ -7008,8 +7054,8 @@ function chromeRun(fontProps, theme, sizeHalfPoints) {
7008
7054
  ...fontProps.italic !== void 0 && { italic: fontProps.italic }
7009
7055
  };
7010
7056
  }
7011
- function createDocumentStyles(themeNameOrObject = "minimal", language) {
7012
- const theme = typeof themeNameOrObject === "string" ? getTheme(themeNameOrObject) || getTheme("minimal") : themeNameOrObject;
7057
+ function createDocumentStyles(themeNameOrObject = "consulting", language) {
7058
+ const theme = typeof themeNameOrObject === "string" ? getTheme(themeNameOrObject) || getTheme("consulting") : themeNameOrObject;
7013
7059
  const paragraphStyles = [
7014
7060
  // Normal body text style
7015
7061
  {
@@ -7480,14 +7526,19 @@ function createDocumentStyles(themeNameOrObject = "minimal", language) {
7480
7526
  };
7481
7527
  }
7482
7528
  function createStatisticStyles(theme) {
7529
+ const sizes = statisticSizes(theme, "medium");
7530
+ const stat = theme.styles?.stat;
7531
+ const faces = ["heading", "body", "mono", "light"];
7532
+ const face = faces.find((candidate) => candidate === stat?.font) ?? "heading";
7533
+ const weight = typeof stat?.fontWeight === "number" ? stat.fontWeight : void 0;
7483
7534
  const definitions = [
7484
7535
  {
7485
7536
  id: STATISTIC_NUMBER_STYLE_ID,
7486
7537
  name: "Statistic Number",
7487
- font: "heading",
7488
- sizePoints: STATISTIC_SIZE_POINTS.medium,
7489
- color: theme.colors.primary,
7490
- bold: true,
7538
+ font: face,
7539
+ sizePoints: sizes.medium,
7540
+ color: typeof stat?.color === "string" ? stat.color : theme.colors.primary,
7541
+ bold: weight === void 0 ? true : weight >= 600,
7491
7542
  // Space above separates the figure from whatever preceded it; none below,
7492
7543
  // because the description is the other half of the same block.
7493
7544
  spacing: { before: 12, after: 0 },
@@ -7500,7 +7551,7 @@ function createStatisticStyles(theme) {
7500
7551
  id: STATISTIC_DESCRIPTION_STYLE_ID,
7501
7552
  name: "Statistic Description",
7502
7553
  font: "body",
7503
- sizePoints: 10,
7554
+ sizePoints: sizes.description,
7504
7555
  color: theme.colors.textMuted,
7505
7556
  bold: false,
7506
7557
  spacing: { before: 0, after: 12 },
@@ -10072,9 +10123,10 @@ function compileStatistic(component, scope) {
10072
10123
  );
10073
10124
  }
10074
10125
  const text = (value) => normalizeUnicodeText(String(value ?? ""));
10075
- const numberPoints = STATISTIC_SIZE_POINTS[String(props.size)] ?? STATISTIC_SIZE_POINTS.medium;
10076
- const resized = numberPoints !== STATISTIC_SIZE_POINTS.medium;
10077
- const suffixHalfPoints = Math.max(12, Math.round(numberPoints));
10126
+ const sizes = statisticSizes(ctx.theme, props.size);
10127
+ const numberPoints = sizes.number;
10128
+ const resized = numberPoints !== sizes.medium;
10129
+ const suffixHalfPoints = Math.round(sizes.suffix * 2);
10078
10130
  const numberRuns = [];
10079
10131
  const numberText = text(props.number);
10080
10132
  if (numberText) {
@@ -12365,10 +12417,10 @@ import {
12365
12417
  } from "@json-to-office/quality";
12366
12418
  import { DEFAULT_DOCX_RENDERER_ID as DEFAULT_DOCX_RENDERER_ID2 } from "@json-to-office/shared-docx";
12367
12419
  import {
12368
- designCanvas as designCanvas2,
12420
+ designCanvas as designCanvas3,
12369
12421
  designColors as designColors2,
12370
12422
  resolveDesignColor as resolveDesignColor2,
12371
- typeScaleSizes
12423
+ typeScaleSizes as typeScaleSizes2
12372
12424
  } from "@json-to-office/shared";
12373
12425
 
12374
12426
  // src/core/generationContext.ts
@@ -12424,7 +12476,7 @@ function applyThemeOverrides(theme, overrides) {
12424
12476
  // src/themes/design-system.ts
12425
12477
  init_defaults();
12426
12478
  import {
12427
- designCanvas,
12479
+ designCanvas as designCanvas2,
12428
12480
  resolveMotif,
12429
12481
  resolveTypeRoles,
12430
12482
  validateDesignColors
@@ -12434,7 +12486,7 @@ function resolveDocxDesignSystem(theme) {
12434
12486
  const motif = resolveMotif(theme.motif);
12435
12487
  if (!theme.typography && !theme.spacing)
12436
12488
  return motif === theme.motif ? theme : withMotif(theme, motif);
12437
- const canvas = designCanvas("docx", theme.page.size);
12489
+ const canvas = designCanvas2("docx", theme.page.size);
12438
12490
  const roles = resolveTypeRoles(theme, canvas, theme.fonts.body.size ?? 11);
12439
12491
  const styles = { ...theme.styles };
12440
12492
  for (const [name, role] of Object.entries(roles)) {
@@ -12519,7 +12571,7 @@ function resolveThemeContext(documentIn, options = {}) {
12519
12571
  }
12520
12572
  const document = documentIn.props === void 0 ? { ...documentIn, props: {} } : documentIn;
12521
12573
  const authoredThemeName = document.props.theme || void 0;
12522
- const baseThemeName = authoredThemeName ?? "minimal";
12574
+ const baseThemeName = authoredThemeName ?? "consulting";
12523
12575
  let theme = resolveNamedTheme ? resolveNamedTheme(
12524
12576
  baseThemeName,
12525
12577
  warnings,
@@ -12883,7 +12935,8 @@ async function expandBlocksWithPlugins(document, theme, plugins, render, preserv
12883
12935
  document: second.standard,
12884
12936
  preserved: first.preserved,
12885
12937
  sourceMap: evaluator.sourceMap,
12886
- blocks: evaluator.blocks
12938
+ blocks: evaluator.blocks,
12939
+ pluginOutputs: evaluator.pluginOutputs
12887
12940
  };
12888
12941
  }
12889
12942
 
@@ -13694,10 +13747,7 @@ function textSizeFact(node, props, path4, typography, role) {
13694
13747
  };
13695
13748
  }
13696
13749
  function statisticSizeFacts(props, path4, typography) {
13697
- const named = STATISTIC_SIZE_POINTS[String(props.size)];
13698
- const styled = (id) => finiteNumber(asRecord2(typography.styles[id])?.size);
13699
- const numberPoints = named ?? STATISTIC_SIZE_POINTS.medium;
13700
- const painted = props.size === "small" || props.size === "large" ? numberPoints : styled("StatisticNumber") ?? numberPoints;
13750
+ const sizes = statisticSizes(typography.theme, props.size);
13701
13751
  const present = (value) => value !== void 0 && value !== null && String(value).trim() !== "";
13702
13752
  const facts = [];
13703
13753
  const add = (suffix, role, fontSizePt) => facts.push({
@@ -13708,19 +13758,11 @@ function statisticSizeFacts(props, path4, typography) {
13708
13758
  fontSizePt,
13709
13759
  authored: false
13710
13760
  });
13711
- if (present(props.number)) add("number", "statistic", painted);
13761
+ if (present(props.number)) add("number", "statistic", sizes.number);
13712
13762
  if (present(props.unit) || present(props.trend) || present(props.trendValue))
13713
- add(
13714
- "suffix",
13715
- "statistic-suffix",
13716
- Math.max(12, Math.round(numberPoints)) / 2
13717
- );
13763
+ add("suffix", "statistic-suffix", sizes.suffix);
13718
13764
  if (present(props.description))
13719
- add(
13720
- "description",
13721
- "statistic-description",
13722
- styled("StatisticDescription") ?? 10
13723
- );
13765
+ add("description", "statistic-description", sizes.description);
13724
13766
  return facts;
13725
13767
  }
13726
13768
  function tocSizeFacts(props, path4, typography) {
@@ -13822,8 +13864,8 @@ function prepareDocxQualityDocument(document, options = {}) {
13822
13864
  fonts: options.fonts,
13823
13865
  warnings: options.warnings
13824
13866
  });
13825
- const expanded = expandBlocks(themed.document, themed.theme);
13826
- const context = expanded.blocks.length > 0 ? { ...themed, document: expanded.document } : themed;
13867
+ const expanded = options.expanded ?? expandBlocks(themed.document, themed.theme);
13868
+ const context = options.expanded !== void 0 || expanded.blocks.length > 0 ? { ...themed, document: expanded.document } : themed;
13827
13869
  const resolved = resolveDocumentTree(context.document, context.theme);
13828
13870
  const basePage = pageBox(resolved.theme, context.themeName);
13829
13871
  const typography = {
@@ -13835,7 +13877,7 @@ function prepareDocxQualityDocument(document, options = {}) {
13835
13877
  let previousHeadingLevel;
13836
13878
  const authoredPath = (path4) => toAuthoredPointer(
13837
13879
  themed.sourceMap ?? {},
13838
- toAuthoredPointer(expanded.sourceMap, path4)
13880
+ toAuthoredPointer(expanded.sourceMap, path4, expanded.pluginOutputs)
13839
13881
  );
13840
13882
  const addFact = (raw) => {
13841
13883
  const fact = {
@@ -13974,7 +14016,7 @@ function prepareDocxQualityDocument(document, options = {}) {
13974
14016
  );
13975
14017
  if (size) roleSizesPt[key] = size.fontSizePt;
13976
14018
  }
13977
- const scale = resolved.theme.typography?.scale?.[designCanvas2("docx", resolved.theme.page?.size)];
14019
+ const scale = resolved.theme.typography?.scale?.[designCanvas3("docx", resolved.theme.page?.size)];
13978
14020
  const typeScalePt = [
13979
14021
  .../* @__PURE__ */ new Set([
13980
14022
  ...Object.values(roleSizesPt),
@@ -13982,7 +14024,7 @@ function prepareDocxQualityDocument(document, options = {}) {
13982
14024
  const size = resolveFontSize(resolved.theme, role);
13983
14025
  return size === void 0 ? [] : [size];
13984
14026
  }),
13985
- ...scale ? typeScaleSizes(scale) : []
14027
+ ...scale ? typeScaleSizes2(scale) : []
13986
14028
  ])
13987
14029
  ].sort((a, b) => a - b);
13988
14030
  addFact({
@@ -14138,6 +14180,7 @@ function prepareDocxQualityDocument(document, options = {}) {
14138
14180
  for (const fact of svgTextFacts(props, path4)) addFact(fact);
14139
14181
  const drawn = drawnRatio(props);
14140
14182
  const natural = readableRatio(props);
14183
+ const authored = authoredPropsAt(path4);
14141
14184
  addFact({
14142
14185
  id: `docx:image:${path4}`,
14143
14186
  kind: "docx/image",
@@ -14145,7 +14188,13 @@ function prepareDocxQualityDocument(document, options = {}) {
14145
14188
  ...typeof props.alt === "string" && props.alt.trim() !== "" && { alt: props.alt },
14146
14189
  captioned: isCaptioned(facts, authoredPath(path4)),
14147
14190
  ...drawn !== void 0 && { drawnRatio: drawn },
14148
- ...natural !== void 0 && { naturalRatio: natural }
14191
+ ...natural !== void 0 && { naturalRatio: natural },
14192
+ ...authoredPath(path4) === path4 && finiteNumber(authored?.width) !== void 0 && finiteNumber(authored?.height) !== void 0 && {
14193
+ authoredSizePx: {
14194
+ width: authored.width,
14195
+ height: authored.height
14196
+ }
14197
+ }
14149
14198
  });
14150
14199
  }
14151
14200
  if (node.name === "heading") {
@@ -14711,6 +14760,8 @@ import {
14711
14760
  // src/quality/rules.ts
14712
14761
  import {
14713
14762
  chartInfoDesignFindings,
14763
+ configurationLabel,
14764
+ configurationSource,
14714
14765
  DEFAULT_MAXIMUM_CHART_SERIES,
14715
14766
  DEFAULT_MAXIMUM_PIE_SLICES,
14716
14767
  fontCountFinding,
@@ -15310,18 +15361,21 @@ var docxRequiredChromeRule = {
15310
15361
  if (required.length === 0) return [];
15311
15362
  return facts.filter(
15312
15363
  (fact) => fact.kind === "docx/chrome-slot"
15313
- ).filter((fact) => required.includes(fact.role) && !fact.present).map((fact) => ({
15314
- path: fact.path,
15315
- relatedPaths: [fact.invocation],
15316
- message: `${fact.block} states no ${fact.role} in its "${fact.slot}" slot; the ${profile?.id ?? "selected"} profile expects one on every ${fact.block}.`,
15317
- suggestion: `Fill the "${fact.slot}" slot. The theme already styles it.`,
15318
- context: { block: fact.block, slot: fact.slot, role: fact.role },
15319
- evidence: {
15320
- actual: "empty",
15321
- expected: fact.role,
15322
- values: { source: "profile", required }
15323
- }
15324
- }));
15364
+ ).filter((fact) => required.includes(fact.role) && !fact.present).map((fact) => {
15365
+ const source = configurationSource(configuration, "required");
15366
+ return {
15367
+ path: fact.path,
15368
+ relatedPaths: [fact.invocation],
15369
+ message: `${fact.block} states no ${fact.role} in its "${fact.slot}" slot; ${configurationLabel(source, profile)} expects one on every ${fact.block}.`,
15370
+ suggestion: `Fill the "${fact.slot}" slot. The theme already styles it.`,
15371
+ context: { block: fact.block, slot: fact.slot, role: fact.role },
15372
+ evidence: {
15373
+ actual: "empty",
15374
+ expected: fact.role,
15375
+ values: { source, required }
15376
+ }
15377
+ };
15378
+ });
15325
15379
  }
15326
15380
  };
15327
15381
  var docxExhibitRequiredRule = {
@@ -15382,16 +15436,21 @@ var docxRunningHeadRule = {
15382
15436
  const missing = required.filter((part) => !fact[part]);
15383
15437
  if (missing.length === 0) return [];
15384
15438
  const parts = missing.map((part) => part === "pageNumber" ? "page-number field" : part).join(", ");
15439
+ const source = configurationSource(
15440
+ configuration,
15441
+ "required",
15442
+ "fromSection"
15443
+ );
15385
15444
  return [
15386
15445
  {
15387
15446
  path: fact.path,
15388
- message: `Section ${fact.index + 1} carries no ${parts}; the ${profile?.id ?? "selected"} profile expects a running head on every section after the cover.`,
15447
+ message: `Section ${fact.index + 1} carries no ${parts}; ${configurationLabel(source, profile)} expects a running head ` + (from === 0 ? "on every section." : `on every section from section ${from + 1} on.`),
15389
15448
  suggestion: "Invoke a running-head block at the top of the first body section: its section effect fills every later section with the title and n / N.",
15390
15449
  context: { section: fact.index, missing },
15391
15450
  evidence: {
15392
15451
  actual: required.filter((part) => fact[part]),
15393
15452
  expected: required,
15394
- values: { source: "profile", fromSection: from }
15453
+ values: { source, fromSection: from }
15395
15454
  }
15396
15455
  }
15397
15456
  ];
@@ -15448,13 +15507,16 @@ var docxSizeCountRule = {
15448
15507
  formats: ["docx"],
15449
15508
  defaultEnabled: false,
15450
15509
  defaultParameters: { maximumSizes: 8 },
15451
- evaluate: ({ facts, configuration, profile }) => sizeCountFinding(
15452
- paintedSizes(facts),
15453
- numberParameter(configuration.parameters, "maximumSizes", 8),
15454
- themeFact(facts)?.path ?? "/props",
15455
- profile?.id,
15456
- DOCX_TYPE_VOCABULARY
15457
- )
15510
+ evaluate: ({ facts, configuration, profile }) => {
15511
+ const source = configurationSource(configuration, "maximumSizes");
15512
+ return sizeCountFinding(
15513
+ paintedSizes(facts),
15514
+ numberParameter(configuration.parameters, "maximumSizes", 8),
15515
+ themeFact(facts)?.path ?? "/props",
15516
+ { source, label: configurationLabel(source, profile) },
15517
+ DOCX_TYPE_VOCABULARY
15518
+ );
15519
+ }
15458
15520
  };
15459
15521
  var docxRoleDriftRule = {
15460
15522
  id: "docx/role-drift",
@@ -15511,7 +15573,12 @@ var docxBodyMeasureRule = {
15511
15573
  actual: fact.charactersPerLine,
15512
15574
  expected: wide ? maximum : minimum,
15513
15575
  unit: "characters",
15514
- values: { source: "profile" }
15576
+ values: {
15577
+ source: configurationSource(
15578
+ configuration,
15579
+ wide ? "maximumCharacters" : "minimumCharacters"
15580
+ )
15581
+ }
15515
15582
  }
15516
15583
  };
15517
15584
  });
@@ -15559,7 +15626,12 @@ var docxSectionContentRule = {
15559
15626
  actual: 0,
15560
15627
  expected: 1,
15561
15628
  unit: "headings",
15562
- values: { source: "profile" }
15629
+ values: {
15630
+ source: configurationSource(
15631
+ configuration,
15632
+ "minimumWordsForHeading"
15633
+ )
15634
+ }
15563
15635
  }
15564
15636
  }
15565
15637
  ];
@@ -15576,7 +15648,7 @@ var docxHeadingKeepNextRule = {
15576
15648
  defaultCertainty: "deterministic",
15577
15649
  formats: ["docx"],
15578
15650
  defaultEnabled: false,
15579
- evaluate: ({ facts }) => facts.filter(
15651
+ evaluate: ({ facts, configuration }) => facts.filter(
15580
15652
  (fact) => fact.kind === "docx/heading" && !fact.keepNext
15581
15653
  ).map((fact) => {
15582
15654
  const heading = /\/props\/level$/.test(fact.path) ? fact.path.replace(/\/props\/level$/, "") : void 0;
@@ -15585,7 +15657,7 @@ var docxHeadingKeepNextRule = {
15585
15657
  message: "This heading is not bound to the content under it: a page break can leave it alone at the foot of a page.",
15586
15658
  suggestion: heading ? "Set keepNext on the heading, or give the theme\u2019s heading style keepNext so every level is bound." : "Give the theme\u2019s heading style keepNext, or set it in the block definition that draws this heading.",
15587
15659
  context: { level: fact.level },
15588
- evidence: { values: { source: "profile" } },
15660
+ evidence: { values: { source: configurationSource(configuration) } },
15589
15661
  ...heading && {
15590
15662
  fixes: [
15591
15663
  {
@@ -15638,7 +15710,20 @@ var docxImageAspectRule = {
15638
15710
  {
15639
15711
  path: fact.path,
15640
15712
  drawn: fact.drawnRatio,
15641
- natural: fact.naturalRatio
15713
+ natural: fact.naturalRatio,
15714
+ ...fact.authoredSizePx && {
15715
+ sides: {
15716
+ width: {
15717
+ path: `${fact.path}/props/width`,
15718
+ value: fact.authoredSizePx.width
15719
+ },
15720
+ height: {
15721
+ path: `${fact.path}/props/height`,
15722
+ value: fact.authoredSizePx.height
15723
+ },
15724
+ decimals: 0
15725
+ }
15726
+ }
15642
15727
  },
15643
15728
  "page",
15644
15729
  tolerance
@@ -15667,17 +15752,18 @@ var docxContentsRule = {
15667
15752
  (fact) => fact.kind === "docx/outline"
15668
15753
  );
15669
15754
  if (!outline || outline.contents || outline.headings < minimum) return [];
15755
+ const source = configurationSource(configuration, "minimumHeadings");
15670
15756
  return [
15671
15757
  {
15672
15758
  path: outline.path,
15673
- message: `The document carries ${outline.headings} headings and no table of contents; the ${profile?.id ?? "selected"} profile expects one from ${minimum}.`,
15759
+ message: `The document carries ${outline.headings} headings and no table of contents; ${configurationLabel(source, profile)} expects one from ${minimum}.`,
15674
15760
  suggestion: "Add a `toc` component after the cover. Word fills it from the headings already there.",
15675
15761
  context: { headings: outline.headings, minimum },
15676
15762
  evidence: {
15677
15763
  actual: outline.headings,
15678
15764
  expected: minimum,
15679
15765
  unit: "headings",
15680
- values: { source: "profile" }
15766
+ values: { source }
15681
15767
  }
15682
15768
  }
15683
15769
  ];
@@ -15722,7 +15808,7 @@ var DOCX_QUALITY_PROFILES = {
15722
15808
  "client-report": {
15723
15809
  id: "client-report",
15724
15810
  formats: ["docx"],
15725
- 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, every size on the theme scale with at most eleven in play, a readable measure, no empty or untitled section, every heading bound to what follows and every figure named, no page rendered empty or left half blank under the running head, and at least one chart or table.",
15811
+ 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, every size on the theme scale with at most nine in play, a readable measure, no empty or untitled section, every heading bound to what follows and every figure named, no page rendered empty or left half blank under the running head, and at least one chart or table.",
15726
15812
  rules: {
15727
15813
  "docx/required-chrome": {
15728
15814
  parameters: { required: ["takeaway", "source"] }
@@ -15735,14 +15821,15 @@ var DOCX_QUALITY_PROFILES = {
15735
15821
  },
15736
15822
  "docx/heading-hierarchy": { severity: "warning" },
15737
15823
  "docx/type-scale": { enabled: true },
15738
- // Measured 2026-09-15 with statistics, lists and contents counted: a
15739
- // client report built from the house blocks paints up to eleven sizes —
15740
- // body, heading 1 and 2, subtitle, display, table cells and headers,
15741
- // source and chrome, and the kpi-row's figure, its unit and its label
15742
- // (28, 14 and 10pt, or 20 and 10 in a row of four). Every one of the
15743
- // reports of the #360 checkpoint and the #409 verification set came in
15744
- // at eleven or fewer; a size past the house set is an ad-hoc one.
15745
- "docx/size-count": { enabled: true, parameters: { maximumSizes: 11 } },
15824
+ // Measured 2026-09-16 with statistics, lists and contents counted, once
15825
+ // a statistic paints in the theme's roles (#454): a client report built
15826
+ // from the house blocks paints up to nine sizes 8 (source, chrome),
15827
+ // 9 (labels, the kpi-row's unit and caption), 9.5 (table cells), 10.5
15828
+ // (body), 12.5 and 16 (headings), 13 (subtitle), 18 (a four-up kpi
15829
+ // figure) and 22 (display, the kpi figure). The 155 client reports of
15830
+ // the checkpoint, host-study and verification runs came in at nine or
15831
+ // fewer; a size past the house set is an ad-hoc one.
15832
+ "docx/size-count": { enabled: true, parameters: { maximumSizes: 9 } },
15746
15833
  "docx/role-drift": { enabled: true },
15747
15834
  // The report blocks place every figure in a captioned block, so a page
15748
15835
  // with no text under the running head is a stray break or an empty
@@ -17792,7 +17879,7 @@ function createBuilderImpl(state) {
17792
17879
  }
17793
17880
  return new Set(list);
17794
17881
  }
17795
- async function expandDocument(document, options) {
17882
+ async function expandDocument(document, options, provenance = false) {
17796
17883
  const preserveSet = resolvePreserveSet(options);
17797
17884
  const rootIn = document;
17798
17885
  const internalDocument = rootIn.props === void 0 ? { ...rootIn, props: {} } : rootIn;
@@ -17852,7 +17939,7 @@ function createBuilderImpl(state) {
17852
17939
  warnings,
17853
17940
  resolveNamedTheme: resolveDocumentTheme
17854
17941
  });
17855
- if (modedRoot.props.blocks !== void 0) {
17942
+ if (modedRoot.props.blocks !== void 0 || provenance) {
17856
17943
  const expanded = await expandBlocksWithPlugins(
17857
17944
  modedRoot,
17858
17945
  modedTheme,
@@ -17900,6 +17987,7 @@ function createBuilderImpl(state) {
17900
17987
  modedDoc: modedDoc2,
17901
17988
  sourceMap: expanded.sourceMap,
17902
17989
  blockPaths: expanded.blocks,
17990
+ pluginOutputs: expanded.pluginOutputs,
17903
17991
  warnings,
17904
17992
  preserveSet,
17905
17993
  processed: {
@@ -17948,8 +18036,64 @@ function createBuilderImpl(state) {
17948
18036
  throw error;
17949
18037
  }
17950
18038
  }
18039
+ async function prepareQuality(document, options) {
18040
+ const {
18041
+ modedRoot,
18042
+ modedTheme,
18043
+ themeName,
18044
+ modedDoc,
18045
+ sourceMap,
18046
+ blockPaths,
18047
+ pluginOutputs,
18048
+ warnings
18049
+ } = await expandDocument(document, options, true);
18050
+ const prepared = prepareDocxQualityDocument(
18051
+ document,
18052
+ {
18053
+ context: { document: modedRoot, theme: modedTheme, themeName },
18054
+ expanded: {
18055
+ document: modedDoc,
18056
+ sourceMap: sourceMap ?? {},
18057
+ blocks: blockPaths ?? [],
18058
+ ...pluginOutputs && { pluginOutputs }
18059
+ },
18060
+ customThemes: state.customThemes,
18061
+ fonts: state.fonts,
18062
+ renderer: options?.renderer ?? state.renderer ?? modedRoot.renderer,
18063
+ warnings
18064
+ }
18065
+ );
18066
+ return { prepared, warnings: warnings.length > 0 ? warnings : null };
18067
+ }
18068
+ async function generateBufferFromPrepared(prepared, options) {
18069
+ const { context } = prepared.model;
18070
+ const warnings = [];
18071
+ const baseDir = options?.baseDir ?? state.baseDir;
18072
+ const deterministic = options?.deterministic ?? state.deterministic;
18073
+ const generatedAt = options?.generatedAt ?? state.generatedAt;
18074
+ const renderer = options?.renderer ?? state.renderer ?? prepared.renderer;
18075
+ const { buffer } = await generateBufferViaIr(context.document, {
18076
+ context,
18077
+ prepared,
18078
+ ...state.services ? { services: state.services } : {},
18079
+ ...state.fonts ? { fonts: state.fonts } : {},
18080
+ ...baseDir !== void 0 ? { baseDir } : {},
18081
+ ...deterministic !== void 0 ? { deterministic } : {},
18082
+ ...generatedAt !== void 0 ? { generatedAt } : {},
18083
+ ...renderer !== void 0 ? { renderer } : {},
18084
+ warnings
18085
+ });
18086
+ return {
18087
+ buffer,
18088
+ warnings: warnings.length > 0 ? warnings : null,
18089
+ standardDefinition: context.document,
18090
+ preservedDefinition: void 0
18091
+ };
18092
+ }
17951
18093
  async function generateBuffer(document, options) {
17952
18094
  try {
18095
+ const reused = options?.prepared && resolvePreserveSet(options) === void 0 ? options.prepared : void 0;
18096
+ if (reused) return await generateBufferFromPrepared(reused, options);
17953
18097
  const {
17954
18098
  modedRoot,
17955
18099
  modedTheme,
@@ -18079,6 +18223,7 @@ function createBuilderImpl(state) {
18079
18223
  generateBuffer,
18080
18224
  generateFile,
18081
18225
  expandStandardDefinition,
18226
+ prepareQuality,
18082
18227
  getComponentNames,
18083
18228
  validate,
18084
18229
  generateSchema,