@heripo/document-processor 0.1.28 → 0.1.30

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.cjs CHANGED
@@ -41,6 +41,7 @@ __export(index_exports, {
41
41
  ChapterConverter: () => ChapterConverter,
42
42
  DocumentProcessor: () => DocumentProcessor,
43
43
  PAGE_NUMBER_PATTERN: () => PAGE_NUMBER_PATTERN,
44
+ PROCESSED_DOCUMENT_SCHEMA_VERSION: () => PROCESSED_DOCUMENT_SCHEMA_VERSION,
44
45
  PagePattern: () => PagePattern,
45
46
  PageRangeParseError: () => PageRangeParseError,
46
47
  PageRangeParser: () => PageRangeParser,
@@ -619,6 +620,52 @@ var RefResolver = class {
619
620
  buildIndex(items, _prefix) {
620
621
  return new Map(items.map((item) => [item.self_ref, item]));
621
622
  }
623
+ /**
624
+ * Check whether a Docling self reference exists in the indexed document.
625
+ */
626
+ hasRef(ref) {
627
+ const match = ref.match(/^#\/(\w+)\//);
628
+ if (!match) {
629
+ return false;
630
+ }
631
+ const collection = match[1];
632
+ if (collection === "texts") {
633
+ return this.textMap.has(ref);
634
+ }
635
+ if (collection === "pictures") {
636
+ return this.pictureMap.has(ref);
637
+ }
638
+ if (collection === "tables") {
639
+ return this.tableMap.has(ref);
640
+ }
641
+ if (collection === "groups") {
642
+ return this.groupMap.has(ref);
643
+ }
644
+ return false;
645
+ }
646
+ /**
647
+ * Assert that a Docling self reference exists in the indexed document.
648
+ */
649
+ assertRef(ref, context) {
650
+ if (this.hasRef(ref)) {
651
+ return;
652
+ }
653
+ const message = `[RefResolver] Missing source reference in ${context}: ${ref}`;
654
+ this.logger.warn(message);
655
+ throw new Error(message);
656
+ }
657
+ /**
658
+ * Assert that every Docling self reference exists in the indexed document.
659
+ */
660
+ assertRefs(refs, context) {
661
+ const missingRefs = refs.filter((ref) => !this.hasRef(ref));
662
+ if (missingRefs.length === 0) {
663
+ return;
664
+ }
665
+ const message = `[RefResolver] Missing source references in ${context}: ${missingRefs.join(", ")}`;
666
+ this.logger.warn(message);
667
+ throw new Error(message);
668
+ }
622
669
  /**
623
670
  * Resolve a $ref string to the actual item
624
671
  * @param ref - Reference string (e.g., "#/texts/0")
@@ -710,6 +757,7 @@ var IdGenerator = class {
710
757
  imageCounter = 0;
711
758
  tableCounter = 0;
712
759
  footnoteCounter = 0;
760
+ textBlockCounter = 0;
713
761
  /**
714
762
  * Generate a chapter ID
715
763
  * @returns A chapter ID in the format "ch-001"
@@ -742,6 +790,14 @@ var IdGenerator = class {
742
790
  this.footnoteCounter++;
743
791
  return `ftn-${this.padNumber(this.footnoteCounter)}`;
744
792
  }
793
+ /**
794
+ * Generate a text block ID
795
+ * @returns A text block ID in the format "txt-001"
796
+ */
797
+ generateTextBlockId() {
798
+ this.textBlockCounter++;
799
+ return `txt-${this.padNumber(this.textBlockCounter)}`;
800
+ }
745
801
  /**
746
802
  * Reset all counters to zero
747
803
  */
@@ -750,6 +806,7 @@ var IdGenerator = class {
750
806
  this.imageCounter = 0;
751
807
  this.tableCounter = 0;
752
808
  this.footnoteCounter = 0;
809
+ this.textBlockCounter = 0;
753
810
  }
754
811
  /**
755
812
  * Get current counter values (for testing/debugging)
@@ -759,7 +816,8 @@ var IdGenerator = class {
759
816
  chapter: this.chapterCounter,
760
817
  image: this.imageCounter,
761
818
  table: this.tableCounter,
762
- footnote: this.footnoteCounter
819
+ footnote: this.footnoteCounter,
820
+ textBlock: this.textBlockCounter
763
821
  };
764
822
  }
765
823
  /**
@@ -1065,6 +1123,7 @@ var ChapterConverter = class _ChapterConverter {
1065
1123
  title: _ChapterConverter.FRONT_MATTER_TITLE,
1066
1124
  pageNo: 1,
1067
1125
  level: 1,
1126
+ sourceRefs: [],
1068
1127
  textBlocks: [],
1069
1128
  imageIds: [],
1070
1129
  tableIds: [],
@@ -1084,6 +1143,7 @@ var ChapterConverter = class _ChapterConverter {
1084
1143
  title: TextCleaner.normalize(entry.title),
1085
1144
  pageNo: entry.pageNo,
1086
1145
  level: entry.level,
1146
+ sourceRefs: entry.sourceRefs ?? [],
1087
1147
  textBlocks: [],
1088
1148
  imageIds: [],
1089
1149
  tableIds: [],
@@ -1174,6 +1234,8 @@ var ChapterConverter = class _ChapterConverter {
1174
1234
  ).map((item) => {
1175
1235
  const pdfPageNo = item.prov?.[0]?.page_no ?? 1;
1176
1236
  return {
1237
+ id: this.idGenerator.generateTextBlockId(),
1238
+ sourceRef: item.self_ref,
1177
1239
  text: TextCleaner.normalize(item.text),
1178
1240
  pdfPageNo
1179
1241
  };
@@ -1278,6 +1340,84 @@ var ChapterConverter = class _ChapterConverter {
1278
1340
  };
1279
1341
 
1280
1342
  // src/converters/resource-converter.ts
1343
+ function getFiniteNumber(value) {
1344
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1345
+ }
1346
+ function getRowSpan(cell) {
1347
+ const startRow = getFiniteNumber(cell.start_row_offset_idx);
1348
+ const endRow = getFiniteNumber(cell.end_row_offset_idx);
1349
+ const offsetSpan = startRow !== void 0 && endRow !== void 0 ? endRow - startRow : 0;
1350
+ return Math.max(1, offsetSpan || getFiniteNumber(cell.row_span) || 1);
1351
+ }
1352
+ function getColSpan(cell) {
1353
+ const startCol = getFiniteNumber(cell.start_col_offset_idx);
1354
+ const endCol = getFiniteNumber(cell.end_col_offset_idx);
1355
+ const offsetSpan = startCol !== void 0 && endCol !== void 0 ? endCol - startCol : 0;
1356
+ return Math.max(1, offsetSpan || getFiniteNumber(cell.col_span) || 1);
1357
+ }
1358
+ function convertTableCell(cell) {
1359
+ return {
1360
+ text: cell.text ?? "",
1361
+ rowSpan: getRowSpan(cell),
1362
+ colSpan: getColSpan(cell),
1363
+ isHeader: cell.column_header || cell.row_header || false
1364
+ };
1365
+ }
1366
+ function getAnchorColumn(cell, rowIndex, colIndex) {
1367
+ const startRow = getFiniteNumber(cell.start_row_offset_idx) ?? rowIndex;
1368
+ const startCol = getFiniteNumber(cell.start_col_offset_idx) ?? colIndex;
1369
+ return startRow === rowIndex ? startCol : void 0;
1370
+ }
1371
+ function buildGridFromRawGrid(rawGrid) {
1372
+ return rawGrid.map((row, rowIndex) => {
1373
+ const cellsByColumn = /* @__PURE__ */ new Map();
1374
+ row.forEach((cell, colIndex) => {
1375
+ const anchorColumn = getAnchorColumn(cell, rowIndex, colIndex);
1376
+ if (anchorColumn === void 0 || cellsByColumn.has(anchorColumn)) {
1377
+ return;
1378
+ }
1379
+ cellsByColumn.set(anchorColumn, convertTableCell(cell));
1380
+ });
1381
+ return Array.from(cellsByColumn.entries()).sort(([left], [right]) => left - right).map(([, cell]) => cell);
1382
+ });
1383
+ }
1384
+ function buildGridFromTableCells(tableCells, rowCount) {
1385
+ if (!tableCells || tableCells.length === 0) return [];
1386
+ const maxRow = tableCells.reduce(
1387
+ (max, cell) => Math.max(max, getFiniteNumber(cell.start_row_offset_idx) ?? 0),
1388
+ rowCount - 1
1389
+ ) + 1;
1390
+ const rows = Array.from({ length: Math.max(0, maxRow) }, () => []);
1391
+ for (const cell of tableCells) {
1392
+ const row = getFiniteNumber(cell.start_row_offset_idx) ?? 0;
1393
+ const col = getFiniteNumber(cell.start_col_offset_idx) ?? 0;
1394
+ if (row < 0 || row >= rows.length) continue;
1395
+ rows[row].push({ col, cell: convertTableCell(cell) });
1396
+ }
1397
+ return rows.map(
1398
+ (row) => row.sort((a, b) => a.col - b.col).map(({ cell }) => cell)
1399
+ );
1400
+ }
1401
+ function getGridColumnCount(grid) {
1402
+ return grid.reduce(
1403
+ (max, row) => Math.max(
1404
+ max,
1405
+ row.reduce((sum, cell) => sum + Math.max(1, cell.colSpan), 0)
1406
+ ),
1407
+ 0
1408
+ );
1409
+ }
1410
+ function buildProcessedTableGrid(table) {
1411
+ const rawGrid = table.data.grid ?? [];
1412
+ const rawNumRows = getFiniteNumber(table.data.num_rows);
1413
+ const rawNumCols = getFiniteNumber(table.data.num_cols);
1414
+ const grid = rawGrid.length > 0 ? buildGridFromRawGrid(rawGrid) : buildGridFromTableCells(table.data.table_cells, rawNumRows ?? 0);
1415
+ return {
1416
+ grid,
1417
+ numRows: rawNumRows !== void 0 && rawNumRows > 0 ? rawNumRows : grid.length,
1418
+ numCols: rawNumCols !== void 0 && rawNumCols > 0 ? rawNumCols : getGridColumnCount(grid)
1419
+ };
1420
+ }
1281
1421
  var ResourceConverter = class {
1282
1422
  logger;
1283
1423
  idGenerator;
@@ -1313,12 +1453,17 @@ var ResourceConverter = class {
1313
1453
  this.logger.info(
1314
1454
  `[ResourceConverter] Converting ${doclingDoc.pictures.length} images...`
1315
1455
  );
1316
- const captionTexts = doclingDoc.pictures.map(
1317
- (picture) => this.captionProcessingPipeline.extractCaptionText(picture.captions)
1456
+ const captionSources = doclingDoc.pictures.map(
1457
+ (picture) => this.captionProcessingPipeline.extractCaptionSource(picture.captions)
1458
+ );
1459
+ const captionTexts = captionSources.map(
1460
+ (captionSource) => captionSource.text
1318
1461
  );
1319
1462
  const images = doclingDoc.pictures.map(
1320
1463
  (picture, index) => ({
1321
1464
  id: this.idGenerator.generateImageId(),
1465
+ sourceRef: picture.self_ref,
1466
+ captionSourceRefs: captionSources[index].sourceRefs,
1322
1467
  path: `${artifactDir}/images/image_${index}.png`,
1323
1468
  pdfPageNo: picture.prov?.[0]?.page_no ?? 0
1324
1469
  })
@@ -1341,23 +1486,21 @@ var ResourceConverter = class {
1341
1486
  this.logger.info(
1342
1487
  `[ResourceConverter] Converting ${doclingDoc.tables.length} tables...`
1343
1488
  );
1344
- const captionTexts = doclingDoc.tables.map(
1345
- (table) => this.captionProcessingPipeline.extractCaptionText(table.captions)
1489
+ const captionSources = doclingDoc.tables.map(
1490
+ (table) => this.captionProcessingPipeline.extractCaptionSource(table.captions)
1346
1491
  );
1347
- const tables = doclingDoc.tables.map((table) => {
1348
- const grid = table.data.grid.map(
1349
- (row) => row.map((cell) => ({
1350
- text: cell.text,
1351
- rowSpan: cell.row_span ?? 1,
1352
- colSpan: cell.col_span ?? 1,
1353
- isHeader: cell.column_header || cell.row_header || false
1354
- }))
1355
- );
1492
+ const captionTexts = captionSources.map(
1493
+ (captionSource) => captionSource.text
1494
+ );
1495
+ const tables = doclingDoc.tables.map((table, index) => {
1496
+ const { grid, numRows, numCols } = buildProcessedTableGrid(table);
1356
1497
  return {
1357
1498
  id: this.idGenerator.generateTableId(),
1499
+ sourceRef: table.self_ref,
1500
+ captionSourceRefs: captionSources[index].sourceRefs,
1358
1501
  pdfPageNo: table.prov?.[0]?.page_no ?? 0,
1359
- numRows: grid.length,
1360
- numCols: grid[0]?.length ?? 0,
1502
+ numRows,
1503
+ numCols,
1361
1504
  grid
1362
1505
  };
1363
1506
  });
@@ -1384,6 +1527,7 @@ var ResourceConverter = class {
1384
1527
  );
1385
1528
  const footnotes = footnoteItems.filter((item) => TextCleaner.isValidText(item.text)).map((item) => ({
1386
1529
  id: this.idGenerator.generateFootnoteId(),
1530
+ sourceRef: item.self_ref,
1387
1531
  text: TextCleaner.normalize(item.text),
1388
1532
  pdfPageNo: item.prov?.[0]?.page_no ?? 1
1389
1533
  }));
@@ -3683,7 +3827,8 @@ Remember: Look for SMALL numbers in MARGINS only. Ignore figure/table/photo numb
3683
3827
  };
3684
3828
 
3685
3829
  // src/pipelines/caption-processing-pipeline.ts
3686
- var CaptionProcessingPipeline = class {
3830
+ var CaptionProcessingPipeline = class _CaptionProcessingPipeline {
3831
+ static CAPTION_JOIN_SEPARATOR = " ";
3687
3832
  logger;
3688
3833
  captionParser;
3689
3834
  captionValidator;
@@ -3832,23 +3977,51 @@ var CaptionProcessingPipeline = class {
3832
3977
  return captionsByIndex;
3833
3978
  }
3834
3979
  /**
3835
- * Extract caption text from resource
3980
+ * Extract combined caption text from resource captions.
3981
+ *
3982
+ * Returns every caption joined with a single space; empty/whitespace-only
3983
+ * entries are skipped. Returns `undefined` when nothing resolves to text.
3836
3984
  *
3837
- * Handles both string references and $ref resolution
3985
+ * @deprecated Use {@link extractCaptionSource} to also receive the
3986
+ * caption `$ref` list. This helper is retained as a thin wrapper for
3987
+ * legacy callers.
3838
3988
  */
3839
3989
  extractCaptionText(captions) {
3840
- if (!captions?.[0]) {
3841
- return void 0;
3842
- }
3843
- const captionRef = captions[0];
3844
- if (typeof captionRef === "string") {
3845
- return captionRef;
3846
- }
3847
- if (this.refResolver && "$ref" in captionRef) {
3848
- const resolved = this.refResolver.resolveText(captionRef.$ref);
3849
- return resolved?.text;
3990
+ return this.extractCaptionSource(captions).text;
3991
+ }
3992
+ /**
3993
+ * Extract caption text and source references from resource captions.
3994
+ *
3995
+ * Iterates captions in order. String captions contribute to text only;
3996
+ * `$ref` captions also push the ref onto `sourceRefs` regardless of whether
3997
+ * the resolver finds text. Resolved/raw text is trimmed, and empty parts
3998
+ * are skipped before joining.
3999
+ */
4000
+ extractCaptionSource(captions) {
4001
+ const sourceRefs = [];
4002
+ const textParts = [];
4003
+ captions?.forEach((caption) => {
4004
+ if (typeof caption === "string") {
4005
+ const text2 = caption.trim();
4006
+ if (text2.length > 0) {
4007
+ textParts.push(text2);
4008
+ }
4009
+ return;
4010
+ }
4011
+ sourceRefs.push(caption.$ref);
4012
+ const resolved = this.refResolver?.resolveText(caption.$ref);
4013
+ const text = resolved?.text.trim();
4014
+ if (text && text.length > 0) {
4015
+ textParts.push(text);
4016
+ }
4017
+ });
4018
+ if (textParts.length === 0) {
4019
+ return { sourceRefs };
3850
4020
  }
3851
- return void 0;
4021
+ return {
4022
+ text: textParts.join(_CaptionProcessingPipeline.CAPTION_JOIN_SEPARATOR),
4023
+ sourceRefs
4024
+ };
3852
4025
  }
3853
4026
  };
3854
4027
 
@@ -4420,6 +4593,7 @@ var CaptionValidationError = class extends Error {
4420
4593
  };
4421
4594
 
4422
4595
  // src/document-processor.ts
4596
+ var PROCESSED_DOCUMENT_SCHEMA_VERSION = "processed-document.v2";
4423
4597
  var DocumentProcessor = class {
4424
4598
  logger;
4425
4599
  fallbackModel;
@@ -4570,18 +4744,26 @@ var DocumentProcessor = class {
4570
4744
  `[DocumentProcessor] Chapter conversion took ${chaptersTime}ms`
4571
4745
  );
4572
4746
  const startTimeAssemble = Date.now();
4573
- const processedDoc = this.assembleProcessedDocument(
4747
+ const processedDoc = this.assembleProcessedDocument({
4574
4748
  reportId,
4575
4749
  pageRangeMap,
4576
4750
  chapters,
4577
4751
  images,
4578
4752
  tables,
4579
- footnotes
4580
- );
4753
+ footnotes,
4754
+ source: processOptions.source
4755
+ });
4581
4756
  const assembleTime = Date.now() - startTimeAssemble;
4582
4757
  this.logger.info(
4583
4758
  `[DocumentProcessor] Document assembly took ${assembleTime}ms`
4584
4759
  );
4760
+ const sourceRefValidationMode = this.resolveSourceRefValidationMode(processOptions);
4761
+ if (sourceRefValidationMode !== "off") {
4762
+ this.validateProcessedDocumentSourceRefs(
4763
+ processedDoc,
4764
+ sourceRefValidationMode
4765
+ );
4766
+ }
4585
4767
  this.logger.info("[DocumentProcessor] Document processing completed");
4586
4768
  return {
4587
4769
  document: processedDoc,
@@ -4720,15 +4902,93 @@ var DocumentProcessor = class {
4720
4902
  );
4721
4903
  return pageRangeMap;
4722
4904
  }
4905
+ resolveSourceRefValidationMode(processOptions) {
4906
+ if (processOptions.sourceRefValidationMode !== void 0) {
4907
+ return processOptions.sourceRefValidationMode;
4908
+ }
4909
+ return processOptions.validateSourceRefs === true ? "error" : "off";
4910
+ }
4911
+ validateProcessedDocumentSourceRefs(processedDoc, mode) {
4912
+ if (!this.refResolver) {
4913
+ throw new Error(
4914
+ "[DocumentProcessor] Cannot validate source references before RefResolver initialization"
4915
+ );
4916
+ }
4917
+ const issues = this.collectMissingSourceRefs(
4918
+ processedDoc,
4919
+ this.refResolver
4920
+ );
4921
+ if (issues.length === 0) {
4922
+ this.logger.info(
4923
+ "[DocumentProcessor] Source reference validation passed"
4924
+ );
4925
+ return;
4926
+ }
4927
+ const details = issues.map((issue) => `${issue.context} (${issue.ref})`).join("; ");
4928
+ const message = `[DocumentProcessor] Source reference validation found ${issues.length} missing reference(s): ${details}`;
4929
+ if (mode === "warn") {
4930
+ this.logger.warn(message);
4931
+ return;
4932
+ }
4933
+ this.logger.error(message);
4934
+ throw new Error(message);
4935
+ }
4936
+ collectMissingSourceRefs(processedDoc, refResolver) {
4937
+ const issues = [];
4938
+ const addRef = (ref, context) => {
4939
+ if (ref !== void 0 && !refResolver.hasRef(ref)) {
4940
+ issues.push({ context, ref });
4941
+ }
4942
+ };
4943
+ const addRefs = (refs, context) => {
4944
+ refs?.forEach((ref, index) => {
4945
+ addRef(ref, `${context}[${index}]`);
4946
+ });
4947
+ };
4948
+ const visitChapter = (chapter) => {
4949
+ addRefs(chapter.sourceRefs, `chapter ${chapter.id} sourceRefs`);
4950
+ chapter.textBlocks.forEach((textBlock, index) => {
4951
+ addRef(
4952
+ textBlock.sourceRef,
4953
+ `chapter ${chapter.id} textBlock ${textBlock.id ?? index} sourceRef`
4954
+ );
4955
+ });
4956
+ chapter.children?.forEach(visitChapter);
4957
+ };
4958
+ processedDoc.chapters.forEach(visitChapter);
4959
+ processedDoc.images.forEach((image) => {
4960
+ addRef(image.sourceRef, `image ${image.id} sourceRef`);
4961
+ addRefs(image.captionSourceRefs, `image ${image.id} captionSourceRefs`);
4962
+ });
4963
+ processedDoc.tables.forEach((table) => {
4964
+ addRef(table.sourceRef, `table ${table.id} sourceRef`);
4965
+ addRefs(table.captionSourceRefs, `table ${table.id} captionSourceRefs`);
4966
+ });
4967
+ processedDoc.footnotes.forEach((footnote) => {
4968
+ addRef(footnote.sourceRef, `footnote ${footnote.id} sourceRef`);
4969
+ });
4970
+ return issues;
4971
+ }
4723
4972
  /**
4724
4973
  * Assemble the final ProcessedDocument
4725
4974
  *
4726
4975
  * Creates the ProcessedDocument structure with all converted components
4727
4976
  */
4728
- assembleProcessedDocument(reportId, pageRangeMap, chapters, images, tables, footnotes) {
4977
+ assembleProcessedDocument(input) {
4729
4978
  this.logger.info("[DocumentProcessor] Assembling ProcessedDocument...");
4979
+ const {
4980
+ reportId,
4981
+ pageRangeMap,
4982
+ chapters,
4983
+ images,
4984
+ tables,
4985
+ footnotes,
4986
+ source
4987
+ } = input;
4730
4988
  const processedDoc = {
4731
4989
  reportId,
4990
+ schemaVersion: PROCESSED_DOCUMENT_SCHEMA_VERSION,
4991
+ ...source !== void 0 ? { source } : {},
4732
4992
  pageRangeMap,
4733
4993
  chapters,
4734
4994
  images,
@@ -4780,6 +5040,7 @@ var DocumentProcessor = class {
4780
5040
  ChapterConverter,
4781
5041
  DocumentProcessor,
4782
5042
  PAGE_NUMBER_PATTERN,
5043
+ PROCESSED_DOCUMENT_SCHEMA_VERSION,
4783
5044
  PagePattern,
4784
5045
  PageRangeParseError,
4785
5046
  PageRangeParser,