@json-to-office/core-docx 1.4.0 → 1.6.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
@@ -937,6 +937,143 @@ var init_devportal_docx_theme = __esm({
937
937
  }
938
938
  });
939
939
 
940
+ // src/templates/themes/vermilion.docx.theme.json
941
+ var vermilion_docx_theme_default;
942
+ var init_vermilion_docx_theme = __esm({
943
+ "src/templates/themes/vermilion.docx.theme.json"() {
944
+ vermilion_docx_theme_default = {
945
+ name: "vermilion",
946
+ displayName: "Vermilion Editorial",
947
+ description: "Poster-red editorial system \u2014 vermilion display headings, ink text, warm creams and hairline rules",
948
+ version: "1.0.0",
949
+ colors: {
950
+ primary: "#282829",
951
+ secondary: "#58595B",
952
+ accent: "#EF4130",
953
+ text: "#282829",
954
+ background: "#FFFFFF",
955
+ border: "#E8E2DC",
956
+ textPrimary: "#282829",
957
+ textSecondary: "#58595B",
958
+ textMuted: "#939598",
959
+ borderPrimary: "#C7C8CA",
960
+ borderSecondary: "#EFEAE4",
961
+ backgroundPrimary: "#FFFFFF",
962
+ backgroundSecondary: "#FAF7F5"
963
+ },
964
+ fonts: {
965
+ heading: {
966
+ family: "Arial",
967
+ size: 24
968
+ },
969
+ body: {
970
+ family: "Calibri",
971
+ size: 10.5
972
+ },
973
+ mono: {
974
+ family: "Consolas",
975
+ size: 10
976
+ },
977
+ light: {
978
+ family: "Arial",
979
+ size: 24
980
+ }
981
+ },
982
+ page: {
983
+ size: "A4",
984
+ margins: {
985
+ top: 1360,
986
+ bottom: 1360,
987
+ left: 1180,
988
+ right: 1180,
989
+ header: 620,
990
+ footer: 620,
991
+ gutter: 0
992
+ }
993
+ },
994
+ styles: {
995
+ normal: {
996
+ font: "body",
997
+ color: "textPrimary",
998
+ lineSpacing: {
999
+ type: "multiple",
1000
+ value: 1.3
1001
+ },
1002
+ alignment: "left",
1003
+ spacing: {
1004
+ after: 8
1005
+ }
1006
+ },
1007
+ heading1: {
1008
+ font: "heading",
1009
+ size: 25,
1010
+ bold: false,
1011
+ color: "accent",
1012
+ characterSpacing: {
1013
+ type: "expanded",
1014
+ value: 50
1015
+ },
1016
+ spacing: {
1017
+ before: 22,
1018
+ after: 12
1019
+ },
1020
+ keepNext: true,
1021
+ alignment: "left"
1022
+ },
1023
+ heading2: {
1024
+ font: "heading",
1025
+ size: 12.5,
1026
+ bold: true,
1027
+ color: "accent",
1028
+ characterSpacing: {
1029
+ type: "expanded",
1030
+ value: 30
1031
+ },
1032
+ spacing: {
1033
+ before: 16,
1034
+ after: 7
1035
+ },
1036
+ keepNext: true,
1037
+ alignment: "left"
1038
+ },
1039
+ heading3: {
1040
+ font: "body",
1041
+ size: 11,
1042
+ bold: true,
1043
+ color: "textPrimary",
1044
+ spacing: {
1045
+ before: 12,
1046
+ after: 5
1047
+ },
1048
+ keepNext: true,
1049
+ alignment: "left"
1050
+ },
1051
+ title: {
1052
+ font: "heading",
1053
+ size: 38,
1054
+ bold: true,
1055
+ color: "primary",
1056
+ alignment: "left",
1057
+ spacing: {
1058
+ before: 0,
1059
+ after: 14
1060
+ }
1061
+ },
1062
+ subtitle: {
1063
+ font: "body",
1064
+ size: 14,
1065
+ color: "textSecondary",
1066
+ alignment: "left",
1067
+ spacing: {
1068
+ before: 0,
1069
+ after: 10
1070
+ }
1071
+ }
1072
+ }
1073
+ };
1074
+ }
1075
+ });
1076
+
940
1077
  // src/templates/themes/index.ts
941
1078
  import fs from "fs";
942
1079
  import path from "path";
@@ -953,7 +1090,8 @@ function loadThemesFromJson() {
953
1090
  corporate: ensureThemeDefaults(corporate_docx_theme_default),
954
1091
  modern: ensureThemeDefaults(modern_docx_theme_default),
955
1092
  apex: ensureThemeDefaults(apex_docx_theme_default),
956
- devportal: ensureThemeDefaults(devportal_docx_theme_default)
1093
+ devportal: ensureThemeDefaults(devportal_docx_theme_default),
1094
+ vermilion: ensureThemeDefaults(vermilion_docx_theme_default)
957
1095
  };
958
1096
  try {
959
1097
  const __filename = fileURLToPath(import.meta.url);
@@ -1001,6 +1139,7 @@ var init_themes = __esm({
1001
1139
  init_modern_docx_theme();
1002
1140
  init_apex_docx_theme();
1003
1141
  init_devportal_docx_theme();
1142
+ init_vermilion_docx_theme();
1004
1143
  _themesCache = null;
1005
1144
  themes = loadThemesFromJson();
1006
1145
  getTheme = (themeName) => {
@@ -1746,6 +1885,14 @@ async function rasterizeSvgFallback(svg, transformation) {
1746
1885
  }
1747
1886
  try {
1748
1887
  const markup = svg.toString("utf-8");
1888
+ if (/<(?:[A-Za-z][\w.-]*:)?text[\s>/]/.test(markup)) {
1889
+ reportWarning(
1890
+ "image",
1891
+ "IMAGE_SVG_RASTER_SKIPPED",
1892
+ "Inline SVG contains <text>, which would rasterize with machine-dependent fonts, so its fallback only renders in Word 2016+. Convert the text to paths for a portable fallback."
1893
+ );
1894
+ return void 0;
1895
+ }
1749
1896
  const probeImage = new Resvg(markup);
1750
1897
  const wide = probeImage.width / probeImage.height > transformation.width / transformation.height;
1751
1898
  const mode = wide ? "height" : "width";
@@ -5037,88 +5184,6 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
5037
5184
  return map;
5038
5185
  }
5039
5186
 
5040
- // src/json/normalizer.ts
5041
- function normalizeComponent(component) {
5042
- if (component.name === "columns") {
5043
- return normalizeColumnsComponent(component);
5044
- }
5045
- if ("children" in component && component.children) {
5046
- return {
5047
- ...component,
5048
- // Preserve ALL properties from validated component
5049
- children: component.children.map(normalizeComponent)
5050
- };
5051
- }
5052
- if (component.name === "section") {
5053
- return normalizeSectionComponent(component);
5054
- }
5055
- return component;
5056
- }
5057
- function normalizeColumnsComponent(component) {
5058
- const cfg = component.props || {};
5059
- const applyTopLevelGap = (cols, topGap) => cols.map((c, i) => {
5060
- const isLast = i === cols.length - 1;
5061
- if (!isLast && c.gap === void 0 && topGap !== void 0) {
5062
- return { ...c, gap: topGap };
5063
- }
5064
- return c;
5065
- });
5066
- const normalizeAutoWidth = (cols) => cols.map((c) => ({
5067
- ...c,
5068
- width: c.width === "auto" ? void 0 : c.width
5069
- }));
5070
- let columnsArray;
5071
- if (typeof cfg.columns === "number" && cfg.columns > 0) {
5072
- const n = cfg.columns;
5073
- const base = Array.from(
5074
- { length: n },
5075
- () => ({})
5076
- );
5077
- const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
5078
- columnsArray = applyTopLevelGap(base, topLevelGap);
5079
- } else if (Array.isArray(cfg.columns)) {
5080
- columnsArray = cfg.columns;
5081
- const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
5082
- columnsArray = applyTopLevelGap(columnsArray, topLevelGap);
5083
- columnsArray = normalizeAutoWidth(columnsArray);
5084
- } else {
5085
- columnsArray = [{}];
5086
- }
5087
- const normalizedProps = {
5088
- columns: columnsArray
5089
- };
5090
- return {
5091
- ...component,
5092
- // Preserve any additional properties
5093
- name: "columns",
5094
- props: normalizedProps,
5095
- children: component.children ? component.children.map(normalizeComponent) : void 0
5096
- };
5097
- }
5098
- function normalizeSectionComponent(component) {
5099
- const props = component.props || {};
5100
- return {
5101
- ...component,
5102
- // Preserve ALL properties
5103
- props: {
5104
- ...props,
5105
- // Preserve ALL props properties
5106
- header: props.header ? props.header === "linkToPrevious" ? "linkToPrevious" : props.header.map((m) => normalizeComponent(m)) : void 0,
5107
- footer: props.footer ? props.footer === "linkToPrevious" ? "linkToPrevious" : props.footer.map((m) => normalizeComponent(m)) : void 0
5108
- },
5109
- children: component.children ? component.children.map(normalizeComponent) : void 0
5110
- };
5111
- }
5112
- function normalizeDocument(document) {
5113
- if (document.name !== "docx") {
5114
- throw new Error(
5115
- 'Top-level document must be a docx component with name="docx"'
5116
- );
5117
- }
5118
- const normalized = normalizeComponent(document);
5119
- return [normalized];
5120
- }
5121
-
5122
5187
  // src/core/desugarExternals.ts
5123
5188
  import {
5124
5189
  clampVisualDpi as clampVisualDpi2,
@@ -6892,6 +6957,17 @@ function mergeBorderSizePerSide(...borders3) {
6892
6957
  left: pick("left")
6893
6958
  };
6894
6959
  }
6960
+ function statedSides(colors, sizes) {
6961
+ const namedColors = colors.filter((value) => typeof value === "object").map(normalizeBorderColor);
6962
+ const namedSizes = sizes.filter((value) => typeof value === "object").map(normalizeBorderSize);
6963
+ const stated = (side) => namedColors.some((c) => c !== void 0 && c[side] !== UNSET_COLOR) || namedSizes.some((s) => s !== void 0 && s[side] !== UNSET_SIZE);
6964
+ return {
6965
+ top: stated("top"),
6966
+ right: stated("right"),
6967
+ bottom: stated("bottom"),
6968
+ left: stated("left")
6969
+ };
6970
+ }
6895
6971
  function mergePaddingPerSide(...paddings) {
6896
6972
  const normalized = paddings.map(normalizePadding);
6897
6973
  if (!normalized.some((p) => p !== void 0)) return void 0;
@@ -7031,6 +7107,10 @@ function mergeCellDefaults(tableDef, columnDef, cellDef, position, tableOuterBor
7031
7107
  tableDef?.borderSize,
7032
7108
  tableOuterBorder.borderSize
7033
7109
  ),
7110
+ borderExplicit: statedSides(
7111
+ [cellDef?.borderColor, columnDef?.borderColor],
7112
+ [cellDef?.borderSize, columnDef?.borderSize]
7113
+ ),
7034
7114
  padding: mergePaddingPerSide(
7035
7115
  cellDef?.padding,
7036
7116
  columnDef?.padding,
@@ -7088,6 +7168,10 @@ function mergeHeaderCellDefaults(tableDef, headerTableDef, columnDef, headerDef,
7088
7168
  tableDef?.borderSize,
7089
7169
  tableOuterBorder.borderSize
7090
7170
  ),
7171
+ borderExplicit: statedSides(
7172
+ [headerDef?.borderColor, columnDef?.borderColor],
7173
+ [headerDef?.borderSize, columnDef?.borderSize]
7174
+ ),
7091
7175
  padding: mergePaddingPerSide(
7092
7176
  headerDef?.padding,
7093
7177
  headerTableDef?.padding,
@@ -7118,28 +7202,20 @@ function resolveTableModel(source, theme, themeName, options = {}) {
7118
7202
  return position.isLastCol ? hidden.right : hidden.insideVertical;
7119
7203
  }
7120
7204
  };
7121
- const borders3 = (merged, position) => ({
7122
- top: {
7123
- size: merged.borderSize.top,
7124
- color: merged.borderColor.top,
7125
- hidden: isHidden("top", position)
7126
- },
7127
- right: {
7128
- size: merged.borderSize.right,
7129
- color: merged.borderColor.right,
7130
- hidden: isHidden("right", position)
7131
- },
7132
- bottom: {
7133
- size: merged.borderSize.bottom,
7134
- color: merged.borderColor.bottom,
7135
- hidden: isHidden("bottom", position)
7136
- },
7137
- left: {
7138
- size: merged.borderSize.left,
7139
- color: merged.borderColor.left,
7140
- hidden: isHidden("left", position)
7141
- }
7142
- });
7205
+ const borders3 = (merged, position) => {
7206
+ const side = (name) => ({
7207
+ size: merged.borderSize[name],
7208
+ color: merged.borderColor[name],
7209
+ hidden: isHidden(name, position) && !merged.borderExplicit[name],
7210
+ explicit: merged.borderExplicit[name]
7211
+ });
7212
+ return {
7213
+ top: side("top"),
7214
+ right: side("right"),
7215
+ bottom: side("bottom"),
7216
+ left: side("left")
7217
+ };
7218
+ };
7143
7219
  const tallest = (heights) => heights.reduce(
7144
7220
  (max, height) => height === void 0 ? max : max === void 0 ? height : Math.max(max, height),
7145
7221
  void 0
@@ -7249,6 +7325,7 @@ function resolveTableModel(source, theme, themeName, options = {}) {
7249
7325
  };
7250
7326
  return row;
7251
7327
  });
7328
+ adjudicateInteriorEdges([header, ...rows]);
7252
7329
  return {
7253
7330
  ...resolveWidths(source, columns, theme, themeName),
7254
7331
  header,
@@ -7256,6 +7333,54 @@ function resolveTableModel(source, theme, themeName, options = {}) {
7256
7333
  repeatHeader: source.repeatHeaderOnPageBreak ?? true
7257
7334
  };
7258
7335
  }
7336
+ function drawsNothing(border3) {
7337
+ return border3.hidden || border3.size <= 0;
7338
+ }
7339
+ function channels(hex) {
7340
+ const digits = hex.startsWith("#") ? hex.slice(1) : hex;
7341
+ return [
7342
+ parseInt(digits.slice(0, 2), 16) || 0,
7343
+ parseInt(digits.slice(2, 4), 16) || 0,
7344
+ parseInt(digits.slice(4, 6), 16) || 0
7345
+ ];
7346
+ }
7347
+ function outweighs(a, b) {
7348
+ if (drawsNothing(a)) return false;
7349
+ if (drawsNothing(b)) return true;
7350
+ if (a.size !== b.size) return a.size > b.size;
7351
+ const [redA, greenA, blueA] = channels(a.color);
7352
+ const [redB, greenB, blueB] = channels(b.color);
7353
+ const brightness = [
7354
+ [redA + blueA + 2 * greenA, redB + blueB + 2 * greenB],
7355
+ [blueA + 2 * greenA, blueB + 2 * greenB],
7356
+ [greenA, greenB]
7357
+ ];
7358
+ for (const [ours, theirs] of brightness) {
7359
+ if (ours !== theirs) return ours < theirs;
7360
+ }
7361
+ return false;
7362
+ }
7363
+ function adjudicateInteriorEdges(rows) {
7364
+ const settle = (earlier, later) => {
7365
+ if (drawsNothing(earlier) && drawsNothing(later)) return;
7366
+ if (earlier.hidden === later.hidden && earlier.size === later.size && earlier.color === later.color) {
7367
+ return;
7368
+ }
7369
+ const winner = earlier.explicit !== later.explicit ? earlier.explicit ? earlier : later : outweighs(later, earlier) ? later : earlier;
7370
+ const loser = winner === earlier ? later : earlier;
7371
+ loser.size = winner.size;
7372
+ loser.color = winner.color;
7373
+ loser.hidden = winner.hidden;
7374
+ };
7375
+ rows.forEach((row, rowIndex) => {
7376
+ row.cells.forEach((cell, colIndex) => {
7377
+ const right = row.cells[colIndex + 1];
7378
+ if (right) settle(cell.borders.right, right.borders.left);
7379
+ const below = rows[rowIndex + 1]?.cells[colIndex];
7380
+ if (below) settle(cell.borders.bottom, below.borders.top);
7381
+ });
7382
+ });
7383
+ }
7259
7384
  function resolveWidths(source, columns, theme, themeName) {
7260
7385
  const hasExplicitWidths = columns.some((col) => col.width !== void 0);
7261
7386
  if (!hasExplicitWidths) {
@@ -10272,6 +10397,7 @@ function compileTableRow(row, spacing, baseStyle, scope) {
10272
10397
  };
10273
10398
  }
10274
10399
  function compileTableCell(cell, spacing, baseStyle, keepNext, scope, rowRevision, markRevision) {
10400
+ const cellLine = compileLineSpacing(cell.font?.lineSpacing);
10275
10401
  const paragraph2 = {
10276
10402
  kind: "paragraph",
10277
10403
  id: scope.id,
@@ -10285,7 +10411,10 @@ function compileTableCell(cell, spacing, baseStyle, keepNext, scope, rowRevision
10285
10411
  spacing: {
10286
10412
  beforeTwips: spacing.before,
10287
10413
  afterTwips: spacing.after,
10288
- ...spacing.line !== void 0 ? { lineTwips: spacing.line, lineRule: lineRule(spacing.lineRule) } : {}
10414
+ ...cellLine ? {
10415
+ ...cellLine.lineTwips !== void 0 ? { lineTwips: cellLine.lineTwips } : {},
10416
+ lineRule: cellLine.lineRule
10417
+ } : spacing.line !== void 0 ? { lineTwips: spacing.line, lineRule: lineRule(spacing.lineRule) } : {}
10289
10418
  },
10290
10419
  ...keepNext ? { keepNext: true } : {}
10291
10420
  },
@@ -10568,102 +10697,7 @@ function isDocxRendererId(value) {
10568
10697
 
10569
10698
  // src/core/generateFromIr.ts
10570
10699
  init_packageDocument();
10571
-
10572
- // src/core/generationContext.ts
10573
- import { applyExportMode } from "@json-to-office/shared";
10574
-
10575
- // src/styles/theme-resolver.ts
10576
- init_themes();
10577
- function resolveBuiltInTheme(themeName, options = {}) {
10578
- if (!hasTheme(themeName)) {
10579
- const available = [
10580
- ...getThemeNames(),
10581
- ...Object.keys(options.customThemes ?? {})
10582
- ].sort();
10583
- options.warnings?.push({
10584
- component: "theme",
10585
- message: `Theme "${themeName}" not found; falling back to "minimal". Available themes: ${available.join(", ")}.`,
10586
- severity: "warning",
10587
- context: { code: "theme_not_found", requested: themeName, available }
10588
- });
10589
- }
10590
- return getThemeWithFallback(themeName);
10591
- }
10592
-
10593
- // src/themes/overrides.ts
10594
- function applyThemeOverrides(theme, overrides) {
10595
- if (!overrides) return theme;
10596
- const fonts = { ...theme.fonts };
10597
- for (const [role, def] of Object.entries(overrides.fonts ?? {})) {
10598
- const key = role;
10599
- fonts[key] = { ...theme.fonts[key], ...def };
10600
- }
10601
- const styles = { ...theme.styles ?? {} };
10602
- for (const [name, def] of Object.entries(overrides.styles ?? {})) {
10603
- styles[name] = { ...styles[name], ...def };
10604
- }
10605
- return {
10606
- ...theme,
10607
- colors: { ...theme.colors, ...overrides.colors ?? {} },
10608
- fonts,
10609
- ...Object.keys(styles).length > 0 && { styles }
10610
- };
10611
- }
10612
-
10613
- // src/core/generationContext.ts
10614
- function defaultNamedThemeLookup(themeName, customThemes, warnings) {
10615
- if (customThemes) {
10616
- if (customThemes[themeName]) return customThemes[themeName];
10617
- const themeNameLower = themeName.toLowerCase();
10618
- const matchingThemeKey = Object.keys(customThemes).find(
10619
- (key) => key.toLowerCase() === themeNameLower
10620
- );
10621
- if (matchingThemeKey) return customThemes[matchingThemeKey];
10622
- return resolveBuiltInTheme(themeName, { customThemes, warnings });
10623
- }
10624
- return resolveBuiltInTheme(themeName, { warnings });
10625
- }
10626
- function resolveThemeContext(documentIn, options = {}) {
10627
- const { customThemes, fonts, warnings, resolveNamedTheme } = options;
10628
- if (documentIn.props === null) {
10629
- throw new Error(
10630
- "Document `props` is null. Omit it, or provide an object \u2014 a null props cannot carry a theme."
10631
- );
10632
- }
10633
- const document = documentIn.props === void 0 ? { ...documentIn, props: {} } : documentIn;
10634
- const authoredThemeName = document.props.theme || void 0;
10635
- const baseThemeName = authoredThemeName ?? "minimal";
10636
- let theme = resolveNamedTheme ? resolveNamedTheme(
10637
- baseThemeName,
10638
- warnings,
10639
- authoredThemeName !== void 0
10640
- ) : defaultNamedThemeLookup(baseThemeName, customThemes, warnings);
10641
- const themeOverrides = document.props.themeOverrides;
10642
- if (themeOverrides) {
10643
- theme = applyThemeOverrides(theme, themeOverrides);
10644
- }
10645
- const mode = applyExportMode({ doc: document, theme, fonts });
10646
- for (const w of mode.warnings) {
10647
- if (warnings) {
10648
- warnings.push({
10649
- component: "fontRegistry",
10650
- message: w.message,
10651
- severity: "warning",
10652
- context: { code: w.code }
10653
- });
10654
- } else {
10655
- console.warn(`[json-to-docx] [${w.code}] ${w.message}`);
10656
- }
10657
- }
10658
- return {
10659
- document: mode.doc,
10660
- theme: mode.theme,
10661
- themeName: baseThemeName
10662
- };
10663
- }
10664
-
10665
- // src/core/generateFromIr.ts
10666
- init_generationContext();
10700
+ init_generationContext();
10667
10701
 
10668
10702
  // src/core/layout.ts
10669
10703
  init_styles();
@@ -11116,8 +11150,7 @@ function resolveComponentTree(components, theme) {
11116
11150
  }
11117
11151
 
11118
11152
  // src/core/structure.ts
11119
- async function processDocument(document, theme, themeName, generationDate) {
11120
- const metadata = createDocumentMetadata(document.props, generationDate);
11153
+ function resolveDocumentTree(document, theme) {
11121
11154
  const docDefaults = document.props.componentDefaults;
11122
11155
  const mergedComponentDefaults = docDefaults ? mergeWithDefaults2(docDefaults, theme.componentDefaults || {}) : void 0;
11123
11156
  const docNoProofWords = document.props.noProofWords;
@@ -11131,6 +11164,14 @@ async function processDocument(document, theme, themeName, generationDate) {
11131
11164
  },
11132
11165
  ...mergedNoProofWords && { noProofWords: mergedNoProofWords }
11133
11166
  } : theme;
11167
+ return {
11168
+ theme: effectiveTheme,
11169
+ children: resolveComponentTree(document.children || [], effectiveTheme)
11170
+ };
11171
+ }
11172
+ async function processResolvedDocument(document, resolved, themeName, generationDate) {
11173
+ const metadata = createDocumentMetadata(document.props, generationDate);
11174
+ const effectiveTheme = resolved.theme;
11134
11175
  const context = createRenderContext(
11135
11176
  {
11136
11177
  metadata,
@@ -11141,11 +11182,7 @@ async function processDocument(document, theme, themeName, generationDate) {
11141
11182
  effectiveTheme,
11142
11183
  themeName
11143
11184
  );
11144
- const resolvedChildren = resolveComponentTree(
11145
- document.children || [],
11146
- effectiveTheme
11147
- );
11148
- const sections = await extractSections(resolvedChildren, context);
11185
+ const sections = await extractSections(resolved.children, context);
11149
11186
  return {
11150
11187
  metadata,
11151
11188
  sections,
@@ -11245,6 +11282,458 @@ function createRenderContext(document, theme, themeName) {
11245
11282
  };
11246
11283
  }
11247
11284
 
11285
+ // src/quality/facts.ts
11286
+ import { DEFAULT_DOCX_RENDERER_ID as DEFAULT_DOCX_RENDERER_ID2 } from "@json-to-office/shared-docx";
11287
+
11288
+ // src/core/generationContext.ts
11289
+ import { applyExportMode } from "@json-to-office/shared";
11290
+
11291
+ // src/styles/theme-resolver.ts
11292
+ init_themes();
11293
+ function resolveBuiltInTheme(themeName, options = {}) {
11294
+ if (!hasTheme(themeName)) {
11295
+ const available = [
11296
+ ...getThemeNames(),
11297
+ ...Object.keys(options.customThemes ?? {})
11298
+ ].sort();
11299
+ options.warnings?.push({
11300
+ component: "theme",
11301
+ message: `Theme "${themeName}" not found; falling back to "minimal". Available themes: ${available.join(", ")}.`,
11302
+ severity: "warning",
11303
+ context: { code: "theme_not_found", requested: themeName, available }
11304
+ });
11305
+ }
11306
+ return getThemeWithFallback(themeName);
11307
+ }
11308
+
11309
+ // src/themes/overrides.ts
11310
+ function applyThemeOverrides(theme, overrides) {
11311
+ if (!overrides) return theme;
11312
+ const fonts = { ...theme.fonts };
11313
+ for (const [role, def] of Object.entries(overrides.fonts ?? {})) {
11314
+ const key = role;
11315
+ fonts[key] = { ...theme.fonts[key], ...def };
11316
+ }
11317
+ const styles = { ...theme.styles ?? {} };
11318
+ for (const [name, def] of Object.entries(overrides.styles ?? {})) {
11319
+ styles[name] = { ...styles[name], ...def };
11320
+ }
11321
+ return {
11322
+ ...theme,
11323
+ colors: { ...theme.colors, ...overrides.colors ?? {} },
11324
+ fonts,
11325
+ ...Object.keys(styles).length > 0 && { styles }
11326
+ };
11327
+ }
11328
+
11329
+ // src/core/generationContext.ts
11330
+ function defaultNamedThemeLookup(themeName, customThemes, warnings) {
11331
+ if (customThemes) {
11332
+ if (customThemes[themeName]) return customThemes[themeName];
11333
+ const themeNameLower = themeName.toLowerCase();
11334
+ const matchingThemeKey = Object.keys(customThemes).find(
11335
+ (key) => key.toLowerCase() === themeNameLower
11336
+ );
11337
+ if (matchingThemeKey) return customThemes[matchingThemeKey];
11338
+ return resolveBuiltInTheme(themeName, { customThemes, warnings });
11339
+ }
11340
+ return resolveBuiltInTheme(themeName, { warnings });
11341
+ }
11342
+ function resolveThemeContext(documentIn, options = {}) {
11343
+ const { customThemes, fonts, warnings, resolveNamedTheme } = options;
11344
+ if (documentIn.props === null) {
11345
+ throw new Error(
11346
+ "Document `props` is null. Omit it, or provide an object \u2014 a null props cannot carry a theme."
11347
+ );
11348
+ }
11349
+ const document = documentIn.props === void 0 ? { ...documentIn, props: {} } : documentIn;
11350
+ const authoredThemeName = document.props.theme || void 0;
11351
+ const baseThemeName = authoredThemeName ?? "minimal";
11352
+ let theme = resolveNamedTheme ? resolveNamedTheme(
11353
+ baseThemeName,
11354
+ warnings,
11355
+ authoredThemeName !== void 0
11356
+ ) : defaultNamedThemeLookup(baseThemeName, customThemes, warnings);
11357
+ const themeOverrides = document.props.themeOverrides;
11358
+ if (themeOverrides) {
11359
+ theme = applyThemeOverrides(theme, themeOverrides);
11360
+ }
11361
+ const mode = applyExportMode({ doc: document, theme, fonts });
11362
+ for (const w of mode.warnings) {
11363
+ if (warnings) {
11364
+ warnings.push({
11365
+ component: "fontRegistry",
11366
+ message: w.message,
11367
+ severity: "warning",
11368
+ context: { code: w.code }
11369
+ });
11370
+ } else {
11371
+ console.warn(`[json-to-docx] [${w.code}] ${w.message}`);
11372
+ }
11373
+ }
11374
+ return {
11375
+ document: mode.doc,
11376
+ theme: mode.theme,
11377
+ themeName: baseThemeName
11378
+ };
11379
+ }
11380
+
11381
+ // src/json/normalizer.ts
11382
+ function normalizeComponent(component) {
11383
+ if (component.name === "columns") {
11384
+ return normalizeColumnsComponent(component);
11385
+ }
11386
+ if ("children" in component && component.children) {
11387
+ return {
11388
+ ...component,
11389
+ // Preserve ALL properties from validated component
11390
+ children: component.children.map(normalizeComponent)
11391
+ };
11392
+ }
11393
+ if (component.name === "section") {
11394
+ return normalizeSectionComponent(component);
11395
+ }
11396
+ return component;
11397
+ }
11398
+ function normalizeColumnsComponent(component) {
11399
+ const cfg = component.props || {};
11400
+ const applyTopLevelGap = (cols, topGap) => cols.map((c, i) => {
11401
+ const isLast = i === cols.length - 1;
11402
+ if (!isLast && c.gap === void 0 && topGap !== void 0) {
11403
+ return { ...c, gap: topGap };
11404
+ }
11405
+ return c;
11406
+ });
11407
+ const normalizeAutoWidth = (cols) => cols.map((c) => ({
11408
+ ...c,
11409
+ width: c.width === "auto" ? void 0 : c.width
11410
+ }));
11411
+ let columnsArray;
11412
+ if (typeof cfg.columns === "number" && cfg.columns > 0) {
11413
+ const n = cfg.columns;
11414
+ const base = Array.from(
11415
+ { length: n },
11416
+ () => ({})
11417
+ );
11418
+ const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
11419
+ columnsArray = applyTopLevelGap(base, topLevelGap);
11420
+ } else if (Array.isArray(cfg.columns)) {
11421
+ columnsArray = cfg.columns;
11422
+ const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
11423
+ columnsArray = applyTopLevelGap(columnsArray, topLevelGap);
11424
+ columnsArray = normalizeAutoWidth(columnsArray);
11425
+ } else {
11426
+ columnsArray = [{}];
11427
+ }
11428
+ const normalizedProps = {
11429
+ columns: columnsArray
11430
+ };
11431
+ return {
11432
+ ...component,
11433
+ // Preserve any additional properties
11434
+ name: "columns",
11435
+ props: normalizedProps,
11436
+ children: component.children ? component.children.map(normalizeComponent) : void 0
11437
+ };
11438
+ }
11439
+ function normalizeSectionComponent(component) {
11440
+ const props = component.props || {};
11441
+ return {
11442
+ ...component,
11443
+ // Preserve ALL properties
11444
+ props: {
11445
+ ...props,
11446
+ // Preserve ALL props properties
11447
+ header: props.header ? props.header === "linkToPrevious" ? "linkToPrevious" : props.header.map((m) => normalizeComponent(m)) : void 0,
11448
+ footer: props.footer ? props.footer === "linkToPrevious" ? "linkToPrevious" : props.footer.map((m) => normalizeComponent(m)) : void 0
11449
+ },
11450
+ children: component.children ? component.children.map(normalizeComponent) : void 0
11451
+ };
11452
+ }
11453
+ function normalizeDocument(document) {
11454
+ if (document.name !== "docx") {
11455
+ throw new Error(
11456
+ 'Top-level document must be a docx component with name="docx"'
11457
+ );
11458
+ }
11459
+ const normalized = normalizeComponent(document);
11460
+ return [normalized];
11461
+ }
11462
+
11463
+ // src/quality/facts.ts
11464
+ init_widthUtils();
11465
+ function asRecord(value) {
11466
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
11467
+ }
11468
+ function pageBox(theme, themeName, pageOverride) {
11469
+ const page = createSectionProperties(
11470
+ getColumnSettings("single"),
11471
+ theme,
11472
+ themeName,
11473
+ "nextPage",
11474
+ pageOverride
11475
+ ).page;
11476
+ return {
11477
+ availableWidthTwips: Math.max(
11478
+ 0,
11479
+ page.size.width - page.margin.left - page.margin.right
11480
+ ),
11481
+ pageBottomTwips: page.size.height
11482
+ };
11483
+ }
11484
+ function tableFact(props, path4, availableWidthTwips) {
11485
+ const columns = Array.isArray(props.columns) ? props.columns : void 0;
11486
+ if (!columns) return void 0;
11487
+ let totalWidthTwips = 0;
11488
+ let hasExplicitWidth = false;
11489
+ let pointSum = 0;
11490
+ let percentSum = 0;
11491
+ const explicitWidths = [];
11492
+ columns.forEach((column, index) => {
11493
+ const width = asRecord(column)?.width;
11494
+ if (typeof width === "number" && Number.isFinite(width)) {
11495
+ hasExplicitWidth = true;
11496
+ explicitWidths.push({ index, width });
11497
+ totalWidthTwips += relativeLengthToTwips(width, availableWidthTwips);
11498
+ pointSum += width;
11499
+ } else if (typeof width === "string") {
11500
+ hasExplicitWidth = true;
11501
+ const percent = width.trim().endsWith("%") ? Number(width.trim().slice(0, -1)) : Number.NaN;
11502
+ if (Number.isFinite(percent)) {
11503
+ explicitWidths.push({ index, width });
11504
+ percentSum += percent;
11505
+ totalWidthTwips += Math.max(
11506
+ 0,
11507
+ Math.round(availableWidthTwips * percent / 100)
11508
+ );
11509
+ }
11510
+ }
11511
+ });
11512
+ return {
11513
+ id: `docx:table-width:${path4}`,
11514
+ kind: "docx/table-width",
11515
+ path: `${path4}/props/columns`,
11516
+ totalWidthTwips,
11517
+ availableWidthTwips,
11518
+ hasExplicitWidth,
11519
+ allColumnsExplicit: explicitWidths.length === columns.length,
11520
+ pointSum,
11521
+ percentSum,
11522
+ explicitWidths
11523
+ };
11524
+ }
11525
+ var TWIPS_PER_POINT3 = 20;
11526
+ var SINGLE_LINE_RATIO = 1.2;
11527
+ function finiteNumber(value) {
11528
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
11529
+ }
11530
+ function lineHeightPt(fontSizePt, spacing) {
11531
+ const rule = asRecord(spacing);
11532
+ const value = finiteNumber(rule?.value);
11533
+ switch (rule?.type) {
11534
+ // `exactly` is an absolute line box, and display type routinely sets it
11535
+ // below the font size — reading it as a multiplier would inflate every
11536
+ // estimate on exactly the headings that matter most.
11537
+ case "exactly":
11538
+ return value ?? fontSizePt * SINGLE_LINE_RATIO;
11539
+ case "atLeast":
11540
+ return Math.max(value ?? 0, fontSizePt * SINGLE_LINE_RATIO);
11541
+ case "multiple":
11542
+ return fontSizePt * SINGLE_LINE_RATIO * (value ?? 1);
11543
+ default:
11544
+ return fontSizePt * SINGLE_LINE_RATIO;
11545
+ }
11546
+ }
11547
+ function characterSpacingPt(spacing) {
11548
+ const rule = asRecord(spacing);
11549
+ const value = finiteNumber(rule?.value);
11550
+ if (rule === void 0 || value === void 0) return 0;
11551
+ return rule.type === "condensed" ? -value / TWIPS_PER_POINT3 : value / TWIPS_PER_POINT3;
11552
+ }
11553
+ function frameSignature(floating) {
11554
+ const horizontal = asRecord(floating.horizontalPosition);
11555
+ const vertical = asRecord(floating.verticalPosition);
11556
+ return JSON.stringify([
11557
+ floating.width ?? null,
11558
+ floating.height ?? null,
11559
+ horizontal?.offset ?? null,
11560
+ horizontal?.relative ?? null,
11561
+ horizontal?.align ?? null,
11562
+ vertical?.offset ?? null,
11563
+ vertical?.relative ?? null,
11564
+ vertical?.align ?? null,
11565
+ asRecord(floating.wrap)?.type ?? null
11566
+ ]);
11567
+ }
11568
+ function frameTextFact(props, path4, page, frameChainId, flowIndex) {
11569
+ const floating = asRecord(props.floating);
11570
+ const frameWidthTwips = finiteNumber(floating?.width);
11571
+ if (frameWidthTwips === void 0) return void 0;
11572
+ const text = typeof props.text === "string" ? props.text : "";
11573
+ if (text.trim() === "") return void 0;
11574
+ const font = asRecord(props.font);
11575
+ const fontSizePt = finiteNumber(font?.size);
11576
+ if (fontSizePt === void 0) return void 0;
11577
+ const longestWord = text.split(/\s+/).reduce(
11578
+ (longest, word) => word.length > longest.length ? word : longest,
11579
+ ""
11580
+ );
11581
+ const horizontal = asRecord(floating?.horizontalPosition);
11582
+ const vertical = asRecord(floating?.verticalPosition);
11583
+ const offsetX = finiteNumber(horizontal?.offset);
11584
+ const offsetY = finiteNumber(vertical?.offset);
11585
+ const absolutelyPinned = horizontal?.offset !== void 0 || vertical?.offset !== void 0;
11586
+ const resolvable = (horizontal?.offset === void 0 || offsetX !== void 0) && (vertical?.offset === void 0 || offsetY !== void 0);
11587
+ return {
11588
+ id: `docx:frame-text:${path4}`,
11589
+ kind: "docx/frame-text",
11590
+ path: path4,
11591
+ text,
11592
+ fontSizePt,
11593
+ lineHeightPt: lineHeightPt(fontSizePt, font?.lineSpacing),
11594
+ characterSpacingPt: characterSpacingPt(font?.characterSpacing),
11595
+ frameWidthTwips,
11596
+ ...finiteNumber(floating?.height) !== void 0 && {
11597
+ frameHeightTwips: finiteNumber(floating?.height)
11598
+ },
11599
+ ...offsetY !== void 0 && { offsetYTwips: offsetY },
11600
+ pageBottomTwips: page.pageBottomTwips,
11601
+ longestWord,
11602
+ ...absolutelyPinned && resolvable && {
11603
+ absoluteOffsetTwips: { x: offsetX ?? 0, y: offsetY ?? 0 }
11604
+ },
11605
+ anchorHorizontal: typeof horizontal?.relative === "string" ? horizontal.relative : "page",
11606
+ anchorVertical: typeof vertical?.relative === "string" ? vertical.relative : "page",
11607
+ frameChainId,
11608
+ flowIndex
11609
+ };
11610
+ }
11611
+ var SVG_VIEWBOX = /viewBox\s*=\s*"\s*(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+(-?[\d.]+)\s*"/;
11612
+ var SVG_TEXT = /<text\b([^>]*)>([^<]*)<\/text>/g;
11613
+ var SVG_ATTR = (name) => new RegExp(`\\b${name}\\s*=\\s*"(-?[\\d.]+)"`);
11614
+ function svgTextFacts(props, path4) {
11615
+ const svg = typeof props.svg === "string" ? props.svg : void 0;
11616
+ if (!svg) return [];
11617
+ const box = SVG_VIEWBOX.exec(svg);
11618
+ if (!box) return [];
11619
+ const viewBoxMinY = Number(box[2]);
11620
+ const viewBoxHeight = Number(box[4]);
11621
+ if (!Number.isFinite(viewBoxMinY) || !Number.isFinite(viewBoxHeight)) {
11622
+ return [];
11623
+ }
11624
+ const facts = [];
11625
+ let match;
11626
+ SVG_TEXT.lastIndex = 0;
11627
+ let index = 0;
11628
+ while ((match = SVG_TEXT.exec(svg)) !== null) {
11629
+ const [, attributes, content] = match;
11630
+ const baselineY = Number(SVG_ATTR("y").exec(attributes)?.[1]);
11631
+ if (!Number.isFinite(baselineY)) {
11632
+ index += 1;
11633
+ continue;
11634
+ }
11635
+ const fontSizeUnits = Number(
11636
+ SVG_ATTR("font-size").exec(attributes)?.[1] ?? 0
11637
+ );
11638
+ facts.push({
11639
+ id: `docx:svg-text:${path4}:${index}`,
11640
+ kind: "docx/svg-text",
11641
+ path: `${path4}/props/svg`,
11642
+ content: content.trim(),
11643
+ baselineY,
11644
+ fontSizeUnits: Number.isFinite(fontSizeUnits) ? fontSizeUnits : 0,
11645
+ viewBoxMinY,
11646
+ viewBoxHeight
11647
+ });
11648
+ index += 1;
11649
+ }
11650
+ return facts;
11651
+ }
11652
+ function walkActive(node, path4, page, visit) {
11653
+ const rec = asRecord(node);
11654
+ if (!rec || rec.enabled === false) return;
11655
+ visit(rec, path4, page);
11656
+ const children = Array.isArray(rec.children) ? rec.children : [];
11657
+ children.forEach(
11658
+ (child, index) => walkActive(child, `${path4}/children/${index}`, page, visit)
11659
+ );
11660
+ }
11661
+ function prepareDocxQualityDocument(document, options = {}) {
11662
+ const context = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
11663
+ customThemes: options.customThemes,
11664
+ fonts: options.fonts,
11665
+ warnings: options.warnings
11666
+ });
11667
+ const resolved = resolveDocumentTree(context.document, context.theme);
11668
+ const basePage = pageBox(resolved.theme, context.themeName);
11669
+ const facts = [];
11670
+ const provenance = {};
11671
+ let previousHeadingLevel;
11672
+ const addFact = (fact) => {
11673
+ facts.push(fact);
11674
+ provenance[fact.id] = {
11675
+ path: fact.path,
11676
+ ...fact.relatedPaths && { relatedPaths: fact.relatedPaths }
11677
+ };
11678
+ };
11679
+ let previousVisitPath;
11680
+ let lastFrame;
11681
+ let flowIndex = 0;
11682
+ const parentOf = (path4) => path4.slice(0, path4.lastIndexOf("/"));
11683
+ const visit = (node, path4, page) => {
11684
+ const props = asRecord(node.props) ?? {};
11685
+ if (node.name === "table") {
11686
+ const fact = tableFact(props, path4, page.availableWidthTwips);
11687
+ if (fact) addFact(fact);
11688
+ }
11689
+ if (node.name === "paragraph" || node.name === "text-box") {
11690
+ const floating = asRecord(props.floating);
11691
+ if (floating) {
11692
+ const signature = frameSignature(floating);
11693
+ const chainId = lastFrame !== void 0 && previousVisitPath === lastFrame.path && parentOf(lastFrame.path) === parentOf(path4) && lastFrame.signature === signature ? lastFrame.chainId : `docx:frame-chain:${path4}`;
11694
+ lastFrame = { path: path4, signature, chainId };
11695
+ const fact = frameTextFact(props, path4, page, chainId, flowIndex);
11696
+ if (fact) addFact(fact);
11697
+ }
11698
+ }
11699
+ if (node.name === "image" || node.name === "visual") {
11700
+ for (const fact of svgTextFacts(props, path4)) addFact(fact);
11701
+ }
11702
+ if (node.name === "heading") {
11703
+ const level = typeof props.level === "number" && Number.isFinite(props.level) ? props.level : 1;
11704
+ addFact({
11705
+ id: `docx:heading:${path4}`,
11706
+ kind: "docx/heading",
11707
+ path: `${path4}/props/level`,
11708
+ level,
11709
+ ...previousHeadingLevel !== void 0 && {
11710
+ previousLevel: previousHeadingLevel
11711
+ }
11712
+ });
11713
+ previousHeadingLevel = level;
11714
+ }
11715
+ previousVisitPath = path4;
11716
+ };
11717
+ resolved.children.forEach((component, index) => {
11718
+ const rec = component;
11719
+ if (rec.name === "section") flowIndex += 1;
11720
+ const page = rec.name === "section" ? pageBox(resolved.theme, context.themeName, rec.props?.page) : basePage;
11721
+ walkActive(component, `/children/${index}`, page, visit);
11722
+ });
11723
+ return {
11724
+ format: "docx",
11725
+ model: {
11726
+ authored: document,
11727
+ context,
11728
+ document: resolved,
11729
+ themeName: context.themeName
11730
+ },
11731
+ facts,
11732
+ provenance,
11733
+ renderer: options.renderer ?? DEFAULT_DOCX_RENDERER_ID2
11734
+ };
11735
+ }
11736
+
11248
11737
  // src/core/generateFromIr.ts
11249
11738
  var UncompiledComponentError = class extends Error {
11250
11739
  code = "UNCOMPILED_COMPONENT";
@@ -11272,11 +11761,14 @@ async function compileDocumentToIr(document, options = {}, collector) {
11272
11761
  );
11273
11762
  }
11274
11763
  async function compileDocumentScoped(document, options, warnings) {
11275
- const context = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
11764
+ const prepared = options.prepared ?? prepareDocxQualityDocument(document, {
11276
11765
  customThemes: options.customThemes,
11277
11766
  fonts: options.fonts,
11278
- warnings
11767
+ warnings,
11768
+ context: options.context,
11769
+ renderer: options.renderer
11279
11770
  });
11771
+ const { context } = prepared.model;
11280
11772
  const hasVisual = collectVisualProps(context.document).length > 0;
11281
11773
  const resolvedFonts = await resolveDocumentFonts(
11282
11774
  context.document,
@@ -11292,9 +11784,10 @@ async function compileDocumentScoped(document, options, warnings) {
11292
11784
  ...options.baseDir !== void 0 ? { baseDir: options.baseDir } : {},
11293
11785
  ...visualFonts.length > 0 ? { visualFonts } : {}
11294
11786
  });
11295
- const structure = await processDocument(
11787
+ const resolved = desugared === context.document ? prepared.model.document : resolveDocumentTree(desugared, context.theme);
11788
+ const structure = await processResolvedDocument(
11296
11789
  desugared,
11297
- context.theme,
11790
+ resolved,
11298
11791
  context.themeName,
11299
11792
  resolveGenerationDate(options)
11300
11793
  );
@@ -11459,7 +11952,8 @@ async function generateBufferWithWarnings(jsonConfig, options) {
11459
11952
  ...options?.baseDir !== void 0 ? { baseDir: options.baseDir } : {},
11460
11953
  ...options?.deterministic !== void 0 ? { deterministic: options.deterministic } : {},
11461
11954
  ...options?.generatedAt !== void 0 ? { generatedAt: options.generatedAt } : {},
11462
- ...renderer ? { renderer } : {}
11955
+ ...renderer ? { renderer } : {},
11956
+ ...options?.prepared ? { prepared: options.prepared } : {}
11463
11957
  });
11464
11958
  }
11465
11959
  function parseDocument(jsonConfig) {
@@ -11600,6 +12094,509 @@ async function rasterizeVisual(obj, ctx) {
11600
12094
  });
11601
12095
  }
11602
12096
 
12097
+ // src/quality/preflight.ts
12098
+ import {
12099
+ assertValidQualityPolicy,
12100
+ assertValidQualityProfile
12101
+ } from "@json-to-office/quality";
12102
+
12103
+ // src/quality/rules.ts
12104
+ import {
12105
+ mergeQualityProfiles,
12106
+ QUALITY_CODES,
12107
+ QualityEngine
12108
+ } from "@json-to-office/quality";
12109
+
12110
+ // src/quality/text-metrics.ts
12111
+ var DEFAULT_ADVANCE = 556;
12112
+ var ADVANCE = {
12113
+ " ": 278,
12114
+ A: 667,
12115
+ B: 667,
12116
+ C: 722,
12117
+ D: 722,
12118
+ E: 667,
12119
+ F: 611,
12120
+ G: 778,
12121
+ H: 722,
12122
+ I: 278,
12123
+ J: 500,
12124
+ K: 667,
12125
+ L: 556,
12126
+ M: 833,
12127
+ N: 722,
12128
+ O: 778,
12129
+ P: 667,
12130
+ Q: 778,
12131
+ R: 722,
12132
+ S: 667,
12133
+ T: 611,
12134
+ U: 722,
12135
+ V: 667,
12136
+ W: 944,
12137
+ X: 667,
12138
+ Y: 667,
12139
+ Z: 611,
12140
+ a: 556,
12141
+ b: 556,
12142
+ c: 500,
12143
+ d: 556,
12144
+ e: 556,
12145
+ f: 278,
12146
+ g: 556,
12147
+ h: 556,
12148
+ i: 222,
12149
+ j: 222,
12150
+ k: 500,
12151
+ l: 222,
12152
+ m: 833,
12153
+ n: 556,
12154
+ o: 556,
12155
+ p: 556,
12156
+ q: 556,
12157
+ r: 333,
12158
+ s: 500,
12159
+ t: 278,
12160
+ u: 556,
12161
+ v: 500,
12162
+ w: 722,
12163
+ x: 500,
12164
+ y: 500,
12165
+ z: 500,
12166
+ ".": 278,
12167
+ ",": 278,
12168
+ ":": 278,
12169
+ ";": 278,
12170
+ "!": 278,
12171
+ "?": 556,
12172
+ "'": 191,
12173
+ '"': 355,
12174
+ "-": 333,
12175
+ "\u2013": 556,
12176
+ "\u2014": 1e3,
12177
+ "&": 667,
12178
+ "%": 889,
12179
+ "(": 333,
12180
+ ")": 333,
12181
+ "/": 278,
12182
+ "@": 1015,
12183
+ "#": 556,
12184
+ "+": 584,
12185
+ "=": 584
12186
+ };
12187
+ function estimateTextWidthPt(text, fontSizePt, trackingPt = 0) {
12188
+ let units = 0;
12189
+ for (const character of text) {
12190
+ units += ADVANCE[character] ?? DEFAULT_ADVANCE;
12191
+ }
12192
+ const width = units / 1e3 * fontSizePt + text.length * trackingPt;
12193
+ return Math.max(0, width);
12194
+ }
12195
+ function estimateWrappedLines(text, widthPt, fontSizePt, trackingPt = 0) {
12196
+ if (widthPt <= 0) return 1;
12197
+ let lines = 0;
12198
+ for (const paragraph2 of text.split("\n")) {
12199
+ const words = paragraph2.split(/\s+/).filter(Boolean);
12200
+ if (words.length === 0) {
12201
+ lines += 1;
12202
+ continue;
12203
+ }
12204
+ let current = "";
12205
+ let used = 0;
12206
+ for (const word of words) {
12207
+ const candidate = current === "" ? word : `${current} ${word}`;
12208
+ if (estimateTextWidthPt(candidate, fontSizePt, trackingPt) <= widthPt) {
12209
+ current = candidate;
12210
+ } else {
12211
+ if (current !== "") used += 1;
12212
+ current = word;
12213
+ }
12214
+ }
12215
+ lines += used + (current === "" ? 0 : 1);
12216
+ }
12217
+ return Math.max(1, lines);
12218
+ }
12219
+
12220
+ // src/quality/rules.ts
12221
+ var WIDTH_TOLERANCE_TWIPS = 10;
12222
+ function numberParameter(parameters, name, fallback) {
12223
+ const value = parameters[name];
12224
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
12225
+ }
12226
+ var docxTableWidthRule = {
12227
+ id: "docx/table-width",
12228
+ code: QUALITY_CODES.TABLE_WIDTH_OVERFLOW,
12229
+ category: "integrity",
12230
+ defaultSeverity: "warning",
12231
+ defaultCertainty: "deterministic",
12232
+ formats: ["docx"],
12233
+ defaultParameters: { toleranceTwips: WIDTH_TOLERANCE_TWIPS },
12234
+ evaluate: ({ facts, configuration }) => {
12235
+ const toleranceTwips = numberParameter(
12236
+ configuration.parameters,
12237
+ "toleranceTwips",
12238
+ WIDTH_TOLERANCE_TWIPS
12239
+ );
12240
+ return facts.filter(
12241
+ (fact) => fact.kind === "docx/table-width"
12242
+ ).filter(
12243
+ (fact) => fact.hasExplicitWidth && fact.totalWidthTwips > fact.availableWidthTwips + toleranceTwips
12244
+ ).map((fact) => {
12245
+ const totalPt = Math.round(fact.totalWidthTwips / 20 * 10) / 10;
12246
+ const availablePt = Math.round(fact.availableWidthTwips / 20 * 10) / 10;
12247
+ const scale = fact.availableWidthTwips / fact.totalWidthTwips;
12248
+ const fixes = fact.allColumnsExplicit ? fact.explicitWidths.map(({ index, width }) => ({
12249
+ op: "replace",
12250
+ path: `${fact.path}/${index}/width`,
12251
+ value: typeof width === "number" ? Math.floor(width * scale * 10) / 10 : `${Math.floor(Number(width.trim().slice(0, -1)) * scale * 10) / 10}%`
12252
+ })) : [];
12253
+ return {
12254
+ message: `Column widths use ${totalPt}pt, but this section has ${availablePt}pt available \u2014 the table will spill off the right edge.`,
12255
+ path: fact.path,
12256
+ suggestion: "Reduce fixed/percentage widths, widen the page, or leave columns unsized so they share the remainder.",
12257
+ context: {
12258
+ totalWidthPt: totalPt,
12259
+ availableWidthPt: availablePt,
12260
+ pointSum: fact.pointSum,
12261
+ percentSum: fact.percentSum
12262
+ },
12263
+ evidence: {
12264
+ actual: totalPt,
12265
+ expected: availablePt,
12266
+ unit: "pt"
12267
+ },
12268
+ ...fixes.length > 0 && { fixes }
12269
+ };
12270
+ });
12271
+ }
12272
+ };
12273
+ var docxHeadingHierarchyRule = {
12274
+ id: "docx/heading-hierarchy",
12275
+ code: QUALITY_CODES.HEADING_SKIP,
12276
+ category: "hierarchy",
12277
+ defaultSeverity: "info",
12278
+ defaultCertainty: "deterministic",
12279
+ formats: ["docx"],
12280
+ evaluate: ({ facts }) => facts.filter((fact) => fact.kind === "docx/heading").flatMap((fact) => {
12281
+ const previousLevel = fact.previousLevel;
12282
+ if (previousLevel === void 0 || fact.level <= previousLevel + 1) {
12283
+ return [];
12284
+ }
12285
+ return [
12286
+ {
12287
+ message: `Heading level ${fact.level} follows level ${previousLevel} \u2014 the skipped level breaks the document outline.`,
12288
+ path: fact.path,
12289
+ suggestion: `Use level ${previousLevel + 1}, or promote this heading's section.`,
12290
+ context: { level: fact.level, previousLevel },
12291
+ evidence: {
12292
+ actual: fact.level,
12293
+ expected: previousLevel + 1
12294
+ },
12295
+ // The fact path already addresses `.../props/level`; RFC 6902
12296
+ // `add` replaces an existing member, so this works whether the
12297
+ // level was explicit or defaulted.
12298
+ fixes: [
12299
+ {
12300
+ op: "add",
12301
+ path: fact.path,
12302
+ value: previousLevel + 1
12303
+ }
12304
+ ]
12305
+ }
12306
+ ];
12307
+ })
12308
+ };
12309
+ var WIDTH_MODEL_TOLERANCE = 1.08;
12310
+ var TWIPS_PER_POINT4 = 20;
12311
+ function frameTextFacts(facts) {
12312
+ return facts.filter(
12313
+ (fact) => fact.kind === "docx/frame-text"
12314
+ );
12315
+ }
12316
+ var docxTextFitRule = {
12317
+ id: "docx/text-fit",
12318
+ code: QUALITY_CODES.TEXT_OVERFLOW,
12319
+ category: "integrity",
12320
+ defaultSeverity: "warning",
12321
+ defaultCertainty: "estimated",
12322
+ formats: ["docx"],
12323
+ defaultParameters: { widthTolerance: WIDTH_MODEL_TOLERANCE },
12324
+ evaluate: ({ facts, configuration }) => {
12325
+ const tolerance = numberParameter(
12326
+ configuration.parameters,
12327
+ "widthTolerance",
12328
+ WIDTH_MODEL_TOLERANCE
12329
+ );
12330
+ const findings = [];
12331
+ for (const fact of frameTextFacts(facts)) {
12332
+ const frameWidthPt = fact.frameWidthTwips / TWIPS_PER_POINT4;
12333
+ const longestWordPt = estimateTextWidthPt(
12334
+ fact.longestWord,
12335
+ fact.fontSizePt,
12336
+ fact.characterSpacingPt
12337
+ );
12338
+ if (longestWordPt > frameWidthPt * tolerance) {
12339
+ findings.push({
12340
+ message: `"${fact.longestWord}" is estimated at ${Math.round(longestWordPt)}pt in a ${Math.round(frameWidthPt)}pt frame \u2014 with no wrap point it will break mid-word.`,
12341
+ path: `${fact.path}/props/floating/width`,
12342
+ suggestion: "Widen the frame, reduce fontSize, or condense the run further.",
12343
+ context: {
12344
+ longestWord: fact.longestWord,
12345
+ estimatedWidthPt: Math.round(longestWordPt),
12346
+ frameWidthPt: Math.round(frameWidthPt),
12347
+ fontSizePt: fact.fontSizePt
12348
+ },
12349
+ evidence: {
12350
+ actual: Math.round(longestWordPt),
12351
+ expected: Math.round(frameWidthPt),
12352
+ unit: "pt"
12353
+ }
12354
+ });
12355
+ continue;
12356
+ }
12357
+ if (fact.offsetYTwips === void 0) continue;
12358
+ const lines = estimateWrappedLines(
12359
+ fact.text,
12360
+ frameWidthPt,
12361
+ fact.fontSizePt,
12362
+ fact.characterSpacingPt
12363
+ );
12364
+ const heightTwips = lines * fact.lineHeightPt * TWIPS_PER_POINT4;
12365
+ const bottomTwips = fact.offsetYTwips + heightTwips;
12366
+ const overrunTwips = bottomTwips - fact.pageBottomTwips;
12367
+ if (overrunTwips <= fact.lineHeightPt * TWIPS_PER_POINT4) continue;
12368
+ findings.push({
12369
+ message: `Frame starts at ${fact.offsetYTwips} twips and needs ${Math.round(heightTwips)} more for ${lines} line${lines === 1 ? "" : "s"} of ${fact.fontSizePt}pt \u2014 it runs ${Math.round(overrunTwips)} twips past the page, so the text breaks onto the next one.`,
12370
+ path: `${fact.path}/props/floating/verticalPosition/offset`,
12371
+ suggestion: "Raise the frame, shorten the text, or reduce fontSize so the block finishes on the page.",
12372
+ context: {
12373
+ offsetYTwips: fact.offsetYTwips,
12374
+ estimatedHeightTwips: Math.round(heightTwips),
12375
+ pageBottomTwips: fact.pageBottomTwips,
12376
+ lines
12377
+ },
12378
+ evidence: {
12379
+ actual: Math.round(bottomTwips),
12380
+ expected: fact.pageBottomTwips,
12381
+ unit: "twips"
12382
+ }
12383
+ });
12384
+ }
12385
+ return findings;
12386
+ }
12387
+ };
12388
+ var FRAME_COLLISION_MIN_WIDTH_TWIPS = 240;
12389
+ var docxFrameCollisionRule = {
12390
+ id: "docx/frame-collision",
12391
+ code: QUALITY_CODES.FRAME_COLLISION,
12392
+ category: "integrity",
12393
+ defaultSeverity: "warning",
12394
+ defaultCertainty: "estimated",
12395
+ formats: ["docx"],
12396
+ defaultParameters: {
12397
+ minOverlapWidthTwips: FRAME_COLLISION_MIN_WIDTH_TWIPS,
12398
+ minOverlapLines: 1
12399
+ },
12400
+ evaluate: ({ facts, configuration }) => {
12401
+ const minOverlapWidthTwips = numberParameter(
12402
+ configuration.parameters,
12403
+ "minOverlapWidthTwips",
12404
+ FRAME_COLLISION_MIN_WIDTH_TWIPS
12405
+ );
12406
+ const minOverlapLines = numberParameter(
12407
+ configuration.parameters,
12408
+ "minOverlapLines",
12409
+ 1
12410
+ );
12411
+ const chains = /* @__PURE__ */ new Map();
12412
+ for (const fact of frameTextFacts(facts)) {
12413
+ if (!fact.absoluteOffsetTwips || fact.anchorHorizontal === "text" || fact.anchorVertical === "text") {
12414
+ continue;
12415
+ }
12416
+ const lines = estimateWrappedLines(
12417
+ fact.text,
12418
+ fact.frameWidthTwips / TWIPS_PER_POINT4,
12419
+ fact.fontSizePt,
12420
+ fact.characterSpacingPt
12421
+ );
12422
+ const heightTwips = lines * fact.lineHeightPt * TWIPS_PER_POINT4;
12423
+ const lineTwips = fact.lineHeightPt * TWIPS_PER_POINT4;
12424
+ const chain = chains.get(fact.frameChainId);
12425
+ if (chain) {
12426
+ chain.heightTwips += heightTwips;
12427
+ chain.lineTwips = Math.max(chain.lineTwips, lineTwips);
12428
+ continue;
12429
+ }
12430
+ chains.set(fact.frameChainId, {
12431
+ path: fact.path,
12432
+ offsetYAuthored: fact.offsetYTwips !== void 0,
12433
+ x: fact.absoluteOffsetTwips.x,
12434
+ y: fact.absoluteOffsetTwips.y,
12435
+ widthTwips: fact.frameWidthTwips,
12436
+ heightTwips,
12437
+ lineTwips,
12438
+ // Offsets only compare within one page flow and one anchor pair.
12439
+ groupKey: `${fact.flowIndex}:${fact.anchorHorizontal}:${fact.anchorVertical}`
12440
+ });
12441
+ }
12442
+ const groups = /* @__PURE__ */ new Map();
12443
+ for (const chain of chains.values()) {
12444
+ const group = groups.get(chain.groupKey);
12445
+ if (group) group.push(chain);
12446
+ else groups.set(chain.groupKey, [chain]);
12447
+ }
12448
+ const findings = [];
12449
+ for (const group of groups.values()) {
12450
+ for (let i = 0; i < group.length; i += 1) {
12451
+ for (let j = i + 1; j < group.length; j += 1) {
12452
+ const a = group[i];
12453
+ const b = group[j];
12454
+ const overlapWidth = Math.min(a.x + a.widthTwips, b.x + b.widthTwips) - Math.max(a.x, b.x);
12455
+ if (overlapWidth <= minOverlapWidthTwips) continue;
12456
+ const overlapHeight = Math.min(a.y + a.heightTwips, b.y + b.heightTwips) - Math.max(a.y, b.y);
12457
+ const floorTwips = Math.max(a.lineTwips, b.lineTwips) * minOverlapLines;
12458
+ if (overlapHeight <= floorTwips) continue;
12459
+ const overlapHeightPt = Math.round(overlapHeight / TWIPS_PER_POINT4);
12460
+ const overlapWidthPt = Math.round(overlapWidth / TWIPS_PER_POINT4);
12461
+ findings.push({
12462
+ message: `This floating frame and an earlier one on the same page overlap by an estimated ${overlapHeightPt}pt of text across ${overlapWidthPt}pt of width \u2014 the two blocks paint on top of each other.`,
12463
+ path: b.offsetYAuthored ? `${b.path}/props/floating/verticalPosition/offset` : `${b.path}/props/floating`,
12464
+ suggestion: "Move one frame, shorten its text, or reduce fontSize until the blocks clear each other.",
12465
+ context: {
12466
+ partnerPath: a.path,
12467
+ overlapWidthTwips: Math.round(overlapWidth),
12468
+ overlapHeightTwips: Math.round(overlapHeight),
12469
+ frameTopTwips: b.y,
12470
+ partnerTopTwips: a.y
12471
+ },
12472
+ relatedPaths: [a.path],
12473
+ evidence: {
12474
+ actual: Math.round(overlapHeight),
12475
+ expected: Math.round(floorTwips),
12476
+ unit: "twips"
12477
+ }
12478
+ });
12479
+ }
12480
+ }
12481
+ }
12482
+ return findings;
12483
+ }
12484
+ };
12485
+ var docxSvgTextBoundsRule = {
12486
+ id: "docx/svg-text-bounds",
12487
+ code: QUALITY_CODES.SVG_TEXT_CLIPPED,
12488
+ category: "integrity",
12489
+ defaultSeverity: "warning",
12490
+ defaultCertainty: "deterministic",
12491
+ formats: ["docx"],
12492
+ evaluate: ({ facts }) => facts.filter((fact) => fact.kind === "docx/svg-text").flatMap((fact) => {
12493
+ const canvasBottom = fact.viewBoxMinY + fact.viewBoxHeight;
12494
+ if (fact.baselineY <= canvasBottom) return [];
12495
+ const overflow = Math.round((fact.baselineY - canvasBottom) * 10) / 10;
12496
+ return [
12497
+ {
12498
+ message: `SVG text "${fact.content}" sits ${overflow} units below the viewBox \u2014 it is clipped away and never reaches the document's text layer.`,
12499
+ path: fact.path,
12500
+ suggestion: `Move the baseline above ${Math.round(canvasBottom)}, or grow the viewBox height.`,
12501
+ context: {
12502
+ content: fact.content,
12503
+ baselineY: fact.baselineY,
12504
+ canvasBottom,
12505
+ overflowUnits: overflow
12506
+ },
12507
+ evidence: {
12508
+ actual: fact.baselineY,
12509
+ expected: canvasBottom,
12510
+ unit: "viewBox units"
12511
+ }
12512
+ }
12513
+ ];
12514
+ })
12515
+ };
12516
+ var DOCX_QUALITY_RULES = {
12517
+ id: "docx/default",
12518
+ rules: [
12519
+ docxTableWidthRule,
12520
+ docxHeadingHierarchyRule,
12521
+ docxTextFitRule,
12522
+ docxFrameCollisionRule,
12523
+ docxSvgTextBoundsRule
12524
+ ]
12525
+ };
12526
+ var DOCX_QUALITY_PROFILES = {
12527
+ "executive-report": {
12528
+ id: "executive-report",
12529
+ formats: ["docx"],
12530
+ description: "Short decision document with strict outline continuity.",
12531
+ rules: {
12532
+ "docx/heading-hierarchy": { severity: "warning" }
12533
+ }
12534
+ },
12535
+ "technical-report": {
12536
+ id: "technical-report",
12537
+ formats: ["docx"],
12538
+ description: "Portable professional report defaults."
12539
+ },
12540
+ "legal-appendix": {
12541
+ id: "legal-appendix",
12542
+ formats: ["docx"],
12543
+ description: "Dense appendix: preserve integrity without editorial taste."
12544
+ }
12545
+ };
12546
+ var DOCX_DEFAULT_QUALITY_PROFILE = DOCX_QUALITY_PROFILES["technical-report"];
12547
+ var DOCX_PROFILES_BY_ID = DOCX_QUALITY_PROFILES;
12548
+ function resolveDocxQualityProfile(requested) {
12549
+ if (!requested) return void 0;
12550
+ const registered = DOCX_PROFILES_BY_ID[requested.id];
12551
+ if (!registered) return requested;
12552
+ return mergeQualityProfiles(registered, requested);
12553
+ }
12554
+ var docxQualityEngine = new QualityEngine(DOCX_QUALITY_RULES.rules);
12555
+
12556
+ // src/quality/preflight.ts
12557
+ var PREPARE_RULE_ID = "quality/prepare";
12558
+ function emptyAnalysis(ruleErrors = [], blocked = false) {
12559
+ return {
12560
+ diagnostics: [],
12561
+ counts: { error: 0, warning: 0, info: 0 },
12562
+ blocked,
12563
+ truncated: false,
12564
+ suppressedCount: 0,
12565
+ evaluatedRuleIds: [],
12566
+ ruleErrors
12567
+ };
12568
+ }
12569
+ function analyzeDocxQuality(doc, options = {}) {
12570
+ assertValidQualityPolicy(options.policy);
12571
+ assertValidQualityProfile(options.profile);
12572
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc) || doc.name !== "docx") {
12573
+ return emptyAnalysis();
12574
+ }
12575
+ let prepared = options.prepared;
12576
+ try {
12577
+ prepared ??= prepareDocxQualityDocument(
12578
+ doc,
12579
+ options
12580
+ );
12581
+ } catch (error) {
12582
+ if (options.policy?.onRuleError === "throw") throw error;
12583
+ const gate = options.policy?.gate;
12584
+ return emptyAnalysis(
12585
+ [
12586
+ {
12587
+ ruleId: PREPARE_RULE_ID,
12588
+ message: error instanceof Error ? error.message : String(error)
12589
+ }
12590
+ ],
12591
+ gate !== void 0 && gate !== "none"
12592
+ );
12593
+ }
12594
+ return docxQualityEngine.analyzeSync(prepared, {
12595
+ profile: resolveDocxQualityProfile(options.profile) ?? DOCX_DEFAULT_QUALITY_PROFILE,
12596
+ policy: options.policy
12597
+ });
12598
+ }
12599
+
11603
12600
  // src/utils/warningsDocument.ts
11604
12601
  function generateWarningsDocument(warnings) {
11605
12602
  if (!warnings || warnings.length === 0) {
@@ -12526,17 +13523,22 @@ export {
12526
13523
  ComponentValidationError2 as ComponentValidationError,
12527
13524
  DocumentGenerator as CoreDocumentGenerator,
12528
13525
  DEFAULT_DOCX_RENDERER_ID,
13526
+ DOCX_DEFAULT_QUALITY_PROFILE,
13527
+ DOCX_QUALITY_PROFILES,
13528
+ DOCX_QUALITY_RULES,
12529
13529
  ThemeFileError,
12530
13530
  ThemeParseError,
12531
13531
  ThemeValidationError,
12532
13532
  UncompiledComponentError,
12533
13533
  UnknownPreservedComponentError,
13534
+ analyzeDocxQuality,
12534
13535
  cleanComponentProps,
12535
13536
  corporateTheme,
12536
13537
  createComponent,
12537
13538
  createDocumentGenerator,
12538
13539
  createMinimalTheme,
12539
13540
  createVersion,
13541
+ docxQualityEngine,
12540
13542
  docxRendererIds,
12541
13543
  docxRendererStatuses,
12542
13544
  examples,
@@ -12579,7 +13581,9 @@ export {
12579
13581
  mergeSchemas,
12580
13582
  minimalTheme,
12581
13583
  modernTheme,
13584
+ prepareDocxQualityDocument,
12582
13585
  resetVisualPrepassStats,
13586
+ resolveDocxQualityProfile,
12583
13587
  runExample,
12584
13588
  themes,
12585
13589
  validateComponentProps,