@json-to-office/core-docx 3.2.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",
@@ -5051,6 +5055,10 @@ function resolveServiceUrl(propsUrl, servicesUrl, defaultUrl) {
5051
5055
  const withScheme = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
5052
5056
  return withScheme.replace(/\/+$/, "");
5053
5057
  }
5058
+ var SERVICE_UNAVAILABLE_CODE = "SERVICE_UNAVAILABLE";
5059
+ function serviceUnavailable(message) {
5060
+ return Object.assign(new Error(message), { code: SERVICE_UNAVAILABLE_CODE });
5061
+ }
5054
5062
  async function postJsonToService(opts) {
5055
5063
  const resolvedHeaders = typeof opts.headers === "function" ? await opts.headers(opts.body) : opts.headers;
5056
5064
  const headers = {
@@ -5070,12 +5078,12 @@ async function postJsonToService(opts) {
5070
5078
  });
5071
5079
  } catch (error) {
5072
5080
  if (error?.name === "AbortError") {
5073
- throw new Error(
5081
+ throw serviceUnavailable(
5074
5082
  `${opts.serviceLabel} timed out after ${timeoutMs}ms at ${opts.url}.`
5075
5083
  );
5076
5084
  }
5077
5085
  const cause = error instanceof Error ? error.message : String(error);
5078
- throw new Error(opts.onUnreachable(opts.url, cause));
5086
+ throw serviceUnavailable(opts.onUnreachable(opts.url, cause));
5079
5087
  } finally {
5080
5088
  clearTimeout(timer);
5081
5089
  }
@@ -11770,6 +11778,7 @@ async function processResolvedDocument(document, resolved, themeName, generation
11770
11778
  };
11771
11779
  }
11772
11780
  function createDocumentMetadata(props, generationDate = /* @__PURE__ */ new Date()) {
11781
+ const parsed = props.metadata?.date ? new Date(props.metadata.date) : void 0;
11773
11782
  return {
11774
11783
  title: props.metadata?.title,
11775
11784
  subtitle: props.metadata?.subtitle,
@@ -11778,7 +11787,7 @@ function createDocumentMetadata(props, generationDate = /* @__PURE__ */ new Date
11778
11787
  company: props.metadata?.company,
11779
11788
  version: props.metadata?.version,
11780
11789
  tags: props.metadata?.tags,
11781
- date: props.metadata?.date ? new Date(props.metadata.date) : generationDate
11790
+ date: parsed && !Number.isNaN(parsed.getTime()) ? parsed : generationDate
11782
11791
  };
11783
11792
  }
11784
11793
  async function extractSections(components, context) {
@@ -11870,7 +11879,12 @@ import {
11870
11879
  normalizeHighchartsChart
11871
11880
  } from "@json-to-office/quality";
11872
11881
  import { DEFAULT_DOCX_RENDERER_ID as DEFAULT_DOCX_RENDERER_ID2 } from "@json-to-office/shared-docx";
11873
- import { designColors as designColors2, resolveDesignColor as resolveDesignColor2 } from "@json-to-office/shared";
11882
+ import {
11883
+ designCanvas as designCanvas2,
11884
+ designColors as designColors2,
11885
+ resolveDesignColor as resolveDesignColor2,
11886
+ typeScaleSizes
11887
+ } from "@json-to-office/shared";
11874
11888
 
11875
11889
  // src/core/generationContext.ts
11876
11890
  import { applyExportMode } from "@json-to-office/shared";
@@ -12809,6 +12823,93 @@ function walkActive(node, path4, page, visit) {
12809
12823
  (child, index) => walkActive(child, `${path4}/children/${index}`, page, visit)
12810
12824
  );
12811
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
+ }
12812
12913
  function prepareDocxQualityDocument(document, options = {}) {
12813
12914
  const themed = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
12814
12915
  customThemes: options.customThemes,
@@ -12856,6 +12957,75 @@ function prepareDocxQualityDocument(document, options = {}) {
12856
12957
  ...budget
12857
12958
  });
12858
12959
  }
12960
+ for (const role of blockSlotRoles2(themed.document, expanded.blocks)) {
12961
+ addFact({
12962
+ id: `docx:chrome-slot:${role.path}`,
12963
+ kind: "docx/chrome-slot",
12964
+ path: role.path,
12965
+ relatedPaths: [role.invocation],
12966
+ block: role.block,
12967
+ slot: role.slot,
12968
+ role: role.role,
12969
+ present: slotIsFilled(role.value),
12970
+ invocation: role.invocation
12971
+ });
12972
+ }
12973
+ const topLevel = Array.isArray(context.document.children) ? context.document.children : [];
12974
+ let inherited = {};
12975
+ topLevel.forEach((node, index) => {
12976
+ if (node?.name !== "section") return;
12977
+ const props = asRecord(node.props) ?? {};
12978
+ const part = (kind) => props[kind] === "linkToPrevious" ? inherited[kind] : props[kind];
12979
+ const header = part("header");
12980
+ const footer = part("footer");
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
+ }
13019
+ addFact({
13020
+ id: `docx:section-chrome:${index}`,
13021
+ kind: "docx/section-chrome",
13022
+ path: `/children/${index}`,
13023
+ index,
13024
+ header: partPresent(header),
13025
+ footer: partPresent(footer),
13026
+ pageNumber: hasPageField(header) || hasPageField(footer)
13027
+ });
13028
+ });
12859
13029
  const paletteHexes = {};
12860
13030
  const visualColors = designColors2(
12861
13031
  resolved.theme.colors,
@@ -12879,12 +13049,34 @@ function prepareDocxQualityDocument(document, options = {}) {
12879
13049
  (value) => `#${resolveDesignColor2(value, visualColors)}`
12880
13050
  ) ?? SERIES_COLOR_TOKENS.filter((token) => paletteHexes[token] !== void 0);
12881
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);
12882
13072
  addFact({
12883
13073
  id: "docx:theme",
12884
13074
  kind: "docx/theme",
12885
13075
  path: "/props",
12886
13076
  themeName: context.themeName,
12887
13077
  paletteHexes,
13078
+ typeScalePt,
13079
+ roleSizesPt,
12888
13080
  // `heading` and `body` only. A theme also names `mono` and `light`, but
12889
13081
  // those paint nothing until a component asks for them — counting an
12890
13082
  // unused `Courier New` against a document's family budget would flag a
@@ -12937,6 +13129,15 @@ function prepareDocxQualityDocument(document, options = {}) {
12937
13129
  if (node.name === "table") {
12938
13130
  const fact = tableFact(props, path4, page.availableWidthTwips);
12939
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
+ }
12940
13141
  const design = tableDesignFact(
12941
13142
  props,
12942
13143
  path4,
@@ -12995,6 +13196,10 @@ function prepareDocxQualityDocument(document, options = {}) {
12995
13196
  if (node.name === "paragraph" || node.name === "heading") {
12996
13197
  const fact = lineBoxFact(node, props, path4, typography, context.document);
12997
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
+ }
12998
13203
  }
12999
13204
  if (node.name === "image" || node.name === "visual") {
13000
13205
  for (const fact of svgTextFacts(props, path4)) addFact(fact);
@@ -13050,6 +13255,17 @@ function prepareDocxQualityDocument(document, options = {}) {
13050
13255
  }
13051
13256
  };
13052
13257
  }
13258
+ function slotIsFilled(value) {
13259
+ if (typeof value === "string") return value.trim() !== "";
13260
+ return value !== void 0 && value !== null && value !== false && (!Array.isArray(value) || value.length > 0);
13261
+ }
13262
+ function partPresent(part) {
13263
+ return Array.isArray(part) && part.length > 0;
13264
+ }
13265
+ var PAGE_FIELD = /\{(PAGE|PAGE_NUMBER|TOTAL_PAGES|NUMPAGES)\}/;
13266
+ function hasPageField(part) {
13267
+ return Array.isArray(part) && PAGE_FIELD.test(JSON.stringify(part));
13268
+ }
13053
13269
 
13054
13270
  // src/core/generateFromIr.ts
13055
13271
  var UncompiledComponentError = class extends Error {
@@ -13556,6 +13772,7 @@ function numberParameter(parameters, name, fallback) {
13556
13772
  }
13557
13773
  var docxTableWidthRule = {
13558
13774
  id: "docx/table-width",
13775
+ description: "Explicit column widths that sum past the usable width of their section.",
13559
13776
  code: QUALITY_CODES.TABLE_WIDTH_OVERFLOW,
13560
13777
  category: "integrity",
13561
13778
  defaultSeverity: "warning",
@@ -13603,6 +13820,7 @@ var docxTableWidthRule = {
13603
13820
  };
13604
13821
  var docxHeadingHierarchyRule = {
13605
13822
  id: "docx/heading-hierarchy",
13823
+ description: "A heading that skips a level and breaks the outline.",
13606
13824
  code: QUALITY_CODES.HEADING_SKIP,
13607
13825
  category: "hierarchy",
13608
13826
  defaultSeverity: "info",
@@ -13646,6 +13864,7 @@ function frameTextFacts(facts) {
13646
13864
  }
13647
13865
  var docxTextFitRule = {
13648
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.",
13649
13868
  code: QUALITY_CODES.TEXT_OVERFLOW,
13650
13869
  category: "integrity",
13651
13870
  defaultSeverity: "warning",
@@ -13719,6 +13938,7 @@ var docxTextFitRule = {
13719
13938
  var FRAME_COLLISION_MIN_WIDTH_TWIPS = 240;
13720
13939
  var docxFrameCollisionRule = {
13721
13940
  id: "docx/frame-collision",
13941
+ description: "Two page-anchored frames whose estimated text lands on the same region of a page.",
13722
13942
  code: QUALITY_CODES.FRAME_COLLISION,
13723
13943
  category: "integrity",
13724
13944
  defaultSeverity: "warning",
@@ -13815,6 +14035,7 @@ var docxFrameCollisionRule = {
13815
14035
  };
13816
14036
  var docxSvgTextBoundsRule = {
13817
14037
  id: "docx/svg-text-bounds",
14038
+ description: "A text baseline outside an inline SVG\u2019s viewBox, so the words are never painted.",
13818
14039
  code: QUALITY_CODES.SVG_TEXT_CLIPPED,
13819
14040
  category: "integrity",
13820
14041
  defaultSeverity: "warning",
@@ -13850,6 +14071,7 @@ function tenths(value) {
13850
14071
  }
13851
14072
  var docxLineBoxRule = {
13852
14073
  id: "docx/line-box",
14074
+ description: "An `exactly` line box shorter than the capitals it has to hold.",
13853
14075
  code: QUALITY_CODES.LINE_BOX_COLLAPSE,
13854
14076
  category: "legibility",
13855
14077
  defaultSeverity: "warning",
@@ -13903,6 +14125,7 @@ var docxLineBoxRule = {
13903
14125
  };
13904
14126
  var docxPlaceholderRule = {
13905
14127
  id: "docx/placeholder-text",
14128
+ description: "An unfilled scaffold slot, or leftover filler copy.",
13906
14129
  code: QUALITY_CODES.PLACEHOLDER_TEXT,
13907
14130
  category: "integrity",
13908
14131
  defaultSeverity: "warning",
@@ -13921,6 +14144,7 @@ var docxPlaceholderRule = {
13921
14144
  };
13922
14145
  var docxSlotBudgetRule = {
13923
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.",
13924
14148
  code: QUALITY_CODES.SLOT_BUDGET,
13925
14149
  category: "composition",
13926
14150
  defaultSeverity: "warning",
@@ -13944,6 +14168,7 @@ var docxSlotBudgetRule = {
13944
14168
  var DEFAULT_MAX_FONT_FAMILIES = 3;
13945
14169
  var docxFontCountRule = {
13946
14170
  id: "docx/font-count",
14171
+ description: "Distinct font families the document can paint.",
13947
14172
  code: QUALITY_CODES.FONT_COUNT,
13948
14173
  category: "brand",
13949
14174
  defaultSeverity: "warning",
@@ -13980,6 +14205,7 @@ var docxFontCountRule = {
13980
14205
  };
13981
14206
  var docxPaletteRule = {
13982
14207
  id: "docx/palette-adherence",
14208
+ description: "A literal colour the resolved theme does not define.",
13983
14209
  code: QUALITY_CODES.OFF_PALETTE,
13984
14210
  category: "brand",
13985
14211
  defaultSeverity: "info",
@@ -14005,6 +14231,7 @@ var docxPaletteRule = {
14005
14231
  var DEFAULT_MAX_TABLE_ROWS_PER_PAGE = 25;
14006
14232
  var docxChartRule = {
14007
14233
  id: "docx/chart-design",
14234
+ description: "What a chart claims about its numbers: the comparison, the palette, the unit and the caption.",
14008
14235
  code: QUALITY_CODES.CHART_OVERLOADED,
14009
14236
  category: "information-design",
14010
14237
  defaultSeverity: "warning",
@@ -14046,6 +14273,7 @@ function seriesColorFix(fact) {
14046
14273
  }
14047
14274
  var docxTableDesignRule = {
14048
14275
  id: "docx/table-design",
14276
+ description: "How a table lays its numbers out: alignment, rounding, rules and length.",
14049
14277
  code: QUALITY_CODES.TABLE_NUMERIC_ALIGN,
14050
14278
  category: "information-design",
14051
14279
  defaultSeverity: "warning",
@@ -14097,6 +14325,233 @@ function alignColumnRight(column) {
14097
14325
  }
14098
14326
  return operations;
14099
14327
  }
14328
+ function stringListParameter(parameters, name) {
14329
+ const value = parameters[name];
14330
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
14331
+ }
14332
+ var docxRequiredChromeRule = {
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.",
14335
+ code: QUALITY_CODES.CHROME_MISSING,
14336
+ category: "consistency",
14337
+ defaultSeverity: "warning",
14338
+ defaultCertainty: "deterministic",
14339
+ formats: ["docx"],
14340
+ defaultParameters: { required: [] },
14341
+ evaluate: ({ facts, configuration, profile }) => {
14342
+ const required = stringListParameter(configuration.parameters, "required");
14343
+ if (required.length === 0) return [];
14344
+ return facts.filter(
14345
+ (fact) => fact.kind === "docx/chrome-slot"
14346
+ ).filter((fact) => required.includes(fact.role) && !fact.present).map((fact) => ({
14347
+ path: fact.path,
14348
+ relatedPaths: [fact.invocation],
14349
+ message: `${fact.block} states no ${fact.role} in its "${fact.slot}" slot; the ${profile?.id ?? "selected"} profile expects one on every ${fact.block}.`,
14350
+ suggestion: `Fill the "${fact.slot}" slot. The theme already styles it.`,
14351
+ context: { block: fact.block, slot: fact.slot, role: fact.role }
14352
+ }));
14353
+ }
14354
+ };
14355
+ var SECTION_CHROME_PARTS = ["header", "footer", "pageNumber"];
14356
+ var docxRunningHeadRule = {
14357
+ id: "docx/running-head",
14358
+ description: "A body section without the running head a profile or policy expects. Off until one names parts.",
14359
+ code: QUALITY_CODES.CHROME_MISSING,
14360
+ category: "consistency",
14361
+ defaultSeverity: "warning",
14362
+ defaultCertainty: "deterministic",
14363
+ formats: ["docx"],
14364
+ defaultParameters: { required: [], fromSection: 1 },
14365
+ evaluate: ({ facts, configuration, profile }) => {
14366
+ const required = stringListParameter(
14367
+ configuration.parameters,
14368
+ "required"
14369
+ ).filter(
14370
+ (part) => SECTION_CHROME_PARTS.includes(part)
14371
+ );
14372
+ if (required.length === 0) return [];
14373
+ const from = numberParameter(configuration.parameters, "fromSection", 1);
14374
+ return facts.filter(
14375
+ (fact) => fact.kind === "docx/section-chrome" && fact.index >= from
14376
+ ).flatMap((fact) => {
14377
+ const missing = required.filter((part) => !fact[part]);
14378
+ if (missing.length === 0) return [];
14379
+ const parts = missing.map((part) => part === "pageNumber" ? "page-number field" : part).join(", ");
14380
+ return [
14381
+ {
14382
+ path: fact.path,
14383
+ message: `Section ${fact.index + 1} carries no ${parts}; the ${profile?.id ?? "selected"} profile expects a running head on every section after the cover.`,
14384
+ suggestion: "Invoke a running-head block at the top of the first body section: its section effect fills every later section with the tracker and n / N.",
14385
+ context: { section: fact.index, missing }
14386
+ }
14387
+ ];
14388
+ });
14389
+ }
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
+ };
14100
14555
  var DOCX_QUALITY_RULES = {
14101
14556
  id: "docx/default",
14102
14557
  rules: [
@@ -14111,10 +14566,35 @@ var DOCX_QUALITY_RULES = {
14111
14566
  docxChartRule,
14112
14567
  docxTableDesignRule,
14113
14568
  docxFontCountRule,
14114
- docxPaletteRule
14569
+ docxPaletteRule,
14570
+ docxRequiredChromeRule,
14571
+ docxRunningHeadRule,
14572
+ docxTypeScaleRule,
14573
+ docxSizeCountRule,
14574
+ docxRoleDriftRule
14115
14575
  ]
14116
14576
  };
14117
14577
  var DOCX_QUALITY_PROFILES = {
14578
+ "client-report": {
14579
+ id: "client-report",
14580
+ formats: ["docx"],
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.",
14582
+ rules: {
14583
+ "docx/required-chrome": {
14584
+ parameters: { required: ["takeaway", "source"] }
14585
+ },
14586
+ "docx/running-head": {
14587
+ parameters: {
14588
+ required: ["header", "footer", "pageNumber"],
14589
+ fromSection: 1
14590
+ }
14591
+ },
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 }
14596
+ }
14597
+ },
14118
14598
  "executive-report": {
14119
14599
  id: "executive-report",
14120
14600
  formats: ["docx"],
@@ -14136,6 +14616,11 @@ var DOCX_QUALITY_PROFILES = {
14136
14616
  };
14137
14617
  var DOCX_DEFAULT_QUALITY_PROFILE = DOCX_QUALITY_PROFILES["technical-report"];
14138
14618
  var DOCX_PROFILES_BY_ID = DOCX_QUALITY_PROFILES;
14619
+ function declaredDocxQualityProfile(document) {
14620
+ const props = document?.props;
14621
+ const id = props?.qualityProfile;
14622
+ return typeof id === "string" && Object.prototype.hasOwnProperty.call(DOCX_PROFILES_BY_ID, id) ? DOCX_PROFILES_BY_ID[id] : void 0;
14623
+ }
14139
14624
  function resolveDocxQualityProfile(requested) {
14140
14625
  if (!requested) return void 0;
14141
14626
  const registered = DOCX_PROFILES_BY_ID[requested.id];
@@ -14183,7 +14668,7 @@ function analyzeDocxQuality(doc, options = {}) {
14183
14668
  );
14184
14669
  }
14185
14670
  return docxQualityEngine.analyzeSync(prepared, {
14186
- profile: resolveDocxQualityProfile(options.profile) ?? DOCX_DEFAULT_QUALITY_PROFILE,
14671
+ profile: resolveDocxQualityProfile(options.profile) ?? declaredDocxQualityProfile(doc) ?? DOCX_DEFAULT_QUALITY_PROFILE,
14187
14672
  policy: options.policy
14188
14673
  });
14189
14674
  }
@@ -14293,17 +14778,655 @@ function formatWarningsText(warnings) {
14293
14778
 
14294
14779
  // src/index.ts
14295
14780
  init_styles();
14781
+
14782
+ // src/blueprints/index.ts
14783
+ import {
14784
+ blockDependencies,
14785
+ validateBlueprint
14786
+ } from "@json-to-office/shared";
14787
+ import { collectPlaceholders as collectPlaceholders2 } from "@json-to-office/quality";
14788
+ import { existsSync, readdirSync, readFileSync as readFileSync2 } from "fs";
14789
+ import { fileURLToPath as fileURLToPath2 } from "url";
14790
+ import { dirname as dirname2, join as join2 } from "path";
14791
+
14792
+ // src/templates/blueprints/client-report.docx.blueprint.json
14793
+ var client_report_docx_blueprint_default = {
14794
+ id: "client-report",
14795
+ format: "docx",
14796
+ title: "Client report",
14797
+ description: "A report for a client or a public administration: a cover, key takeaways, KPIs, chart-first sections with a takeaway and a source under every figure, data tables, notes and sources, under a running head that tracks the section and numbers the pages.",
14798
+ whenToUse: "A periodic or one-off report to a client, a board or a public body that reads for the conclusion first and checks the evidence second. Not for a memo or a technical specification.",
14799
+ theme: "consulting",
14800
+ profile: "client-report",
14801
+ definitions: "client-report-blocks.docx.json",
14802
+ numbering: "sections",
14803
+ toc: false,
14804
+ variants: {
14805
+ "data-heavy": {
14806
+ description: "Evidence-led: a KPI row under the takeaways, a chart with its takeaway in the second section, a data table with a note in the third, next steps last.",
14807
+ whenToUse: "The brief comes with numbers: performance, finance, operations, anything the reader will check against a figure.",
14808
+ pages: {
14809
+ min: 4,
14810
+ max: 8
14811
+ },
14812
+ metadata: {
14813
+ title: "{{Title: as on the cover}}",
14814
+ author: "{{Author or team}}",
14815
+ company: "{{Client name}}",
14816
+ date: "{{Month YYYY}}"
14817
+ },
14818
+ children: [
14819
+ {
14820
+ name: "section",
14821
+ children: [
14822
+ {
14823
+ name: "block",
14824
+ props: {
14825
+ ref: "cover",
14826
+ slots: {
14827
+ title: "{{Title: the report's conclusion in one sentence}}",
14828
+ subtitle: "{{Subtitle: what the report answers, for whom}}",
14829
+ client: "{{Client name}}",
14830
+ date: "{{Month YYYY}}",
14831
+ confidentiality: "{{Confidentiality: Confidential, or For internal use}}"
14832
+ }
14833
+ }
14834
+ }
14835
+ ]
14836
+ },
14837
+ {
14838
+ name: "section",
14839
+ children: [
14840
+ {
14841
+ name: "block",
14842
+ props: {
14843
+ ref: "running-head",
14844
+ slots: {
14845
+ confidentiality: "{{Confidentiality: as on the cover}}",
14846
+ date: "{{Month YYYY}}"
14847
+ }
14848
+ }
14849
+ },
14850
+ {
14851
+ name: "block",
14852
+ props: {
14853
+ ref: "section-opener",
14854
+ slots: {
14855
+ number: "01",
14856
+ title: "{{Section title: the finding, as a statement}}",
14857
+ tracker: "{{Tracker: one or two words}}"
14858
+ }
14859
+ }
14860
+ },
14861
+ {
14862
+ name: "block",
14863
+ props: {
14864
+ ref: "key-takeaways",
14865
+ slots: {
14866
+ items: [
14867
+ "{{Takeaway 1: one claim, one sentence, with its number}}",
14868
+ "{{Takeaway 2: one claim, one sentence, with its number}}",
14869
+ "{{Takeaway 3: the recommendation that follows}}"
14870
+ ]
14871
+ }
14872
+ }
14873
+ },
14874
+ {
14875
+ name: "block",
14876
+ props: {
14877
+ ref: "kpi-row",
14878
+ slots: {
14879
+ items: [
14880
+ {
14881
+ value: "{{0.0}}",
14882
+ label: "{{What it measures}}"
14883
+ },
14884
+ {
14885
+ value: "{{0.0}}",
14886
+ label: "{{What it measures}}"
14887
+ },
14888
+ {
14889
+ value: "{{0.0}}",
14890
+ label: "{{What it measures}}"
14891
+ }
14892
+ ],
14893
+ source: "{{Source: system or document, date}}"
14894
+ }
14895
+ }
14896
+ },
14897
+ {
14898
+ name: "paragraph",
14899
+ props: {
14900
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
14901
+ }
14902
+ }
14903
+ ]
14904
+ },
14905
+ {
14906
+ name: "section",
14907
+ children: [
14908
+ {
14909
+ name: "block",
14910
+ props: {
14911
+ ref: "section-opener",
14912
+ slots: {
14913
+ number: "02",
14914
+ title: "{{Section title: the finding, as a statement}}",
14915
+ tracker: "{{Tracker: one or two words}}"
14916
+ }
14917
+ }
14918
+ },
14919
+ {
14920
+ name: "paragraph",
14921
+ props: {
14922
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
14923
+ }
14924
+ },
14925
+ {
14926
+ name: "block",
14927
+ props: {
14928
+ ref: "chart-figure",
14929
+ slots: {
14930
+ chart: {
14931
+ name: "highcharts",
14932
+ props: {
14933
+ width: "100%",
14934
+ options: {
14935
+ chart: {
14936
+ type: "column",
14937
+ width: 900,
14938
+ height: 460
14939
+ },
14940
+ title: {
14941
+ text: null
14942
+ },
14943
+ xAxis: {
14944
+ categories: [
14945
+ "{{Period 1}}",
14946
+ "{{Period 2}}",
14947
+ "{{Period 3}}"
14948
+ ]
14949
+ },
14950
+ yAxis: {
14951
+ title: {
14952
+ text: "{{Measure (unit)}}"
14953
+ }
14954
+ },
14955
+ series: [
14956
+ {
14957
+ name: "{{Series name}}",
14958
+ data: [0, 0, 0]
14959
+ }
14960
+ ]
14961
+ }
14962
+ }
14963
+ },
14964
+ caption: "{{Caption: what the chart shows, as a statement}}",
14965
+ takeaway: "{{Takeaway: the one sentence the reader should keep}}",
14966
+ source: "{{Source: system or document, date}}"
14967
+ }
14968
+ }
14969
+ },
14970
+ {
14971
+ name: "paragraph",
14972
+ props: {
14973
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
14974
+ }
14975
+ }
14976
+ ]
14977
+ },
14978
+ {
14979
+ name: "section",
14980
+ children: [
14981
+ {
14982
+ name: "block",
14983
+ props: {
14984
+ ref: "section-opener",
14985
+ slots: {
14986
+ number: "03",
14987
+ title: "{{Section title: the finding, as a statement}}",
14988
+ tracker: "{{Tracker: one or two words}}"
14989
+ }
14990
+ }
14991
+ },
14992
+ {
14993
+ name: "paragraph",
14994
+ props: {
14995
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
14996
+ }
14997
+ },
14998
+ {
14999
+ name: "block",
15000
+ props: {
15001
+ ref: "data-table",
15002
+ slots: {
15003
+ title: "{{Table title: what it compares, and the period}}",
15004
+ labelHeader: "{{Row label}}",
15005
+ labels: ["{{Row 1}}", "{{Row 2}}", "{{Row 3}}"],
15006
+ columns: [
15007
+ {
15008
+ header: "{{Measure (unit)}}",
15009
+ cells: ["{{0.0}}", "{{0.0}}", "{{0.0}}"]
15010
+ },
15011
+ {
15012
+ header: "{{Change}}",
15013
+ cells: ["{{0.0}}", "{{0.0}}", "{{0.0}}"]
15014
+ }
15015
+ ],
15016
+ source: "{{Source: system or document, date}}"
15017
+ }
15018
+ }
15019
+ },
15020
+ {
15021
+ name: "block",
15022
+ props: {
15023
+ ref: "callout",
15024
+ slots: {
15025
+ label: "{{Note label}}",
15026
+ text: "{{Note: a caveat about the numbers, a definition or the method}}"
15027
+ }
15028
+ }
15029
+ }
15030
+ ]
15031
+ },
15032
+ {
15033
+ name: "section",
15034
+ children: [
15035
+ {
15036
+ name: "block",
15037
+ props: {
15038
+ ref: "section-opener",
15039
+ slots: {
15040
+ number: "04",
15041
+ title: "{{Section title: the finding, as a statement}}",
15042
+ tracker: "{{Tracker: one or two words}}"
15043
+ }
15044
+ }
15045
+ },
15046
+ {
15047
+ name: "paragraph",
15048
+ props: {
15049
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
15050
+ }
15051
+ },
15052
+ {
15053
+ name: "block",
15054
+ props: {
15055
+ ref: "footnotes"
15056
+ }
15057
+ }
15058
+ ]
15059
+ }
15060
+ ]
15061
+ },
15062
+ narrative: {
15063
+ description: "Argument-led: takeaways, then three sections of prose with one KPI row and one note as evidence, next steps last.",
15064
+ whenToUse: "The brief is a position, an assessment or a recommendation with few numbers; the reader follows an argument.",
15065
+ pages: {
15066
+ min: 3,
15067
+ max: 6
15068
+ },
15069
+ metadata: {
15070
+ title: "{{Title: as on the cover}}",
15071
+ author: "{{Author or team}}",
15072
+ company: "{{Client name}}",
15073
+ date: "{{Month YYYY}}"
15074
+ },
15075
+ children: [
15076
+ {
15077
+ name: "section",
15078
+ children: [
15079
+ {
15080
+ name: "block",
15081
+ props: {
15082
+ ref: "cover",
15083
+ slots: {
15084
+ title: "{{Title: the report's conclusion in one sentence}}",
15085
+ subtitle: "{{Subtitle: what the report answers, for whom}}",
15086
+ client: "{{Client name}}",
15087
+ date: "{{Month YYYY}}",
15088
+ confidentiality: "{{Confidentiality: Confidential, or For internal use}}"
15089
+ }
15090
+ }
15091
+ }
15092
+ ]
15093
+ },
15094
+ {
15095
+ name: "section",
15096
+ children: [
15097
+ {
15098
+ name: "block",
15099
+ props: {
15100
+ ref: "running-head",
15101
+ slots: {
15102
+ confidentiality: "{{Confidentiality: as on the cover}}",
15103
+ date: "{{Month YYYY}}"
15104
+ }
15105
+ }
15106
+ },
15107
+ {
15108
+ name: "block",
15109
+ props: {
15110
+ ref: "section-opener",
15111
+ slots: {
15112
+ number: "01",
15113
+ title: "{{Section title: the finding, as a statement}}",
15114
+ tracker: "{{Tracker: one or two words}}"
15115
+ }
15116
+ }
15117
+ },
15118
+ {
15119
+ name: "block",
15120
+ props: {
15121
+ ref: "key-takeaways",
15122
+ slots: {
15123
+ items: [
15124
+ "{{Takeaway 1: one claim, one sentence, with its number}}",
15125
+ "{{Takeaway 2: one claim, one sentence, with its number}}",
15126
+ "{{Takeaway 3: the recommendation that follows}}"
15127
+ ]
15128
+ }
15129
+ }
15130
+ },
15131
+ {
15132
+ name: "paragraph",
15133
+ props: {
15134
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
15135
+ }
15136
+ },
15137
+ {
15138
+ name: "paragraph",
15139
+ props: {
15140
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
15141
+ }
15142
+ }
15143
+ ]
15144
+ },
15145
+ {
15146
+ name: "section",
15147
+ children: [
15148
+ {
15149
+ name: "block",
15150
+ props: {
15151
+ ref: "section-opener",
15152
+ slots: {
15153
+ number: "02",
15154
+ title: "{{Section title: the finding, as a statement}}",
15155
+ tracker: "{{Tracker: one or two words}}"
15156
+ }
15157
+ }
15158
+ },
15159
+ {
15160
+ name: "paragraph",
15161
+ props: {
15162
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
15163
+ }
15164
+ },
15165
+ {
15166
+ name: "block",
15167
+ props: {
15168
+ ref: "callout",
15169
+ slots: {
15170
+ label: "{{Note label}}",
15171
+ text: "{{Note: a caveat about the numbers, a definition or the method}}"
15172
+ }
15173
+ }
15174
+ },
15175
+ {
15176
+ name: "paragraph",
15177
+ props: {
15178
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
15179
+ }
15180
+ }
15181
+ ]
15182
+ },
15183
+ {
15184
+ name: "section",
15185
+ children: [
15186
+ {
15187
+ name: "block",
15188
+ props: {
15189
+ ref: "section-opener",
15190
+ slots: {
15191
+ number: "03",
15192
+ title: "{{Section title: the finding, as a statement}}",
15193
+ tracker: "{{Tracker: one or two words}}"
15194
+ }
15195
+ }
15196
+ },
15197
+ {
15198
+ name: "paragraph",
15199
+ props: {
15200
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
15201
+ }
15202
+ },
15203
+ {
15204
+ name: "block",
15205
+ props: {
15206
+ ref: "kpi-row",
15207
+ slots: {
15208
+ items: [
15209
+ {
15210
+ value: "{{0.0}}",
15211
+ label: "{{What it measures}}"
15212
+ },
15213
+ {
15214
+ value: "{{0.0}}",
15215
+ label: "{{What it measures}}"
15216
+ },
15217
+ {
15218
+ value: "{{0.0}}",
15219
+ label: "{{What it measures}}"
15220
+ }
15221
+ ],
15222
+ source: "{{Source: system or document, date}}"
15223
+ }
15224
+ }
15225
+ },
15226
+ {
15227
+ name: "paragraph",
15228
+ props: {
15229
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
15230
+ }
15231
+ }
15232
+ ]
15233
+ },
15234
+ {
15235
+ name: "section",
15236
+ children: [
15237
+ {
15238
+ name: "block",
15239
+ props: {
15240
+ ref: "section-opener",
15241
+ slots: {
15242
+ number: "04",
15243
+ title: "{{Section title: the finding, as a statement}}",
15244
+ tracker: "{{Tracker: one or two words}}"
15245
+ }
15246
+ }
15247
+ },
15248
+ {
15249
+ name: "paragraph",
15250
+ props: {
15251
+ text: "{{Body: two or three short paragraphs of evidence for the section title, each opening with its claim}}"
15252
+ }
15253
+ },
15254
+ {
15255
+ name: "block",
15256
+ props: {
15257
+ ref: "footnotes"
15258
+ }
15259
+ }
15260
+ ]
15261
+ }
15262
+ ]
15263
+ }
15264
+ }
15265
+ };
15266
+
15267
+ // src/blueprints/index.ts
15268
+ function register(...candidates) {
15269
+ const registry2 = {};
15270
+ for (const candidate of candidates) {
15271
+ const issues = validateBlueprint(candidate);
15272
+ if (issues.length > 0)
15273
+ throw new Error(
15274
+ `Invalid bundled blueprint: ${issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`
15275
+ );
15276
+ const blueprint = candidate;
15277
+ if (blueprint.format !== "docx")
15278
+ throw new Error(`Blueprint ${blueprint.id} is not a DOCX blueprint.`);
15279
+ if (registry2[blueprint.id])
15280
+ throw new Error(
15281
+ `Two bundled blueprints share the id "${blueprint.id}"; the later one would silently replace the earlier.`
15282
+ );
15283
+ registry2[blueprint.id] = blueprint;
15284
+ }
15285
+ return registry2;
15286
+ }
15287
+ function scanned(known) {
15288
+ let here;
15289
+ try {
15290
+ here = dirname2(fileURLToPath2(import.meta.url));
15291
+ } catch {
15292
+ return [];
15293
+ }
15294
+ const directory = [
15295
+ join2(here, "../templates/blueprints"),
15296
+ join2(here, "templates/blueprints")
15297
+ ].find((path4) => existsSync(path4));
15298
+ if (!directory) return [];
15299
+ return readdirSync(directory).filter((file) => file.endsWith(".docx.blueprint.json")).map((file) => JSON.parse(readFileSync2(join2(directory, file), "utf8"))).filter((candidate) => {
15300
+ const id = candidate?.id;
15301
+ return !(typeof id === "string" && id in known);
15302
+ });
15303
+ }
15304
+ var bundled = register(client_report_docx_blueprint_default);
15305
+ var DOCX_BLUEPRINTS = {
15306
+ ...bundled,
15307
+ ...register(...scanned(bundled))
15308
+ };
15309
+ function docxBlueprint(id) {
15310
+ return DOCX_BLUEPRINTS[id];
15311
+ }
15312
+ var MARKER = /^\{\{\s*([\s\S]*?)\s*\}\}$/;
15313
+ function instantiateDocxBlueprint(blueprint, options) {
15314
+ if (blueprint.format !== "docx")
15315
+ throw new Error(
15316
+ `Blueprint ${blueprint.id} is a ${blueprint.format} blueprint; this instantiates DOCX ones.`
15317
+ );
15318
+ const variantId = options.variant ?? Object.keys(blueprint.variants)[0];
15319
+ const variant = blueprint.variants[variantId];
15320
+ if (!variant)
15321
+ throw new Error(
15322
+ `Blueprint ${blueprint.id} has no variant "${variantId}"; it has ${Object.keys(
15323
+ blueprint.variants
15324
+ ).join(", ")}.`
15325
+ );
15326
+ const children = structuredClone(variant.children);
15327
+ const blocks = definitionsFor(children, options.definitions, blueprint);
15328
+ const document = {
15329
+ name: "docx",
15330
+ props: {
15331
+ theme: options.theme ?? blueprint.theme,
15332
+ qualityProfile: blueprint.profile,
15333
+ ...Object.keys(blocks).length > 0 && { blocks },
15334
+ metadata: { ...variant.metadata, ...options.metadata }
15335
+ },
15336
+ children
15337
+ };
15338
+ return { document, fillMap: fillMap(document, blocks), variant: variantId };
15339
+ }
15340
+ function definitionsFor(children, available, blueprint) {
15341
+ const refs = /* @__PURE__ */ new Set();
15342
+ const visit = (node) => {
15343
+ if (Array.isArray(node)) return node.forEach(visit);
15344
+ if (!node || typeof node !== "object") return;
15345
+ const record = node;
15346
+ const props = record.props;
15347
+ if (record.name === "block" && typeof props?.ref === "string")
15348
+ refs.add(props.ref);
15349
+ Object.values(record).forEach(visit);
15350
+ };
15351
+ visit(children);
15352
+ const result = {};
15353
+ for (const ref of refs) {
15354
+ if (!available[ref])
15355
+ throw new Error(
15356
+ `Blueprint ${blueprint.id} invokes "${ref}", which ${blueprint.definitions} does not define.`
15357
+ );
15358
+ for (const name of [...blockDependencies(available, ref), ref])
15359
+ result[name] ??= structuredClone(available[name]);
15360
+ }
15361
+ return result;
15362
+ }
15363
+ function fillMap(document, blocks) {
15364
+ return collectPlaceholders2(document).filter((occurrence) => occurrence.match.kind === "scaffold-marker").map((occurrence) => {
15365
+ const guidance = occurrence.text.match(MARKER)?.[1] ?? occurrence.text;
15366
+ const base = { path: occurrence.path, marker: occurrence.text, guidance };
15367
+ if (occurrence.path.startsWith("/props/metadata/"))
15368
+ return { ...base, kind: "metadata" };
15369
+ const slot = slotAt(document, occurrence.path, blocks);
15370
+ return slot ? { ...base, kind: "slot", ...slot } : { ...base, kind: "text" };
15371
+ });
15372
+ }
15373
+ function slotAt(document, pointer, blocks) {
15374
+ const at = pointer.lastIndexOf("/props/slots/");
15375
+ if (at < 0) return void 0;
15376
+ const invocation = valueAt(document, pointer.slice(0, at));
15377
+ const ref = invocation?.props?.ref;
15378
+ if (typeof ref !== "string" || !blocks[ref]) return void 0;
15379
+ const segments = pointer.slice(at + "/props/slots/".length).split("/").map(unescape);
15380
+ let slot = blocks[ref].slots[segments[0]];
15381
+ const names = [segments[0]];
15382
+ for (const segment of segments.slice(1)) {
15383
+ if (!slot) break;
15384
+ if (slot.type === "array" && /^\d+$/.test(segment)) slot = slot.items;
15385
+ else if (slot.type === "object") {
15386
+ slot = slot.properties?.[segment];
15387
+ names.push(segment);
15388
+ } else if (slot.type === "component")
15389
+ break;
15390
+ else slot = void 0;
15391
+ }
15392
+ if (!slot) return void 0;
15393
+ return {
15394
+ block: ref,
15395
+ slot: names.join("."),
15396
+ type: slot.type,
15397
+ ...slot.maxWords !== void 0 && { maxWords: slot.maxWords },
15398
+ ...slot.maxLength !== void 0 && { maxLength: slot.maxLength },
15399
+ ...slot.oneLine !== void 0 && { oneLine: slot.oneLine },
15400
+ ...slot.required !== void 0 && { required: slot.required }
15401
+ };
15402
+ }
15403
+ function unescape(segment) {
15404
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
15405
+ }
15406
+ function valueAt(root, pointer) {
15407
+ if (pointer === "") return root;
15408
+ let current = root;
15409
+ for (const segment of pointer.split("/").slice(1).map(unescape)) {
15410
+ if (Array.isArray(current)) current = current[Number(segment)];
15411
+ else if (current && typeof current === "object")
15412
+ current = current[segment];
15413
+ else return void 0;
15414
+ }
15415
+ return current;
15416
+ }
15417
+
15418
+ // src/index.ts
14296
15419
  init_json();
14297
15420
 
14298
15421
  // src/templates/documents/index.ts
14299
15422
  import fs4 from "fs";
14300
15423
  import path2 from "path";
14301
- import { fileURLToPath as fileURLToPath2 } from "url";
15424
+ import { fileURLToPath as fileURLToPath3 } from "url";
14302
15425
  var _dirname = null;
14303
15426
  function getDirname() {
14304
15427
  if (_dirname === null) {
14305
15428
  try {
14306
- _dirname = path2.dirname(fileURLToPath2(import.meta.url));
15429
+ _dirname = path2.dirname(fileURLToPath3(import.meta.url));
14307
15430
  } catch {
14308
15431
  throw new Error(
14309
15432
  "Cannot resolve file paths in a bundled environment. Example loading from disk is not available."
@@ -15189,6 +16312,7 @@ export {
15189
16312
  ComponentValidationError2 as ComponentValidationError,
15190
16313
  DocumentGenerator as CoreDocumentGenerator,
15191
16314
  DEFAULT_DOCX_RENDERER_ID,
16315
+ DOCX_BLUEPRINTS,
15192
16316
  DOCX_DEFAULT_QUALITY_PROFILE,
15193
16317
  DOCX_QUALITY_PROFILES,
15194
16318
  DOCX_QUALITY_RULES,
@@ -15205,7 +16329,9 @@ export {
15205
16329
  createDocumentGenerator,
15206
16330
  createMinimalTheme,
15207
16331
  createVersion,
16332
+ declaredDocxQualityProfile,
15208
16333
  devportalTheme,
16334
+ docxBlueprint,
15209
16335
  docxQualityEngine,
15210
16336
  docxRendererIds,
15211
16337
  docxRendererStatuses,
@@ -15230,6 +16356,7 @@ export {
15230
16356
  getExampleNames,
15231
16357
  getVisualPrepassStats,
15232
16358
  hasNodeBuiltins,
16359
+ instantiateDocxBlueprint,
15233
16360
  isColumnsComponent,
15234
16361
  isDocxRendererId,
15235
16362
  isHeadingComponent,