@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.js CHANGED
@@ -559,6 +559,52 @@ var RefResolver = class {
559
559
  buildIndex(items, _prefix) {
560
560
  return new Map(items.map((item) => [item.self_ref, item]));
561
561
  }
562
+ /**
563
+ * Check whether a Docling self reference exists in the indexed document.
564
+ */
565
+ hasRef(ref) {
566
+ const match = ref.match(/^#\/(\w+)\//);
567
+ if (!match) {
568
+ return false;
569
+ }
570
+ const collection = match[1];
571
+ if (collection === "texts") {
572
+ return this.textMap.has(ref);
573
+ }
574
+ if (collection === "pictures") {
575
+ return this.pictureMap.has(ref);
576
+ }
577
+ if (collection === "tables") {
578
+ return this.tableMap.has(ref);
579
+ }
580
+ if (collection === "groups") {
581
+ return this.groupMap.has(ref);
582
+ }
583
+ return false;
584
+ }
585
+ /**
586
+ * Assert that a Docling self reference exists in the indexed document.
587
+ */
588
+ assertRef(ref, context) {
589
+ if (this.hasRef(ref)) {
590
+ return;
591
+ }
592
+ const message = `[RefResolver] Missing source reference in ${context}: ${ref}`;
593
+ this.logger.warn(message);
594
+ throw new Error(message);
595
+ }
596
+ /**
597
+ * Assert that every Docling self reference exists in the indexed document.
598
+ */
599
+ assertRefs(refs, context) {
600
+ const missingRefs = refs.filter((ref) => !this.hasRef(ref));
601
+ if (missingRefs.length === 0) {
602
+ return;
603
+ }
604
+ const message = `[RefResolver] Missing source references in ${context}: ${missingRefs.join(", ")}`;
605
+ this.logger.warn(message);
606
+ throw new Error(message);
607
+ }
562
608
  /**
563
609
  * Resolve a $ref string to the actual item
564
610
  * @param ref - Reference string (e.g., "#/texts/0")
@@ -650,6 +696,7 @@ var IdGenerator = class {
650
696
  imageCounter = 0;
651
697
  tableCounter = 0;
652
698
  footnoteCounter = 0;
699
+ textBlockCounter = 0;
653
700
  /**
654
701
  * Generate a chapter ID
655
702
  * @returns A chapter ID in the format "ch-001"
@@ -682,6 +729,14 @@ var IdGenerator = class {
682
729
  this.footnoteCounter++;
683
730
  return `ftn-${this.padNumber(this.footnoteCounter)}`;
684
731
  }
732
+ /**
733
+ * Generate a text block ID
734
+ * @returns A text block ID in the format "txt-001"
735
+ */
736
+ generateTextBlockId() {
737
+ this.textBlockCounter++;
738
+ return `txt-${this.padNumber(this.textBlockCounter)}`;
739
+ }
685
740
  /**
686
741
  * Reset all counters to zero
687
742
  */
@@ -690,6 +745,7 @@ var IdGenerator = class {
690
745
  this.imageCounter = 0;
691
746
  this.tableCounter = 0;
692
747
  this.footnoteCounter = 0;
748
+ this.textBlockCounter = 0;
693
749
  }
694
750
  /**
695
751
  * Get current counter values (for testing/debugging)
@@ -699,7 +755,8 @@ var IdGenerator = class {
699
755
  chapter: this.chapterCounter,
700
756
  image: this.imageCounter,
701
757
  table: this.tableCounter,
702
- footnote: this.footnoteCounter
758
+ footnote: this.footnoteCounter,
759
+ textBlock: this.textBlockCounter
703
760
  };
704
761
  }
705
762
  /**
@@ -1005,6 +1062,7 @@ var ChapterConverter = class _ChapterConverter {
1005
1062
  title: _ChapterConverter.FRONT_MATTER_TITLE,
1006
1063
  pageNo: 1,
1007
1064
  level: 1,
1065
+ sourceRefs: [],
1008
1066
  textBlocks: [],
1009
1067
  imageIds: [],
1010
1068
  tableIds: [],
@@ -1024,6 +1082,7 @@ var ChapterConverter = class _ChapterConverter {
1024
1082
  title: TextCleaner.normalize(entry.title),
1025
1083
  pageNo: entry.pageNo,
1026
1084
  level: entry.level,
1085
+ sourceRefs: entry.sourceRefs ?? [],
1027
1086
  textBlocks: [],
1028
1087
  imageIds: [],
1029
1088
  tableIds: [],
@@ -1114,6 +1173,8 @@ var ChapterConverter = class _ChapterConverter {
1114
1173
  ).map((item) => {
1115
1174
  const pdfPageNo = item.prov?.[0]?.page_no ?? 1;
1116
1175
  return {
1176
+ id: this.idGenerator.generateTextBlockId(),
1177
+ sourceRef: item.self_ref,
1117
1178
  text: TextCleaner.normalize(item.text),
1118
1179
  pdfPageNo
1119
1180
  };
@@ -1218,6 +1279,84 @@ var ChapterConverter = class _ChapterConverter {
1218
1279
  };
1219
1280
 
1220
1281
  // src/converters/resource-converter.ts
1282
+ function getFiniteNumber(value) {
1283
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1284
+ }
1285
+ function getRowSpan(cell) {
1286
+ const startRow = getFiniteNumber(cell.start_row_offset_idx);
1287
+ const endRow = getFiniteNumber(cell.end_row_offset_idx);
1288
+ const offsetSpan = startRow !== void 0 && endRow !== void 0 ? endRow - startRow : 0;
1289
+ return Math.max(1, offsetSpan || getFiniteNumber(cell.row_span) || 1);
1290
+ }
1291
+ function getColSpan(cell) {
1292
+ const startCol = getFiniteNumber(cell.start_col_offset_idx);
1293
+ const endCol = getFiniteNumber(cell.end_col_offset_idx);
1294
+ const offsetSpan = startCol !== void 0 && endCol !== void 0 ? endCol - startCol : 0;
1295
+ return Math.max(1, offsetSpan || getFiniteNumber(cell.col_span) || 1);
1296
+ }
1297
+ function convertTableCell(cell) {
1298
+ return {
1299
+ text: cell.text ?? "",
1300
+ rowSpan: getRowSpan(cell),
1301
+ colSpan: getColSpan(cell),
1302
+ isHeader: cell.column_header || cell.row_header || false
1303
+ };
1304
+ }
1305
+ function getAnchorColumn(cell, rowIndex, colIndex) {
1306
+ const startRow = getFiniteNumber(cell.start_row_offset_idx) ?? rowIndex;
1307
+ const startCol = getFiniteNumber(cell.start_col_offset_idx) ?? colIndex;
1308
+ return startRow === rowIndex ? startCol : void 0;
1309
+ }
1310
+ function buildGridFromRawGrid(rawGrid) {
1311
+ return rawGrid.map((row, rowIndex) => {
1312
+ const cellsByColumn = /* @__PURE__ */ new Map();
1313
+ row.forEach((cell, colIndex) => {
1314
+ const anchorColumn = getAnchorColumn(cell, rowIndex, colIndex);
1315
+ if (anchorColumn === void 0 || cellsByColumn.has(anchorColumn)) {
1316
+ return;
1317
+ }
1318
+ cellsByColumn.set(anchorColumn, convertTableCell(cell));
1319
+ });
1320
+ return Array.from(cellsByColumn.entries()).sort(([left], [right]) => left - right).map(([, cell]) => cell);
1321
+ });
1322
+ }
1323
+ function buildGridFromTableCells(tableCells, rowCount) {
1324
+ if (!tableCells || tableCells.length === 0) return [];
1325
+ const maxRow = tableCells.reduce(
1326
+ (max, cell) => Math.max(max, getFiniteNumber(cell.start_row_offset_idx) ?? 0),
1327
+ rowCount - 1
1328
+ ) + 1;
1329
+ const rows = Array.from({ length: Math.max(0, maxRow) }, () => []);
1330
+ for (const cell of tableCells) {
1331
+ const row = getFiniteNumber(cell.start_row_offset_idx) ?? 0;
1332
+ const col = getFiniteNumber(cell.start_col_offset_idx) ?? 0;
1333
+ if (row < 0 || row >= rows.length) continue;
1334
+ rows[row].push({ col, cell: convertTableCell(cell) });
1335
+ }
1336
+ return rows.map(
1337
+ (row) => row.sort((a, b) => a.col - b.col).map(({ cell }) => cell)
1338
+ );
1339
+ }
1340
+ function getGridColumnCount(grid) {
1341
+ return grid.reduce(
1342
+ (max, row) => Math.max(
1343
+ max,
1344
+ row.reduce((sum, cell) => sum + Math.max(1, cell.colSpan), 0)
1345
+ ),
1346
+ 0
1347
+ );
1348
+ }
1349
+ function buildProcessedTableGrid(table) {
1350
+ const rawGrid = table.data.grid ?? [];
1351
+ const rawNumRows = getFiniteNumber(table.data.num_rows);
1352
+ const rawNumCols = getFiniteNumber(table.data.num_cols);
1353
+ const grid = rawGrid.length > 0 ? buildGridFromRawGrid(rawGrid) : buildGridFromTableCells(table.data.table_cells, rawNumRows ?? 0);
1354
+ return {
1355
+ grid,
1356
+ numRows: rawNumRows !== void 0 && rawNumRows > 0 ? rawNumRows : grid.length,
1357
+ numCols: rawNumCols !== void 0 && rawNumCols > 0 ? rawNumCols : getGridColumnCount(grid)
1358
+ };
1359
+ }
1221
1360
  var ResourceConverter = class {
1222
1361
  logger;
1223
1362
  idGenerator;
@@ -1253,12 +1392,17 @@ var ResourceConverter = class {
1253
1392
  this.logger.info(
1254
1393
  `[ResourceConverter] Converting ${doclingDoc.pictures.length} images...`
1255
1394
  );
1256
- const captionTexts = doclingDoc.pictures.map(
1257
- (picture) => this.captionProcessingPipeline.extractCaptionText(picture.captions)
1395
+ const captionSources = doclingDoc.pictures.map(
1396
+ (picture) => this.captionProcessingPipeline.extractCaptionSource(picture.captions)
1397
+ );
1398
+ const captionTexts = captionSources.map(
1399
+ (captionSource) => captionSource.text
1258
1400
  );
1259
1401
  const images = doclingDoc.pictures.map(
1260
1402
  (picture, index) => ({
1261
1403
  id: this.idGenerator.generateImageId(),
1404
+ sourceRef: picture.self_ref,
1405
+ captionSourceRefs: captionSources[index].sourceRefs,
1262
1406
  path: `${artifactDir}/images/image_${index}.png`,
1263
1407
  pdfPageNo: picture.prov?.[0]?.page_no ?? 0
1264
1408
  })
@@ -1281,23 +1425,21 @@ var ResourceConverter = class {
1281
1425
  this.logger.info(
1282
1426
  `[ResourceConverter] Converting ${doclingDoc.tables.length} tables...`
1283
1427
  );
1284
- const captionTexts = doclingDoc.tables.map(
1285
- (table) => this.captionProcessingPipeline.extractCaptionText(table.captions)
1428
+ const captionSources = doclingDoc.tables.map(
1429
+ (table) => this.captionProcessingPipeline.extractCaptionSource(table.captions)
1286
1430
  );
1287
- const tables = doclingDoc.tables.map((table) => {
1288
- const grid = table.data.grid.map(
1289
- (row) => row.map((cell) => ({
1290
- text: cell.text,
1291
- rowSpan: cell.row_span ?? 1,
1292
- colSpan: cell.col_span ?? 1,
1293
- isHeader: cell.column_header || cell.row_header || false
1294
- }))
1295
- );
1431
+ const captionTexts = captionSources.map(
1432
+ (captionSource) => captionSource.text
1433
+ );
1434
+ const tables = doclingDoc.tables.map((table, index) => {
1435
+ const { grid, numRows, numCols } = buildProcessedTableGrid(table);
1296
1436
  return {
1297
1437
  id: this.idGenerator.generateTableId(),
1438
+ sourceRef: table.self_ref,
1439
+ captionSourceRefs: captionSources[index].sourceRefs,
1298
1440
  pdfPageNo: table.prov?.[0]?.page_no ?? 0,
1299
- numRows: grid.length,
1300
- numCols: grid[0]?.length ?? 0,
1441
+ numRows,
1442
+ numCols,
1301
1443
  grid
1302
1444
  };
1303
1445
  });
@@ -1324,6 +1466,7 @@ var ResourceConverter = class {
1324
1466
  );
1325
1467
  const footnotes = footnoteItems.filter((item) => TextCleaner.isValidText(item.text)).map((item) => ({
1326
1468
  id: this.idGenerator.generateFootnoteId(),
1469
+ sourceRef: item.self_ref,
1327
1470
  text: TextCleaner.normalize(item.text),
1328
1471
  pdfPageNo: item.prov?.[0]?.page_no ?? 1
1329
1472
  }));
@@ -3623,7 +3766,8 @@ Remember: Look for SMALL numbers in MARGINS only. Ignore figure/table/photo numb
3623
3766
  };
3624
3767
 
3625
3768
  // src/pipelines/caption-processing-pipeline.ts
3626
- var CaptionProcessingPipeline = class {
3769
+ var CaptionProcessingPipeline = class _CaptionProcessingPipeline {
3770
+ static CAPTION_JOIN_SEPARATOR = " ";
3627
3771
  logger;
3628
3772
  captionParser;
3629
3773
  captionValidator;
@@ -3772,23 +3916,51 @@ var CaptionProcessingPipeline = class {
3772
3916
  return captionsByIndex;
3773
3917
  }
3774
3918
  /**
3775
- * Extract caption text from resource
3919
+ * Extract combined caption text from resource captions.
3920
+ *
3921
+ * Returns every caption joined with a single space; empty/whitespace-only
3922
+ * entries are skipped. Returns `undefined` when nothing resolves to text.
3776
3923
  *
3777
- * Handles both string references and $ref resolution
3924
+ * @deprecated Use {@link extractCaptionSource} to also receive the
3925
+ * caption `$ref` list. This helper is retained as a thin wrapper for
3926
+ * legacy callers.
3778
3927
  */
3779
3928
  extractCaptionText(captions) {
3780
- if (!captions?.[0]) {
3781
- return void 0;
3782
- }
3783
- const captionRef = captions[0];
3784
- if (typeof captionRef === "string") {
3785
- return captionRef;
3786
- }
3787
- if (this.refResolver && "$ref" in captionRef) {
3788
- const resolved = this.refResolver.resolveText(captionRef.$ref);
3789
- return resolved?.text;
3929
+ return this.extractCaptionSource(captions).text;
3930
+ }
3931
+ /**
3932
+ * Extract caption text and source references from resource captions.
3933
+ *
3934
+ * Iterates captions in order. String captions contribute to text only;
3935
+ * `$ref` captions also push the ref onto `sourceRefs` regardless of whether
3936
+ * the resolver finds text. Resolved/raw text is trimmed, and empty parts
3937
+ * are skipped before joining.
3938
+ */
3939
+ extractCaptionSource(captions) {
3940
+ const sourceRefs = [];
3941
+ const textParts = [];
3942
+ captions?.forEach((caption) => {
3943
+ if (typeof caption === "string") {
3944
+ const text2 = caption.trim();
3945
+ if (text2.length > 0) {
3946
+ textParts.push(text2);
3947
+ }
3948
+ return;
3949
+ }
3950
+ sourceRefs.push(caption.$ref);
3951
+ const resolved = this.refResolver?.resolveText(caption.$ref);
3952
+ const text = resolved?.text.trim();
3953
+ if (text && text.length > 0) {
3954
+ textParts.push(text);
3955
+ }
3956
+ });
3957
+ if (textParts.length === 0) {
3958
+ return { sourceRefs };
3790
3959
  }
3791
- return void 0;
3960
+ return {
3961
+ text: textParts.join(_CaptionProcessingPipeline.CAPTION_JOIN_SEPARATOR),
3962
+ sourceRefs
3963
+ };
3792
3964
  }
3793
3965
  };
3794
3966
 
@@ -4360,6 +4532,7 @@ var CaptionValidationError = class extends Error {
4360
4532
  };
4361
4533
 
4362
4534
  // src/document-processor.ts
4535
+ var PROCESSED_DOCUMENT_SCHEMA_VERSION = "processed-document.v2";
4363
4536
  var DocumentProcessor = class {
4364
4537
  logger;
4365
4538
  fallbackModel;
@@ -4510,18 +4683,26 @@ var DocumentProcessor = class {
4510
4683
  `[DocumentProcessor] Chapter conversion took ${chaptersTime}ms`
4511
4684
  );
4512
4685
  const startTimeAssemble = Date.now();
4513
- const processedDoc = this.assembleProcessedDocument(
4686
+ const processedDoc = this.assembleProcessedDocument({
4514
4687
  reportId,
4515
4688
  pageRangeMap,
4516
4689
  chapters,
4517
4690
  images,
4518
4691
  tables,
4519
- footnotes
4520
- );
4692
+ footnotes,
4693
+ source: processOptions.source
4694
+ });
4521
4695
  const assembleTime = Date.now() - startTimeAssemble;
4522
4696
  this.logger.info(
4523
4697
  `[DocumentProcessor] Document assembly took ${assembleTime}ms`
4524
4698
  );
4699
+ const sourceRefValidationMode = this.resolveSourceRefValidationMode(processOptions);
4700
+ if (sourceRefValidationMode !== "off") {
4701
+ this.validateProcessedDocumentSourceRefs(
4702
+ processedDoc,
4703
+ sourceRefValidationMode
4704
+ );
4705
+ }
4525
4706
  this.logger.info("[DocumentProcessor] Document processing completed");
4526
4707
  return {
4527
4708
  document: processedDoc,
@@ -4660,15 +4841,93 @@ var DocumentProcessor = class {
4660
4841
  );
4661
4842
  return pageRangeMap;
4662
4843
  }
4844
+ resolveSourceRefValidationMode(processOptions) {
4845
+ if (processOptions.sourceRefValidationMode !== void 0) {
4846
+ return processOptions.sourceRefValidationMode;
4847
+ }
4848
+ return processOptions.validateSourceRefs === true ? "error" : "off";
4849
+ }
4850
+ validateProcessedDocumentSourceRefs(processedDoc, mode) {
4851
+ if (!this.refResolver) {
4852
+ throw new Error(
4853
+ "[DocumentProcessor] Cannot validate source references before RefResolver initialization"
4854
+ );
4855
+ }
4856
+ const issues = this.collectMissingSourceRefs(
4857
+ processedDoc,
4858
+ this.refResolver
4859
+ );
4860
+ if (issues.length === 0) {
4861
+ this.logger.info(
4862
+ "[DocumentProcessor] Source reference validation passed"
4863
+ );
4864
+ return;
4865
+ }
4866
+ const details = issues.map((issue) => `${issue.context} (${issue.ref})`).join("; ");
4867
+ const message = `[DocumentProcessor] Source reference validation found ${issues.length} missing reference(s): ${details}`;
4868
+ if (mode === "warn") {
4869
+ this.logger.warn(message);
4870
+ return;
4871
+ }
4872
+ this.logger.error(message);
4873
+ throw new Error(message);
4874
+ }
4875
+ collectMissingSourceRefs(processedDoc, refResolver) {
4876
+ const issues = [];
4877
+ const addRef = (ref, context) => {
4878
+ if (ref !== void 0 && !refResolver.hasRef(ref)) {
4879
+ issues.push({ context, ref });
4880
+ }
4881
+ };
4882
+ const addRefs = (refs, context) => {
4883
+ refs?.forEach((ref, index) => {
4884
+ addRef(ref, `${context}[${index}]`);
4885
+ });
4886
+ };
4887
+ const visitChapter = (chapter) => {
4888
+ addRefs(chapter.sourceRefs, `chapter ${chapter.id} sourceRefs`);
4889
+ chapter.textBlocks.forEach((textBlock, index) => {
4890
+ addRef(
4891
+ textBlock.sourceRef,
4892
+ `chapter ${chapter.id} textBlock ${textBlock.id ?? index} sourceRef`
4893
+ );
4894
+ });
4895
+ chapter.children?.forEach(visitChapter);
4896
+ };
4897
+ processedDoc.chapters.forEach(visitChapter);
4898
+ processedDoc.images.forEach((image) => {
4899
+ addRef(image.sourceRef, `image ${image.id} sourceRef`);
4900
+ addRefs(image.captionSourceRefs, `image ${image.id} captionSourceRefs`);
4901
+ });
4902
+ processedDoc.tables.forEach((table) => {
4903
+ addRef(table.sourceRef, `table ${table.id} sourceRef`);
4904
+ addRefs(table.captionSourceRefs, `table ${table.id} captionSourceRefs`);
4905
+ });
4906
+ processedDoc.footnotes.forEach((footnote) => {
4907
+ addRef(footnote.sourceRef, `footnote ${footnote.id} sourceRef`);
4908
+ });
4909
+ return issues;
4910
+ }
4663
4911
  /**
4664
4912
  * Assemble the final ProcessedDocument
4665
4913
  *
4666
4914
  * Creates the ProcessedDocument structure with all converted components
4667
4915
  */
4668
- assembleProcessedDocument(reportId, pageRangeMap, chapters, images, tables, footnotes) {
4916
+ assembleProcessedDocument(input) {
4669
4917
  this.logger.info("[DocumentProcessor] Assembling ProcessedDocument...");
4918
+ const {
4919
+ reportId,
4920
+ pageRangeMap,
4921
+ chapters,
4922
+ images,
4923
+ tables,
4924
+ footnotes,
4925
+ source
4926
+ } = input;
4670
4927
  const processedDoc = {
4671
4928
  reportId,
4929
+ schemaVersion: PROCESSED_DOCUMENT_SCHEMA_VERSION,
4930
+ ...source !== void 0 ? { source } : {},
4672
4931
  pageRangeMap,
4673
4932
  chapters,
4674
4933
  images,
@@ -4719,6 +4978,7 @@ export {
4719
4978
  ChapterConverter,
4720
4979
  DocumentProcessor,
4721
4980
  PAGE_NUMBER_PATTERN,
4981
+ PROCESSED_DOCUMENT_SCHEMA_VERSION,
4722
4982
  PagePattern,
4723
4983
  PageRangeParseError,
4724
4984
  PageRangeParser,