@json-to-office/jto-ops 7.1.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,
@@ -2346,10 +2356,12 @@ function groupRows(words2, indices) {
2346
2356
  }
2347
2357
  return rows;
2348
2358
  }
2359
+ var COLUMN_EDGE_TOLERANCE_PT = 0.05;
2360
+ var WORD_SPACE_TOLERANCE_PT = 0.1;
2349
2361
  function uprightOrder(words2, indices) {
2350
2362
  const grouped = groupRows(words2, indices);
2351
2363
  const rows = grouped.map((row) => row.words);
2352
- const fragmentsOf = (row) => {
2364
+ const fragmentsOf = (row, onEdge) => {
2353
2365
  const sorted = [...row].sort((a, b) => words2[a].xMin - words2[b].xMin);
2354
2366
  const gaps = sorted.slice(1).map((i, n) => {
2355
2367
  const height = words2[i].yMax - words2[i].yMin;
@@ -2360,7 +2372,7 @@ function uprightOrder(words2, indices) {
2360
2372
  const typical = spaces.length > 1 ? median(spaces) : void 0;
2361
2373
  const isCellBoundary = (n) => {
2362
2374
  const g = gaps[n - 1];
2363
- 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]]);
2364
2376
  };
2365
2377
  const out = [];
2366
2378
  sorted.forEach((i, n) => {
@@ -2373,7 +2385,26 @@ function uprightOrder(words2, indices) {
2373
2385
  });
2374
2386
  return out;
2375
2387
  };
2376
- 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
+ );
2377
2408
  const boxes = grouped;
2378
2409
  const order = [];
2379
2410
  const read = /* @__PURE__ */ new Set();
@@ -3176,6 +3207,53 @@ function draftRenderedFindings(input) {
3176
3207
  })
3177
3208
  );
3178
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
+ });
3179
3257
  let substituted = 0;
3180
3258
  if (input.fonts && input.fonts.length > 0 && input.requestedFonts) {
3181
3259
  const seen = /* @__PURE__ */ new Set();
@@ -3516,6 +3594,9 @@ function renderedInventoryFromFacts(format, facts) {
3516
3594
  ...typeof fact.level === "number" && { level: fact.level },
3517
3595
  ...fact.repeats === true && { repeats: true },
3518
3596
  ...fact.optional === true && { optional: true },
3597
+ ...typeof fact.unrendered === "string" && {
3598
+ unrendered: fact.unrendered
3599
+ },
3519
3600
  ...frame && {
3520
3601
  box: {
3521
3602
  ...frame.widthPt !== void 0 && { widthPt: frame.widthPt },