@json-to-office/jto-ops 5.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +78 -26
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -2208,40 +2208,61 @@ function readingOrder(words2) {
|
|
|
2208
2208
|
}
|
|
2209
2209
|
return order;
|
|
2210
2210
|
}
|
|
2211
|
-
function
|
|
2211
|
+
function median(values) {
|
|
2212
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2213
|
+
const mid = sorted.length >> 1;
|
|
2214
|
+
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
2215
|
+
}
|
|
2216
|
+
function groupRows(words2, indices) {
|
|
2212
2217
|
const rows = [];
|
|
2213
2218
|
const byY = [...indices].sort(
|
|
2214
2219
|
(a, b) => words2[a].yMin - words2[b].yMin || words2[a].xMin - words2[b].xMin
|
|
2215
2220
|
);
|
|
2216
2221
|
for (const i of byY) {
|
|
2222
|
+
const word = words2[i];
|
|
2217
2223
|
const row = rows[rows.length - 1];
|
|
2218
|
-
if (row && sameRow(words2[row[0]],
|
|
2219
|
-
|
|
2224
|
+
if (row && sameRow(words2[row.words[0]], word)) {
|
|
2225
|
+
row.words.push(i);
|
|
2226
|
+
row.yMin = Math.min(row.yMin, word.yMin);
|
|
2227
|
+
row.yMax = Math.max(row.yMax, word.yMax);
|
|
2228
|
+
} else rows.push({ words: [i], yMin: word.yMin, yMax: word.yMax });
|
|
2220
2229
|
}
|
|
2230
|
+
return rows;
|
|
2231
|
+
}
|
|
2232
|
+
function uprightOrder(words2, indices) {
|
|
2233
|
+
const grouped = groupRows(words2, indices);
|
|
2234
|
+
const rows = grouped.map((row) => row.words);
|
|
2221
2235
|
const fragmentsOf = (row) => {
|
|
2222
2236
|
const sorted = [...row].sort((a, b) => words2[a].xMin - words2[b].xMin);
|
|
2237
|
+
const gaps = sorted.slice(1).map((i, n) => {
|
|
2238
|
+
const height = words2[i].yMax - words2[i].yMin;
|
|
2239
|
+
const gap = words2[i].xMin - words2[sorted[n]].xMax;
|
|
2240
|
+
return { gap, height, wide: gap > height / 2 };
|
|
2241
|
+
});
|
|
2242
|
+
const spaces = gaps.filter((g) => !g.wide).map((g) => g.gap);
|
|
2243
|
+
const typical = spaces.length > 1 ? median(spaces) : void 0;
|
|
2244
|
+
const isCellBoundary = (n) => {
|
|
2245
|
+
const g = gaps[n - 1];
|
|
2246
|
+
return g.wide || typical !== void 0 && g.gap > g.height / 3 && g.gap >= typical * 1.5;
|
|
2247
|
+
};
|
|
2223
2248
|
const out = [];
|
|
2224
|
-
|
|
2249
|
+
sorted.forEach((i, n) => {
|
|
2225
2250
|
const last = out[out.length - 1];
|
|
2226
2251
|
const w = words2[i];
|
|
2227
|
-
|
|
2228
|
-
if (last && w.xMin - last.xMax <= height / 2) {
|
|
2252
|
+
if (last && !isCellBoundary(n)) {
|
|
2229
2253
|
last.words.push(i);
|
|
2230
2254
|
last.xMax = Math.max(last.xMax, w.xMax);
|
|
2231
2255
|
} else out.push({ xMin: w.xMin, xMax: w.xMax, words: [i] });
|
|
2232
|
-
}
|
|
2256
|
+
});
|
|
2233
2257
|
return out;
|
|
2234
2258
|
};
|
|
2235
2259
|
const fragments = rows.map(fragmentsOf);
|
|
2236
|
-
const boxes =
|
|
2237
|
-
yMin: Math.min(...row.map((i) => words2[i].yMin)),
|
|
2238
|
-
yMax: Math.max(...row.map((i) => words2[i].yMax))
|
|
2239
|
-
}));
|
|
2260
|
+
const boxes = grouped;
|
|
2240
2261
|
const order = [];
|
|
2241
2262
|
let cluster = null;
|
|
2242
2263
|
const flush = () => {
|
|
2243
2264
|
if (!cluster) return;
|
|
2244
|
-
if (
|
|
2265
|
+
if (cluster.columns.length < 2) {
|
|
2245
2266
|
for (const r of cluster.rows)
|
|
2246
2267
|
for (const f of fragments[r]) order.push(...f.words);
|
|
2247
2268
|
} else {
|
|
@@ -2263,9 +2284,8 @@ function uprightOrder(words2, indices) {
|
|
|
2263
2284
|
const adjacent = cluster !== null && boxes[r].yMin - boxes[r - 1].yMax <= lineHeight * 0.9;
|
|
2264
2285
|
const wide = cluster !== null && fragments[r].some((f) => overlapping(cluster.columns, f).length > 1);
|
|
2265
2286
|
if (!adjacent || wide) flush();
|
|
2266
|
-
if (!cluster) cluster = { rows: [], columns: []
|
|
2287
|
+
if (!cluster) cluster = { rows: [], columns: [] };
|
|
2267
2288
|
cluster.rows.push(r);
|
|
2268
|
-
if (fragments[r].length > 1) cluster.multi = true;
|
|
2269
2289
|
for (const f of fragments[r]) {
|
|
2270
2290
|
const hit = overlapping(cluster.columns, f)[0];
|
|
2271
2291
|
if (hit) {
|
|
@@ -2414,6 +2434,34 @@ function chromeOccurrences(occurrences, pages) {
|
|
|
2414
2434
|
)
|
|
2415
2435
|
);
|
|
2416
2436
|
}
|
|
2437
|
+
function pageNumberRows(pages, claimed) {
|
|
2438
|
+
const hits = [];
|
|
2439
|
+
pages.forEach((page, pageIndex) => {
|
|
2440
|
+
const rows = groupRows(
|
|
2441
|
+
page.words,
|
|
2442
|
+
page.words.map((_, i) => i)
|
|
2443
|
+
);
|
|
2444
|
+
const free = rows.map(
|
|
2445
|
+
(row) => row.words.some((i) => !claimed.has(`${pageIndex}:${i}`))
|
|
2446
|
+
);
|
|
2447
|
+
rows.forEach((row, r) => {
|
|
2448
|
+
if (!free[r] || !inChromeBand(page, row)) return;
|
|
2449
|
+
if (!row.words.every((i) => PAGE_NUMBER_ROW.test(page.words[i].text)))
|
|
2450
|
+
return;
|
|
2451
|
+
const line = row.yMax - row.yMin;
|
|
2452
|
+
const attached = rows.some(
|
|
2453
|
+
(other, o) => o !== r && free[o] && Math.max(other.yMin - row.yMax, row.yMin - other.yMax) < line
|
|
2454
|
+
);
|
|
2455
|
+
if (!attached) hits.push({ pageIndex, box: row });
|
|
2456
|
+
});
|
|
2457
|
+
});
|
|
2458
|
+
if (pages.length < 2) return hits;
|
|
2459
|
+
return hits.filter(
|
|
2460
|
+
(hit) => hits.some(
|
|
2461
|
+
(other) => other.pageIndex !== hit.pageIndex && Math.abs(other.box.yMin - hit.box.yMin) <= CHROME_ROW_TOLERANCE_PT
|
|
2462
|
+
)
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2417
2465
|
function assignInventory(pages, inventory) {
|
|
2418
2466
|
const results = /* @__PURE__ */ new Map();
|
|
2419
2467
|
const chromeWords = /* @__PURE__ */ new Set();
|
|
@@ -2450,16 +2498,8 @@ function assignInventory(pages, inventory) {
|
|
|
2450
2498
|
});
|
|
2451
2499
|
}
|
|
2452
2500
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
if (chromeWords.has(`${pageIndex}:${i}`)) return;
|
|
2456
|
-
if (!inChromeBand(page, word)) return;
|
|
2457
|
-
const row = page.words.filter((w) => sameRow(w, word));
|
|
2458
|
-
if (row.every((w) => PAGE_NUMBER_ROW.test(w.text))) {
|
|
2459
|
-
claimRow(pageIndex, word);
|
|
2460
|
-
}
|
|
2461
|
-
});
|
|
2462
|
-
});
|
|
2501
|
+
for (const row of pageNumberRows(pages, chromeWords))
|
|
2502
|
+
claimRow(row.pageIndex, row.box);
|
|
2463
2503
|
const body = indexDocument(pages, chromeWords);
|
|
2464
2504
|
const claimed = /* @__PURE__ */ new Map();
|
|
2465
2505
|
let cursor = 0;
|
|
@@ -2855,7 +2895,8 @@ function draftRenderedFindings(input) {
|
|
|
2855
2895
|
}
|
|
2856
2896
|
}
|
|
2857
2897
|
}
|
|
2858
|
-
const
|
|
2898
|
+
const owned = owner ? /^\/children\/\d+/.exec(owner.path)?.[0] : void 0;
|
|
2899
|
+
const section = owned && last ? closingSection(input.inventory) ?? owned : owned;
|
|
2859
2900
|
findings.push(
|
|
2860
2901
|
finding({
|
|
2861
2902
|
ruleId: "rendered/page-underfilled",
|
|
@@ -2863,7 +2904,7 @@ function draftRenderedFindings(input) {
|
|
|
2863
2904
|
page: pageIndex + 1,
|
|
2864
2905
|
path: section ?? "",
|
|
2865
2906
|
message: last ? `Page ${pageIndex + 1}, the last, is ${Math.round(fill * 100)}% filled: only the tail of the document reached it.` : `Page ${pageIndex + 1} is ${Math.round(fill * 100)}% filled: its content stops ${Math.round(footerTop - bottom)}pt above the footer and the next page begins anyway.`,
|
|
2866
|
-
suggestion: last ? "
|
|
2907
|
+
suggestion: last ? "Set `pageBreak: true` on this section \u2014 or on the one before it, when this section already starts a page \u2014 so the closing argument and its notes share one designed page instead of the last page taking only their tail, then re-preview. Keeping the notes with the paragraph above them moves the tail, not the page." : "Merge this section into its neighbour, or give the page the table or chart its argument owes. Do not pad it with prose: a short section on its own page reads as unfinished, a padded one reads as filler.",
|
|
2867
2908
|
evidence: {
|
|
2868
2909
|
summary: "Share of the body area the page paints, header to footer",
|
|
2869
2910
|
expected: `\u2265 ${Math.round((last ? MINIMUM_LAST_PAGE_FILL : MINIMUM_PAGE_FILL) * 100)}%`,
|
|
@@ -2964,6 +3005,17 @@ function renderedEngine() {
|
|
|
2964
3005
|
engine ??= new QualityEngine(RENDERED_QUALITY_RULES.rules);
|
|
2965
3006
|
return engine;
|
|
2966
3007
|
}
|
|
3008
|
+
function closingSection(inventory) {
|
|
3009
|
+
let highest;
|
|
3010
|
+
for (const entry of inventory) {
|
|
3011
|
+
if (entry.repeats) continue;
|
|
3012
|
+
const index = /^\/children\/(\d+)/.exec(entry.path)?.[1];
|
|
3013
|
+
if (index === void 0) continue;
|
|
3014
|
+
const value = Number(index);
|
|
3015
|
+
if (highest === void 0 || value > highest) highest = value;
|
|
3016
|
+
}
|
|
3017
|
+
return highest === void 0 ? void 0 : `/children/${highest}`;
|
|
3018
|
+
}
|
|
2967
3019
|
function analyzeRenderedDocument(input, options = {}) {
|
|
2968
3020
|
const fact = {
|
|
2969
3021
|
id: "rendered:geometry",
|