@json-to-office/core-docx 6.0.0 → 6.2.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/components/highcharts.d.ts.map +1 -1
- package/dist/components/visual.d.ts.map +1 -1
- package/dist/core/desugarExternals.d.ts.map +1 -1
- package/dist/core/prerasterizeVisuals.d.ts.map +1 -1
- package/dist/index.js +548 -273
- package/dist/index.js.map +1 -1
- package/dist/plugin/example/index.js +245 -37
- package/dist/plugin/example/index.js.map +1 -1
- package/dist/quality/facts.d.ts +55 -1
- package/dist/quality/facts.d.ts.map +1 -1
- package/dist/quality/rules.d.ts +73 -12
- package/dist/quality/rules.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5605,7 +5605,7 @@ async function rasterizeVisualSlide(presentation, dpi, propsServerUrl, serviceCo
|
|
|
5605
5605
|
serviceConfig?.serverUrl,
|
|
5606
5606
|
DEFAULT_RASTERIZE_SERVER_URL
|
|
5607
5607
|
);
|
|
5608
|
-
const
|
|
5608
|
+
const body = await postJsonToService({
|
|
5609
5609
|
url: serverUrl,
|
|
5610
5610
|
path: "/rasterize",
|
|
5611
5611
|
body: {
|
|
@@ -5617,11 +5617,12 @@ async function rasterizeVisualSlide(presentation, dpi, propsServerUrl, serviceCo
|
|
|
5617
5617
|
headers: serviceConfig?.headers,
|
|
5618
5618
|
serviceLabel: "PPTX rasterization service",
|
|
5619
5619
|
onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Configure services.pptx with a \`render\` callback or a running \`serverUrl\`.
|
|
5620
|
-
Cause: ${cause}
|
|
5620
|
+
Cause: ${cause}`,
|
|
5621
|
+
decode: (response) => response.text()
|
|
5621
5622
|
});
|
|
5622
5623
|
let result;
|
|
5623
5624
|
try {
|
|
5624
|
-
result =
|
|
5625
|
+
result = JSON.parse(body);
|
|
5625
5626
|
} catch {
|
|
5626
5627
|
throw new Error(
|
|
5627
5628
|
"PPTX rasterization service returned a non-JSON response (expected { base64DataUri, width, height })."
|
|
@@ -5817,9 +5818,9 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
|
|
|
5817
5818
|
DEFAULT_RASTERIZE_SERVER_URL
|
|
5818
5819
|
);
|
|
5819
5820
|
const postBatch = async (chunk, fonts) => {
|
|
5820
|
-
let
|
|
5821
|
+
let body;
|
|
5821
5822
|
try {
|
|
5822
|
-
|
|
5823
|
+
body = await postJsonToService2({
|
|
5823
5824
|
url: serverUrl,
|
|
5824
5825
|
path: "/rasterize/batch",
|
|
5825
5826
|
body: {
|
|
@@ -5832,14 +5833,21 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
|
|
|
5832
5833
|
},
|
|
5833
5834
|
headers: serviceConfig?.headers,
|
|
5834
5835
|
timeoutMs: BATCH_TIMEOUT_BASE_MS + BATCH_TIMEOUT_PER_SLIDE_MS * chunk.length,
|
|
5836
|
+
// The per-visual fallback below IS this call's retry, and a better
|
|
5837
|
+
// one: it degrades to smaller work instead of repeating the same
|
|
5838
|
+
// large request. Retrying here first would spend three full batch
|
|
5839
|
+
// timeouts — up to seventeen minutes on a 32-slide chunk — before
|
|
5840
|
+
// the fallback that was going to run anyway gets its turn.
|
|
5841
|
+
retries: 0,
|
|
5835
5842
|
serviceLabel: "PPTX batch rasterization service",
|
|
5836
|
-
onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Cause: ${cause}
|
|
5843
|
+
onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Cause: ${cause}`,
|
|
5844
|
+
decode: (response) => response.text()
|
|
5837
5845
|
});
|
|
5838
5846
|
} catch (error) {
|
|
5839
5847
|
return { applied: false, schemaRejected: isSchemaRejection(error) };
|
|
5840
5848
|
}
|
|
5841
5849
|
try {
|
|
5842
|
-
if (applyBatchResponse(chunk,
|
|
5850
|
+
if (applyBatchResponse(chunk, JSON.parse(body), map)) {
|
|
5843
5851
|
return { applied: true };
|
|
5844
5852
|
}
|
|
5845
5853
|
} catch {
|
|
@@ -5946,7 +5954,7 @@ function containsComponent(root, name) {
|
|
|
5946
5954
|
import {
|
|
5947
5955
|
clampVisualDpi as clampVisualDpi2,
|
|
5948
5956
|
DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2,
|
|
5949
|
-
|
|
5957
|
+
recordChartCollected
|
|
5950
5958
|
} from "@json-to-office/shared";
|
|
5951
5959
|
import {
|
|
5952
5960
|
isNativeVisualProps as isNativeVisualProps2
|
|
@@ -5965,9 +5973,8 @@ import {
|
|
|
5965
5973
|
withChartTypography,
|
|
5966
5974
|
resolveServiceUrl as resolveServiceUrl3,
|
|
5967
5975
|
postJsonToService as postJsonToService3,
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
recordChartRetry
|
|
5976
|
+
recordChartRetry,
|
|
5977
|
+
sendChartRequest
|
|
5971
5978
|
} from "@json-to-office/shared";
|
|
5972
5979
|
var DEFAULT_EXPORT_SERVER_URL = "http://localhost:7801";
|
|
5973
5980
|
function effectiveChartServerUrl(props, servicesConfig) {
|
|
@@ -6008,14 +6015,16 @@ async function generateChart(config, servicesConfig, warnings, cache) {
|
|
|
6008
6015
|
// byte-identical to before for callers that omit it.
|
|
6009
6016
|
...config.resources ? { resources: config.resources } : {}
|
|
6010
6017
|
};
|
|
6011
|
-
return
|
|
6018
|
+
return sendChartRequest({
|
|
6012
6019
|
cache,
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6020
|
+
serverUrl,
|
|
6021
|
+
concurrency: servicesConfig?.concurrency,
|
|
6022
|
+
requestBody,
|
|
6023
|
+
send: () => postChart(serverUrl, requestBody, config, servicesConfig)
|
|
6024
|
+
});
|
|
6016
6025
|
}
|
|
6017
6026
|
async function postChart(serverUrl, requestBody, config, servicesConfig) {
|
|
6018
|
-
const
|
|
6027
|
+
const base64Data = await postJsonToService3({
|
|
6019
6028
|
url: serverUrl,
|
|
6020
6029
|
path: "/export",
|
|
6021
6030
|
body: requestBody,
|
|
@@ -6025,16 +6034,13 @@ async function postChart(serverUrl, requestBody, config, servicesConfig) {
|
|
|
6025
6034
|
onRetry: recordChartRetry,
|
|
6026
6035
|
serviceLabel: "Highcharts export server",
|
|
6027
6036
|
onUnreachable: (url, cause) => `Highcharts Export Server is not running at ${url}. Start it with: npx highcharts-export-server --enableServer true
|
|
6028
|
-
Cause: ${cause}
|
|
6037
|
+
Cause: ${cause}`,
|
|
6038
|
+
decode: (response) => response.text()
|
|
6029
6039
|
});
|
|
6030
|
-
const base64Data = await response.text();
|
|
6031
|
-
const base64DataUri = `data:image/png;base64,${base64Data}`;
|
|
6032
|
-
const width = config.options.chart.width;
|
|
6033
|
-
const height = config.options.chart.height;
|
|
6034
6040
|
return {
|
|
6035
|
-
base64DataUri
|
|
6036
|
-
width,
|
|
6037
|
-
height
|
|
6041
|
+
base64DataUri: `data:image/png;base64,${base64Data}`,
|
|
6042
|
+
width: config.options.chart.width,
|
|
6043
|
+
height: config.options.chart.height
|
|
6038
6044
|
};
|
|
6039
6045
|
}
|
|
6040
6046
|
function toChartColor(value, theme) {
|
|
@@ -6162,21 +6168,16 @@ async function desugarExternals(document, options) {
|
|
|
6162
6168
|
});
|
|
6163
6169
|
}
|
|
6164
6170
|
if (node.name === "highcharts") {
|
|
6165
|
-
|
|
6166
|
-
const chartConfig = options.services?.highcharts;
|
|
6171
|
+
recordChartCollected();
|
|
6167
6172
|
return withNodeIdentity(node, {
|
|
6168
6173
|
name: "image",
|
|
6169
|
-
props: await
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
options.chartFonts,
|
|
6177
|
-
options.warnings,
|
|
6178
|
-
chartCache
|
|
6179
|
-
)
|
|
6174
|
+
props: await renderChartToImageProps(
|
|
6175
|
+
node.props,
|
|
6176
|
+
options.theme,
|
|
6177
|
+
options.services?.highcharts,
|
|
6178
|
+
options.chartFonts,
|
|
6179
|
+
options.warnings,
|
|
6180
|
+
chartCache
|
|
6180
6181
|
)
|
|
6181
6182
|
});
|
|
6182
6183
|
}
|
|
@@ -12883,6 +12884,7 @@ async function expandBlocksWithPlugins(document, theme, plugins, render, preserv
|
|
|
12883
12884
|
init_styleHelpers();
|
|
12884
12885
|
init_defaults();
|
|
12885
12886
|
init_widthUtils();
|
|
12887
|
+
import probe2 from "probe-image-size";
|
|
12886
12888
|
|
|
12887
12889
|
// src/quality/text-inventory.ts
|
|
12888
12890
|
function tocDepth(depth) {
|
|
@@ -13133,6 +13135,116 @@ function collectDocxTextInventory(children, basePath = "/children") {
|
|
|
13133
13135
|
return entries;
|
|
13134
13136
|
}
|
|
13135
13137
|
|
|
13138
|
+
// src/quality/text-metrics.ts
|
|
13139
|
+
var DEFAULT_ADVANCE = 556;
|
|
13140
|
+
var ADVANCE = {
|
|
13141
|
+
" ": 278,
|
|
13142
|
+
A: 667,
|
|
13143
|
+
B: 667,
|
|
13144
|
+
C: 722,
|
|
13145
|
+
D: 722,
|
|
13146
|
+
E: 667,
|
|
13147
|
+
F: 611,
|
|
13148
|
+
G: 778,
|
|
13149
|
+
H: 722,
|
|
13150
|
+
I: 278,
|
|
13151
|
+
J: 500,
|
|
13152
|
+
K: 667,
|
|
13153
|
+
L: 556,
|
|
13154
|
+
M: 833,
|
|
13155
|
+
N: 722,
|
|
13156
|
+
O: 778,
|
|
13157
|
+
P: 667,
|
|
13158
|
+
Q: 778,
|
|
13159
|
+
R: 722,
|
|
13160
|
+
S: 667,
|
|
13161
|
+
T: 611,
|
|
13162
|
+
U: 722,
|
|
13163
|
+
V: 667,
|
|
13164
|
+
W: 944,
|
|
13165
|
+
X: 667,
|
|
13166
|
+
Y: 667,
|
|
13167
|
+
Z: 611,
|
|
13168
|
+
a: 556,
|
|
13169
|
+
b: 556,
|
|
13170
|
+
c: 500,
|
|
13171
|
+
d: 556,
|
|
13172
|
+
e: 556,
|
|
13173
|
+
f: 278,
|
|
13174
|
+
g: 556,
|
|
13175
|
+
h: 556,
|
|
13176
|
+
i: 222,
|
|
13177
|
+
j: 222,
|
|
13178
|
+
k: 500,
|
|
13179
|
+
l: 222,
|
|
13180
|
+
m: 833,
|
|
13181
|
+
n: 556,
|
|
13182
|
+
o: 556,
|
|
13183
|
+
p: 556,
|
|
13184
|
+
q: 556,
|
|
13185
|
+
r: 333,
|
|
13186
|
+
s: 500,
|
|
13187
|
+
t: 278,
|
|
13188
|
+
u: 556,
|
|
13189
|
+
v: 500,
|
|
13190
|
+
w: 722,
|
|
13191
|
+
x: 500,
|
|
13192
|
+
y: 500,
|
|
13193
|
+
z: 500,
|
|
13194
|
+
".": 278,
|
|
13195
|
+
",": 278,
|
|
13196
|
+
":": 278,
|
|
13197
|
+
";": 278,
|
|
13198
|
+
"!": 278,
|
|
13199
|
+
"?": 556,
|
|
13200
|
+
"'": 191,
|
|
13201
|
+
'"': 355,
|
|
13202
|
+
"-": 333,
|
|
13203
|
+
"\u2013": 556,
|
|
13204
|
+
"\u2014": 1e3,
|
|
13205
|
+
"&": 667,
|
|
13206
|
+
"%": 889,
|
|
13207
|
+
"(": 333,
|
|
13208
|
+
")": 333,
|
|
13209
|
+
"/": 278,
|
|
13210
|
+
"@": 1015,
|
|
13211
|
+
"#": 556,
|
|
13212
|
+
"+": 584,
|
|
13213
|
+
"=": 584
|
|
13214
|
+
};
|
|
13215
|
+
function estimateTextWidthPt(text, fontSizePt, trackingPt = 0) {
|
|
13216
|
+
let units = 0;
|
|
13217
|
+
for (const character of text) {
|
|
13218
|
+
units += ADVANCE[character] ?? DEFAULT_ADVANCE;
|
|
13219
|
+
}
|
|
13220
|
+
const width = units / 1e3 * fontSizePt + text.length * trackingPt;
|
|
13221
|
+
return Math.max(0, width);
|
|
13222
|
+
}
|
|
13223
|
+
function estimateWrappedLines(text, widthPt, fontSizePt, trackingPt = 0) {
|
|
13224
|
+
if (widthPt <= 0) return 1;
|
|
13225
|
+
let lines = 0;
|
|
13226
|
+
for (const paragraph2 of text.split("\n")) {
|
|
13227
|
+
const words = paragraph2.split(/\s+/).filter(Boolean);
|
|
13228
|
+
if (words.length === 0) {
|
|
13229
|
+
lines += 1;
|
|
13230
|
+
continue;
|
|
13231
|
+
}
|
|
13232
|
+
let current = "";
|
|
13233
|
+
let used = 0;
|
|
13234
|
+
for (const word of words) {
|
|
13235
|
+
const candidate = current === "" ? word : `${current} ${word}`;
|
|
13236
|
+
if (estimateTextWidthPt(candidate, fontSizePt, trackingPt) <= widthPt) {
|
|
13237
|
+
current = candidate;
|
|
13238
|
+
} else {
|
|
13239
|
+
if (current !== "") used += 1;
|
|
13240
|
+
current = word;
|
|
13241
|
+
}
|
|
13242
|
+
}
|
|
13243
|
+
lines += used + (current === "" ? 0 : 1);
|
|
13244
|
+
}
|
|
13245
|
+
return Math.max(1, lines);
|
|
13246
|
+
}
|
|
13247
|
+
|
|
13136
13248
|
// src/quality/facts.ts
|
|
13137
13249
|
function asRecord2(value) {
|
|
13138
13250
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
@@ -13944,14 +14056,29 @@ function prepareDocxQualityDocument(document, options = {}) {
|
|
|
13944
14056
|
}
|
|
13945
14057
|
if (node.name === "image" || node.name === "visual") {
|
|
13946
14058
|
for (const fact of svgTextFacts(props, path4)) addFact(fact);
|
|
14059
|
+
const drawn = drawnRatio(props);
|
|
14060
|
+
const natural = readableRatio(props);
|
|
14061
|
+
addFact({
|
|
14062
|
+
id: `docx:image:${path4}`,
|
|
14063
|
+
kind: "docx/image",
|
|
14064
|
+
path: path4,
|
|
14065
|
+
...typeof props.alt === "string" && props.alt.trim() !== "" && { alt: props.alt },
|
|
14066
|
+
captioned: isCaptioned(facts, authoredPath(path4)),
|
|
14067
|
+
...drawn !== void 0 && { drawnRatio: drawn },
|
|
14068
|
+
...natural !== void 0 && { naturalRatio: natural }
|
|
14069
|
+
});
|
|
13947
14070
|
}
|
|
13948
14071
|
if (node.name === "heading") {
|
|
13949
14072
|
const level = typeof props.level === "number" && Number.isFinite(props.level) ? props.level : 1;
|
|
14073
|
+
const styleKeepNext = asRecord2(
|
|
14074
|
+
typography.styles[`heading${level}`]
|
|
14075
|
+
)?.keepNext;
|
|
13950
14076
|
addFact({
|
|
13951
14077
|
id: `docx:heading:${path4}`,
|
|
13952
14078
|
kind: "docx/heading",
|
|
13953
14079
|
path: `${path4}/props/level`,
|
|
13954
14080
|
level,
|
|
14081
|
+
keepNext: props.keepNext === true || props.keepNext === void 0 && styleKeepNext === true,
|
|
13955
14082
|
...previousHeadingLevel !== void 0 && {
|
|
13956
14083
|
previousLevel: previousHeadingLevel
|
|
13957
14084
|
}
|
|
@@ -13966,6 +14093,65 @@ function prepareDocxQualityDocument(document, options = {}) {
|
|
|
13966
14093
|
const page = rec.name === "section" ? pageBox(resolved.theme, context.themeName, rec.props?.page) : basePage;
|
|
13967
14094
|
walkActive(component, `/children/${index}`, page, visit);
|
|
13968
14095
|
});
|
|
14096
|
+
const wordsOf = (text) => text.split(/\s+/).filter(Boolean).length;
|
|
14097
|
+
const bodyRoles = /* @__PURE__ */ new Set([
|
|
14098
|
+
"body",
|
|
14099
|
+
"list-item",
|
|
14100
|
+
"table-header",
|
|
14101
|
+
"table-cell",
|
|
14102
|
+
"statistic",
|
|
14103
|
+
"caption"
|
|
14104
|
+
]);
|
|
14105
|
+
const exhibitKinds = /* @__PURE__ */ new Set(["docx/table", "docx/chart", "docx/image"]);
|
|
14106
|
+
resolved.children.forEach((component, index) => {
|
|
14107
|
+
const rec = component;
|
|
14108
|
+
if (rec.name !== "section") return;
|
|
14109
|
+
const prefix = `/children/${index}/`;
|
|
14110
|
+
const own = facts.filter((fact) => fact.path.startsWith(prefix));
|
|
14111
|
+
const texts = own.filter(
|
|
14112
|
+
(fact) => fact.kind === "docx/text"
|
|
14113
|
+
);
|
|
14114
|
+
addFact({
|
|
14115
|
+
id: `docx:section:${index}`,
|
|
14116
|
+
kind: "docx/section",
|
|
14117
|
+
path: `/children/${index}`,
|
|
14118
|
+
index,
|
|
14119
|
+
words: texts.filter((fact) => bodyRoles.has(fact.role)).reduce((total, fact) => total + wordsOf(fact.text), 0),
|
|
14120
|
+
exhibits: own.filter((fact) => exhibitKinds.has(fact.kind)).length,
|
|
14121
|
+
headings: texts.filter((fact) => fact.role === "heading").length
|
|
14122
|
+
});
|
|
14123
|
+
if (hasColumnsComponent(rec)) return;
|
|
14124
|
+
const page = pageBox(
|
|
14125
|
+
resolved.theme,
|
|
14126
|
+
context.themeName,
|
|
14127
|
+
rec.props?.page
|
|
14128
|
+
);
|
|
14129
|
+
const fontSizePt = effectiveFontSize({ name: "paragraph" }, {}, typography)?.fontSizePt ?? 11;
|
|
14130
|
+
const widthPt = page.availableWidthTwips / 20;
|
|
14131
|
+
const averageAdvancePt = estimateTextWidthPt(MEASURE_SAMPLE, fontSizePt) / MEASURE_SAMPLE.length;
|
|
14132
|
+
if (averageAdvancePt > 0)
|
|
14133
|
+
addFact({
|
|
14134
|
+
id: `docx:measure:${index}`,
|
|
14135
|
+
kind: "docx/measure",
|
|
14136
|
+
path: `/children/${index}`,
|
|
14137
|
+
index,
|
|
14138
|
+
charactersPerLine: Math.round(widthPt / averageAdvancePt),
|
|
14139
|
+
widthTwips: page.availableWidthTwips,
|
|
14140
|
+
fontSizePt
|
|
14141
|
+
});
|
|
14142
|
+
});
|
|
14143
|
+
const headingCount = facts.filter(
|
|
14144
|
+
(fact) => fact.kind === "docx/text" && fact.role === "heading"
|
|
14145
|
+
).length;
|
|
14146
|
+
addFact({
|
|
14147
|
+
id: "docx:outline",
|
|
14148
|
+
kind: "docx/outline",
|
|
14149
|
+
path: "/props",
|
|
14150
|
+
headings: headingCount,
|
|
14151
|
+
contents: facts.some(
|
|
14152
|
+
(fact) => fact.kind === "docx/text" && fact.role === "toc-entry"
|
|
14153
|
+
)
|
|
14154
|
+
});
|
|
13969
14155
|
return {
|
|
13970
14156
|
format: "docx",
|
|
13971
14157
|
model: {
|
|
@@ -13996,6 +14182,52 @@ function prepareDocxQualityDocument(document, options = {}) {
|
|
|
13996
14182
|
}
|
|
13997
14183
|
};
|
|
13998
14184
|
}
|
|
14185
|
+
function hasColumnsComponent(node) {
|
|
14186
|
+
if (Array.isArray(node)) return node.some(hasColumnsComponent);
|
|
14187
|
+
const rec = asRecord2(node);
|
|
14188
|
+
if (!rec) return false;
|
|
14189
|
+
if (rec.name === "columns") return true;
|
|
14190
|
+
return Object.values(rec).some(hasColumnsComponent);
|
|
14191
|
+
}
|
|
14192
|
+
var CAPTION_LABEL = /^\s*(?:\*\*)?\s*(figure|exhibit|table|chart|image)\b/i;
|
|
14193
|
+
function isCaptioned(facts, image) {
|
|
14194
|
+
return facts.some((fact) => {
|
|
14195
|
+
if (fact.kind !== "docx/text") return false;
|
|
14196
|
+
const text = fact;
|
|
14197
|
+
const near = siblingOf(text.path) === siblingOf(image) || text.path.startsWith(`${image}/`) || image.startsWith(`${text.path}/`);
|
|
14198
|
+
return near && (text.role === "caption" || CAPTION_LABEL.test(text.text));
|
|
14199
|
+
});
|
|
14200
|
+
}
|
|
14201
|
+
function siblingOf(pointer) {
|
|
14202
|
+
return pointer.slice(0, pointer.lastIndexOf("/"));
|
|
14203
|
+
}
|
|
14204
|
+
var MEASURE_SAMPLE = "the quick brown fox jumps over the lazy dog and then settles under it ";
|
|
14205
|
+
function drawnRatio(props) {
|
|
14206
|
+
const width = finiteNumber(props.width);
|
|
14207
|
+
const height = finiteNumber(props.height);
|
|
14208
|
+
return width !== void 0 && height !== void 0 && height > 0 ? width / height : void 0;
|
|
14209
|
+
}
|
|
14210
|
+
function readableRatio(props) {
|
|
14211
|
+
if (typeof props.svg === "string") {
|
|
14212
|
+
const box = /viewBox\s*=\s*["']\s*[-\d.]+[ ,]+[-\d.]+[ ,]+([\d.]+)[ ,]+([\d.]+)/.exec(
|
|
14213
|
+
props.svg
|
|
14214
|
+
);
|
|
14215
|
+
const width = box ? Number(box[1]) : NaN;
|
|
14216
|
+
const height = box ? Number(box[2]) : NaN;
|
|
14217
|
+
return Number.isFinite(width) && Number.isFinite(height) && height > 0 ? width / height : void 0;
|
|
14218
|
+
}
|
|
14219
|
+
if (typeof props.base64 !== "string") return void 0;
|
|
14220
|
+
const comma = props.base64.indexOf(",");
|
|
14221
|
+
if (comma < 0) return void 0;
|
|
14222
|
+
try {
|
|
14223
|
+
const size = probe2.sync(
|
|
14224
|
+
Buffer.from(props.base64.slice(comma + 1), "base64")
|
|
14225
|
+
);
|
|
14226
|
+
return size && size.height > 0 ? size.width / size.height : void 0;
|
|
14227
|
+
} catch {
|
|
14228
|
+
return void 0;
|
|
14229
|
+
}
|
|
14230
|
+
}
|
|
13999
14231
|
function slotIsFilled(value) {
|
|
14000
14232
|
if (typeof value === "string") return value.trim() !== "";
|
|
14001
14233
|
return value !== void 0 && value !== null && value !== false && (!Array.isArray(value) || value.length > 0);
|
|
@@ -14397,122 +14629,16 @@ import {
|
|
|
14397
14629
|
nearestPaletteToken,
|
|
14398
14630
|
offPaletteFinding,
|
|
14399
14631
|
placeholderFinding,
|
|
14632
|
+
DEFAULT_IMAGE_ASPECT_TOLERANCE,
|
|
14633
|
+
driftingSizes,
|
|
14634
|
+
imageAspectFinding,
|
|
14635
|
+
offScaleFindings,
|
|
14400
14636
|
QUALITY_CODES,
|
|
14637
|
+
roleDriftFindings,
|
|
14638
|
+
sizeCountFinding,
|
|
14401
14639
|
QualityEngine,
|
|
14402
14640
|
tableInfoDesignFindings
|
|
14403
14641
|
} from "@json-to-office/quality";
|
|
14404
|
-
|
|
14405
|
-
// src/quality/text-metrics.ts
|
|
14406
|
-
var DEFAULT_ADVANCE = 556;
|
|
14407
|
-
var ADVANCE = {
|
|
14408
|
-
" ": 278,
|
|
14409
|
-
A: 667,
|
|
14410
|
-
B: 667,
|
|
14411
|
-
C: 722,
|
|
14412
|
-
D: 722,
|
|
14413
|
-
E: 667,
|
|
14414
|
-
F: 611,
|
|
14415
|
-
G: 778,
|
|
14416
|
-
H: 722,
|
|
14417
|
-
I: 278,
|
|
14418
|
-
J: 500,
|
|
14419
|
-
K: 667,
|
|
14420
|
-
L: 556,
|
|
14421
|
-
M: 833,
|
|
14422
|
-
N: 722,
|
|
14423
|
-
O: 778,
|
|
14424
|
-
P: 667,
|
|
14425
|
-
Q: 778,
|
|
14426
|
-
R: 722,
|
|
14427
|
-
S: 667,
|
|
14428
|
-
T: 611,
|
|
14429
|
-
U: 722,
|
|
14430
|
-
V: 667,
|
|
14431
|
-
W: 944,
|
|
14432
|
-
X: 667,
|
|
14433
|
-
Y: 667,
|
|
14434
|
-
Z: 611,
|
|
14435
|
-
a: 556,
|
|
14436
|
-
b: 556,
|
|
14437
|
-
c: 500,
|
|
14438
|
-
d: 556,
|
|
14439
|
-
e: 556,
|
|
14440
|
-
f: 278,
|
|
14441
|
-
g: 556,
|
|
14442
|
-
h: 556,
|
|
14443
|
-
i: 222,
|
|
14444
|
-
j: 222,
|
|
14445
|
-
k: 500,
|
|
14446
|
-
l: 222,
|
|
14447
|
-
m: 833,
|
|
14448
|
-
n: 556,
|
|
14449
|
-
o: 556,
|
|
14450
|
-
p: 556,
|
|
14451
|
-
q: 556,
|
|
14452
|
-
r: 333,
|
|
14453
|
-
s: 500,
|
|
14454
|
-
t: 278,
|
|
14455
|
-
u: 556,
|
|
14456
|
-
v: 500,
|
|
14457
|
-
w: 722,
|
|
14458
|
-
x: 500,
|
|
14459
|
-
y: 500,
|
|
14460
|
-
z: 500,
|
|
14461
|
-
".": 278,
|
|
14462
|
-
",": 278,
|
|
14463
|
-
":": 278,
|
|
14464
|
-
";": 278,
|
|
14465
|
-
"!": 278,
|
|
14466
|
-
"?": 556,
|
|
14467
|
-
"'": 191,
|
|
14468
|
-
'"': 355,
|
|
14469
|
-
"-": 333,
|
|
14470
|
-
"\u2013": 556,
|
|
14471
|
-
"\u2014": 1e3,
|
|
14472
|
-
"&": 667,
|
|
14473
|
-
"%": 889,
|
|
14474
|
-
"(": 333,
|
|
14475
|
-
")": 333,
|
|
14476
|
-
"/": 278,
|
|
14477
|
-
"@": 1015,
|
|
14478
|
-
"#": 556,
|
|
14479
|
-
"+": 584,
|
|
14480
|
-
"=": 584
|
|
14481
|
-
};
|
|
14482
|
-
function estimateTextWidthPt(text, fontSizePt, trackingPt = 0) {
|
|
14483
|
-
let units = 0;
|
|
14484
|
-
for (const character of text) {
|
|
14485
|
-
units += ADVANCE[character] ?? DEFAULT_ADVANCE;
|
|
14486
|
-
}
|
|
14487
|
-
const width = units / 1e3 * fontSizePt + text.length * trackingPt;
|
|
14488
|
-
return Math.max(0, width);
|
|
14489
|
-
}
|
|
14490
|
-
function estimateWrappedLines(text, widthPt, fontSizePt, trackingPt = 0) {
|
|
14491
|
-
if (widthPt <= 0) return 1;
|
|
14492
|
-
let lines = 0;
|
|
14493
|
-
for (const paragraph2 of text.split("\n")) {
|
|
14494
|
-
const words = paragraph2.split(/\s+/).filter(Boolean);
|
|
14495
|
-
if (words.length === 0) {
|
|
14496
|
-
lines += 1;
|
|
14497
|
-
continue;
|
|
14498
|
-
}
|
|
14499
|
-
let current = "";
|
|
14500
|
-
let used = 0;
|
|
14501
|
-
for (const word of words) {
|
|
14502
|
-
const candidate = current === "" ? word : `${current} ${word}`;
|
|
14503
|
-
if (estimateTextWidthPt(candidate, fontSizePt, trackingPt) <= widthPt) {
|
|
14504
|
-
current = candidate;
|
|
14505
|
-
} else {
|
|
14506
|
-
if (current !== "") used += 1;
|
|
14507
|
-
current = word;
|
|
14508
|
-
}
|
|
14509
|
-
}
|
|
14510
|
-
lines += used + (current === "" ? 0 : 1);
|
|
14511
|
-
}
|
|
14512
|
-
return Math.max(1, lines);
|
|
14513
|
-
}
|
|
14514
|
-
|
|
14515
|
-
// src/quality/rules.ts
|
|
14516
14642
|
var WIDTH_TOLERANCE_TWIPS = 10;
|
|
14517
14643
|
function numberParameter(parameters, name, fallback) {
|
|
14518
14644
|
const value = parameters[name];
|
|
@@ -15169,44 +15295,23 @@ var docxRunningHeadRule = {
|
|
|
15169
15295
|
});
|
|
15170
15296
|
}
|
|
15171
15297
|
};
|
|
15172
|
-
var SIZE_TOLERANCE_PT = 0.25;
|
|
15173
15298
|
function themeFact(facts) {
|
|
15174
15299
|
return facts.find(
|
|
15175
15300
|
(fact) => fact.kind === "docx/theme"
|
|
15176
15301
|
);
|
|
15177
15302
|
}
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
|
|
15184
|
-
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
15188
|
-
|
|
15189
|
-
|
|
15190
|
-
const sizes = [...new Set(members.map((fact) => fact.fontSizePt))].sort(
|
|
15191
|
-
(a, b) => a - b
|
|
15192
|
-
);
|
|
15193
|
-
if (sizes.length < 2) continue;
|
|
15194
|
-
const expected = theme?.roleSizesPt[role];
|
|
15195
|
-
if (expected === void 0) continue;
|
|
15196
|
-
for (const fact of members) {
|
|
15197
|
-
if (fact.sizePath !== void 0 && !fact.generated && Math.abs(fact.fontSizePt - expected) > SIZE_TOLERANCE_PT)
|
|
15198
|
-
drifting.set(fact, { expected, sizes });
|
|
15199
|
-
}
|
|
15200
|
-
}
|
|
15201
|
-
return drifting;
|
|
15202
|
-
}
|
|
15203
|
-
function nearestSize(size, scale) {
|
|
15204
|
-
let best;
|
|
15205
|
-
for (const candidate of scale) {
|
|
15206
|
-
if (best === void 0 || Math.abs(candidate - size) < Math.abs(best - size))
|
|
15207
|
-
best = candidate;
|
|
15208
|
-
}
|
|
15209
|
-
return best;
|
|
15303
|
+
var DOCX_TYPE_VOCABULARY = {
|
|
15304
|
+
subject: "document",
|
|
15305
|
+
keepTo: "Keep to the theme styles \u2014 title, headings, body, label, source \u2014 and drop the ad-hoc sizes."
|
|
15306
|
+
};
|
|
15307
|
+
function paintedSizes(facts) {
|
|
15308
|
+
return facts.filter((fact) => fact.kind === "docx/text-size").map((fact) => ({
|
|
15309
|
+
path: fact.path,
|
|
15310
|
+
role: fact.role,
|
|
15311
|
+
fontSizePt: fact.fontSizePt,
|
|
15312
|
+
...fact.sizePath !== void 0 && { sizePath: fact.sizePath },
|
|
15313
|
+
generated: fact.generated
|
|
15314
|
+
}));
|
|
15210
15315
|
}
|
|
15211
15316
|
var docxTypeScaleRule = {
|
|
15212
15317
|
id: "docx/type-scale",
|
|
@@ -15219,43 +15324,15 @@ var docxTypeScaleRule = {
|
|
|
15219
15324
|
defaultEnabled: false,
|
|
15220
15325
|
evaluate: ({ facts }) => {
|
|
15221
15326
|
const theme = themeFact(facts);
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15228
|
-
|
|
15327
|
+
if (!theme) return [];
|
|
15328
|
+
const sizes = paintedSizes(facts);
|
|
15329
|
+
return offScaleFindings(
|
|
15330
|
+
sizes,
|
|
15331
|
+
theme.typeScalePt,
|
|
15332
|
+
theme.themeName,
|
|
15333
|
+
DOCX_TYPE_VOCABULARY,
|
|
15334
|
+
driftingSizes(sizes, theme.roleSizesPt)
|
|
15229
15335
|
);
|
|
15230
|
-
const groups = /* @__PURE__ */ new Map();
|
|
15231
|
-
for (const fact of offScale) {
|
|
15232
|
-
const key = `${fact.role}@${fact.fontSizePt}`;
|
|
15233
|
-
groups.set(key, [...groups.get(key) ?? [], fact]);
|
|
15234
|
-
}
|
|
15235
|
-
return [...groups.values()].map((members) => {
|
|
15236
|
-
const [first] = members;
|
|
15237
|
-
const nearest = nearestSize(first.fontSizePt, scale);
|
|
15238
|
-
const sizePaths = members.map((fact) => fact.sizePath);
|
|
15239
|
-
const count = members.length === 1 ? "" : ` (${members.length} places, patched together)`;
|
|
15240
|
-
return {
|
|
15241
|
-
path: sizePaths[0],
|
|
15242
|
-
...sizePaths.length > 1 && { relatedPaths: sizePaths.slice(1) },
|
|
15243
|
-
message: `${first.fontSizePt}pt is not a size the ${theme.themeName} theme paints; the nearest on its scale is ${nearest}pt${count}.`,
|
|
15244
|
-
suggestion: `Use ${nearest}pt, or drop the size and let the "${first.role}" style set it.`,
|
|
15245
|
-
context: { role: first.role, scale, paths: sizePaths },
|
|
15246
|
-
evidence: {
|
|
15247
|
-
actual: first.fontSizePt,
|
|
15248
|
-
expected: nearest,
|
|
15249
|
-
unit: "pt",
|
|
15250
|
-
values: { source: "theme" }
|
|
15251
|
-
},
|
|
15252
|
-
fixes: sizePaths.map((path4) => ({
|
|
15253
|
-
op: "replace",
|
|
15254
|
-
path: path4,
|
|
15255
|
-
value: nearest
|
|
15256
|
-
}))
|
|
15257
|
-
};
|
|
15258
|
-
});
|
|
15259
15336
|
}
|
|
15260
15337
|
};
|
|
15261
15338
|
var docxSizeCountRule = {
|
|
@@ -15268,34 +15345,13 @@ var docxSizeCountRule = {
|
|
|
15268
15345
|
formats: ["docx"],
|
|
15269
15346
|
defaultEnabled: false,
|
|
15270
15347
|
defaultParameters: { maximumSizes: 8 },
|
|
15271
|
-
evaluate: ({ facts, configuration, profile }) =>
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
for (const fact of textSizeFacts(facts)) {
|
|
15279
|
-
const size = Math.round(fact.fontSizePt * 4) / 4;
|
|
15280
|
-
if (!firstPathBySize.has(size)) firstPathBySize.set(size, fact.path);
|
|
15281
|
-
}
|
|
15282
|
-
if (firstPathBySize.size <= maximum) return [];
|
|
15283
|
-
const sizes = [...firstPathBySize.keys()].sort((a, b) => a - b);
|
|
15284
|
-
return [
|
|
15285
|
-
{
|
|
15286
|
-
path: themeFact(facts)?.path ?? "/props",
|
|
15287
|
-
relatedPaths: sizes.map((size) => firstPathBySize.get(size)),
|
|
15288
|
-
message: `The document paints ${sizes.length} distinct text sizes (${sizes.join(", ")}pt); the ${profile?.id ?? "selected"} profile allows ${maximum}.`,
|
|
15289
|
-
suggestion: "Keep to the theme styles \u2014 title, headings, body, label, source \u2014 and drop the ad-hoc sizes.",
|
|
15290
|
-
context: { sizes, maximum },
|
|
15291
|
-
evidence: {
|
|
15292
|
-
actual: sizes.length,
|
|
15293
|
-
expected: maximum,
|
|
15294
|
-
values: { source: "profile" }
|
|
15295
|
-
}
|
|
15296
|
-
}
|
|
15297
|
-
];
|
|
15298
|
-
}
|
|
15348
|
+
evaluate: ({ facts, configuration, profile }) => sizeCountFinding(
|
|
15349
|
+
paintedSizes(facts),
|
|
15350
|
+
numberParameter(configuration.parameters, "maximumSizes", 8),
|
|
15351
|
+
themeFact(facts)?.path ?? "/props",
|
|
15352
|
+
profile?.id,
|
|
15353
|
+
DOCX_TYPE_VOCABULARY
|
|
15354
|
+
)
|
|
15299
15355
|
};
|
|
15300
15356
|
var docxRoleDriftRule = {
|
|
15301
15357
|
id: "docx/role-drift",
|
|
@@ -15308,29 +15364,220 @@ var docxRoleDriftRule = {
|
|
|
15308
15364
|
defaultEnabled: false,
|
|
15309
15365
|
evaluate: ({ facts }) => {
|
|
15310
15366
|
const theme = themeFact(facts);
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
|
|
15318
|
-
|
|
15319
|
-
|
|
15320
|
-
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
|
|
15367
|
+
return theme ? roleDriftFindings(paintedSizes(facts), theme.roleSizesPt) : [];
|
|
15368
|
+
}
|
|
15369
|
+
};
|
|
15370
|
+
var docxBodyMeasureRule = {
|
|
15371
|
+
id: "docx/body-measure",
|
|
15372
|
+
description: "A section whose body copy runs outside the readable measure. Off until a profile or policy enables it.",
|
|
15373
|
+
code: QUALITY_CODES.BODY_MEASURE,
|
|
15374
|
+
category: "legibility",
|
|
15375
|
+
defaultSeverity: "warning",
|
|
15376
|
+
defaultCertainty: "estimated",
|
|
15377
|
+
formats: ["docx"],
|
|
15378
|
+
defaultEnabled: false,
|
|
15379
|
+
defaultParameters: { minimumCharacters: 45, maximumCharacters: 90 },
|
|
15380
|
+
evaluate: ({ facts, configuration }) => {
|
|
15381
|
+
const minimum = numberParameter(
|
|
15382
|
+
configuration.parameters,
|
|
15383
|
+
"minimumCharacters",
|
|
15384
|
+
45
|
|
15385
|
+
);
|
|
15386
|
+
const maximum = numberParameter(
|
|
15387
|
+
configuration.parameters,
|
|
15388
|
+
"maximumCharacters",
|
|
15389
|
+
90
|
|
15390
|
+
);
|
|
15391
|
+
const words = new Map(
|
|
15392
|
+
facts.filter((fact) => fact.kind === "docx/section").map((fact) => [fact.index, fact.words])
|
|
15393
|
+
);
|
|
15394
|
+
return facts.filter((fact) => fact.kind === "docx/measure").filter((fact) => (words.get(fact.index) ?? 0) >= 40).filter(
|
|
15395
|
+
(fact) => fact.charactersPerLine < minimum || fact.charactersPerLine > maximum
|
|
15396
|
+
).map((fact) => {
|
|
15397
|
+
const wide = fact.charactersPerLine > maximum;
|
|
15398
|
+
return {
|
|
15399
|
+
path: fact.path,
|
|
15400
|
+
message: `Body copy runs about ${fact.charactersPerLine} characters a line at ${fact.fontSizePt}pt; a readable measure is ${minimum}\u2013${maximum}.`,
|
|
15401
|
+
suggestion: wide ? "Widen the margins, set the body a size larger, or put the text in columns." : "Narrow the margins or set the body a size smaller.",
|
|
15402
|
+
context: {
|
|
15403
|
+
charactersPerLine: fact.charactersPerLine,
|
|
15404
|
+
minimum,
|
|
15405
|
+
maximum
|
|
15406
|
+
},
|
|
15324
15407
|
evidence: {
|
|
15325
|
-
actual: fact.
|
|
15326
|
-
expected,
|
|
15327
|
-
unit: "
|
|
15328
|
-
values: {
|
|
15408
|
+
actual: fact.charactersPerLine,
|
|
15409
|
+
expected: wide ? maximum : minimum,
|
|
15410
|
+
unit: "characters",
|
|
15411
|
+
values: { source: "profile" }
|
|
15412
|
+
}
|
|
15413
|
+
};
|
|
15414
|
+
});
|
|
15415
|
+
}
|
|
15416
|
+
};
|
|
15417
|
+
var docxSectionContentRule = {
|
|
15418
|
+
id: "docx/section-content",
|
|
15419
|
+
description: "A section that renders nothing, or one that carries body copy under no heading. Off until a profile or policy enables it.",
|
|
15420
|
+
code: QUALITY_CODES.SECTION_EMPTY,
|
|
15421
|
+
category: "hierarchy",
|
|
15422
|
+
defaultSeverity: "warning",
|
|
15423
|
+
defaultCertainty: "deterministic",
|
|
15424
|
+
formats: ["docx"],
|
|
15425
|
+
defaultEnabled: false,
|
|
15426
|
+
defaultParameters: { minimumWordsForHeading: 60 },
|
|
15427
|
+
evaluate: ({ facts, configuration }) => {
|
|
15428
|
+
const minimumWords = numberParameter(
|
|
15429
|
+
configuration.parameters,
|
|
15430
|
+
"minimumWordsForHeading",
|
|
15431
|
+
60
|
|
15432
|
+
);
|
|
15433
|
+
const sections = facts.filter(
|
|
15434
|
+
(fact) => fact.kind === "docx/section"
|
|
15435
|
+
);
|
|
15436
|
+
return sections.flatMap((fact) => {
|
|
15437
|
+
if (fact.words === 0 && fact.exhibits === 0)
|
|
15438
|
+
return [
|
|
15439
|
+
{
|
|
15440
|
+
path: fact.path,
|
|
15441
|
+
code: QUALITY_CODES.SECTION_EMPTY,
|
|
15442
|
+
message: "This section renders nothing: no text, no table, no figure. It still starts a page.",
|
|
15443
|
+
suggestion: "Fill the section, or remove it so the document does not open a page on nothing.",
|
|
15444
|
+
context: { index: fact.index }
|
|
15445
|
+
}
|
|
15446
|
+
];
|
|
15447
|
+
if (fact.headings === 0 && fact.words >= minimumWords)
|
|
15448
|
+
return [
|
|
15449
|
+
{
|
|
15450
|
+
path: fact.path,
|
|
15451
|
+
code: QUALITY_CODES.SECTION_UNTITLED,
|
|
15452
|
+
message: `This section runs to ${fact.words} words under no heading, so nothing names it in the outline or the contents.`,
|
|
15453
|
+
suggestion: "Open the section with a heading \u2014 a section-opener block, or a level-1 heading.",
|
|
15454
|
+
context: { index: fact.index, words: fact.words },
|
|
15455
|
+
evidence: {
|
|
15456
|
+
actual: 0,
|
|
15457
|
+
expected: 1,
|
|
15458
|
+
unit: "headings",
|
|
15459
|
+
values: { source: "profile" }
|
|
15460
|
+
}
|
|
15461
|
+
}
|
|
15462
|
+
];
|
|
15463
|
+
return [];
|
|
15464
|
+
});
|
|
15465
|
+
}
|
|
15466
|
+
};
|
|
15467
|
+
var docxHeadingKeepNextRule = {
|
|
15468
|
+
id: "docx/heading-keep-next",
|
|
15469
|
+
description: "A heading that is not bound to the content under it, so a page break can strand it; the fix sets keepNext on an authored heading. Off until a profile or policy enables it.",
|
|
15470
|
+
code: QUALITY_CODES.HEADING_ORPHAN,
|
|
15471
|
+
category: "hierarchy",
|
|
15472
|
+
defaultSeverity: "warning",
|
|
15473
|
+
defaultCertainty: "deterministic",
|
|
15474
|
+
formats: ["docx"],
|
|
15475
|
+
defaultEnabled: false,
|
|
15476
|
+
evaluate: ({ facts }) => facts.filter(
|
|
15477
|
+
(fact) => fact.kind === "docx/heading" && !fact.keepNext
|
|
15478
|
+
).map((fact) => {
|
|
15479
|
+
const heading = /\/props\/level$/.test(fact.path) ? fact.path.replace(/\/props\/level$/, "") : void 0;
|
|
15480
|
+
return {
|
|
15481
|
+
path: heading ?? fact.path,
|
|
15482
|
+
message: "This heading is not bound to the content under it: a page break can leave it alone at the foot of a page.",
|
|
15483
|
+
suggestion: heading ? "Set keepNext on the heading, or give the theme\u2019s heading style keepNext so every level is bound." : "Give the theme\u2019s heading style keepNext, or set it in the block definition that draws this heading.",
|
|
15484
|
+
context: { level: fact.level },
|
|
15485
|
+
evidence: { values: { source: "profile" } },
|
|
15486
|
+
...heading && {
|
|
15487
|
+
fixes: [
|
|
15488
|
+
{
|
|
15489
|
+
op: "add",
|
|
15490
|
+
path: `${heading}/props/keepNext`,
|
|
15491
|
+
value: true
|
|
15492
|
+
}
|
|
15493
|
+
]
|
|
15494
|
+
}
|
|
15495
|
+
};
|
|
15496
|
+
})
|
|
15497
|
+
};
|
|
15498
|
+
var docxFigureLabelRule = {
|
|
15499
|
+
id: "docx/figure-label",
|
|
15500
|
+
description: "An image with neither a caption beside it nor alt text on it. Off until a profile or policy enables it.",
|
|
15501
|
+
code: QUALITY_CODES.FIGURE_UNLABELLED,
|
|
15502
|
+
category: "accessibility",
|
|
15503
|
+
defaultSeverity: "warning",
|
|
15504
|
+
defaultCertainty: "deterministic",
|
|
15505
|
+
formats: ["docx"],
|
|
15506
|
+
defaultEnabled: false,
|
|
15507
|
+
evaluate: ({ facts }) => facts.filter(
|
|
15508
|
+
(fact) => fact.kind === "docx/image" && fact.alt === void 0 && !fact.captioned
|
|
15509
|
+
).map((fact) => ({
|
|
15510
|
+
path: fact.path,
|
|
15511
|
+
message: "This figure carries neither a caption nor alt text, so nothing in the document says what it shows.",
|
|
15512
|
+
suggestion: "Put the image in a figure block, which numbers and captions it, or write alt text on the image.",
|
|
15513
|
+
context: {}
|
|
15514
|
+
}))
|
|
15515
|
+
};
|
|
15516
|
+
var docxImageAspectRule = {
|
|
15517
|
+
id: "docx/image-aspect",
|
|
15518
|
+
description: "An image drawn at an aspect the asset does not have, where the asset can be read from the document.",
|
|
15519
|
+
code: QUALITY_CODES.IMAGE_ASPECT,
|
|
15520
|
+
category: "integrity",
|
|
15521
|
+
defaultSeverity: "warning",
|
|
15522
|
+
defaultCertainty: "deterministic",
|
|
15523
|
+
formats: ["docx"],
|
|
15524
|
+
defaultParameters: { tolerance: DEFAULT_IMAGE_ASPECT_TOLERANCE },
|
|
15525
|
+
evaluate: ({ facts, configuration }) => {
|
|
15526
|
+
const tolerance = numberParameter(
|
|
15527
|
+
configuration.parameters,
|
|
15528
|
+
"tolerance",
|
|
15529
|
+
DEFAULT_IMAGE_ASPECT_TOLERANCE
|
|
15530
|
+
);
|
|
15531
|
+
return facts.filter(
|
|
15532
|
+
(fact) => fact.kind === "docx/image" && fact.drawnRatio !== void 0 && fact.naturalRatio !== void 0
|
|
15533
|
+
).flatMap((fact) => {
|
|
15534
|
+
const finding = imageAspectFinding(
|
|
15535
|
+
{
|
|
15536
|
+
path: fact.path,
|
|
15537
|
+
drawn: fact.drawnRatio,
|
|
15538
|
+
natural: fact.naturalRatio
|
|
15329
15539
|
},
|
|
15330
|
-
|
|
15331
|
-
|
|
15332
|
-
|
|
15333
|
-
|
|
15540
|
+
"page",
|
|
15541
|
+
tolerance
|
|
15542
|
+
);
|
|
15543
|
+
return finding ? [finding] : [];
|
|
15544
|
+
});
|
|
15545
|
+
}
|
|
15546
|
+
};
|
|
15547
|
+
var docxContentsRule = {
|
|
15548
|
+
id: "docx/contents-missing",
|
|
15549
|
+
description: "More headings than minimumHeadings with no table of contents. Off at 0.",
|
|
15550
|
+
code: QUALITY_CODES.CONTENTS_MISSING,
|
|
15551
|
+
category: "hierarchy",
|
|
15552
|
+
defaultSeverity: "info",
|
|
15553
|
+
defaultCertainty: "deterministic",
|
|
15554
|
+
formats: ["docx"],
|
|
15555
|
+
defaultParameters: { minimumHeadings: 0 },
|
|
15556
|
+
evaluate: ({ facts, configuration, profile }) => {
|
|
15557
|
+
const minimum = numberParameter(
|
|
15558
|
+
configuration.parameters,
|
|
15559
|
+
"minimumHeadings",
|
|
15560
|
+
0
|
|
15561
|
+
);
|
|
15562
|
+
if (minimum <= 0) return [];
|
|
15563
|
+
const outline = facts.find(
|
|
15564
|
+
(fact) => fact.kind === "docx/outline"
|
|
15565
|
+
);
|
|
15566
|
+
if (!outline || outline.contents || outline.headings < minimum) return [];
|
|
15567
|
+
return [
|
|
15568
|
+
{
|
|
15569
|
+
path: outline.path,
|
|
15570
|
+
message: `The document carries ${outline.headings} headings and no table of contents; the ${profile?.id ?? "selected"} profile expects one from ${minimum}.`,
|
|
15571
|
+
suggestion: "Add a `toc` component after the cover. Word fills it from the headings already there.",
|
|
15572
|
+
context: { headings: outline.headings, minimum },
|
|
15573
|
+
evidence: {
|
|
15574
|
+
actual: outline.headings,
|
|
15575
|
+
expected: minimum,
|
|
15576
|
+
unit: "headings",
|
|
15577
|
+
values: { source: "profile" }
|
|
15578
|
+
}
|
|
15579
|
+
}
|
|
15580
|
+
];
|
|
15334
15581
|
}
|
|
15335
15582
|
};
|
|
15336
15583
|
var DOCX_QUALITY_RULES = {
|
|
@@ -15353,14 +15600,26 @@ var DOCX_QUALITY_RULES = {
|
|
|
15353
15600
|
docxExhibitRequiredRule,
|
|
15354
15601
|
docxTypeScaleRule,
|
|
15355
15602
|
docxSizeCountRule,
|
|
15356
|
-
docxRoleDriftRule
|
|
15603
|
+
docxRoleDriftRule,
|
|
15604
|
+
docxBodyMeasureRule,
|
|
15605
|
+
docxSectionContentRule,
|
|
15606
|
+
docxHeadingKeepNextRule,
|
|
15607
|
+
docxFigureLabelRule,
|
|
15608
|
+
docxImageAspectRule,
|
|
15609
|
+
docxContentsRule
|
|
15357
15610
|
]
|
|
15358
15611
|
};
|
|
15612
|
+
var REPORT_MEASURE = {
|
|
15613
|
+
"docx/body-measure": {
|
|
15614
|
+
enabled: true,
|
|
15615
|
+
parameters: { maximumCharacters: 125 }
|
|
15616
|
+
}
|
|
15617
|
+
};
|
|
15359
15618
|
var DOCX_QUALITY_PROFILES = {
|
|
15360
15619
|
"client-report": {
|
|
15361
15620
|
id: "client-report",
|
|
15362
15621
|
formats: ["docx"],
|
|
15363
|
-
description: "Client or public-administration report: a running head with page numbers on every section after the cover, a takeaway and a source wherever a block declares them, no heading skipped, every size on the theme scale with at most eight in play, no page rendered empty or left half blank under the running head, and at least one chart or table.",
|
|
15622
|
+
description: "Client or public-administration report: a running head with page numbers on every section after the cover, a takeaway and a source wherever a block declares them, no heading skipped, every size on the theme scale with at most eight in play, a readable measure, no empty or untitled section, every heading bound to what follows and every figure named, no page rendered empty or left half blank under the running head, and at least one chart or table.",
|
|
15364
15623
|
rules: {
|
|
15365
15624
|
"docx/required-chrome": {
|
|
15366
15625
|
parameters: { required: ["takeaway", "source"] }
|
|
@@ -15384,7 +15643,11 @@ var DOCX_QUALITY_PROFILES = {
|
|
|
15384
15643
|
// judge calls a page two-thirds blank a defect.
|
|
15385
15644
|
"rendered/page-underfilled": { severity: "warning" },
|
|
15386
15645
|
// A client report argues numbers: at least one chart or real table.
|
|
15387
|
-
"docx/exhibit-required": { enabled: true }
|
|
15646
|
+
"docx/exhibit-required": { enabled: true },
|
|
15647
|
+
...REPORT_MEASURE,
|
|
15648
|
+
"docx/section-content": { enabled: true },
|
|
15649
|
+
"docx/heading-keep-next": { enabled: true },
|
|
15650
|
+
"docx/figure-label": { enabled: true }
|
|
15388
15651
|
}
|
|
15389
15652
|
},
|
|
15390
15653
|
"executive-report": {
|
|
@@ -15392,13 +15655,15 @@ var DOCX_QUALITY_PROFILES = {
|
|
|
15392
15655
|
formats: ["docx"],
|
|
15393
15656
|
description: "Short decision document with strict outline continuity.",
|
|
15394
15657
|
rules: {
|
|
15395
|
-
"docx/heading-hierarchy": { severity: "warning" }
|
|
15658
|
+
"docx/heading-hierarchy": { severity: "warning" },
|
|
15659
|
+
"docx/section-content": { enabled: true },
|
|
15660
|
+
"docx/heading-keep-next": { enabled: true }
|
|
15396
15661
|
}
|
|
15397
15662
|
},
|
|
15398
15663
|
"technical-report": {
|
|
15399
15664
|
id: "technical-report",
|
|
15400
15665
|
formats: ["docx"],
|
|
15401
|
-
description: "Technical report or memo: numbered sections under a running head with page numbers on every section after the cover, a source wherever a block declares one, no heading skipped, every size on the theme scale with at most nine in play, no page rendered empty or left half blank, and at least one chart or table.",
|
|
15666
|
+
description: "Technical report or memo: numbered sections under a running head with page numbers on every section after the cover, a source wherever a block declares one, no heading skipped, every size on the theme scale with at most nine in play, a readable measure, no empty or untitled section, every heading bound to what follows, every figure named, a contents page past eight headings, no page rendered empty or left half blank, and at least one chart or table.",
|
|
15402
15667
|
rules: {
|
|
15403
15668
|
// A figure or table in a technical report cites where its numbers come
|
|
15404
15669
|
// from; the takeaway is the client report's ask, the caption is the
|
|
@@ -15419,7 +15684,17 @@ var DOCX_QUALITY_PROFILES = {
|
|
|
15419
15684
|
"rendered/empty-page": { severity: "warning" },
|
|
15420
15685
|
"rendered/page-underfilled": { severity: "warning" },
|
|
15421
15686
|
// A technical report argues from measurements: a runs table, a chart.
|
|
15422
|
-
"docx/exhibit-required": { enabled: true }
|
|
15687
|
+
"docx/exhibit-required": { enabled: true },
|
|
15688
|
+
...REPORT_MEASURE,
|
|
15689
|
+
"docx/section-content": { enabled: true },
|
|
15690
|
+
"docx/heading-keep-next": { enabled: true },
|
|
15691
|
+
"docx/figure-label": { enabled: true },
|
|
15692
|
+
// Numbered sections are meant to be reached by number: past eight
|
|
15693
|
+
// headings a technical report owes the reader a contents page.
|
|
15694
|
+
"docx/contents-missing": {
|
|
15695
|
+
severity: "warning",
|
|
15696
|
+
parameters: { minimumHeadings: 8 }
|
|
15697
|
+
}
|
|
15423
15698
|
}
|
|
15424
15699
|
},
|
|
15425
15700
|
general: {
|