@json-to-office/core-docx 1.4.0 → 1.5.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/core/generateFromIr.d.ts +5 -1
- package/dist/core/generateFromIr.d.ts.map +1 -1
- package/dist/core/generator.d.ts +4 -0
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/structure.d.ts +13 -0
- package/dist/core/structure.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +845 -189
- package/dist/index.js.map +1 -1
- package/dist/plugin/example/index.js +338 -93
- package/dist/plugin/example/index.js.map +1 -1
- package/dist/quality/facts.d.ts +82 -0
- package/dist/quality/facts.d.ts.map +1 -0
- package/dist/quality/preflight.d.ts +15 -0
- package/dist/quality/preflight.d.ts.map +1 -0
- package/dist/quality/rules.d.ts +38 -0
- package/dist/quality/rules.d.ts.map +1 -0
- package/dist/quality/text-metrics.d.ts +34 -0
- package/dist/quality/text-metrics.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -5037,88 +5037,6 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
|
|
|
5037
5037
|
return map;
|
|
5038
5038
|
}
|
|
5039
5039
|
|
|
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
5040
|
// src/core/desugarExternals.ts
|
|
5123
5041
|
import {
|
|
5124
5042
|
clampVisualDpi as clampVisualDpi2,
|
|
@@ -10568,101 +10486,6 @@ function isDocxRendererId(value) {
|
|
|
10568
10486
|
|
|
10569
10487
|
// src/core/generateFromIr.ts
|
|
10570
10488
|
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
10489
|
init_generationContext();
|
|
10667
10490
|
|
|
10668
10491
|
// src/core/layout.ts
|
|
@@ -11116,8 +10939,7 @@ function resolveComponentTree(components, theme) {
|
|
|
11116
10939
|
}
|
|
11117
10940
|
|
|
11118
10941
|
// src/core/structure.ts
|
|
11119
|
-
|
|
11120
|
-
const metadata = createDocumentMetadata(document.props, generationDate);
|
|
10942
|
+
function resolveDocumentTree(document, theme) {
|
|
11121
10943
|
const docDefaults = document.props.componentDefaults;
|
|
11122
10944
|
const mergedComponentDefaults = docDefaults ? mergeWithDefaults2(docDefaults, theme.componentDefaults || {}) : void 0;
|
|
11123
10945
|
const docNoProofWords = document.props.noProofWords;
|
|
@@ -11131,6 +10953,14 @@ async function processDocument(document, theme, themeName, generationDate) {
|
|
|
11131
10953
|
},
|
|
11132
10954
|
...mergedNoProofWords && { noProofWords: mergedNoProofWords }
|
|
11133
10955
|
} : theme;
|
|
10956
|
+
return {
|
|
10957
|
+
theme: effectiveTheme,
|
|
10958
|
+
children: resolveComponentTree(document.children || [], effectiveTheme)
|
|
10959
|
+
};
|
|
10960
|
+
}
|
|
10961
|
+
async function processResolvedDocument(document, resolved, themeName, generationDate) {
|
|
10962
|
+
const metadata = createDocumentMetadata(document.props, generationDate);
|
|
10963
|
+
const effectiveTheme = resolved.theme;
|
|
11134
10964
|
const context = createRenderContext(
|
|
11135
10965
|
{
|
|
11136
10966
|
metadata,
|
|
@@ -11141,11 +10971,7 @@ async function processDocument(document, theme, themeName, generationDate) {
|
|
|
11141
10971
|
effectiveTheme,
|
|
11142
10972
|
themeName
|
|
11143
10973
|
);
|
|
11144
|
-
const
|
|
11145
|
-
document.children || [],
|
|
11146
|
-
effectiveTheme
|
|
11147
|
-
);
|
|
11148
|
-
const sections = await extractSections(resolvedChildren, context);
|
|
10974
|
+
const sections = await extractSections(resolved.children, context);
|
|
11149
10975
|
return {
|
|
11150
10976
|
metadata,
|
|
11151
10977
|
sections,
|
|
@@ -11245,6 +11071,419 @@ function createRenderContext(document, theme, themeName) {
|
|
|
11245
11071
|
};
|
|
11246
11072
|
}
|
|
11247
11073
|
|
|
11074
|
+
// src/quality/facts.ts
|
|
11075
|
+
import { DEFAULT_DOCX_RENDERER_ID as DEFAULT_DOCX_RENDERER_ID2 } from "@json-to-office/shared-docx";
|
|
11076
|
+
|
|
11077
|
+
// src/core/generationContext.ts
|
|
11078
|
+
import { applyExportMode } from "@json-to-office/shared";
|
|
11079
|
+
|
|
11080
|
+
// src/styles/theme-resolver.ts
|
|
11081
|
+
init_themes();
|
|
11082
|
+
function resolveBuiltInTheme(themeName, options = {}) {
|
|
11083
|
+
if (!hasTheme(themeName)) {
|
|
11084
|
+
const available = [
|
|
11085
|
+
...getThemeNames(),
|
|
11086
|
+
...Object.keys(options.customThemes ?? {})
|
|
11087
|
+
].sort();
|
|
11088
|
+
options.warnings?.push({
|
|
11089
|
+
component: "theme",
|
|
11090
|
+
message: `Theme "${themeName}" not found; falling back to "minimal". Available themes: ${available.join(", ")}.`,
|
|
11091
|
+
severity: "warning",
|
|
11092
|
+
context: { code: "theme_not_found", requested: themeName, available }
|
|
11093
|
+
});
|
|
11094
|
+
}
|
|
11095
|
+
return getThemeWithFallback(themeName);
|
|
11096
|
+
}
|
|
11097
|
+
|
|
11098
|
+
// src/themes/overrides.ts
|
|
11099
|
+
function applyThemeOverrides(theme, overrides) {
|
|
11100
|
+
if (!overrides) return theme;
|
|
11101
|
+
const fonts = { ...theme.fonts };
|
|
11102
|
+
for (const [role, def] of Object.entries(overrides.fonts ?? {})) {
|
|
11103
|
+
const key = role;
|
|
11104
|
+
fonts[key] = { ...theme.fonts[key], ...def };
|
|
11105
|
+
}
|
|
11106
|
+
const styles = { ...theme.styles ?? {} };
|
|
11107
|
+
for (const [name, def] of Object.entries(overrides.styles ?? {})) {
|
|
11108
|
+
styles[name] = { ...styles[name], ...def };
|
|
11109
|
+
}
|
|
11110
|
+
return {
|
|
11111
|
+
...theme,
|
|
11112
|
+
colors: { ...theme.colors, ...overrides.colors ?? {} },
|
|
11113
|
+
fonts,
|
|
11114
|
+
...Object.keys(styles).length > 0 && { styles }
|
|
11115
|
+
};
|
|
11116
|
+
}
|
|
11117
|
+
|
|
11118
|
+
// src/core/generationContext.ts
|
|
11119
|
+
function defaultNamedThemeLookup(themeName, customThemes, warnings) {
|
|
11120
|
+
if (customThemes) {
|
|
11121
|
+
if (customThemes[themeName]) return customThemes[themeName];
|
|
11122
|
+
const themeNameLower = themeName.toLowerCase();
|
|
11123
|
+
const matchingThemeKey = Object.keys(customThemes).find(
|
|
11124
|
+
(key) => key.toLowerCase() === themeNameLower
|
|
11125
|
+
);
|
|
11126
|
+
if (matchingThemeKey) return customThemes[matchingThemeKey];
|
|
11127
|
+
return resolveBuiltInTheme(themeName, { customThemes, warnings });
|
|
11128
|
+
}
|
|
11129
|
+
return resolveBuiltInTheme(themeName, { warnings });
|
|
11130
|
+
}
|
|
11131
|
+
function resolveThemeContext(documentIn, options = {}) {
|
|
11132
|
+
const { customThemes, fonts, warnings, resolveNamedTheme } = options;
|
|
11133
|
+
if (documentIn.props === null) {
|
|
11134
|
+
throw new Error(
|
|
11135
|
+
"Document `props` is null. Omit it, or provide an object \u2014 a null props cannot carry a theme."
|
|
11136
|
+
);
|
|
11137
|
+
}
|
|
11138
|
+
const document = documentIn.props === void 0 ? { ...documentIn, props: {} } : documentIn;
|
|
11139
|
+
const authoredThemeName = document.props.theme || void 0;
|
|
11140
|
+
const baseThemeName = authoredThemeName ?? "minimal";
|
|
11141
|
+
let theme = resolveNamedTheme ? resolveNamedTheme(
|
|
11142
|
+
baseThemeName,
|
|
11143
|
+
warnings,
|
|
11144
|
+
authoredThemeName !== void 0
|
|
11145
|
+
) : defaultNamedThemeLookup(baseThemeName, customThemes, warnings);
|
|
11146
|
+
const themeOverrides = document.props.themeOverrides;
|
|
11147
|
+
if (themeOverrides) {
|
|
11148
|
+
theme = applyThemeOverrides(theme, themeOverrides);
|
|
11149
|
+
}
|
|
11150
|
+
const mode = applyExportMode({ doc: document, theme, fonts });
|
|
11151
|
+
for (const w of mode.warnings) {
|
|
11152
|
+
if (warnings) {
|
|
11153
|
+
warnings.push({
|
|
11154
|
+
component: "fontRegistry",
|
|
11155
|
+
message: w.message,
|
|
11156
|
+
severity: "warning",
|
|
11157
|
+
context: { code: w.code }
|
|
11158
|
+
});
|
|
11159
|
+
} else {
|
|
11160
|
+
console.warn(`[json-to-docx] [${w.code}] ${w.message}`);
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
return {
|
|
11164
|
+
document: mode.doc,
|
|
11165
|
+
theme: mode.theme,
|
|
11166
|
+
themeName: baseThemeName
|
|
11167
|
+
};
|
|
11168
|
+
}
|
|
11169
|
+
|
|
11170
|
+
// src/json/normalizer.ts
|
|
11171
|
+
function normalizeComponent(component) {
|
|
11172
|
+
if (component.name === "columns") {
|
|
11173
|
+
return normalizeColumnsComponent(component);
|
|
11174
|
+
}
|
|
11175
|
+
if ("children" in component && component.children) {
|
|
11176
|
+
return {
|
|
11177
|
+
...component,
|
|
11178
|
+
// Preserve ALL properties from validated component
|
|
11179
|
+
children: component.children.map(normalizeComponent)
|
|
11180
|
+
};
|
|
11181
|
+
}
|
|
11182
|
+
if (component.name === "section") {
|
|
11183
|
+
return normalizeSectionComponent(component);
|
|
11184
|
+
}
|
|
11185
|
+
return component;
|
|
11186
|
+
}
|
|
11187
|
+
function normalizeColumnsComponent(component) {
|
|
11188
|
+
const cfg = component.props || {};
|
|
11189
|
+
const applyTopLevelGap = (cols, topGap) => cols.map((c, i) => {
|
|
11190
|
+
const isLast = i === cols.length - 1;
|
|
11191
|
+
if (!isLast && c.gap === void 0 && topGap !== void 0) {
|
|
11192
|
+
return { ...c, gap: topGap };
|
|
11193
|
+
}
|
|
11194
|
+
return c;
|
|
11195
|
+
});
|
|
11196
|
+
const normalizeAutoWidth = (cols) => cols.map((c) => ({
|
|
11197
|
+
...c,
|
|
11198
|
+
width: c.width === "auto" ? void 0 : c.width
|
|
11199
|
+
}));
|
|
11200
|
+
let columnsArray;
|
|
11201
|
+
if (typeof cfg.columns === "number" && cfg.columns > 0) {
|
|
11202
|
+
const n = cfg.columns;
|
|
11203
|
+
const base = Array.from(
|
|
11204
|
+
{ length: n },
|
|
11205
|
+
() => ({})
|
|
11206
|
+
);
|
|
11207
|
+
const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
|
|
11208
|
+
columnsArray = applyTopLevelGap(base, topLevelGap);
|
|
11209
|
+
} else if (Array.isArray(cfg.columns)) {
|
|
11210
|
+
columnsArray = cfg.columns;
|
|
11211
|
+
const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
|
|
11212
|
+
columnsArray = applyTopLevelGap(columnsArray, topLevelGap);
|
|
11213
|
+
columnsArray = normalizeAutoWidth(columnsArray);
|
|
11214
|
+
} else {
|
|
11215
|
+
columnsArray = [{}];
|
|
11216
|
+
}
|
|
11217
|
+
const normalizedProps = {
|
|
11218
|
+
columns: columnsArray
|
|
11219
|
+
};
|
|
11220
|
+
return {
|
|
11221
|
+
...component,
|
|
11222
|
+
// Preserve any additional properties
|
|
11223
|
+
name: "columns",
|
|
11224
|
+
props: normalizedProps,
|
|
11225
|
+
children: component.children ? component.children.map(normalizeComponent) : void 0
|
|
11226
|
+
};
|
|
11227
|
+
}
|
|
11228
|
+
function normalizeSectionComponent(component) {
|
|
11229
|
+
const props = component.props || {};
|
|
11230
|
+
return {
|
|
11231
|
+
...component,
|
|
11232
|
+
// Preserve ALL properties
|
|
11233
|
+
props: {
|
|
11234
|
+
...props,
|
|
11235
|
+
// Preserve ALL props properties
|
|
11236
|
+
header: props.header ? props.header === "linkToPrevious" ? "linkToPrevious" : props.header.map((m) => normalizeComponent(m)) : void 0,
|
|
11237
|
+
footer: props.footer ? props.footer === "linkToPrevious" ? "linkToPrevious" : props.footer.map((m) => normalizeComponent(m)) : void 0
|
|
11238
|
+
},
|
|
11239
|
+
children: component.children ? component.children.map(normalizeComponent) : void 0
|
|
11240
|
+
};
|
|
11241
|
+
}
|
|
11242
|
+
function normalizeDocument(document) {
|
|
11243
|
+
if (document.name !== "docx") {
|
|
11244
|
+
throw new Error(
|
|
11245
|
+
'Top-level document must be a docx component with name="docx"'
|
|
11246
|
+
);
|
|
11247
|
+
}
|
|
11248
|
+
const normalized = normalizeComponent(document);
|
|
11249
|
+
return [normalized];
|
|
11250
|
+
}
|
|
11251
|
+
|
|
11252
|
+
// src/quality/facts.ts
|
|
11253
|
+
init_widthUtils();
|
|
11254
|
+
function asRecord(value) {
|
|
11255
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
11256
|
+
}
|
|
11257
|
+
function pageBox(theme, themeName, pageOverride) {
|
|
11258
|
+
const page = createSectionProperties(
|
|
11259
|
+
getColumnSettings("single"),
|
|
11260
|
+
theme,
|
|
11261
|
+
themeName,
|
|
11262
|
+
"nextPage",
|
|
11263
|
+
pageOverride
|
|
11264
|
+
).page;
|
|
11265
|
+
return {
|
|
11266
|
+
availableWidthTwips: Math.max(
|
|
11267
|
+
0,
|
|
11268
|
+
page.size.width - page.margin.left - page.margin.right
|
|
11269
|
+
),
|
|
11270
|
+
pageBottomTwips: page.size.height
|
|
11271
|
+
};
|
|
11272
|
+
}
|
|
11273
|
+
function tableFact(props, path4, availableWidthTwips) {
|
|
11274
|
+
const columns = Array.isArray(props.columns) ? props.columns : void 0;
|
|
11275
|
+
if (!columns) return void 0;
|
|
11276
|
+
let totalWidthTwips = 0;
|
|
11277
|
+
let hasExplicitWidth = false;
|
|
11278
|
+
let pointSum = 0;
|
|
11279
|
+
let percentSum = 0;
|
|
11280
|
+
const explicitWidths = [];
|
|
11281
|
+
columns.forEach((column, index) => {
|
|
11282
|
+
const width = asRecord(column)?.width;
|
|
11283
|
+
if (typeof width === "number" && Number.isFinite(width)) {
|
|
11284
|
+
hasExplicitWidth = true;
|
|
11285
|
+
explicitWidths.push({ index, width });
|
|
11286
|
+
totalWidthTwips += relativeLengthToTwips(width, availableWidthTwips);
|
|
11287
|
+
pointSum += width;
|
|
11288
|
+
} else if (typeof width === "string") {
|
|
11289
|
+
hasExplicitWidth = true;
|
|
11290
|
+
const percent = width.trim().endsWith("%") ? Number(width.trim().slice(0, -1)) : Number.NaN;
|
|
11291
|
+
if (Number.isFinite(percent)) {
|
|
11292
|
+
explicitWidths.push({ index, width });
|
|
11293
|
+
percentSum += percent;
|
|
11294
|
+
totalWidthTwips += Math.max(
|
|
11295
|
+
0,
|
|
11296
|
+
Math.round(availableWidthTwips * percent / 100)
|
|
11297
|
+
);
|
|
11298
|
+
}
|
|
11299
|
+
}
|
|
11300
|
+
});
|
|
11301
|
+
return {
|
|
11302
|
+
id: `docx:table-width:${path4}`,
|
|
11303
|
+
kind: "docx/table-width",
|
|
11304
|
+
path: `${path4}/props/columns`,
|
|
11305
|
+
totalWidthTwips,
|
|
11306
|
+
availableWidthTwips,
|
|
11307
|
+
hasExplicitWidth,
|
|
11308
|
+
allColumnsExplicit: explicitWidths.length === columns.length,
|
|
11309
|
+
pointSum,
|
|
11310
|
+
percentSum,
|
|
11311
|
+
explicitWidths
|
|
11312
|
+
};
|
|
11313
|
+
}
|
|
11314
|
+
var TWIPS_PER_POINT3 = 20;
|
|
11315
|
+
var SINGLE_LINE_RATIO = 1.2;
|
|
11316
|
+
function finiteNumber(value) {
|
|
11317
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
11318
|
+
}
|
|
11319
|
+
function lineHeightPt(fontSizePt, spacing) {
|
|
11320
|
+
const rule = asRecord(spacing);
|
|
11321
|
+
const value = finiteNumber(rule?.value);
|
|
11322
|
+
switch (rule?.type) {
|
|
11323
|
+
// `exactly` is an absolute line box, and display type routinely sets it
|
|
11324
|
+
// below the font size — reading it as a multiplier would inflate every
|
|
11325
|
+
// estimate on exactly the headings that matter most.
|
|
11326
|
+
case "exactly":
|
|
11327
|
+
return value ?? fontSizePt * SINGLE_LINE_RATIO;
|
|
11328
|
+
case "atLeast":
|
|
11329
|
+
return Math.max(value ?? 0, fontSizePt * SINGLE_LINE_RATIO);
|
|
11330
|
+
case "multiple":
|
|
11331
|
+
return fontSizePt * SINGLE_LINE_RATIO * (value ?? 1);
|
|
11332
|
+
default:
|
|
11333
|
+
return fontSizePt * SINGLE_LINE_RATIO;
|
|
11334
|
+
}
|
|
11335
|
+
}
|
|
11336
|
+
function characterSpacingPt(spacing) {
|
|
11337
|
+
const rule = asRecord(spacing);
|
|
11338
|
+
const value = finiteNumber(rule?.value);
|
|
11339
|
+
if (rule === void 0 || value === void 0) return 0;
|
|
11340
|
+
return rule.type === "condensed" ? -value / TWIPS_PER_POINT3 : value / TWIPS_PER_POINT3;
|
|
11341
|
+
}
|
|
11342
|
+
function frameTextFact(props, path4, page) {
|
|
11343
|
+
const floating = asRecord(props.floating);
|
|
11344
|
+
const frameWidthTwips = finiteNumber(floating?.width);
|
|
11345
|
+
if (frameWidthTwips === void 0) return void 0;
|
|
11346
|
+
const text = typeof props.text === "string" ? props.text : "";
|
|
11347
|
+
if (text.trim() === "") return void 0;
|
|
11348
|
+
const font = asRecord(props.font);
|
|
11349
|
+
const fontSizePt = finiteNumber(font?.size);
|
|
11350
|
+
if (fontSizePt === void 0) return void 0;
|
|
11351
|
+
const longestWord = text.split(/\s+/).reduce(
|
|
11352
|
+
(longest, word) => word.length > longest.length ? word : longest,
|
|
11353
|
+
""
|
|
11354
|
+
);
|
|
11355
|
+
const offsetY = finiteNumber(asRecord(floating?.verticalPosition)?.offset);
|
|
11356
|
+
return {
|
|
11357
|
+
id: `docx:frame-text:${path4}`,
|
|
11358
|
+
kind: "docx/frame-text",
|
|
11359
|
+
path: path4,
|
|
11360
|
+
text,
|
|
11361
|
+
fontSizePt,
|
|
11362
|
+
lineHeightPt: lineHeightPt(fontSizePt, font?.lineSpacing),
|
|
11363
|
+
characterSpacingPt: characterSpacingPt(font?.characterSpacing),
|
|
11364
|
+
frameWidthTwips,
|
|
11365
|
+
...finiteNumber(floating?.height) !== void 0 && {
|
|
11366
|
+
frameHeightTwips: finiteNumber(floating?.height)
|
|
11367
|
+
},
|
|
11368
|
+
...offsetY !== void 0 && { offsetYTwips: offsetY },
|
|
11369
|
+
pageBottomTwips: page.pageBottomTwips,
|
|
11370
|
+
longestWord
|
|
11371
|
+
};
|
|
11372
|
+
}
|
|
11373
|
+
var SVG_VIEWBOX = /viewBox\s*=\s*"\s*(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+(-?[\d.]+)\s*"/;
|
|
11374
|
+
var SVG_TEXT = /<text\b([^>]*)>([^<]*)<\/text>/g;
|
|
11375
|
+
var SVG_ATTR = (name) => new RegExp(`\\b${name}\\s*=\\s*"(-?[\\d.]+)"`);
|
|
11376
|
+
function svgTextFacts(props, path4) {
|
|
11377
|
+
const svg = typeof props.svg === "string" ? props.svg : void 0;
|
|
11378
|
+
if (!svg) return [];
|
|
11379
|
+
const box = SVG_VIEWBOX.exec(svg);
|
|
11380
|
+
if (!box) return [];
|
|
11381
|
+
const viewBoxMinY = Number(box[2]);
|
|
11382
|
+
const viewBoxHeight = Number(box[4]);
|
|
11383
|
+
if (!Number.isFinite(viewBoxMinY) || !Number.isFinite(viewBoxHeight)) {
|
|
11384
|
+
return [];
|
|
11385
|
+
}
|
|
11386
|
+
const facts = [];
|
|
11387
|
+
let match;
|
|
11388
|
+
SVG_TEXT.lastIndex = 0;
|
|
11389
|
+
let index = 0;
|
|
11390
|
+
while ((match = SVG_TEXT.exec(svg)) !== null) {
|
|
11391
|
+
const [, attributes, content] = match;
|
|
11392
|
+
const baselineY = Number(SVG_ATTR("y").exec(attributes)?.[1]);
|
|
11393
|
+
if (!Number.isFinite(baselineY)) {
|
|
11394
|
+
index += 1;
|
|
11395
|
+
continue;
|
|
11396
|
+
}
|
|
11397
|
+
const fontSizeUnits = Number(
|
|
11398
|
+
SVG_ATTR("font-size").exec(attributes)?.[1] ?? 0
|
|
11399
|
+
);
|
|
11400
|
+
facts.push({
|
|
11401
|
+
id: `docx:svg-text:${path4}:${index}`,
|
|
11402
|
+
kind: "docx/svg-text",
|
|
11403
|
+
path: `${path4}/props/svg`,
|
|
11404
|
+
content: content.trim(),
|
|
11405
|
+
baselineY,
|
|
11406
|
+
fontSizeUnits: Number.isFinite(fontSizeUnits) ? fontSizeUnits : 0,
|
|
11407
|
+
viewBoxMinY,
|
|
11408
|
+
viewBoxHeight
|
|
11409
|
+
});
|
|
11410
|
+
index += 1;
|
|
11411
|
+
}
|
|
11412
|
+
return facts;
|
|
11413
|
+
}
|
|
11414
|
+
function walkActive(node, path4, page, visit) {
|
|
11415
|
+
const rec = asRecord(node);
|
|
11416
|
+
if (!rec || rec.enabled === false) return;
|
|
11417
|
+
visit(rec, path4, page);
|
|
11418
|
+
const children = Array.isArray(rec.children) ? rec.children : [];
|
|
11419
|
+
children.forEach(
|
|
11420
|
+
(child, index) => walkActive(child, `${path4}/children/${index}`, page, visit)
|
|
11421
|
+
);
|
|
11422
|
+
}
|
|
11423
|
+
function prepareDocxQualityDocument(document, options = {}) {
|
|
11424
|
+
const context = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
|
|
11425
|
+
customThemes: options.customThemes,
|
|
11426
|
+
fonts: options.fonts,
|
|
11427
|
+
warnings: options.warnings
|
|
11428
|
+
});
|
|
11429
|
+
const resolved = resolveDocumentTree(context.document, context.theme);
|
|
11430
|
+
const basePage = pageBox(resolved.theme, context.themeName);
|
|
11431
|
+
const facts = [];
|
|
11432
|
+
const provenance = {};
|
|
11433
|
+
let previousHeadingLevel;
|
|
11434
|
+
const addFact = (fact) => {
|
|
11435
|
+
facts.push(fact);
|
|
11436
|
+
provenance[fact.id] = {
|
|
11437
|
+
path: fact.path,
|
|
11438
|
+
...fact.relatedPaths && { relatedPaths: fact.relatedPaths }
|
|
11439
|
+
};
|
|
11440
|
+
};
|
|
11441
|
+
const visit = (node, path4, page) => {
|
|
11442
|
+
const props = asRecord(node.props) ?? {};
|
|
11443
|
+
if (node.name === "table") {
|
|
11444
|
+
const fact = tableFact(props, path4, page.availableWidthTwips);
|
|
11445
|
+
if (fact) addFact(fact);
|
|
11446
|
+
}
|
|
11447
|
+
if (node.name === "paragraph" || node.name === "text-box") {
|
|
11448
|
+
const fact = frameTextFact(props, path4, page);
|
|
11449
|
+
if (fact) addFact(fact);
|
|
11450
|
+
}
|
|
11451
|
+
if (node.name === "image" || node.name === "visual") {
|
|
11452
|
+
for (const fact of svgTextFacts(props, path4)) addFact(fact);
|
|
11453
|
+
}
|
|
11454
|
+
if (node.name === "heading") {
|
|
11455
|
+
const level = typeof props.level === "number" && Number.isFinite(props.level) ? props.level : 1;
|
|
11456
|
+
addFact({
|
|
11457
|
+
id: `docx:heading:${path4}`,
|
|
11458
|
+
kind: "docx/heading",
|
|
11459
|
+
path: `${path4}/props/level`,
|
|
11460
|
+
level,
|
|
11461
|
+
...previousHeadingLevel !== void 0 && {
|
|
11462
|
+
previousLevel: previousHeadingLevel
|
|
11463
|
+
}
|
|
11464
|
+
});
|
|
11465
|
+
previousHeadingLevel = level;
|
|
11466
|
+
}
|
|
11467
|
+
};
|
|
11468
|
+
resolved.children.forEach((component, index) => {
|
|
11469
|
+
const rec = component;
|
|
11470
|
+
const page = rec.name === "section" ? pageBox(resolved.theme, context.themeName, rec.props?.page) : basePage;
|
|
11471
|
+
walkActive(component, `/children/${index}`, page, visit);
|
|
11472
|
+
});
|
|
11473
|
+
return {
|
|
11474
|
+
format: "docx",
|
|
11475
|
+
model: {
|
|
11476
|
+
authored: document,
|
|
11477
|
+
context,
|
|
11478
|
+
document: resolved,
|
|
11479
|
+
themeName: context.themeName
|
|
11480
|
+
},
|
|
11481
|
+
facts,
|
|
11482
|
+
provenance,
|
|
11483
|
+
renderer: options.renderer ?? DEFAULT_DOCX_RENDERER_ID2
|
|
11484
|
+
};
|
|
11485
|
+
}
|
|
11486
|
+
|
|
11248
11487
|
// src/core/generateFromIr.ts
|
|
11249
11488
|
var UncompiledComponentError = class extends Error {
|
|
11250
11489
|
code = "UNCOMPILED_COMPONENT";
|
|
@@ -11272,11 +11511,14 @@ async function compileDocumentToIr(document, options = {}, collector) {
|
|
|
11272
11511
|
);
|
|
11273
11512
|
}
|
|
11274
11513
|
async function compileDocumentScoped(document, options, warnings) {
|
|
11275
|
-
const
|
|
11514
|
+
const prepared = options.prepared ?? prepareDocxQualityDocument(document, {
|
|
11276
11515
|
customThemes: options.customThemes,
|
|
11277
11516
|
fonts: options.fonts,
|
|
11278
|
-
warnings
|
|
11517
|
+
warnings,
|
|
11518
|
+
context: options.context,
|
|
11519
|
+
renderer: options.renderer
|
|
11279
11520
|
});
|
|
11521
|
+
const { context } = prepared.model;
|
|
11280
11522
|
const hasVisual = collectVisualProps(context.document).length > 0;
|
|
11281
11523
|
const resolvedFonts = await resolveDocumentFonts(
|
|
11282
11524
|
context.document,
|
|
@@ -11292,9 +11534,10 @@ async function compileDocumentScoped(document, options, warnings) {
|
|
|
11292
11534
|
...options.baseDir !== void 0 ? { baseDir: options.baseDir } : {},
|
|
11293
11535
|
...visualFonts.length > 0 ? { visualFonts } : {}
|
|
11294
11536
|
});
|
|
11295
|
-
const
|
|
11537
|
+
const resolved = desugared === context.document ? prepared.model.document : resolveDocumentTree(desugared, context.theme);
|
|
11538
|
+
const structure = await processResolvedDocument(
|
|
11296
11539
|
desugared,
|
|
11297
|
-
|
|
11540
|
+
resolved,
|
|
11298
11541
|
context.themeName,
|
|
11299
11542
|
resolveGenerationDate(options)
|
|
11300
11543
|
);
|
|
@@ -11459,7 +11702,8 @@ async function generateBufferWithWarnings(jsonConfig, options) {
|
|
|
11459
11702
|
...options?.baseDir !== void 0 ? { baseDir: options.baseDir } : {},
|
|
11460
11703
|
...options?.deterministic !== void 0 ? { deterministic: options.deterministic } : {},
|
|
11461
11704
|
...options?.generatedAt !== void 0 ? { generatedAt: options.generatedAt } : {},
|
|
11462
|
-
...renderer ? { renderer } : {}
|
|
11705
|
+
...renderer ? { renderer } : {},
|
|
11706
|
+
...options?.prepared ? { prepared: options.prepared } : {}
|
|
11463
11707
|
});
|
|
11464
11708
|
}
|
|
11465
11709
|
function parseDocument(jsonConfig) {
|
|
@@ -11600,6 +11844,411 @@ async function rasterizeVisual(obj, ctx) {
|
|
|
11600
11844
|
});
|
|
11601
11845
|
}
|
|
11602
11846
|
|
|
11847
|
+
// src/quality/preflight.ts
|
|
11848
|
+
import {
|
|
11849
|
+
assertValidQualityPolicy,
|
|
11850
|
+
assertValidQualityProfile
|
|
11851
|
+
} from "@json-to-office/quality";
|
|
11852
|
+
|
|
11853
|
+
// src/quality/rules.ts
|
|
11854
|
+
import {
|
|
11855
|
+
mergeQualityProfiles,
|
|
11856
|
+
QUALITY_CODES,
|
|
11857
|
+
QualityEngine
|
|
11858
|
+
} from "@json-to-office/quality";
|
|
11859
|
+
|
|
11860
|
+
// src/quality/text-metrics.ts
|
|
11861
|
+
var DEFAULT_ADVANCE = 556;
|
|
11862
|
+
var ADVANCE = {
|
|
11863
|
+
" ": 278,
|
|
11864
|
+
A: 667,
|
|
11865
|
+
B: 667,
|
|
11866
|
+
C: 722,
|
|
11867
|
+
D: 722,
|
|
11868
|
+
E: 667,
|
|
11869
|
+
F: 611,
|
|
11870
|
+
G: 778,
|
|
11871
|
+
H: 722,
|
|
11872
|
+
I: 278,
|
|
11873
|
+
J: 500,
|
|
11874
|
+
K: 667,
|
|
11875
|
+
L: 556,
|
|
11876
|
+
M: 833,
|
|
11877
|
+
N: 722,
|
|
11878
|
+
O: 778,
|
|
11879
|
+
P: 667,
|
|
11880
|
+
Q: 778,
|
|
11881
|
+
R: 722,
|
|
11882
|
+
S: 667,
|
|
11883
|
+
T: 611,
|
|
11884
|
+
U: 722,
|
|
11885
|
+
V: 667,
|
|
11886
|
+
W: 944,
|
|
11887
|
+
X: 667,
|
|
11888
|
+
Y: 667,
|
|
11889
|
+
Z: 611,
|
|
11890
|
+
a: 556,
|
|
11891
|
+
b: 556,
|
|
11892
|
+
c: 500,
|
|
11893
|
+
d: 556,
|
|
11894
|
+
e: 556,
|
|
11895
|
+
f: 278,
|
|
11896
|
+
g: 556,
|
|
11897
|
+
h: 556,
|
|
11898
|
+
i: 222,
|
|
11899
|
+
j: 222,
|
|
11900
|
+
k: 500,
|
|
11901
|
+
l: 222,
|
|
11902
|
+
m: 833,
|
|
11903
|
+
n: 556,
|
|
11904
|
+
o: 556,
|
|
11905
|
+
p: 556,
|
|
11906
|
+
q: 556,
|
|
11907
|
+
r: 333,
|
|
11908
|
+
s: 500,
|
|
11909
|
+
t: 278,
|
|
11910
|
+
u: 556,
|
|
11911
|
+
v: 500,
|
|
11912
|
+
w: 722,
|
|
11913
|
+
x: 500,
|
|
11914
|
+
y: 500,
|
|
11915
|
+
z: 500,
|
|
11916
|
+
".": 278,
|
|
11917
|
+
",": 278,
|
|
11918
|
+
":": 278,
|
|
11919
|
+
";": 278,
|
|
11920
|
+
"!": 278,
|
|
11921
|
+
"?": 556,
|
|
11922
|
+
"'": 191,
|
|
11923
|
+
'"': 355,
|
|
11924
|
+
"-": 333,
|
|
11925
|
+
"\u2013": 556,
|
|
11926
|
+
"\u2014": 1e3,
|
|
11927
|
+
"&": 667,
|
|
11928
|
+
"%": 889,
|
|
11929
|
+
"(": 333,
|
|
11930
|
+
")": 333,
|
|
11931
|
+
"/": 278,
|
|
11932
|
+
"@": 1015,
|
|
11933
|
+
"#": 556,
|
|
11934
|
+
"+": 584,
|
|
11935
|
+
"=": 584
|
|
11936
|
+
};
|
|
11937
|
+
function estimateTextWidthPt(text, fontSizePt, trackingPt = 0) {
|
|
11938
|
+
let units = 0;
|
|
11939
|
+
for (const character of text) {
|
|
11940
|
+
units += ADVANCE[character] ?? DEFAULT_ADVANCE;
|
|
11941
|
+
}
|
|
11942
|
+
const width = units / 1e3 * fontSizePt + text.length * trackingPt;
|
|
11943
|
+
return Math.max(0, width);
|
|
11944
|
+
}
|
|
11945
|
+
function estimateWrappedLines(text, widthPt, fontSizePt, trackingPt = 0) {
|
|
11946
|
+
if (widthPt <= 0) return 1;
|
|
11947
|
+
let lines = 0;
|
|
11948
|
+
for (const paragraph2 of text.split("\n")) {
|
|
11949
|
+
const words = paragraph2.split(/\s+/).filter(Boolean);
|
|
11950
|
+
if (words.length === 0) {
|
|
11951
|
+
lines += 1;
|
|
11952
|
+
continue;
|
|
11953
|
+
}
|
|
11954
|
+
let current = "";
|
|
11955
|
+
let used = 0;
|
|
11956
|
+
for (const word of words) {
|
|
11957
|
+
const candidate = current === "" ? word : `${current} ${word}`;
|
|
11958
|
+
if (estimateTextWidthPt(candidate, fontSizePt, trackingPt) <= widthPt) {
|
|
11959
|
+
current = candidate;
|
|
11960
|
+
} else {
|
|
11961
|
+
if (current !== "") used += 1;
|
|
11962
|
+
current = word;
|
|
11963
|
+
}
|
|
11964
|
+
}
|
|
11965
|
+
lines += used + (current === "" ? 0 : 1);
|
|
11966
|
+
}
|
|
11967
|
+
return Math.max(1, lines);
|
|
11968
|
+
}
|
|
11969
|
+
|
|
11970
|
+
// src/quality/rules.ts
|
|
11971
|
+
var WIDTH_TOLERANCE_TWIPS = 10;
|
|
11972
|
+
function numberParameter(parameters, name, fallback) {
|
|
11973
|
+
const value = parameters[name];
|
|
11974
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
11975
|
+
}
|
|
11976
|
+
var docxTableWidthRule = {
|
|
11977
|
+
id: "docx/table-width",
|
|
11978
|
+
code: QUALITY_CODES.TABLE_WIDTH_OVERFLOW,
|
|
11979
|
+
category: "integrity",
|
|
11980
|
+
defaultSeverity: "warning",
|
|
11981
|
+
defaultCertainty: "deterministic",
|
|
11982
|
+
formats: ["docx"],
|
|
11983
|
+
defaultParameters: { toleranceTwips: WIDTH_TOLERANCE_TWIPS },
|
|
11984
|
+
evaluate: ({ facts, configuration }) => {
|
|
11985
|
+
const toleranceTwips = numberParameter(
|
|
11986
|
+
configuration.parameters,
|
|
11987
|
+
"toleranceTwips",
|
|
11988
|
+
WIDTH_TOLERANCE_TWIPS
|
|
11989
|
+
);
|
|
11990
|
+
return facts.filter(
|
|
11991
|
+
(fact) => fact.kind === "docx/table-width"
|
|
11992
|
+
).filter(
|
|
11993
|
+
(fact) => fact.hasExplicitWidth && fact.totalWidthTwips > fact.availableWidthTwips + toleranceTwips
|
|
11994
|
+
).map((fact) => {
|
|
11995
|
+
const totalPt = Math.round(fact.totalWidthTwips / 20 * 10) / 10;
|
|
11996
|
+
const availablePt = Math.round(fact.availableWidthTwips / 20 * 10) / 10;
|
|
11997
|
+
const scale = fact.availableWidthTwips / fact.totalWidthTwips;
|
|
11998
|
+
const fixes = fact.allColumnsExplicit ? fact.explicitWidths.map(({ index, width }) => ({
|
|
11999
|
+
op: "replace",
|
|
12000
|
+
path: `${fact.path}/${index}/width`,
|
|
12001
|
+
value: typeof width === "number" ? Math.floor(width * scale * 10) / 10 : `${Math.floor(Number(width.trim().slice(0, -1)) * scale * 10) / 10}%`
|
|
12002
|
+
})) : [];
|
|
12003
|
+
return {
|
|
12004
|
+
message: `Column widths use ${totalPt}pt, but this section has ${availablePt}pt available \u2014 the table will spill off the right edge.`,
|
|
12005
|
+
path: fact.path,
|
|
12006
|
+
suggestion: "Reduce fixed/percentage widths, widen the page, or leave columns unsized so they share the remainder.",
|
|
12007
|
+
context: {
|
|
12008
|
+
totalWidthPt: totalPt,
|
|
12009
|
+
availableWidthPt: availablePt,
|
|
12010
|
+
pointSum: fact.pointSum,
|
|
12011
|
+
percentSum: fact.percentSum
|
|
12012
|
+
},
|
|
12013
|
+
evidence: {
|
|
12014
|
+
actual: totalPt,
|
|
12015
|
+
expected: availablePt,
|
|
12016
|
+
unit: "pt"
|
|
12017
|
+
},
|
|
12018
|
+
...fixes.length > 0 && { fixes }
|
|
12019
|
+
};
|
|
12020
|
+
});
|
|
12021
|
+
}
|
|
12022
|
+
};
|
|
12023
|
+
var docxHeadingHierarchyRule = {
|
|
12024
|
+
id: "docx/heading-hierarchy",
|
|
12025
|
+
code: QUALITY_CODES.HEADING_SKIP,
|
|
12026
|
+
category: "hierarchy",
|
|
12027
|
+
defaultSeverity: "info",
|
|
12028
|
+
defaultCertainty: "deterministic",
|
|
12029
|
+
formats: ["docx"],
|
|
12030
|
+
evaluate: ({ facts }) => facts.filter((fact) => fact.kind === "docx/heading").flatMap((fact) => {
|
|
12031
|
+
const previousLevel = fact.previousLevel;
|
|
12032
|
+
if (previousLevel === void 0 || fact.level <= previousLevel + 1) {
|
|
12033
|
+
return [];
|
|
12034
|
+
}
|
|
12035
|
+
return [
|
|
12036
|
+
{
|
|
12037
|
+
message: `Heading level ${fact.level} follows level ${previousLevel} \u2014 the skipped level breaks the document outline.`,
|
|
12038
|
+
path: fact.path,
|
|
12039
|
+
suggestion: `Use level ${previousLevel + 1}, or promote this heading's section.`,
|
|
12040
|
+
context: { level: fact.level, previousLevel },
|
|
12041
|
+
evidence: {
|
|
12042
|
+
actual: fact.level,
|
|
12043
|
+
expected: previousLevel + 1
|
|
12044
|
+
},
|
|
12045
|
+
// The fact path already addresses `.../props/level`; RFC 6902
|
|
12046
|
+
// `add` replaces an existing member, so this works whether the
|
|
12047
|
+
// level was explicit or defaulted.
|
|
12048
|
+
fixes: [
|
|
12049
|
+
{
|
|
12050
|
+
op: "add",
|
|
12051
|
+
path: fact.path,
|
|
12052
|
+
value: previousLevel + 1
|
|
12053
|
+
}
|
|
12054
|
+
]
|
|
12055
|
+
}
|
|
12056
|
+
];
|
|
12057
|
+
})
|
|
12058
|
+
};
|
|
12059
|
+
var WIDTH_MODEL_TOLERANCE = 1.08;
|
|
12060
|
+
var TWIPS_PER_POINT4 = 20;
|
|
12061
|
+
function frameTextFacts(facts) {
|
|
12062
|
+
return facts.filter(
|
|
12063
|
+
(fact) => fact.kind === "docx/frame-text"
|
|
12064
|
+
);
|
|
12065
|
+
}
|
|
12066
|
+
var docxTextFitRule = {
|
|
12067
|
+
id: "docx/text-fit",
|
|
12068
|
+
code: QUALITY_CODES.TEXT_OVERFLOW,
|
|
12069
|
+
category: "integrity",
|
|
12070
|
+
defaultSeverity: "warning",
|
|
12071
|
+
defaultCertainty: "estimated",
|
|
12072
|
+
formats: ["docx"],
|
|
12073
|
+
defaultParameters: { widthTolerance: WIDTH_MODEL_TOLERANCE },
|
|
12074
|
+
evaluate: ({ facts, configuration }) => {
|
|
12075
|
+
const tolerance = numberParameter(
|
|
12076
|
+
configuration.parameters,
|
|
12077
|
+
"widthTolerance",
|
|
12078
|
+
WIDTH_MODEL_TOLERANCE
|
|
12079
|
+
);
|
|
12080
|
+
const findings = [];
|
|
12081
|
+
for (const fact of frameTextFacts(facts)) {
|
|
12082
|
+
const frameWidthPt = fact.frameWidthTwips / TWIPS_PER_POINT4;
|
|
12083
|
+
const longestWordPt = estimateTextWidthPt(
|
|
12084
|
+
fact.longestWord,
|
|
12085
|
+
fact.fontSizePt,
|
|
12086
|
+
fact.characterSpacingPt
|
|
12087
|
+
);
|
|
12088
|
+
if (longestWordPt > frameWidthPt * tolerance) {
|
|
12089
|
+
findings.push({
|
|
12090
|
+
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.`,
|
|
12091
|
+
path: `${fact.path}/props/floating/width`,
|
|
12092
|
+
suggestion: "Widen the frame, reduce fontSize, or condense the run further.",
|
|
12093
|
+
context: {
|
|
12094
|
+
longestWord: fact.longestWord,
|
|
12095
|
+
estimatedWidthPt: Math.round(longestWordPt),
|
|
12096
|
+
frameWidthPt: Math.round(frameWidthPt),
|
|
12097
|
+
fontSizePt: fact.fontSizePt
|
|
12098
|
+
},
|
|
12099
|
+
evidence: {
|
|
12100
|
+
actual: Math.round(longestWordPt),
|
|
12101
|
+
expected: Math.round(frameWidthPt),
|
|
12102
|
+
unit: "pt"
|
|
12103
|
+
}
|
|
12104
|
+
});
|
|
12105
|
+
continue;
|
|
12106
|
+
}
|
|
12107
|
+
if (fact.offsetYTwips === void 0) continue;
|
|
12108
|
+
const lines = estimateWrappedLines(
|
|
12109
|
+
fact.text,
|
|
12110
|
+
frameWidthPt,
|
|
12111
|
+
fact.fontSizePt,
|
|
12112
|
+
fact.characterSpacingPt
|
|
12113
|
+
);
|
|
12114
|
+
const heightTwips = lines * fact.lineHeightPt * TWIPS_PER_POINT4;
|
|
12115
|
+
const bottomTwips = fact.offsetYTwips + heightTwips;
|
|
12116
|
+
const overrunTwips = bottomTwips - fact.pageBottomTwips;
|
|
12117
|
+
if (overrunTwips <= fact.lineHeightPt * TWIPS_PER_POINT4) continue;
|
|
12118
|
+
findings.push({
|
|
12119
|
+
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.`,
|
|
12120
|
+
path: `${fact.path}/props/floating/verticalPosition/offset`,
|
|
12121
|
+
suggestion: "Raise the frame, shorten the text, or reduce fontSize so the block finishes on the page.",
|
|
12122
|
+
context: {
|
|
12123
|
+
offsetYTwips: fact.offsetYTwips,
|
|
12124
|
+
estimatedHeightTwips: Math.round(heightTwips),
|
|
12125
|
+
pageBottomTwips: fact.pageBottomTwips,
|
|
12126
|
+
lines
|
|
12127
|
+
},
|
|
12128
|
+
evidence: {
|
|
12129
|
+
actual: Math.round(bottomTwips),
|
|
12130
|
+
expected: fact.pageBottomTwips,
|
|
12131
|
+
unit: "twips"
|
|
12132
|
+
}
|
|
12133
|
+
});
|
|
12134
|
+
}
|
|
12135
|
+
return findings;
|
|
12136
|
+
}
|
|
12137
|
+
};
|
|
12138
|
+
var docxSvgTextBoundsRule = {
|
|
12139
|
+
id: "docx/svg-text-bounds",
|
|
12140
|
+
code: QUALITY_CODES.SVG_TEXT_CLIPPED,
|
|
12141
|
+
category: "integrity",
|
|
12142
|
+
defaultSeverity: "warning",
|
|
12143
|
+
defaultCertainty: "deterministic",
|
|
12144
|
+
formats: ["docx"],
|
|
12145
|
+
evaluate: ({ facts }) => facts.filter((fact) => fact.kind === "docx/svg-text").flatMap((fact) => {
|
|
12146
|
+
const canvasBottom = fact.viewBoxMinY + fact.viewBoxHeight;
|
|
12147
|
+
if (fact.baselineY <= canvasBottom) return [];
|
|
12148
|
+
const overflow = Math.round((fact.baselineY - canvasBottom) * 10) / 10;
|
|
12149
|
+
return [
|
|
12150
|
+
{
|
|
12151
|
+
message: `SVG text "${fact.content}" sits ${overflow} units below the viewBox \u2014 it is clipped away and never reaches the document's text layer.`,
|
|
12152
|
+
path: fact.path,
|
|
12153
|
+
suggestion: `Move the baseline above ${Math.round(canvasBottom)}, or grow the viewBox height.`,
|
|
12154
|
+
context: {
|
|
12155
|
+
content: fact.content,
|
|
12156
|
+
baselineY: fact.baselineY,
|
|
12157
|
+
canvasBottom,
|
|
12158
|
+
overflowUnits: overflow
|
|
12159
|
+
},
|
|
12160
|
+
evidence: {
|
|
12161
|
+
actual: fact.baselineY,
|
|
12162
|
+
expected: canvasBottom,
|
|
12163
|
+
unit: "viewBox units"
|
|
12164
|
+
}
|
|
12165
|
+
}
|
|
12166
|
+
];
|
|
12167
|
+
})
|
|
12168
|
+
};
|
|
12169
|
+
var DOCX_QUALITY_RULES = {
|
|
12170
|
+
id: "docx/default",
|
|
12171
|
+
rules: [
|
|
12172
|
+
docxTableWidthRule,
|
|
12173
|
+
docxHeadingHierarchyRule,
|
|
12174
|
+
docxTextFitRule,
|
|
12175
|
+
docxSvgTextBoundsRule
|
|
12176
|
+
]
|
|
12177
|
+
};
|
|
12178
|
+
var DOCX_QUALITY_PROFILES = {
|
|
12179
|
+
"executive-report": {
|
|
12180
|
+
id: "executive-report",
|
|
12181
|
+
formats: ["docx"],
|
|
12182
|
+
description: "Short decision document with strict outline continuity.",
|
|
12183
|
+
rules: {
|
|
12184
|
+
"docx/heading-hierarchy": { severity: "warning" }
|
|
12185
|
+
}
|
|
12186
|
+
},
|
|
12187
|
+
"technical-report": {
|
|
12188
|
+
id: "technical-report",
|
|
12189
|
+
formats: ["docx"],
|
|
12190
|
+
description: "Portable professional report defaults."
|
|
12191
|
+
},
|
|
12192
|
+
"legal-appendix": {
|
|
12193
|
+
id: "legal-appendix",
|
|
12194
|
+
formats: ["docx"],
|
|
12195
|
+
description: "Dense appendix: preserve integrity without editorial taste."
|
|
12196
|
+
}
|
|
12197
|
+
};
|
|
12198
|
+
var DOCX_DEFAULT_QUALITY_PROFILE = DOCX_QUALITY_PROFILES["technical-report"];
|
|
12199
|
+
var DOCX_PROFILES_BY_ID = DOCX_QUALITY_PROFILES;
|
|
12200
|
+
function resolveDocxQualityProfile(requested) {
|
|
12201
|
+
if (!requested) return void 0;
|
|
12202
|
+
const registered = DOCX_PROFILES_BY_ID[requested.id];
|
|
12203
|
+
if (!registered) return requested;
|
|
12204
|
+
return mergeQualityProfiles(registered, requested);
|
|
12205
|
+
}
|
|
12206
|
+
var docxQualityEngine = new QualityEngine(DOCX_QUALITY_RULES.rules);
|
|
12207
|
+
|
|
12208
|
+
// src/quality/preflight.ts
|
|
12209
|
+
var PREPARE_RULE_ID = "quality/prepare";
|
|
12210
|
+
function emptyAnalysis(ruleErrors = [], blocked = false) {
|
|
12211
|
+
return {
|
|
12212
|
+
diagnostics: [],
|
|
12213
|
+
counts: { error: 0, warning: 0, info: 0 },
|
|
12214
|
+
blocked,
|
|
12215
|
+
truncated: false,
|
|
12216
|
+
suppressedCount: 0,
|
|
12217
|
+
evaluatedRuleIds: [],
|
|
12218
|
+
ruleErrors
|
|
12219
|
+
};
|
|
12220
|
+
}
|
|
12221
|
+
function analyzeDocxQuality(doc, options = {}) {
|
|
12222
|
+
assertValidQualityPolicy(options.policy);
|
|
12223
|
+
assertValidQualityProfile(options.profile);
|
|
12224
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc) || doc.name !== "docx") {
|
|
12225
|
+
return emptyAnalysis();
|
|
12226
|
+
}
|
|
12227
|
+
let prepared = options.prepared;
|
|
12228
|
+
try {
|
|
12229
|
+
prepared ??= prepareDocxQualityDocument(
|
|
12230
|
+
doc,
|
|
12231
|
+
options
|
|
12232
|
+
);
|
|
12233
|
+
} catch (error) {
|
|
12234
|
+
if (options.policy?.onRuleError === "throw") throw error;
|
|
12235
|
+
const gate = options.policy?.gate;
|
|
12236
|
+
return emptyAnalysis(
|
|
12237
|
+
[
|
|
12238
|
+
{
|
|
12239
|
+
ruleId: PREPARE_RULE_ID,
|
|
12240
|
+
message: error instanceof Error ? error.message : String(error)
|
|
12241
|
+
}
|
|
12242
|
+
],
|
|
12243
|
+
gate !== void 0 && gate !== "none"
|
|
12244
|
+
);
|
|
12245
|
+
}
|
|
12246
|
+
return docxQualityEngine.analyzeSync(prepared, {
|
|
12247
|
+
profile: resolveDocxQualityProfile(options.profile) ?? DOCX_DEFAULT_QUALITY_PROFILE,
|
|
12248
|
+
policy: options.policy
|
|
12249
|
+
});
|
|
12250
|
+
}
|
|
12251
|
+
|
|
11603
12252
|
// src/utils/warningsDocument.ts
|
|
11604
12253
|
function generateWarningsDocument(warnings) {
|
|
11605
12254
|
if (!warnings || warnings.length === 0) {
|
|
@@ -12526,17 +13175,22 @@ export {
|
|
|
12526
13175
|
ComponentValidationError2 as ComponentValidationError,
|
|
12527
13176
|
DocumentGenerator as CoreDocumentGenerator,
|
|
12528
13177
|
DEFAULT_DOCX_RENDERER_ID,
|
|
13178
|
+
DOCX_DEFAULT_QUALITY_PROFILE,
|
|
13179
|
+
DOCX_QUALITY_PROFILES,
|
|
13180
|
+
DOCX_QUALITY_RULES,
|
|
12529
13181
|
ThemeFileError,
|
|
12530
13182
|
ThemeParseError,
|
|
12531
13183
|
ThemeValidationError,
|
|
12532
13184
|
UncompiledComponentError,
|
|
12533
13185
|
UnknownPreservedComponentError,
|
|
13186
|
+
analyzeDocxQuality,
|
|
12534
13187
|
cleanComponentProps,
|
|
12535
13188
|
corporateTheme,
|
|
12536
13189
|
createComponent,
|
|
12537
13190
|
createDocumentGenerator,
|
|
12538
13191
|
createMinimalTheme,
|
|
12539
13192
|
createVersion,
|
|
13193
|
+
docxQualityEngine,
|
|
12540
13194
|
docxRendererIds,
|
|
12541
13195
|
docxRendererStatuses,
|
|
12542
13196
|
examples,
|
|
@@ -12579,7 +13233,9 @@ export {
|
|
|
12579
13233
|
mergeSchemas,
|
|
12580
13234
|
minimalTheme,
|
|
12581
13235
|
modernTheme,
|
|
13236
|
+
prepareDocxQualityDocument,
|
|
12582
13237
|
resetVisualPrepassStats,
|
|
13238
|
+
resolveDocxQualityProfile,
|
|
12583
13239
|
runExample,
|
|
12584
13240
|
themes,
|
|
12585
13241
|
validateComponentProps,
|