@json-to-office/jto-ops 6.2.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.js CHANGED
@@ -2184,43 +2184,77 @@ function indexDocument(pages, exclude = /* @__PURE__ */ new Set()) {
2184
2184
  const fragments = [];
2185
2185
  const positions = [];
2186
2186
  let cursor = 0;
2187
- pages.forEach((page, pageIndex) => {
2188
- for (const index of readingOrder(page.words)) {
2189
- const word = page.words[index];
2190
- const key = `${pageIndex}:${index}`;
2191
- const fragment = exclude.has(key) ? "" : normalizeForMatch(word.text);
2192
- refs.push({ pageIndex, word: index });
2193
- fragments.push(fragment);
2194
- positions.push(cursor);
2195
- cursor += fragment.length;
2196
- }
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);
2197
2199
  });
2198
- return { pages, refs, fragments, positions, stream: fragments.join("") };
2200
+ const rotatedFrom = cursor;
2201
+ orders.forEach((order, pageIndex) => {
2202
+ for (const index of order.rotated) push(pageIndex, index);
2203
+ });
2204
+ return {
2205
+ pages,
2206
+ refs,
2207
+ fragments,
2208
+ positions,
2209
+ stream: fragments.join(""),
2210
+ rotatedFrom
2211
+ };
2199
2212
  }
2200
- 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) {
2201
2228
  const rotated = new Set(
2202
2229
  words2.map((_, i) => i).filter((i) => {
2203
2230
  const w = words2[i];
2204
2231
  return w.text.length > 2 && w.yMax - w.yMin > 1.5 * (w.xMax - w.xMin);
2205
2232
  })
2206
2233
  );
2207
- const upright = uprightOrder(
2208
- words2,
2209
- words2.map((_, i) => i).filter((i) => !rotated.has(i))
2210
- );
2211
- const order = [];
2212
- let next = 0;
2213
- for (let i = 0; i < words2.length; i++) {
2214
- if (rotated.has(i)) order.push(i);
2215
- else order.push(upright[next++]);
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);
2216
2244
  }
2217
- return order;
2245
+ return rotated;
2218
2246
  }
2219
2247
  function median(values) {
2220
2248
  const sorted = [...values].sort((a, b) => a - b);
2221
2249
  const mid = sorted.length >> 1;
2222
2250
  return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
2223
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
+ }
2224
2258
  function groupRows(words2, indices) {
2225
2259
  const rows = [];
2226
2260
  const byY = [...indices].sort(
@@ -2229,7 +2263,8 @@ function groupRows(words2, indices) {
2229
2263
  for (const i of byY) {
2230
2264
  const word = words2[i];
2231
2265
  const row = rows[rows.length - 1];
2232
- if (row && sameRow(words2[row.words[0]], word)) {
2266
+ const peer = row?.words.find((w) => comparableSize(words2[w], word));
2267
+ if (row && peer !== void 0 && sameRow(words2[peer], word)) {
2233
2268
  row.words.push(i);
2234
2269
  row.yMin = Math.min(row.yMin, word.yMin);
2235
2270
  row.yMax = Math.max(row.yMax, word.yMax);
@@ -2267,20 +2302,25 @@ function uprightOrder(words2, indices) {
2267
2302
  const fragments = rows.map(fragmentsOf);
2268
2303
  const boxes = grouped;
2269
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
+ };
2270
2315
  let cluster = null;
2271
2316
  const flush = () => {
2272
2317
  if (!cluster) return;
2273
2318
  if (cluster.columns.length < 2) {
2274
2319
  for (const r of cluster.rows)
2275
- 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);
2276
2321
  } else {
2277
2322
  cluster.columns.sort((a, b) => a.xMin - b.xMin);
2278
- for (const column of cluster.columns) {
2279
- column.fragments.sort(
2280
- (a, b) => words2[a.words[0]].yMin - words2[b.words[0]].yMin || a.xMin - b.xMin
2281
- );
2282
- for (const f of column.fragments) order.push(...f.words);
2283
- }
2323
+ for (const column of cluster.columns) readColumn(column);
2284
2324
  }
2285
2325
  cluster = null;
2286
2326
  };
@@ -2294,6 +2334,16 @@ function uprightOrder(words2, indices) {
2294
2334
  if (!adjacent || wide) flush();
2295
2335
  if (!cluster) cluster = { rows: [], columns: [] };
2296
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
+ }
2297
2347
  for (const f of fragments[r]) {
2298
2348
  const hit = overlapping(cluster.columns, f)[0];
2299
2349
  if (hit) {
@@ -2361,6 +2411,7 @@ function occurrence(index, at, end) {
2361
2411
  const parts = partsOf(index, at, end);
2362
2412
  return {
2363
2413
  at,
2414
+ end,
2364
2415
  pageIndex: parts[0].pageIndex,
2365
2416
  endPageIndex: parts[parts.length - 1].pageIndex,
2366
2417
  parts
@@ -2386,7 +2437,11 @@ function findOccurrences(index, segments) {
2386
2437
  end = next + segments[s].length;
2387
2438
  }
2388
2439
  if (!complete || !alignedToFragments(index, at, end)) continue;
2389
- hits.push(occurrence(index, at, end));
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);
2390
2445
  searchFrom = end;
2391
2446
  }
2392
2447
  return hits;
@@ -2394,7 +2449,7 @@ function findOccurrences(index, segments) {
2394
2449
  var MIN_NEEDLE_LENGTH = 3;
2395
2450
  var MIN_PARTIAL_PREFIX = 16;
2396
2451
  var CHROME_BAND = 0.2;
2397
- function longestRenderedPrefix(index, needle) {
2452
+ function longestRenderedPrefix(index, needle, page, region) {
2398
2453
  if (needle.length < MIN_PARTIAL_PREFIX) return void 0;
2399
2454
  const anchor = needle.slice(0, MIN_PARTIAL_PREFIX);
2400
2455
  let best;
@@ -2407,14 +2462,36 @@ function longestRenderedPrefix(index, needle) {
2407
2462
  if (first >= index.fragments.length || index.positions[first] !== at) {
2408
2463
  continue;
2409
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;
2410
2472
  let length = MIN_PARTIAL_PREFIX;
2411
- 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)) {
2412
2474
  length++;
2413
2475
  }
2476
+ if (region && !touchesRegion(partsOf(index, at, at + length), region)) {
2477
+ continue;
2478
+ }
2414
2479
  if (!best || length > best.length) best = { length, at };
2415
2480
  }
2416
2481
  return best;
2417
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
+ }
2418
2495
  function sameRow(a, b) {
2419
2496
  const overlap = Math.min(a.yMax, b.yMax) - Math.max(a.yMin, b.yMin);
2420
2497
  const shorter = Math.min(a.yMax - a.yMin, b.yMax - b.yMin);
@@ -2470,6 +2547,96 @@ function pageNumberRows(pages, claimed) {
2470
2547
  )
2471
2548
  );
2472
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
+ }
2473
2640
  function assignInventory(pages, inventory) {
2474
2641
  const results = /* @__PURE__ */ new Map();
2475
2642
  const chromeWords = /* @__PURE__ */ new Set();
@@ -2509,7 +2676,8 @@ function assignInventory(pages, inventory) {
2509
2676
  for (const row of pageNumberRows(pages, chromeWords))
2510
2677
  claimRow(row.pageIndex, row.box);
2511
2678
  const body = indexDocument(pages, chromeWords);
2512
- const claimed = /* @__PURE__ */ new Map();
2679
+ const claims = [];
2680
+ const claimsOver = (o) => claims.filter((claim) => o.at < claim.end && claim.at < o.end);
2513
2681
  let cursor = 0;
2514
2682
  for (const entry of inventory) {
2515
2683
  if (entry.repeats) continue;
@@ -2519,10 +2687,31 @@ function assignInventory(pages, inventory) {
2519
2687
  results.set(entry, { entry, needle, status: "skipped", occurrences: [] });
2520
2688
  continue;
2521
2689
  }
2522
- 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
+ );
2523
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
+ }
2524
2708
  if (all.length === 0 && segments.length === 1 && !entry.optional) {
2525
- const prefix = longestRenderedPrefix(body, needle);
2709
+ const prefix = longestRenderedPrefix(
2710
+ body,
2711
+ needle,
2712
+ entry.page,
2713
+ entry.region
2714
+ );
2526
2715
  if (prefix) {
2527
2716
  all = [occurrence(body, prefix.at, prefix.at + prefix.length)];
2528
2717
  partial = { matchedChars: prefix.length, totalChars: needle.length };
@@ -2537,18 +2726,22 @@ function assignInventory(pages, inventory) {
2537
2726
  });
2538
2727
  continue;
2539
2728
  }
2540
- let free = all.filter((o) => !claimed.has(o.at));
2729
+ let free = all.filter((o) => claimsOver(o).length === 0);
2541
2730
  if (free.length === 0 && !entry.optional) {
2542
- const held = all.find((o) => claimed.get(o.at)?.optional);
2731
+ const held = all.find((o) => {
2732
+ const over = claimsOver(o);
2733
+ return over.length > 0 && over.every((claim) => claim.entry.optional);
2734
+ });
2543
2735
  if (held) {
2544
- const holder = claimed.get(held.at);
2545
- claimed.delete(held.at);
2546
- results.set(holder, {
2547
- entry: holder,
2548
- needle: results.get(holder)?.needle ?? "",
2549
- status: "skipped",
2550
- occurrences: []
2551
- });
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
+ }
2552
2745
  free = [held];
2553
2746
  }
2554
2747
  }
@@ -2561,9 +2754,10 @@ function assignInventory(pages, inventory) {
2561
2754
  });
2562
2755
  continue;
2563
2756
  }
2564
- const chosen = free.find((o) => o.at >= cursor) ?? free[0];
2565
- claimed.set(chosen.at, entry);
2566
- cursor = chosen.at;
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;
2567
2761
  results.set(entry, {
2568
2762
  entry,
2569
2763
  needle,
@@ -2595,6 +2789,11 @@ function renderedDraftFor(fact) {
2595
2789
  }
2596
2790
  var VISIBLE_SPILL_PT = 2;
2597
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
+ }
2598
2797
  function finding(draft) {
2599
2798
  const { mapping, page, context, ...rest } = draft;
2600
2799
  return {
@@ -2610,12 +2809,15 @@ function round(value) {
2610
2809
  return Math.round(value * 10) / 10;
2611
2810
  }
2612
2811
  function intersects(a, b) {
2812
+ const inkA = inkBand(a);
2813
+ const inkB = inkBand(b);
2613
2814
  const w = Math.min(a.xMax, b.xMax) - Math.max(a.xMin, b.xMin);
2614
- const h = Math.min(a.yMax, b.yMax) - Math.max(a.yMin, b.yMin);
2815
+ const h = Math.min(inkA.yMax, inkB.yMax) - Math.max(inkA.yMin, inkB.yMin);
2615
2816
  return w > 0 && h > 0 ? w * h : 0;
2616
2817
  }
2617
2818
  function area(w) {
2618
- return Math.max(0, w.xMax - w.xMin) * Math.max(0, w.yMax - w.yMin);
2819
+ const ink = inkBand(w);
2820
+ return Math.max(0, w.xMax - w.xMin) * Math.max(0, ink.yMax - ink.yMin);
2619
2821
  }
2620
2822
  function excerpt(text, max = 60) {
2621
2823
  const flat = text.replace(/\s+/g, " ").trim();
@@ -2731,14 +2933,51 @@ function draftRenderedFindings(input) {
2731
2933
  }
2732
2934
  for (const match of matches) {
2733
2935
  const box = match.entry.box;
2936
+ const region = match.entry.region;
2734
2937
  if (!box || match.status !== "mapped") continue;
2735
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
+ }
2736
2975
  const extent = unionBox(o.parts);
2737
2976
  const rendered = {
2738
2977
  heightPt: extent.yMax - extent.yMin,
2739
2978
  widthPt: extent.xMax - extent.xMin
2740
2979
  };
2741
- 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) => ({
2742
2981
  axis,
2743
2982
  declared: box[axis],
2744
2983
  rendered: rendered[axis],
@@ -3152,6 +3391,93 @@ function analyzeRenderedDocument(input, options = {}) {
3152
3391
  };
3153
3392
  }
3154
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
+
3155
3481
  // src/block-matrix.ts
3156
3482
  import {
3157
3483
  blockDependencies,
@@ -3598,7 +3924,9 @@ export {
3598
3924
  pdffontsAvailable,
3599
3925
  pdftotextAvailable,
3600
3926
  readingOrder,
3927
+ renderedInventoryFromFacts,
3601
3928
  reportCaseDocument,
3929
+ requestedFontsFromFacts,
3602
3930
  runWithDiagnosticSink,
3603
3931
  stderrDiagnosticSink,
3604
3932
  widestNumber