@json-to-office/jto-ops 6.0.0 → 6.8.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.d.ts +67 -4
- package/dist/index.js +461 -52
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -2149,6 +2149,14 @@ var RENDERED_QUALITY_RULES = {
|
|
|
2149
2149
|
defaultSeverity: "warning",
|
|
2150
2150
|
formats: ["docx"]
|
|
2151
2151
|
}),
|
|
2152
|
+
renderedRule({
|
|
2153
|
+
id: "rendered/table-split",
|
|
2154
|
+
code: QUALITY_CODES.RENDERED_TABLE_SPLIT,
|
|
2155
|
+
description: "A table broken across a page badly: its header left alone at the foot of a page (`kind: header-alone`), or one row alone on either side of the break (`kind: orphan-row` or `widow-row`).",
|
|
2156
|
+
category: "composition",
|
|
2157
|
+
defaultSeverity: "info",
|
|
2158
|
+
formats: ["docx"]
|
|
2159
|
+
}),
|
|
2152
2160
|
renderedRule({
|
|
2153
2161
|
id: "rendered/paragraph-split",
|
|
2154
2162
|
code: QUALITY_CODES.RENDERED_PARAGRAPH_SPLIT,
|
|
@@ -2176,43 +2184,77 @@ function indexDocument(pages, exclude = /* @__PURE__ */ new Set()) {
|
|
|
2176
2184
|
const fragments = [];
|
|
2177
2185
|
const positions = [];
|
|
2178
2186
|
let cursor = 0;
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2187
|
+
const push = (pageIndex, index) => {
|
|
2188
|
+
const word = pages[pageIndex].words[index];
|
|
2189
|
+
const key = `${pageIndex}:${index}`;
|
|
2190
|
+
const fragment = exclude.has(key) ? "" : normalizeForMatch(word.text);
|
|
2191
|
+
refs.push({ pageIndex, word: index });
|
|
2192
|
+
fragments.push(fragment);
|
|
2193
|
+
positions.push(cursor);
|
|
2194
|
+
cursor += fragment.length;
|
|
2195
|
+
};
|
|
2196
|
+
const orders = pages.map((page) => pageOrder(page.words, page.lines));
|
|
2197
|
+
orders.forEach((order, pageIndex) => {
|
|
2198
|
+
for (const index of order.upright) push(pageIndex, index);
|
|
2199
|
+
});
|
|
2200
|
+
const rotatedFrom = cursor;
|
|
2201
|
+
orders.forEach((order, pageIndex) => {
|
|
2202
|
+
for (const index of order.rotated) push(pageIndex, index);
|
|
2189
2203
|
});
|
|
2190
|
-
return {
|
|
2204
|
+
return {
|
|
2205
|
+
pages,
|
|
2206
|
+
refs,
|
|
2207
|
+
fragments,
|
|
2208
|
+
positions,
|
|
2209
|
+
stream: fragments.join(""),
|
|
2210
|
+
rotatedFrom
|
|
2211
|
+
};
|
|
2191
2212
|
}
|
|
2192
|
-
function readingOrder(words2) {
|
|
2213
|
+
function readingOrder(words2, lines = []) {
|
|
2214
|
+
const { upright, rotated } = pageOrder(words2, lines);
|
|
2215
|
+
return [...upright, ...rotated];
|
|
2216
|
+
}
|
|
2217
|
+
function pageOrder(words2, lines = []) {
|
|
2218
|
+
const rotated = rotatedWords(words2, lines);
|
|
2219
|
+
return {
|
|
2220
|
+
upright: uprightOrder(
|
|
2221
|
+
words2,
|
|
2222
|
+
words2.map((_, i) => i).filter((i) => !rotated.has(i))
|
|
2223
|
+
),
|
|
2224
|
+
rotated: [...rotated].sort((a, b) => a - b)
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
function rotatedWords(words2, lines) {
|
|
2193
2228
|
const rotated = new Set(
|
|
2194
2229
|
words2.map((_, i) => i).filter((i) => {
|
|
2195
2230
|
const w = words2[i];
|
|
2196
2231
|
return w.text.length > 2 && w.yMax - w.yMin > 1.5 * (w.xMax - w.xMin);
|
|
2197
2232
|
})
|
|
2198
2233
|
);
|
|
2199
|
-
const
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2234
|
+
for (const line of lines) {
|
|
2235
|
+
if (line.words.length < 2) continue;
|
|
2236
|
+
const stacked = line.words.slice(1).every((index, n) => {
|
|
2237
|
+
const a = words2[line.words[n]];
|
|
2238
|
+
const b = words2[index];
|
|
2239
|
+
const across = Math.min(a.xMax, b.xMax) - Math.max(a.xMin, b.xMin);
|
|
2240
|
+
const along = Math.min(a.yMax, b.yMax) - Math.max(a.yMin, b.yMin);
|
|
2241
|
+
return across > 0 && along <= 0;
|
|
2242
|
+
});
|
|
2243
|
+
if (stacked) for (const index of line.words) rotated.add(index);
|
|
2208
2244
|
}
|
|
2209
|
-
return
|
|
2245
|
+
return rotated;
|
|
2210
2246
|
}
|
|
2211
2247
|
function median(values) {
|
|
2212
2248
|
const sorted = [...values].sort((a, b) => a - b);
|
|
2213
2249
|
const mid = sorted.length >> 1;
|
|
2214
2250
|
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
2215
2251
|
}
|
|
2252
|
+
var ROW_SIZE_RATIO = 2;
|
|
2253
|
+
function comparableSize(a, b) {
|
|
2254
|
+
const ha = a.yMax - a.yMin;
|
|
2255
|
+
const hb = b.yMax - b.yMin;
|
|
2256
|
+
return Math.max(ha, hb) <= ROW_SIZE_RATIO * Math.min(ha, hb);
|
|
2257
|
+
}
|
|
2216
2258
|
function groupRows(words2, indices) {
|
|
2217
2259
|
const rows = [];
|
|
2218
2260
|
const byY = [...indices].sort(
|
|
@@ -2221,7 +2263,8 @@ function groupRows(words2, indices) {
|
|
|
2221
2263
|
for (const i of byY) {
|
|
2222
2264
|
const word = words2[i];
|
|
2223
2265
|
const row = rows[rows.length - 1];
|
|
2224
|
-
|
|
2266
|
+
const peer = row?.words.find((w) => comparableSize(words2[w], word));
|
|
2267
|
+
if (row && peer !== void 0 && sameRow(words2[peer], word)) {
|
|
2225
2268
|
row.words.push(i);
|
|
2226
2269
|
row.yMin = Math.min(row.yMin, word.yMin);
|
|
2227
2270
|
row.yMax = Math.max(row.yMax, word.yMax);
|
|
@@ -2259,20 +2302,25 @@ function uprightOrder(words2, indices) {
|
|
|
2259
2302
|
const fragments = rows.map(fragmentsOf);
|
|
2260
2303
|
const boxes = grouped;
|
|
2261
2304
|
const order = [];
|
|
2305
|
+
const read = /* @__PURE__ */ new Set();
|
|
2306
|
+
const readColumn = (column) => {
|
|
2307
|
+
column.fragments.sort(
|
|
2308
|
+
(a, b) => words2[a.words[0]].yMin - words2[b.words[0]].yMin || a.xMin - b.xMin
|
|
2309
|
+
);
|
|
2310
|
+
for (const f of column.fragments) {
|
|
2311
|
+
order.push(...f.words);
|
|
2312
|
+
read.add(f);
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2262
2315
|
let cluster = null;
|
|
2263
2316
|
const flush = () => {
|
|
2264
2317
|
if (!cluster) return;
|
|
2265
2318
|
if (cluster.columns.length < 2) {
|
|
2266
2319
|
for (const r of cluster.rows)
|
|
2267
|
-
for (const f of fragments[r]) order.push(...f.words);
|
|
2320
|
+
for (const f of fragments[r]) if (!read.has(f)) order.push(...f.words);
|
|
2268
2321
|
} else {
|
|
2269
2322
|
cluster.columns.sort((a, b) => a.xMin - b.xMin);
|
|
2270
|
-
for (const column of cluster.columns)
|
|
2271
|
-
column.fragments.sort(
|
|
2272
|
-
(a, b) => words2[a.words[0]].yMin - words2[b.words[0]].yMin || a.xMin - b.xMin
|
|
2273
|
-
);
|
|
2274
|
-
for (const f of column.fragments) order.push(...f.words);
|
|
2275
|
-
}
|
|
2323
|
+
for (const column of cluster.columns) readColumn(column);
|
|
2276
2324
|
}
|
|
2277
2325
|
cluster = null;
|
|
2278
2326
|
};
|
|
@@ -2286,6 +2334,16 @@ function uprightOrder(words2, indices) {
|
|
|
2286
2334
|
if (!adjacent || wide) flush();
|
|
2287
2335
|
if (!cluster) cluster = { rows: [], columns: [] };
|
|
2288
2336
|
cluster.rows.push(r);
|
|
2337
|
+
const landing = /* @__PURE__ */ new Map();
|
|
2338
|
+
for (const f of fragments[r]) {
|
|
2339
|
+
const hit = overlapping(cluster.columns, f)[0];
|
|
2340
|
+
if (hit) landing.set(hit, (landing.get(hit) ?? 0) + 1);
|
|
2341
|
+
}
|
|
2342
|
+
for (const [column, count] of landing) {
|
|
2343
|
+
if (count < 2) continue;
|
|
2344
|
+
readColumn(column);
|
|
2345
|
+
cluster.columns.splice(cluster.columns.indexOf(column), 1);
|
|
2346
|
+
}
|
|
2289
2347
|
for (const f of fragments[r]) {
|
|
2290
2348
|
const hit = overlapping(cluster.columns, f)[0];
|
|
2291
2349
|
if (hit) {
|
|
@@ -2353,6 +2411,7 @@ function occurrence(index, at, end) {
|
|
|
2353
2411
|
const parts = partsOf(index, at, end);
|
|
2354
2412
|
return {
|
|
2355
2413
|
at,
|
|
2414
|
+
end,
|
|
2356
2415
|
pageIndex: parts[0].pageIndex,
|
|
2357
2416
|
endPageIndex: parts[parts.length - 1].pageIndex,
|
|
2358
2417
|
parts
|
|
@@ -2378,7 +2437,11 @@ function findOccurrences(index, segments) {
|
|
|
2378
2437
|
end = next + segments[s].length;
|
|
2379
2438
|
}
|
|
2380
2439
|
if (!complete || !alignedToFragments(index, at, end)) continue;
|
|
2381
|
-
|
|
2440
|
+
const hit = occurrence(index, at, end);
|
|
2441
|
+
if (at < index.rotatedFrom ? end > index.rotatedFrom : hit.pageIndex !== hit.endPageIndex) {
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
hits.push(hit);
|
|
2382
2445
|
searchFrom = end;
|
|
2383
2446
|
}
|
|
2384
2447
|
return hits;
|
|
@@ -2386,7 +2449,7 @@ function findOccurrences(index, segments) {
|
|
|
2386
2449
|
var MIN_NEEDLE_LENGTH = 3;
|
|
2387
2450
|
var MIN_PARTIAL_PREFIX = 16;
|
|
2388
2451
|
var CHROME_BAND = 0.2;
|
|
2389
|
-
function longestRenderedPrefix(index, needle) {
|
|
2452
|
+
function longestRenderedPrefix(index, needle, page, region) {
|
|
2390
2453
|
if (needle.length < MIN_PARTIAL_PREFIX) return void 0;
|
|
2391
2454
|
const anchor = needle.slice(0, MIN_PARTIAL_PREFIX);
|
|
2392
2455
|
let best;
|
|
@@ -2399,14 +2462,36 @@ function longestRenderedPrefix(index, needle) {
|
|
|
2399
2462
|
if (first >= index.fragments.length || index.positions[first] !== at) {
|
|
2400
2463
|
continue;
|
|
2401
2464
|
}
|
|
2465
|
+
if (page !== void 0 && index.refs[first].pageIndex !== page) continue;
|
|
2466
|
+
const rotated = at >= index.rotatedFrom;
|
|
2467
|
+
const limit = Math.min(
|
|
2468
|
+
page !== void 0 || rotated ? pageEnd(index, first) : index.stream.length,
|
|
2469
|
+
rotated ? index.stream.length : index.rotatedFrom
|
|
2470
|
+
);
|
|
2471
|
+
if (at + MIN_PARTIAL_PREFIX > limit) continue;
|
|
2402
2472
|
let length = MIN_PARTIAL_PREFIX;
|
|
2403
|
-
while (length < needle.length && index.stream.charCodeAt(at + length) === needle.charCodeAt(length)) {
|
|
2473
|
+
while (length < needle.length && at + length < limit && index.stream.charCodeAt(at + length) === needle.charCodeAt(length)) {
|
|
2404
2474
|
length++;
|
|
2405
2475
|
}
|
|
2476
|
+
if (region && !touchesRegion(partsOf(index, at, at + length), region)) {
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2406
2479
|
if (!best || length > best.length) best = { length, at };
|
|
2407
2480
|
}
|
|
2408
2481
|
return best;
|
|
2409
2482
|
}
|
|
2483
|
+
function pageEnd(index, from) {
|
|
2484
|
+
const page = index.refs[from].pageIndex;
|
|
2485
|
+
let i = from;
|
|
2486
|
+
while (i < index.refs.length && index.refs[i].pageIndex === page) i++;
|
|
2487
|
+
return i < index.positions.length ? index.positions[i] : index.stream.length;
|
|
2488
|
+
}
|
|
2489
|
+
var REGION_TOLERANCE_PT = 2;
|
|
2490
|
+
function touchesRegion(parts, region) {
|
|
2491
|
+
return parts.some(
|
|
2492
|
+
(part) => part.xMax >= region.xMin - REGION_TOLERANCE_PT && part.xMin <= region.xMax + REGION_TOLERANCE_PT && part.yMax >= region.yMin - REGION_TOLERANCE_PT && part.yMin <= region.yMax + REGION_TOLERANCE_PT
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2410
2495
|
function sameRow(a, b) {
|
|
2411
2496
|
const overlap = Math.min(a.yMax, b.yMax) - Math.max(a.yMin, b.yMin);
|
|
2412
2497
|
const shorter = Math.min(a.yMax - a.yMin, b.yMax - b.yMin);
|
|
@@ -2462,6 +2547,96 @@ function pageNumberRows(pages, claimed) {
|
|
|
2462
2547
|
)
|
|
2463
2548
|
);
|
|
2464
2549
|
}
|
|
2550
|
+
function insideRegion(occurrences, region) {
|
|
2551
|
+
let best;
|
|
2552
|
+
for (const o of occurrences) {
|
|
2553
|
+
let inside = 0;
|
|
2554
|
+
let total = 0;
|
|
2555
|
+
for (const part of o.parts) {
|
|
2556
|
+
const w = Math.min(part.xMax, region.xMax) - Math.max(part.xMin, region.xMin);
|
|
2557
|
+
const h = Math.min(part.yMax, region.yMax) - Math.max(part.yMin, region.yMin);
|
|
2558
|
+
if (w > 0 && h > 0) inside += w * h;
|
|
2559
|
+
total += (part.xMax - part.xMin) * (part.yMax - part.yMin);
|
|
2560
|
+
}
|
|
2561
|
+
const share = total > 0 ? inside / total : 0;
|
|
2562
|
+
if (share > 0 && (!best || share > best.share)) {
|
|
2563
|
+
best = { occurrence: o, share };
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
return best?.occurrence;
|
|
2567
|
+
}
|
|
2568
|
+
function overprintedOccurrence(index, page, region, needle, claimed) {
|
|
2569
|
+
const words2 = index.pages[page]?.words;
|
|
2570
|
+
if (!words2) return void 0;
|
|
2571
|
+
const fragmentOf = /* @__PURE__ */ new Map();
|
|
2572
|
+
index.refs.forEach((ref, i) => {
|
|
2573
|
+
if (ref.pageIndex !== page || index.fragments[i] === "") return;
|
|
2574
|
+
const w = words2[ref.word];
|
|
2575
|
+
const centre = (w.xMin + w.xMax) / 2;
|
|
2576
|
+
if (centre < region.xMin || centre > region.xMax) return;
|
|
2577
|
+
if (w.yMax < region.yMin) return;
|
|
2578
|
+
fragmentOf.set(ref.word, i);
|
|
2579
|
+
});
|
|
2580
|
+
const sequence = groupRows(words2, [...fragmentOf.keys()]).flatMap(
|
|
2581
|
+
(row) => [...row.words].sort((a, b) => words2[a].xMin - words2[b].xMin)
|
|
2582
|
+
);
|
|
2583
|
+
let reach = /* @__PURE__ */ new Map();
|
|
2584
|
+
let best;
|
|
2585
|
+
for (const word of sequence) {
|
|
2586
|
+
const i = fragmentOf.get(word);
|
|
2587
|
+
const text = index.fragments[i];
|
|
2588
|
+
const usable = !claimed(index.positions[i]);
|
|
2589
|
+
const next = /* @__PURE__ */ new Map();
|
|
2590
|
+
const offer = (consumed, path9) => {
|
|
2591
|
+
const held = next.get(consumed);
|
|
2592
|
+
if (!held || path9.skips < held.skips) next.set(consumed, path9);
|
|
2593
|
+
};
|
|
2594
|
+
const inside = words2[word].yMin + words2[word].yMax <= 2 * region.yMax && words2[word].yMin + words2[word].yMax >= 2 * region.yMin;
|
|
2595
|
+
if (usable && inside && needle.startsWith(text)) {
|
|
2596
|
+
offer(text.length, { skips: 0, words: [word] });
|
|
2597
|
+
}
|
|
2598
|
+
for (const [consumed, path9] of reach) {
|
|
2599
|
+
if (usable && needle.startsWith(text, consumed)) {
|
|
2600
|
+
offer(consumed + text.length, {
|
|
2601
|
+
skips: path9.skips,
|
|
2602
|
+
words: [...path9.words, word]
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
offer(consumed, { skips: path9.skips + 1, words: path9.words });
|
|
2606
|
+
}
|
|
2607
|
+
const done = next.get(needle.length);
|
|
2608
|
+
if (done && (!best || done.skips < best.skips)) best = done;
|
|
2609
|
+
next.delete(needle.length);
|
|
2610
|
+
reach = next;
|
|
2611
|
+
}
|
|
2612
|
+
if (!best || best.skips > best.words.length) return void 0;
|
|
2613
|
+
const first = fragmentOf.get(best.words[0]);
|
|
2614
|
+
const part = {
|
|
2615
|
+
pageIndex: page,
|
|
2616
|
+
words: best.words,
|
|
2617
|
+
xMin: Math.min(...best.words.map((w) => words2[w].xMin)),
|
|
2618
|
+
yMin: Math.min(...best.words.map((w) => words2[w].yMin)),
|
|
2619
|
+
xMax: Math.max(...best.words.map((w) => words2[w].xMax)),
|
|
2620
|
+
yMax: Math.max(...best.words.map((w) => words2[w].yMax))
|
|
2621
|
+
};
|
|
2622
|
+
const at = index.positions[first];
|
|
2623
|
+
return {
|
|
2624
|
+
occurrence: {
|
|
2625
|
+
at,
|
|
2626
|
+
end: at,
|
|
2627
|
+
pageIndex: page,
|
|
2628
|
+
endPageIndex: page,
|
|
2629
|
+
parts: [part]
|
|
2630
|
+
},
|
|
2631
|
+
claims: best.words.map((w) => {
|
|
2632
|
+
const i = fragmentOf.get(w);
|
|
2633
|
+
return {
|
|
2634
|
+
at: index.positions[i],
|
|
2635
|
+
end: index.positions[i] + index.fragments[i].length
|
|
2636
|
+
};
|
|
2637
|
+
})
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2465
2640
|
function assignInventory(pages, inventory) {
|
|
2466
2641
|
const results = /* @__PURE__ */ new Map();
|
|
2467
2642
|
const chromeWords = /* @__PURE__ */ new Set();
|
|
@@ -2501,7 +2676,8 @@ function assignInventory(pages, inventory) {
|
|
|
2501
2676
|
for (const row of pageNumberRows(pages, chromeWords))
|
|
2502
2677
|
claimRow(row.pageIndex, row.box);
|
|
2503
2678
|
const body = indexDocument(pages, chromeWords);
|
|
2504
|
-
const
|
|
2679
|
+
const claims = [];
|
|
2680
|
+
const claimsOver = (o) => claims.filter((claim) => o.at < claim.end && claim.at < o.end);
|
|
2505
2681
|
let cursor = 0;
|
|
2506
2682
|
for (const entry of inventory) {
|
|
2507
2683
|
if (entry.repeats) continue;
|
|
@@ -2511,10 +2687,31 @@ function assignInventory(pages, inventory) {
|
|
|
2511
2687
|
results.set(entry, { entry, needle, status: "skipped", occurrences: [] });
|
|
2512
2688
|
continue;
|
|
2513
2689
|
}
|
|
2514
|
-
let all = findOccurrences(body, segments)
|
|
2690
|
+
let all = findOccurrences(body, segments).filter(
|
|
2691
|
+
(o) => (entry.page === void 0 || o.pageIndex === entry.page && o.endPageIndex === entry.page) && (!entry.region || touchesRegion(o.parts, entry.region))
|
|
2692
|
+
);
|
|
2515
2693
|
let partial;
|
|
2694
|
+
let sparse;
|
|
2695
|
+
if (all.length === 0 && segments.length === 1 && entry.page !== void 0 && entry.region) {
|
|
2696
|
+
const overprinted = overprintedOccurrence(
|
|
2697
|
+
body,
|
|
2698
|
+
entry.page,
|
|
2699
|
+
entry.region,
|
|
2700
|
+
needle,
|
|
2701
|
+
(offset) => claims.some((claim) => claim.at <= offset && offset < claim.end)
|
|
2702
|
+
);
|
|
2703
|
+
if (overprinted) {
|
|
2704
|
+
all = [overprinted.occurrence];
|
|
2705
|
+
sparse = overprinted.claims;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2516
2708
|
if (all.length === 0 && segments.length === 1 && !entry.optional) {
|
|
2517
|
-
const prefix = longestRenderedPrefix(
|
|
2709
|
+
const prefix = longestRenderedPrefix(
|
|
2710
|
+
body,
|
|
2711
|
+
needle,
|
|
2712
|
+
entry.page,
|
|
2713
|
+
entry.region
|
|
2714
|
+
);
|
|
2518
2715
|
if (prefix) {
|
|
2519
2716
|
all = [occurrence(body, prefix.at, prefix.at + prefix.length)];
|
|
2520
2717
|
partial = { matchedChars: prefix.length, totalChars: needle.length };
|
|
@@ -2529,18 +2726,22 @@ function assignInventory(pages, inventory) {
|
|
|
2529
2726
|
});
|
|
2530
2727
|
continue;
|
|
2531
2728
|
}
|
|
2532
|
-
let free = all.filter((o) =>
|
|
2729
|
+
let free = all.filter((o) => claimsOver(o).length === 0);
|
|
2533
2730
|
if (free.length === 0 && !entry.optional) {
|
|
2534
|
-
const held = all.find((o) =>
|
|
2731
|
+
const held = all.find((o) => {
|
|
2732
|
+
const over = claimsOver(o);
|
|
2733
|
+
return over.length > 0 && over.every((claim) => claim.entry.optional);
|
|
2734
|
+
});
|
|
2535
2735
|
if (held) {
|
|
2536
|
-
const
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2736
|
+
for (const claim of claimsOver(held)) {
|
|
2737
|
+
claims.splice(claims.indexOf(claim), 1);
|
|
2738
|
+
results.set(claim.entry, {
|
|
2739
|
+
entry: claim.entry,
|
|
2740
|
+
needle: results.get(claim.entry)?.needle ?? "",
|
|
2741
|
+
status: "skipped",
|
|
2742
|
+
occurrences: []
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2544
2745
|
free = [held];
|
|
2545
2746
|
}
|
|
2546
2747
|
}
|
|
@@ -2553,9 +2754,10 @@ function assignInventory(pages, inventory) {
|
|
|
2553
2754
|
});
|
|
2554
2755
|
continue;
|
|
2555
2756
|
}
|
|
2556
|
-
const chosen = free.find((o) => o.at >= cursor) ?? free[0];
|
|
2557
|
-
|
|
2558
|
-
|
|
2757
|
+
const chosen = (entry.region && insideRegion(free, entry.region)) ?? free.find((o) => o.at >= cursor) ?? free[0];
|
|
2758
|
+
for (const range of sparse ?? [chosen])
|
|
2759
|
+
claims.push({ at: range.at, end: range.end, entry });
|
|
2760
|
+
if (chosen.at < body.rotatedFrom) cursor = chosen.at;
|
|
2559
2761
|
results.set(entry, {
|
|
2560
2762
|
entry,
|
|
2561
2763
|
needle,
|
|
@@ -2587,6 +2789,11 @@ function renderedDraftFor(fact) {
|
|
|
2587
2789
|
}
|
|
2588
2790
|
var VISIBLE_SPILL_PT = 2;
|
|
2589
2791
|
var OVERLAP_FRACTION = 0.3;
|
|
2792
|
+
var WORD_BOX_SLACK = 0.2;
|
|
2793
|
+
function inkBand(word) {
|
|
2794
|
+
const slack = WORD_BOX_SLACK * Math.max(0, word.yMax - word.yMin);
|
|
2795
|
+
return { yMin: word.yMin + slack, yMax: word.yMax - slack };
|
|
2796
|
+
}
|
|
2590
2797
|
function finding(draft) {
|
|
2591
2798
|
const { mapping, page, context, ...rest } = draft;
|
|
2592
2799
|
return {
|
|
@@ -2602,12 +2809,15 @@ function round(value) {
|
|
|
2602
2809
|
return Math.round(value * 10) / 10;
|
|
2603
2810
|
}
|
|
2604
2811
|
function intersects(a, b) {
|
|
2812
|
+
const inkA = inkBand(a);
|
|
2813
|
+
const inkB = inkBand(b);
|
|
2605
2814
|
const w = Math.min(a.xMax, b.xMax) - Math.max(a.xMin, b.xMin);
|
|
2606
|
-
const h = Math.min(
|
|
2815
|
+
const h = Math.min(inkA.yMax, inkB.yMax) - Math.max(inkA.yMin, inkB.yMin);
|
|
2607
2816
|
return w > 0 && h > 0 ? w * h : 0;
|
|
2608
2817
|
}
|
|
2609
2818
|
function area(w) {
|
|
2610
|
-
|
|
2819
|
+
const ink = inkBand(w);
|
|
2820
|
+
return Math.max(0, w.xMax - w.xMin) * Math.max(0, ink.yMax - ink.yMin);
|
|
2611
2821
|
}
|
|
2612
2822
|
function excerpt(text, max = 60) {
|
|
2613
2823
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
@@ -2723,14 +2933,51 @@ function draftRenderedFindings(input) {
|
|
|
2723
2933
|
}
|
|
2724
2934
|
for (const match of matches) {
|
|
2725
2935
|
const box = match.entry.box;
|
|
2936
|
+
const region = match.entry.region;
|
|
2726
2937
|
if (!box || match.status !== "mapped") continue;
|
|
2727
2938
|
for (const o of match.occurrences) {
|
|
2939
|
+
const boxHeight = box.heightPt;
|
|
2940
|
+
const byEdges = region !== void 0 && boxHeight !== void 0;
|
|
2941
|
+
if (region && boxHeight !== void 0) {
|
|
2942
|
+
const words2 = o.parts.flatMap(
|
|
2943
|
+
(part) => part.words.map((w) => pages[part.pageIndex].words[w])
|
|
2944
|
+
);
|
|
2945
|
+
const inkTop = Math.min(...words2.map((w) => inkBand(w).yMin));
|
|
2946
|
+
const inkBottom = Math.max(...words2.map((w) => inkBand(w).yMax));
|
|
2947
|
+
const edges = [
|
|
2948
|
+
{ edge: "bottom", over: inkBottom - region.yMax, at: inkBottom },
|
|
2949
|
+
{ edge: "top", over: region.yMin - inkTop, at: inkTop }
|
|
2950
|
+
].sort((a, b) => b.over - a.over);
|
|
2951
|
+
const [worst2] = edges;
|
|
2952
|
+
if (worst2.over > VISIBLE_SPILL_PT) {
|
|
2953
|
+
findings.push(
|
|
2954
|
+
finding({
|
|
2955
|
+
ruleId: "rendered/spill",
|
|
2956
|
+
mapping: "mapped",
|
|
2957
|
+
page: o.pageIndex + 1,
|
|
2958
|
+
path: match.entry.path,
|
|
2959
|
+
message: `"${excerpt(match.entry.text)}" runs ${round(worst2.over)} pt past the ${worst2.edge} of its ${round(boxHeight)} pt box on page ${o.pageIndex + 1}.`,
|
|
2960
|
+
suggestion: "Shorten the text, reduce its size, or enlarge the box; the renderer let it spill past the edge the author drew.",
|
|
2961
|
+
evidence: {
|
|
2962
|
+
summary: "Rendered ink against the edge of the declared box",
|
|
2963
|
+
actual: round(worst2.at),
|
|
2964
|
+
expected: round(
|
|
2965
|
+
worst2.edge === "bottom" ? region.yMax : region.yMin
|
|
2966
|
+
),
|
|
2967
|
+
unit: "pt",
|
|
2968
|
+
values: { edge: worst2.edge, overPt: round(worst2.over) }
|
|
2969
|
+
}
|
|
2970
|
+
})
|
|
2971
|
+
);
|
|
2972
|
+
continue;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2728
2975
|
const extent = unionBox(o.parts);
|
|
2729
2976
|
const rendered = {
|
|
2730
2977
|
heightPt: extent.yMax - extent.yMin,
|
|
2731
2978
|
widthPt: extent.xMax - extent.xMin
|
|
2732
2979
|
};
|
|
2733
|
-
const worst = ["heightPt", "widthPt"].filter((axis) => box[axis] !== void 0).map((axis) => ({
|
|
2980
|
+
const worst = ["heightPt", "widthPt"].filter((axis) => box[axis] !== void 0).filter((axis) => !(byEdges && axis === "heightPt")).map((axis) => ({
|
|
2734
2981
|
axis,
|
|
2735
2982
|
declared: box[axis],
|
|
2736
2983
|
rendered: rendered[axis],
|
|
@@ -2975,6 +3222,79 @@ function draftRenderedFindings(input) {
|
|
|
2975
3222
|
})
|
|
2976
3223
|
);
|
|
2977
3224
|
}
|
|
3225
|
+
const TABLE_CELL = /^(.*)\/props\/(?:slots\/)?columns\/(\d+)\/(?:header|cells\/(\d+))(?:\/.*)?$/;
|
|
3226
|
+
const tables = /* @__PURE__ */ new Map();
|
|
3227
|
+
for (const match of matches) {
|
|
3228
|
+
if (match.entry.role !== "table-header" && match.entry.role !== "table-cell" || match.status !== "mapped")
|
|
3229
|
+
continue;
|
|
3230
|
+
const parsed = TABLE_CELL.exec(match.entry.path);
|
|
3231
|
+
if (!parsed) continue;
|
|
3232
|
+
const [, table, column, row] = parsed;
|
|
3233
|
+
const rowIndex = row === void 0 ? -1 : Number(row);
|
|
3234
|
+
const entry = tables.get(table) ?? {
|
|
3235
|
+
pages: /* @__PURE__ */ new Map(),
|
|
3236
|
+
paths: /* @__PURE__ */ new Map()
|
|
3237
|
+
};
|
|
3238
|
+
for (const occurrence2 of match.occurrences)
|
|
3239
|
+
for (const part of occurrence2.parts) {
|
|
3240
|
+
const rows = entry.pages.get(part.pageIndex) ?? /* @__PURE__ */ new Set();
|
|
3241
|
+
rows.add(rowIndex);
|
|
3242
|
+
entry.pages.set(part.pageIndex, rows);
|
|
3243
|
+
}
|
|
3244
|
+
const held = entry.paths.get(rowIndex);
|
|
3245
|
+
if (!held || Number(column) < held.column)
|
|
3246
|
+
entry.paths.set(rowIndex, {
|
|
3247
|
+
column: Number(column),
|
|
3248
|
+
path: match.entry.path
|
|
3249
|
+
});
|
|
3250
|
+
tables.set(table, entry);
|
|
3251
|
+
}
|
|
3252
|
+
for (const [table, { pages: onPage, paths }] of tables) {
|
|
3253
|
+
const ordered = [...onPage.keys()].sort((a, b) => a - b);
|
|
3254
|
+
if (ordered.length < 2) continue;
|
|
3255
|
+
const bodyRows = (page) => [...onPage.get(page) ?? []].filter((row) => row >= 0);
|
|
3256
|
+
const report = (kind, page, path9, message, suggestion) => findings.push(
|
|
3257
|
+
finding({
|
|
3258
|
+
ruleId: "rendered/table-split",
|
|
3259
|
+
mapping: "mapped",
|
|
3260
|
+
page: page + 1,
|
|
3261
|
+
path: path9,
|
|
3262
|
+
message,
|
|
3263
|
+
suggestion,
|
|
3264
|
+
context: { kind, table }
|
|
3265
|
+
})
|
|
3266
|
+
);
|
|
3267
|
+
const firstPage = ordered[0];
|
|
3268
|
+
const lastPage = ordered[ordered.length - 1];
|
|
3269
|
+
if (onPage.get(firstPage).has(-1) && bodyRows(firstPage).length === 0) {
|
|
3270
|
+
report(
|
|
3271
|
+
"header-alone",
|
|
3272
|
+
firstPage,
|
|
3273
|
+
paths.get(-1).path,
|
|
3274
|
+
`The table's header is the only part of it on page ${firstPage + 1}; its rows start on the next page.`,
|
|
3275
|
+
"Keep the header with its first rows, or move the table so it starts on the next page."
|
|
3276
|
+
);
|
|
3277
|
+
continue;
|
|
3278
|
+
}
|
|
3279
|
+
const opening = bodyRows(firstPage);
|
|
3280
|
+
const closing = bodyRows(lastPage);
|
|
3281
|
+
if (opening.length === 1)
|
|
3282
|
+
report(
|
|
3283
|
+
"orphan-row",
|
|
3284
|
+
firstPage,
|
|
3285
|
+
paths.get(opening[0]).path,
|
|
3286
|
+
`The table leaves one row alone at the foot of page ${firstPage + 1}.`,
|
|
3287
|
+
"Keep the table together on one page, or edit the rows above it so the break falls elsewhere."
|
|
3288
|
+
);
|
|
3289
|
+
else if (closing.length === 1)
|
|
3290
|
+
report(
|
|
3291
|
+
"widow-row",
|
|
3292
|
+
lastPage,
|
|
3293
|
+
paths.get(closing[0]).path,
|
|
3294
|
+
`The table leaves one row alone at the top of page ${lastPage + 1}.`,
|
|
3295
|
+
"Keep the table together on one page, or edit the rows above it so the break falls elsewhere."
|
|
3296
|
+
);
|
|
3297
|
+
}
|
|
2978
3298
|
}
|
|
2979
3299
|
const inventory = {
|
|
2980
3300
|
mapped: 0,
|
|
@@ -3071,6 +3391,93 @@ function analyzeRenderedDocument(input, options = {}) {
|
|
|
3071
3391
|
};
|
|
3072
3392
|
}
|
|
3073
3393
|
|
|
3394
|
+
// src/rendered-inventory.ts
|
|
3395
|
+
function renderedInventoryFromFacts(format, facts) {
|
|
3396
|
+
const entries = [];
|
|
3397
|
+
for (const fact of facts) {
|
|
3398
|
+
if (format === "docx" && fact.kind === "docx/text") {
|
|
3399
|
+
const frame = fact.frame;
|
|
3400
|
+
entries.push({
|
|
3401
|
+
path: fact.path,
|
|
3402
|
+
text: String(fact.text),
|
|
3403
|
+
role: fact.role,
|
|
3404
|
+
...typeof fact.level === "number" && { level: fact.level },
|
|
3405
|
+
...fact.repeats === true && { repeats: true },
|
|
3406
|
+
...fact.optional === true && { optional: true },
|
|
3407
|
+
...frame && {
|
|
3408
|
+
box: {
|
|
3409
|
+
...frame.widthPt !== void 0 && { widthPt: frame.widthPt },
|
|
3410
|
+
...frame.heightPt !== void 0 && { heightPt: frame.heightPt }
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
} else if (format === "pptx" && fact.kind === "pptx/text") {
|
|
3415
|
+
if (fact.slideHidden === true) continue;
|
|
3416
|
+
const turned = typeof fact.rotationDeg === "number" && (fact.rotationDeg % 360 + 360) % 360 !== 0;
|
|
3417
|
+
if (turned) {
|
|
3418
|
+
entries.push({
|
|
3419
|
+
path: fact.path,
|
|
3420
|
+
text: String(fact.text),
|
|
3421
|
+
role: "slide-text",
|
|
3422
|
+
optional: true,
|
|
3423
|
+
...typeof fact.page === "number" && { page: fact.page }
|
|
3424
|
+
});
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
const { boxXPt: x, boxYPt: y } = fact;
|
|
3428
|
+
const widthPt = fact.boxWidthPt;
|
|
3429
|
+
const heightPt = fact.boxHeightPt;
|
|
3430
|
+
entries.push({
|
|
3431
|
+
path: fact.path,
|
|
3432
|
+
text: String(fact.text),
|
|
3433
|
+
role: "slide-text",
|
|
3434
|
+
// A slide is its own page and its boxes are where the deck put
|
|
3435
|
+
// them, so the text is looked for there before anywhere else.
|
|
3436
|
+
...typeof fact.page === "number" && { page: fact.page },
|
|
3437
|
+
...typeof x === "number" && typeof y === "number" && typeof widthPt === "number" && typeof heightPt === "number" && {
|
|
3438
|
+
region: {
|
|
3439
|
+
xMin: x,
|
|
3440
|
+
yMin: y,
|
|
3441
|
+
xMax: x + widthPt,
|
|
3442
|
+
yMax: y + heightPt
|
|
3443
|
+
}
|
|
3444
|
+
},
|
|
3445
|
+
...(typeof widthPt === "number" || typeof heightPt === "number") && {
|
|
3446
|
+
box: {
|
|
3447
|
+
...typeof widthPt === "number" && { widthPt },
|
|
3448
|
+
...typeof heightPt === "number" && { heightPt }
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
return entries;
|
|
3455
|
+
}
|
|
3456
|
+
function requestedFontsFromFacts(format, facts, resolvedFonts) {
|
|
3457
|
+
const kind = format === "docx" ? "docx/font-family" : "pptx/font-family";
|
|
3458
|
+
const declared = new Set(
|
|
3459
|
+
resolvedFonts.filter((font) => font.declared).map((font) => font.family.trim().toLowerCase())
|
|
3460
|
+
);
|
|
3461
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3462
|
+
const requested = [];
|
|
3463
|
+
const add = (family, path9) => {
|
|
3464
|
+
if (typeof family !== "string" || family.trim() === "") return;
|
|
3465
|
+
const key = family.trim().toLowerCase();
|
|
3466
|
+
if (seen.has(key)) return;
|
|
3467
|
+
seen.add(key);
|
|
3468
|
+
requested.push({
|
|
3469
|
+
family: family.trim(),
|
|
3470
|
+
...path9 && { path: path9 },
|
|
3471
|
+
...declared.has(key) && { declared: true }
|
|
3472
|
+
});
|
|
3473
|
+
};
|
|
3474
|
+
for (const fact of facts) {
|
|
3475
|
+
if (fact.kind === kind) add(fact.family, fact.path);
|
|
3476
|
+
}
|
|
3477
|
+
for (const font of resolvedFonts) if (font.declared) add(font.family);
|
|
3478
|
+
return requested;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3074
3481
|
// src/block-matrix.ts
|
|
3075
3482
|
import {
|
|
3076
3483
|
blockDependencies,
|
|
@@ -3517,7 +3924,9 @@ export {
|
|
|
3517
3924
|
pdffontsAvailable,
|
|
3518
3925
|
pdftotextAvailable,
|
|
3519
3926
|
readingOrder,
|
|
3927
|
+
renderedInventoryFromFacts,
|
|
3520
3928
|
reportCaseDocument,
|
|
3929
|
+
requestedFontsFromFacts,
|
|
3521
3930
|
runWithDiagnosticSink,
|
|
3522
3931
|
stderrDiagnosticSink,
|
|
3523
3932
|
widestNumber
|