@json-to-office/jto-ops 7.0.1 → 7.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/index.d.ts CHANGED
@@ -475,7 +475,7 @@ declare function extractPdfFonts(pdfPath: string, options?: {
475
475
  * pass over it, memoised per fact, so eight rules cost one search.
476
476
  */
477
477
 
478
- type RenderedRuleId = 'rendered/clip' | 'rendered/spill' | 'rendered/overlap' | 'rendered/text-missing' | 'rendered/font-substituted' | 'rendered/empty-page' | 'rendered/heading-stranded' | 'rendered/paragraph-split' | 'rendered/table-split' | 'rendered/page-underfilled';
478
+ type RenderedRuleId = 'rendered/clip' | 'rendered/spill' | 'rendered/overlap' | 'rendered/text-missing' | 'rendered/placeholder' | 'rendered/font-substituted' | 'rendered/empty-page' | 'rendered/heading-stranded' | 'rendered/paragraph-split' | 'rendered/table-split' | 'rendered/page-underfilled';
479
479
  declare const RENDERED_QUALITY_RULES: QualityRulePack;
480
480
 
481
481
  /**
@@ -544,8 +544,11 @@ interface TextOccurrence {
544
544
  * row line by line across every cell, poppler 26 lists it cell by cell, and
545
545
  * an authored string in a wrapped cell only survives the second.
546
546
  *
547
- * Rows are grouped by vertical overlap and split into fragments at gaps
548
- * wider than half the line is tall. A row of two or more fragments opens a
547
+ * Rows are grouped by vertical overlap and split into fragments, a cell's
548
+ * line each: at gaps wider than half the line is tall, at gaps wider than
549
+ * the row's own spaces account for, and before a word that starts on the
550
+ * edge of a column the rows around it mark, where two cells ran together
551
+ * at a gap no wider than a space. A row of two or more fragments opens a
549
552
  * column block; the rows under it belong to the block while each of their
550
553
  * fragments sits inside one column (a right-aligned cell's second line
551
554
  * starts further right; a wrapped label's later lines are the only
@@ -644,7 +647,8 @@ declare function assignInventory<T extends InventoryEntry>(pages: readonly PdfTe
644
647
  * the preview PDF, the fonts the PDF embeds, and the document's authored
645
648
  * text inventory, and reports the defects only ink can show: text past the
646
649
  * page or its frame, words drawn over each other, text that never rendered
647
- * (fully clipped, or set in a place poppler cannot read), faces substituted
650
+ * (fully clipped, or set in a place poppler cannot read), a placeholder the
651
+ * renderer painted where it could not draw a component, faces substituted
648
652
  * on the way, pages with nothing on them, headings stranded at a page foot
649
653
  * and paragraphs split into a lone line.
650
654
  *
@@ -673,6 +677,12 @@ interface RenderedTextEntry extends InventoryEntry {
673
677
  widthPt?: number;
674
678
  heightPt?: number;
675
679
  };
680
+ /**
681
+ * Set when the entry is the placeholder a renderer paints for a component
682
+ * it could not draw there: the component's name. Authored text that quotes
683
+ * the placeholder's wording carries none.
684
+ */
685
+ unrendered?: string;
676
686
  }
677
687
  interface RequestedFont {
678
688
  family: string;
package/dist/index.js CHANGED
@@ -2113,7 +2113,9 @@ async function extractPdfFonts(pdfPath, options = {}) {
2113
2113
 
2114
2114
  // src/rendered-analysis.ts
2115
2115
  import {
2116
- QualityEngine
2116
+ QualityEngine,
2117
+ unrenderedComponentName,
2118
+ unrenderedComponentText
2117
2119
  } from "@json-to-office/quality";
2118
2120
 
2119
2121
  // src/rendered-rules.ts
@@ -2167,6 +2169,14 @@ var RENDERED_QUALITY_RULES = {
2167
2169
  category: "integrity",
2168
2170
  defaultSeverity: "warning"
2169
2171
  }),
2172
+ renderedRule({
2173
+ id: "rendered/placeholder",
2174
+ code: QUALITY_CODES.RENDERED_PLACEHOLDER,
2175
+ description: `DOCX: the renderer painted "[Unsupported component type: \u2026]" where it could not draw a component \u2014 a table cell holding one it cannot render. Mapped to the cell when the document puts it there; reported on its page, unmapped, when it came from somewhere the document does not show, such as a plugin's output.`,
2176
+ category: "integrity",
2177
+ defaultSeverity: "warning",
2178
+ formats: ["docx"]
2179
+ }),
2170
2180
  renderedRule({
2171
2181
  id: "rendered/font-substituted",
2172
2182
  code: QUALITY_CODES.RENDERED_FONT_SUBSTITUTED,
@@ -2283,10 +2293,26 @@ function linedOrder(words2, lines = []) {
2283
2293
  };
2284
2294
  }
2285
2295
  function rotatedWords(words2, lines) {
2296
+ const upright = words2.filter(
2297
+ (w) => w.text.trim() !== "" && w.xMax - w.xMin >= w.yMax - w.yMin
2298
+ );
2299
+ const standsInALine = (w) => {
2300
+ const height = w.yMax - w.yMin;
2301
+ return upright.some((other) => {
2302
+ if (other === w) return false;
2303
+ const overlap = Math.min(w.yMax, other.yMax) - Math.max(w.yMin, other.yMin);
2304
+ const shorter = Math.min(height, other.yMax - other.yMin);
2305
+ if (shorter <= 0 || overlap <= shorter / 2) return false;
2306
+ const ratio = height / (other.yMax - other.yMin);
2307
+ return ratio > 0.8 && ratio < 1.25;
2308
+ });
2309
+ };
2286
2310
  const rotated = new Set(
2287
2311
  words2.map((_, i) => i).filter((i) => {
2288
2312
  const w = words2[i];
2289
- return w.text.length > 2 && w.yMax - w.yMin > 1.5 * (w.xMax - w.xMin);
2313
+ if (w.text.length <= 2) return false;
2314
+ if (w.yMax - w.yMin <= 1.5 * (w.xMax - w.xMin)) return false;
2315
+ return !standsInALine(w);
2290
2316
  })
2291
2317
  );
2292
2318
  for (const line of lines) {
@@ -2330,10 +2356,12 @@ function groupRows(words2, indices) {
2330
2356
  }
2331
2357
  return rows;
2332
2358
  }
2359
+ var COLUMN_EDGE_TOLERANCE_PT = 0.05;
2360
+ var WORD_SPACE_TOLERANCE_PT = 0.1;
2333
2361
  function uprightOrder(words2, indices) {
2334
2362
  const grouped = groupRows(words2, indices);
2335
2363
  const rows = grouped.map((row) => row.words);
2336
- const fragmentsOf = (row) => {
2364
+ const fragmentsOf = (row, onEdge) => {
2337
2365
  const sorted = [...row].sort((a, b) => words2[a].xMin - words2[b].xMin);
2338
2366
  const gaps = sorted.slice(1).map((i, n) => {
2339
2367
  const height = words2[i].yMax - words2[i].yMin;
@@ -2344,7 +2372,7 @@ function uprightOrder(words2, indices) {
2344
2372
  const typical = spaces.length > 1 ? median(spaces) : void 0;
2345
2373
  const isCellBoundary = (n) => {
2346
2374
  const g = gaps[n - 1];
2347
- return g.wide || typical !== void 0 && g.gap > g.height / 3 && g.gap >= typical * 1.5;
2375
+ return g.wide || typical !== void 0 && g.gap > g.height / 3 && g.gap >= typical * 1.5 || onEdge !== void 0 && typical !== void 0 && spaces.length >= 3 && Math.abs(g.gap - typical) > WORD_SPACE_TOLERANCE_PT && onEdge(words2[sorted[n - 1]], words2[sorted[n]]);
2348
2376
  };
2349
2377
  const out = [];
2350
2378
  sorted.forEach((i, n) => {
@@ -2357,7 +2385,26 @@ function uprightOrder(words2, indices) {
2357
2385
  });
2358
2386
  return out;
2359
2387
  };
2360
- const fragments = rows.map(fragmentsOf);
2388
+ const byGaps = rows.map((row) => fragmentsOf(row));
2389
+ const onColumnEdge = (r, before, word) => {
2390
+ const x = word.xMin;
2391
+ for (const step of [-1, 1]) {
2392
+ for (let t = r + step; t >= 0 && t < rows.length; t += step) {
2393
+ const edge = byGaps[t].slice(1).some(
2394
+ (f) => Math.abs(f.xMin - x) <= COLUMN_EDGE_TOLERANCE_PT && before.xMax <= f.xMin
2395
+ );
2396
+ if (edge) return true;
2397
+ const across = rows[t].some(
2398
+ (i) => words2[i].xMin < x - COLUMN_EDGE_TOLERANCE_PT && words2[i].xMax > x + COLUMN_EDGE_TOLERANCE_PT
2399
+ );
2400
+ if (across) break;
2401
+ }
2402
+ }
2403
+ return false;
2404
+ };
2405
+ const fragments = rows.map(
2406
+ (row, r) => fragmentsOf(row, (before, word) => onColumnEdge(r, before, word))
2407
+ );
2361
2408
  const boxes = grouped;
2362
2409
  const order = [];
2363
2410
  const read = /* @__PURE__ */ new Set();
@@ -3160,6 +3207,53 @@ function draftRenderedFindings(input) {
3160
3207
  })
3161
3208
  );
3162
3209
  }
3210
+ const placeholderFinding = (name, page, path9, mapping) => {
3211
+ const text = name ? unrenderedComponentText(name) : "[Unsupported component type: \u2026]";
3212
+ return finding({
3213
+ ruleId: "rendered/placeholder",
3214
+ mapping,
3215
+ page,
3216
+ path: path9,
3217
+ message: `Page ${page} shows "${text}": the renderer could not draw ${name ? `a ${name}` : "a component"} there and painted this instead.`,
3218
+ suggestion: "Put only what the position renders there \u2014 a table cell takes a string, a paragraph, an image, a visual or a highcharts chart \u2014 or move the component out of the table; statistics side by side are a columns component.",
3219
+ evidence: {
3220
+ summary: "Renderer placeholder text in the PDF",
3221
+ actual: text
3222
+ },
3223
+ context: {
3224
+ kind: "unrendered-component",
3225
+ ...name && { component: name }
3226
+ }
3227
+ });
3228
+ };
3229
+ for (const match of matches) {
3230
+ const name = match.entry.unrendered;
3231
+ if (match.status !== "mapped" || name === void 0) continue;
3232
+ for (const o of match.occurrences) {
3233
+ findings.push(
3234
+ placeholderFinding(name, o.pageIndex + 1, match.entry.path, "mapped")
3235
+ );
3236
+ }
3237
+ }
3238
+ pages.forEach((page, pageIndex) => {
3239
+ page.words.forEach((word, index) => {
3240
+ if (!word.text.startsWith("[Unsupported")) return;
3241
+ if (ownerOf(pageIndex, index)) return;
3242
+ const phrase = [];
3243
+ for (const next of page.words.slice(index, index + 6)) {
3244
+ phrase.push(next.text);
3245
+ if (next.text.endsWith("]")) break;
3246
+ }
3247
+ findings.push(
3248
+ placeholderFinding(
3249
+ unrenderedComponentName(phrase.join(" ")),
3250
+ pageIndex + 1,
3251
+ "",
3252
+ "unmapped"
3253
+ )
3254
+ );
3255
+ });
3256
+ });
3163
3257
  let substituted = 0;
3164
3258
  if (input.fonts && input.fonts.length > 0 && input.requestedFonts) {
3165
3259
  const seen = /* @__PURE__ */ new Set();
@@ -3500,6 +3594,9 @@ function renderedInventoryFromFacts(format, facts) {
3500
3594
  ...typeof fact.level === "number" && { level: fact.level },
3501
3595
  ...fact.repeats === true && { repeats: true },
3502
3596
  ...fact.optional === true && { optional: true },
3597
+ ...typeof fact.unrendered === "string" && {
3598
+ unrendered: fact.unrendered
3599
+ },
3503
3600
  ...frame && {
3504
3601
  box: {
3505
3602
  ...frame.widthPt !== void 0 && { widthPt: frame.widthPt },