docling-core 2.77.0__tar.gz → 2.78.0__tar.gz

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.
Files changed (148) hide show
  1. {docling_core-2.77.0 → docling_core-2.78.0}/PKG-INFO +1 -1
  2. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/hierarchical_chunker.py +54 -13
  3. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/html.py +39 -10
  4. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/html_styles.py +0 -6
  5. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/doc/__init__.py +1 -0
  6. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/doc/document.py +103 -31
  7. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core.egg-info/PKG-INFO +1 -1
  8. {docling_core-2.77.0 → docling_core-2.78.0}/pyproject.toml +2 -2
  9. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_docling_doc.py +240 -4
  10. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_hierarchical_chunker.py +92 -1
  11. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_hybrid_chunker.py +20 -2
  12. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_metadata.py +2 -2
  13. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_search_meta.py +1 -1
  14. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_serialization.py +161 -6
  15. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_utils.py +22 -0
  16. {docling_core-2.77.0 → docling_core-2.78.0}/LICENSE +0 -0
  17. {docling_core-2.77.0 → docling_core-2.78.0}/README.md +0 -0
  18. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/__init__.py +0 -0
  19. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/cli/__init__.py +0 -0
  20. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/cli/serialize.py +0 -0
  21. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/cli/view.py +0 -0
  22. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/experimental/__init__.py +0 -0
  23. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/experimental/doclang.py +0 -0
  24. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/experimental/serializer/__init__.py +0 -0
  25. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/experimental/serializer/outline.py +0 -0
  26. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/py.typed +0 -0
  27. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/resources/schemas/doc/ANN.json +0 -0
  28. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/resources/schemas/doc/DOC.json +0 -0
  29. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/resources/schemas/doc/OCR-output.json +0 -0
  30. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/resources/schemas/doc/RAW.json +0 -0
  31. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/resources/schemas/generated/ccs_document_schema.json +0 -0
  32. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/resources/schemas/generated/minimal_document_schema_flat.json +0 -0
  33. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/resources/schemas/search/search_doc_mapping.json +0 -0
  34. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/resources/schemas/search/search_doc_mapping_v2.json +0 -0
  35. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/search/__init__.py +0 -0
  36. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/search/json_schema_to_search_mapper.py +0 -0
  37. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/search/mapping.py +0 -0
  38. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/search/meta.py +0 -0
  39. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/search/package.py +0 -0
  40. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/__init__.py +0 -0
  41. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/__init__.py +0 -0
  42. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/base.py +0 -0
  43. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/chunk_expander.py +0 -0
  44. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/code_chunking/__init__.py +0 -0
  45. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/code_chunking/_language_code_chunkers.py +0 -0
  46. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/code_chunking/_utils.py +0 -0
  47. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/code_chunking/base_code_chunking_strategy.py +0 -0
  48. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/code_chunking/code_chunk.py +0 -0
  49. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/code_chunking/standard_code_chunking_strategy.py +0 -0
  50. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/doc_chunk.py +0 -0
  51. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/hybrid_chunker.py +0 -0
  52. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/line_chunker.py +0 -0
  53. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/page_chunker.py +0 -0
  54. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/tokenizer/__init__.py +0 -0
  55. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/tokenizer/base.py +0 -0
  56. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/tokenizer/huggingface.py +0 -0
  57. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/chunker/tokenizer/openai.py +0 -0
  58. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/profiler/__init__.py +0 -0
  59. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/profiler/doc_profiler.py +0 -0
  60. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/__init__.py +0 -0
  61. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/azure.py +0 -0
  62. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/base.py +0 -0
  63. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/common.py +0 -0
  64. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/doctags.py +0 -0
  65. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/latex.py +0 -0
  66. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/markdown.py +0 -0
  67. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/markdown_excel.py +0 -0
  68. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/plain_text.py +0 -0
  69. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/serializer/webvtt.py +0 -0
  70. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/visualizer/__init__.py +0 -0
  71. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/visualizer/base.py +0 -0
  72. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/visualizer/key_value_visualizer.py +0 -0
  73. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/visualizer/layout_visualizer.py +0 -0
  74. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/visualizer/reading_order_visualizer.py +0 -0
  75. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/transforms/visualizer/table_visualizer.py +0 -0
  76. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/__init__.py +0 -0
  77. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/base.py +0 -0
  78. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/doc/base.py +0 -0
  79. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/doc/labels.py +0 -0
  80. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/doc/page.py +0 -0
  81. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/doc/tokens.py +0 -0
  82. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/doc/utils.py +0 -0
  83. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/doc/webvtt.py +0 -0
  84. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/gen/__init__.py +0 -0
  85. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/gen/generic.py +0 -0
  86. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/io/__init__.py +0 -0
  87. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/legacy_doc/__init__.py +0 -0
  88. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/legacy_doc/base.py +0 -0
  89. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/legacy_doc/doc_ann.py +0 -0
  90. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/legacy_doc/doc_ocr.py +0 -0
  91. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/legacy_doc/doc_raw.py +0 -0
  92. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/legacy_doc/document.py +0 -0
  93. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/legacy_doc/tokens.py +0 -0
  94. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/nlp/__init__.py +0 -0
  95. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/nlp/qa.py +0 -0
  96. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/nlp/qa_labels.py +0 -0
  97. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/rec/__init__.py +0 -0
  98. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/rec/attribute.py +0 -0
  99. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/rec/base.py +0 -0
  100. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/rec/predicate.py +0 -0
  101. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/rec/record.py +0 -0
  102. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/rec/statement.py +0 -0
  103. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/types/rec/subject.py +0 -0
  104. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/utils/__init__.py +0 -0
  105. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/utils/alias.py +0 -0
  106. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/utils/file.py +0 -0
  107. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/utils/generate_docs.py +0 -0
  108. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/utils/generate_jsonschema.py +0 -0
  109. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/utils/legacy.py +0 -0
  110. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/utils/settings.py +0 -0
  111. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/utils/validate.py +0 -0
  112. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core/utils/validators.py +0 -0
  113. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core.egg-info/SOURCES.txt +0 -0
  114. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core.egg-info/dependency_links.txt +0 -0
  115. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core.egg-info/entry_points.txt +0 -0
  116. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core.egg-info/requires.txt +0 -0
  117. {docling_core-2.77.0 → docling_core-2.78.0}/docling_core.egg-info/top_level.txt +0 -0
  118. {docling_core-2.77.0 → docling_core-2.78.0}/setup.cfg +0 -0
  119. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_azure_serializer.py +0 -0
  120. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_base.py +0 -0
  121. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_chunk_expander.py +0 -0
  122. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_code_chunker.py +0 -0
  123. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_code_chunking_strategy.py +0 -0
  124. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_collection.py +0 -0
  125. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_data_gen_flag.py +0 -0
  126. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_deserializer_doclang.py +0 -0
  127. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_doc_base.py +0 -0
  128. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_doc_legacy_convert.py +0 -0
  129. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_doc_schema.py +0 -0
  130. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_doc_schema_extractor.py +0 -0
  131. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_doctags_load.py +0 -0
  132. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_hierarchy.py +0 -0
  133. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_json_schema_to_search_mapper.py +0 -0
  134. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_latex_serialization.py +0 -0
  135. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_line_chunker.py +0 -0
  136. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_nlp_qa.py +0 -0
  137. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_otsl_table_export.py +0 -0
  138. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_page.py +0 -0
  139. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_page_chunker.py +0 -0
  140. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_plain_text_serialization.py +0 -0
  141. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_profiler.py +0 -0
  142. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_rec_schema.py +0 -0
  143. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_regions_to_table.py +0 -0
  144. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_serialization_doclang.py +0 -0
  145. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_serialization_doctag.py +0 -0
  146. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_serialization_outline.py +0 -0
  147. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_visualization.py +0 -0
  148. {docling_core-2.77.0 → docling_core-2.78.0}/test/test_webvtt.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docling-core
3
- Version: 2.77.0
3
+ Version: 2.78.0
4
4
  Summary: A python library to define and validate data types in Docling.
5
5
  Author-email: Cesar Berrospi Ramis <ceb@zurich.ibm.com>, Panos Vagenas <pva@zurich.ibm.com>, Michele Dolfi <dol@zurich.ibm.com>, Christoph Auer <cau@zurich.ibm.com>, Peter Staar <taa@zurich.ibm.com>
6
6
  Maintainer-email: Panos Vagenas <pva@zurich.ibm.com>, Michele Dolfi <dol@zurich.ibm.com>, Christoph Auer <cau@zurich.ibm.com>, Peter Staar <taa@zurich.ibm.com>, Cesar Berrospi Ramis <ceb@zurich.ibm.com>
@@ -6,6 +6,7 @@ import logging
6
6
  from collections.abc import Iterator
7
7
  from typing import Annotated, Any, Optional, Union
8
8
 
9
+ import pandas as pd
9
10
  from pydantic import ConfigDict, Field
10
11
  from typing_extensions import override
11
12
 
@@ -45,6 +46,25 @@ _logger = logging.getLogger(__name__)
45
46
  class TripletTableSerializer(BaseTableSerializer):
46
47
  """Triplet-based table item serializer."""
47
48
 
49
+ @staticmethod
50
+ def _flatten_table_text(table_df: pd.DataFrame) -> str:
51
+ """Last-resort fallback that turns a table into plain text.
52
+
53
+ The preferred output of this serializer is the triplet form
54
+ 'row, column = value'. When that comes out empty - for example on
55
+ single-cell RichTableCell layout tables where the header and row
56
+ labels are blank - we fall back to this helper so the table still
57
+ contributes something chunkable and does not end up consuming its
58
+ sibling refs.
59
+
60
+ Cells are read row by row, blanks are skipped, and the remaining
61
+ values are joined with '. '. So a table like [['A', ''], ['B', 'C']]
62
+ becomes 'A. B. C'.
63
+ """
64
+ return ". ".join(
65
+ text for row in table_df.itertuples(index=False, name=None) for value in row if (text := str(value).strip())
66
+ )
67
+
48
68
  @override
49
69
  def serialize(
50
70
  self,
@@ -56,6 +76,7 @@ class TripletTableSerializer(BaseTableSerializer):
56
76
  ) -> SerializationResult:
57
77
  """Serializes the passed item."""
58
78
  parts: list[SerializationResult] = []
79
+ shared_visited = kwargs.get("visited")
59
80
 
60
81
  cap_res = doc_serializer.serialize_captions(
61
82
  item=item,
@@ -65,40 +86,60 @@ class TripletTableSerializer(BaseTableSerializer):
65
86
  parts.append(cap_res)
66
87
 
67
88
  if item.self_ref not in doc_serializer.get_excluded_refs(**kwargs):
89
+ table_text = ""
90
+ local_kwargs = {**kwargs, "visited": set(shared_visited)} if shared_visited is not None else kwargs
68
91
  table_df = item._export_to_dataframe_with_options(
69
92
  doc,
70
93
  doc_serializer=doc_serializer,
71
- **kwargs,
94
+ **local_kwargs,
72
95
  )
96
+
97
+ # Header-only tables produce a dataframe with 0 rows. Emit the
98
+ # header text directly instead of dropping the table on the floor.
99
+ if table_df.shape[0] == 0 and len(table_df.columns) > 0:
100
+ table_text = ". ".join(text for col in table_df.columns if (text := str(col).strip()))
101
+
73
102
  if table_df.shape[0] >= 1 and table_df.shape[1] >= 1:
103
+ fallback_df = table_df
74
104
  # Handle single-column tables
75
105
  if table_df.shape[1] == 1:
76
106
  # For single-column tables, use first row as column name
77
107
  # and remaining rows as values
78
108
  col_name = str(table_df.iloc[0, 0]).strip()
79
109
  values = [str(val).strip() for val in table_df.iloc[1:, 0].to_list()]
80
- table_text_parts = [f"{col_name} = {val}" for val in values]
81
- table_text = ". ".join(table_text_parts)
82
- parts.append(create_ser_result(text=table_text, span_source=item))
110
+ if values:
111
+ table_text_parts = [f"{col_name} = {val}" for val in values]
112
+ table_text = ". ".join(table_text_parts)
113
+ else:
114
+ # Single-row single-column table: emit the cell text
115
+ table_text = col_name
83
116
  else:
84
117
  # For multi-column tables
85
118
  # copy header as first row and shift all rows by one
86
- table_df.loc[-1] = table_df.columns # type: ignore[call-overload]
87
- table_df.index = table_df.index + 1
88
- table_df = table_df.sort_index()
119
+ triplet_df = table_df.copy()
120
+ triplet_df.loc[-1] = triplet_df.columns # type: ignore[call-overload]
121
+ triplet_df.index = triplet_df.index + 1
122
+ triplet_df = triplet_df.sort_index()
89
123
 
90
- rows = [str(item).strip() for item in table_df.iloc[:, 0].to_list()]
91
- cols = [str(item).strip() for item in table_df.iloc[0, :].to_list()]
124
+ rows = [str(item).strip() for item in triplet_df.iloc[:, 0].to_list()]
125
+ cols = [str(item).strip() for item in triplet_df.iloc[0, :].to_list()]
92
126
 
93
- nrows = table_df.shape[0]
94
- ncols = table_df.shape[1]
127
+ nrows = triplet_df.shape[0]
128
+ ncols = triplet_df.shape[1]
95
129
  table_text_parts = [
96
- f"{rows[i]}, {cols[j]} = {str(table_df.iloc[i, j]).strip()}"
130
+ f"{rows[i]}, {cols[j]} = {str(triplet_df.iloc[i, j]).strip()}"
97
131
  for i in range(1, nrows)
98
132
  for j in range(1, ncols)
99
133
  ]
100
134
  table_text = ". ".join(table_text_parts)
101
- parts.append(create_ser_result(text=table_text, span_source=item))
135
+
136
+ if not table_text:
137
+ table_text = self._flatten_table_text(fallback_df)
138
+
139
+ if table_text:
140
+ if shared_visited is not None:
141
+ shared_visited.update(local_kwargs["visited"])
142
+ parts.append(create_ser_result(text=table_text, span_source=item))
102
143
 
103
144
  text_res = "\n\n".join([r.text for r in parts])
104
145
 
@@ -230,7 +230,7 @@ class HTMLTextSerializer(BaseModel, BaseTextSerializer):
230
230
  # Regular text item
231
231
  text = get_html_tag_with_text_direction(html_tag="p", text=text)
232
232
 
233
- # Apply formatting and hyperlinks
233
+ # Apply formatting and hyperlinks to the parent's own text+tag.
234
234
  if not post_processed:
235
235
  text = doc_serializer.post_process(
236
236
  text=text,
@@ -238,6 +238,23 @@ class HTMLTextSerializer(BaseModel, BaseTextSerializer):
238
238
  hyperlink=item.hyperlink,
239
239
  )
240
240
 
241
+ # Recurse into children for branches that don't already consume them.
242
+ # has_inline_repr already consumed the single InlineGroup child; ListItem
243
+ # recursion is handled inline in its branch above.
244
+ if not has_inline_repr and not isinstance(item, ListItem) and item.children:
245
+ nested_text = "\n".join(
246
+ r.text
247
+ for r in doc_serializer.get_parts(
248
+ item=item,
249
+ is_inline_scope=is_inline_scope,
250
+ visited=my_visited,
251
+ **kwargs,
252
+ )
253
+ if r.text
254
+ )
255
+ if nested_text:
256
+ text = f"{text}\n{nested_text}" if text else nested_text
257
+
241
258
  if text:
242
259
  text_res = create_ser_result(text=text, span_source=item)
243
260
  res_parts.append(text_res)
@@ -945,6 +962,8 @@ class HTMLMetaSerializer(BaseModel, BaseMetaSerializer):
945
962
 
946
963
  def _serialize_meta_field(self, meta: BaseMeta, name: str) -> Optional[str]:
947
964
  if (field_val := getattr(meta, name)) is not None:
965
+ is_html_markup = False
966
+
948
967
  if isinstance(field_val, SummaryMetaField):
949
968
  txt = field_val.text
950
969
  elif isinstance(field_val, LanguageMetaField):
@@ -952,14 +971,13 @@ class HTMLMetaSerializer(BaseModel, BaseMetaSerializer):
952
971
  elif isinstance(field_val, EntitiesMetaField):
953
972
  txt = ", ".join(
954
973
  (
955
- f"{html.escape(mention.text)} "
956
- f"({html.escape(mention.label)}, [{mention.charspan[0]},{mention.charspan[1]}])"
974
+ f"{mention.text} ({mention.label}, [{mention.charspan[0]},{mention.charspan[1]}])"
957
975
  if mention.label is not None and mention.charspan
958
- else f"{html.escape(mention.text)} ({html.escape(mention.label)})"
976
+ else f"{mention.text} ({mention.label})"
959
977
  if mention.label is not None
960
- else f"{html.escape(mention.text)} ([{mention.charspan[0]},{mention.charspan[1]}])"
978
+ else f"{mention.text} ([{mention.charspan[0]},{mention.charspan[1]}])"
961
979
  if mention.charspan
962
- else html.escape(mention.text)
980
+ else mention.text
963
981
  )
964
982
  for mention in field_val.mentions
965
983
  )
@@ -975,19 +993,30 @@ class HTMLMetaSerializer(BaseModel, BaseMetaSerializer):
975
993
  table_content = temp_table.export_to_html(temp_doc).strip()
976
994
  if table_content:
977
995
  txt = table_content
996
+ is_html_markup = True
978
997
  else:
979
998
  return None
980
999
  elif isinstance(field_val, CodeMetaField):
981
1000
  lang = field_val.language.value.lower() if field_val.language else ""
982
- code_class = f' class="language-{html.escape(lang)}"' if lang else ""
983
- txt = f'<pre class="docling-meta-code"><code{code_class}>{html.escape(field_val.text)}</code></pre>'
1001
+ escaped_lang = html.escape(lang)
1002
+ escaped_code = html.escape(field_val.text)
1003
+ code_class = f' class="language-{escaped_lang}"' if lang else ""
1004
+ txt = f'<pre class="docling-meta-code"><code{code_class}>{escaped_code}</code></pre>'
1005
+ is_html_markup = True
984
1006
  elif tmp := str(field_val or ""):
985
1007
  txt = tmp
986
1008
  else:
987
1009
  return None
1010
+
1011
+ # Escape plain text content for safe HTML output.
1012
+ # HTML markup fields (TabularChartMetaField, CodeMetaField) are already properly formatted.
1013
+ if not is_html_markup:
1014
+ txt = html.escape(txt, quote=False)
1015
+
1016
+ escaped_name = html.escape(name, quote=True)
988
1017
  return (
989
- f'<div class="docling-meta-field" data-meta-{name}>'
990
- f'<span class="docling-meta-field-label">{name}:</span> '
1018
+ f'<div class="docling-meta-field" data-meta-name="{escaped_name}">'
1019
+ f'<span class="docling-meta-field-label">{escaped_name}:</span> '
991
1020
  f'<span class="docling-meta-field-value">{txt}</span>'
992
1021
  f"</div>"
993
1022
  )
@@ -113,9 +113,6 @@ def _get_css_for_split_page() -> str:
113
113
  font-weight: bold;
114
114
  color: #444;
115
115
  }
116
- .docling-meta-field[data-meta-classification] {
117
- text-align: left;
118
- }
119
116
  pre.docling-meta-code {
120
117
  background-color: #1e1e1e;
121
118
  color: #d4d4d4;
@@ -287,9 +284,6 @@ def _get_css_for_single_column() -> str:
287
284
  font-weight: bold;
288
285
  color: #444;
289
286
  }
290
- .docling-meta-field[data-meta-classification] {
291
- text-align: left;
292
- }
293
287
  pre.docling-meta-code {
294
288
  background-color: #1e1e1e;
295
289
  color: #d4d4d4;
@@ -43,6 +43,7 @@ from .document import (
43
43
  MoleculeMetaField,
44
44
  NodeItem,
45
45
  OrderedList,
46
+ Orientation,
46
47
  PageItem,
47
48
  PictureBarChartData,
48
49
  PictureChartData,
@@ -385,12 +385,28 @@ AnyTableCell = Annotated[
385
385
  ]
386
386
 
387
387
 
388
+ class Orientation(str, Enum):
389
+ """Counter-clockwise rotation of a table on the page, in degrees.
390
+
391
+ Follows the convention used by PIL/Pillow's ``Image.rotate``: positive
392
+ angles rotate the table counter-clockwise. ``ROT_0`` / ``ROT_180`` keep
393
+ rows running horizontally on the page; ``ROT_90`` / ``ROT_270`` turn
394
+ rows into vertical stripes.
395
+ """
396
+
397
+ ROT_0 = "rot_0" # no rotation; row 0 at top, rows horizontal
398
+ ROT_90 = "rot_90" # 90° CCW; row 0 on the left, rows are vertical stripes
399
+ ROT_180 = "rot_180" # 180°; row 0 at bottom (upside-down), rows horizontal
400
+ ROT_270 = "rot_270" # 270° CCW (= 90° CW); row 0 on the right, rows are vertical stripes
401
+
402
+
388
403
  class TableData(BaseModel): # TBD
389
404
  """BaseTableData."""
390
405
 
391
406
  table_cells: list[AnyTableCell] = []
392
407
  num_rows: int = 0
393
408
  num_cols: int = 0
409
+ orientation: Orientation = Orientation.ROT_0
394
410
 
395
411
  @computed_field # type: ignore
396
412
  @property
@@ -577,15 +593,23 @@ class TableData(BaseModel): # TBD
577
593
  def get_row_bounding_boxes(self, *, minimal: bool = True) -> dict[int, BoundingBox]:
578
594
  """Get the bounding box for each row in the table.
579
595
 
596
+ Layout follows the table's ``orientation`` field: ``ROT_0`` / ``ROT_180``
597
+ keep rows running left-to-right on the page; ``ROT_90`` / ``ROT_270``
598
+ turn rows into vertical stripes. This affects both the axis along which
599
+ span cells extend a row's bbox and, when ``minimal=False``, the axis
600
+ equalized across rows.
601
+
580
602
  Args:
581
603
  minimal: If True (default), returns the minimal bounding box for each
582
- row based on its cells. If False, all rows will have uniform
583
- horizontal extent (same x0/x1 values) spanning the full table width.
604
+ row based on its cells. If False, all rows will have a uniform
605
+ extent perpendicular to the row direction (l/r for ROT_0/ROT_180,
606
+ t/b for ROT_90/ROT_270).
584
607
 
585
608
  Returns:
586
609
  dict[int, BoundingBox]: A dictionary mapping row indices to their
587
610
  bounding boxes. Only rows with cells that have bounding boxes are included.
588
611
  """
612
+ horizontal = self.orientation in (Orientation.ROT_0, Orientation.ROT_180)
589
613
  coords = []
590
614
  for cell in self.table_cells:
591
615
  if cell.bbox is not None:
@@ -616,35 +640,68 @@ class TableData(BaseModel): # TBD
616
640
  min_row_span = min(row_cells_with_bbox.keys())
617
641
  row_bbox: BoundingBox = BoundingBox.enclosing_bbox(row_cells_with_bbox[min_row_span])
618
642
 
643
+ # Spanning cells extend along the row's natural axis:
644
+ # horizontal table → row runs l/r; vertical table → row runs t/b.
619
645
  for rspan, bboxs in row_cells_with_bbox.items():
620
646
  for bbox in bboxs:
621
- row_bbox.l = min(row_bbox.l, bbox.l)
622
- row_bbox.r = max(row_bbox.r, bbox.r)
647
+ if horizontal:
648
+ row_bbox.l = min(row_bbox.l, bbox.l)
649
+ row_bbox.r = max(row_bbox.r, bbox.r)
650
+ else:
651
+ if bbox.coord_origin == CoordOrigin.TOPLEFT:
652
+ row_bbox.t = min(row_bbox.t, bbox.t)
653
+ row_bbox.b = max(row_bbox.b, bbox.b)
654
+ else: # BOTTOMLEFT
655
+ row_bbox.t = max(row_bbox.t, bbox.t)
656
+ row_bbox.b = min(row_bbox.b, bbox.b)
623
657
 
624
658
  row_bboxes[row_idx] = row_bbox
625
659
 
626
- # If not minimal, make all rows have uniform horizontal extent
660
+ # If not minimal, make all rows have uniform extent on the axis
661
+ # perpendicular to the row direction.
627
662
  if not minimal and row_bboxes:
628
- global_l = min(bbox.l for bbox in row_bboxes.values())
629
- global_r = max(bbox.r for bbox in row_bboxes.values())
630
- for bbox in row_bboxes.values():
631
- bbox.l = global_l
632
- bbox.r = global_r
663
+ if horizontal:
664
+ # Rows run left-to-right; equalize horizontal extent.
665
+ global_l = min(bbox.l for bbox in row_bboxes.values())
666
+ global_r = max(bbox.r for bbox in row_bboxes.values())
667
+ for bbox in row_bboxes.values():
668
+ bbox.l = global_l
669
+ bbox.r = global_r
670
+ else:
671
+ # Vertical table: rows are vertical stripes; equalize vertical extent.
672
+ first_bbox = next(iter(row_bboxes.values()))
673
+ if first_bbox.coord_origin == CoordOrigin.TOPLEFT:
674
+ global_t = min(bbox.t for bbox in row_bboxes.values())
675
+ global_b = max(bbox.b for bbox in row_bboxes.values())
676
+ else: # BOTTOMLEFT
677
+ global_t = max(bbox.t for bbox in row_bboxes.values())
678
+ global_b = min(bbox.b for bbox in row_bboxes.values())
679
+ for bbox in row_bboxes.values():
680
+ bbox.t = global_t
681
+ bbox.b = global_b
633
682
 
634
683
  return row_bboxes
635
684
 
636
685
  def get_column_bounding_boxes(self, *, minimal: bool = True) -> dict[int, BoundingBox]:
637
686
  """Get the bounding box for each column in the table.
638
687
 
688
+ Layout follows the table's ``orientation`` field: ``ROT_0`` / ``ROT_180``
689
+ keep columns running top-to-bottom on the page; ``ROT_90`` / ``ROT_270``
690
+ turn columns into horizontal stripes. This affects both the axis along
691
+ which span cells extend a column's bbox and, when ``minimal=False``, the
692
+ axis equalized across columns.
693
+
639
694
  Args:
640
695
  minimal: If True (default), returns the minimal bounding box for each
641
- column based on its cells. If False, all columns will have uniform
642
- vertical extent (same y0/y1 values) spanning the full table height.
696
+ column based on its cells. If False, all columns will have a
697
+ uniform extent perpendicular to the column direction (t/b for
698
+ ROT_0/ROT_180, l/r for ROT_90/ROT_270).
643
699
 
644
700
  Returns:
645
701
  dict[int, BoundingBox]: A dictionary mapping column indices to their
646
702
  bounding boxes. Only columns with cells that have bounding boxes are included.
647
703
  """
704
+ horizontal = self.orientation in (Orientation.ROT_0, Orientation.ROT_180)
648
705
  coords = []
649
706
  for cell in self.table_cells:
650
707
  if cell.bbox is not None:
@@ -675,31 +732,46 @@ class TableData(BaseModel): # TBD
675
732
  min_col_span = min(col_cells_with_bbox.keys())
676
733
  col_bbox: BoundingBox = BoundingBox.enclosing_bbox(col_cells_with_bbox[min_col_span])
677
734
 
735
+ # Spanning cells extend along the column's natural axis:
736
+ # horizontal table → column runs t/b; vertical table → column runs l/r.
678
737
  for rspan, bboxs in col_cells_with_bbox.items():
679
738
  for bbox in bboxs:
680
- if bbox.coord_origin == CoordOrigin.TOPLEFT:
681
- col_bbox.b = max(col_bbox.b, bbox.b)
682
- col_bbox.t = min(col_bbox.t, bbox.t)
683
-
684
- elif bbox.coord_origin == CoordOrigin.BOTTOMLEFT:
685
- col_bbox.b = min(col_bbox.b, bbox.b)
686
- col_bbox.t = max(col_bbox.t, bbox.t)
739
+ if horizontal:
740
+ if bbox.coord_origin == CoordOrigin.TOPLEFT:
741
+ col_bbox.b = max(col_bbox.b, bbox.b)
742
+ col_bbox.t = min(col_bbox.t, bbox.t)
743
+ elif bbox.coord_origin == CoordOrigin.BOTTOMLEFT:
744
+ col_bbox.b = min(col_bbox.b, bbox.b)
745
+ col_bbox.t = max(col_bbox.t, bbox.t)
746
+ else:
747
+ col_bbox.l = min(col_bbox.l, bbox.l)
748
+ col_bbox.r = max(col_bbox.r, bbox.r)
687
749
 
688
750
  col_bboxes[col_idx] = col_bbox
689
751
 
690
- # If not minimal, make all columns have uniform vertical extent
752
+ # If not minimal, make all columns have uniform extent on the axis
753
+ # perpendicular to the column direction.
691
754
  if not minimal and col_bboxes:
692
- # Get the coord_origin from the first bbox (they're all the same)
693
- first_bbox = next(iter(col_bboxes.values()))
694
- if first_bbox.coord_origin == CoordOrigin.TOPLEFT:
695
- global_t = min(bbox.t for bbox in col_bboxes.values())
696
- global_b = max(bbox.b for bbox in col_bboxes.values())
697
- else: # BOTTOMLEFT
698
- global_t = max(bbox.t for bbox in col_bboxes.values())
699
- global_b = min(bbox.b for bbox in col_bboxes.values())
700
- for bbox in col_bboxes.values():
701
- bbox.t = global_t
702
- bbox.b = global_b
755
+ if horizontal:
756
+ # Columns run top-to-bottom; equalize vertical extent.
757
+ # Get the coord_origin from the first bbox (they're all the same)
758
+ first_bbox = next(iter(col_bboxes.values()))
759
+ if first_bbox.coord_origin == CoordOrigin.TOPLEFT:
760
+ global_t = min(bbox.t for bbox in col_bboxes.values())
761
+ global_b = max(bbox.b for bbox in col_bboxes.values())
762
+ else: # BOTTOMLEFT
763
+ global_t = max(bbox.t for bbox in col_bboxes.values())
764
+ global_b = min(bbox.b for bbox in col_bboxes.values())
765
+ for bbox in col_bboxes.values():
766
+ bbox.t = global_t
767
+ bbox.b = global_b
768
+ else:
769
+ # Vertical table: columns are horizontal stripes; equalize horizontal extent.
770
+ global_l = min(bbox.l for bbox in col_bboxes.values())
771
+ global_r = max(bbox.r for bbox in col_bboxes.values())
772
+ for bbox in col_bboxes.values():
773
+ bbox.l = global_l
774
+ bbox.r = global_r
703
775
 
704
776
  return col_bboxes
705
777
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docling-core
3
- Version: 2.77.0
3
+ Version: 2.78.0
4
4
  Summary: A python library to define and validate data types in Docling.
5
5
  Author-email: Cesar Berrospi Ramis <ceb@zurich.ibm.com>, Panos Vagenas <pva@zurich.ibm.com>, Michele Dolfi <dol@zurich.ibm.com>, Christoph Auer <cau@zurich.ibm.com>, Peter Staar <taa@zurich.ibm.com>
6
6
  Maintainer-email: Panos Vagenas <pva@zurich.ibm.com>, Michele Dolfi <dol@zurich.ibm.com>, Christoph Auer <cau@zurich.ibm.com>, Peter Staar <taa@zurich.ibm.com>, Cesar Berrospi Ramis <ceb@zurich.ibm.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "docling-core"
3
- version = "2.77.0" # DO NOT EDIT, updated automatically
3
+ version = "2.78.0" # DO NOT EDIT, updated automatically
4
4
  description = "A python library to define and validate data types in Docling."
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -113,7 +113,7 @@ dev = [
113
113
  "pandas-stubs~=2.1",
114
114
  "ipykernel~=6.29",
115
115
  "coverage~=7.6",
116
- "pytest~=8.3",
116
+ "pytest~=9.0.3",
117
117
  "pytest-cov>=6.1.1",
118
118
  "python-semantic-release~=7.32",
119
119
  'tree-sitter-java-orchard (>=0.3.0,<1.0.0); python_version >= "3.10"',