Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docling_core/transforms/serializer/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,11 @@ def serialize(
text_res = "".join([r.text for r in res_parts])
text_res = f"<table>{text_res}</table>" if text_res else ""

ftn_res = doc_serializer.serialize_footnotes(item=item, **kwargs)
if ftn_res.text:
text_res = f"{text_res}{ftn_res.text}" if text_res else ftn_res.text
res_parts.append(ftn_res)

return create_ser_result(text=text_res, span_source=res_parts)

@override
Expand Down Expand Up @@ -1276,6 +1281,33 @@ def serialize_captions(
text_res = f"<{tag}>{text_res}</{tag}>"
return create_ser_result(text=text_res, span_source=results)

@override
def serialize_footnotes(
self,
item: FloatingItem,
**kwargs: Any,
) -> SerializationResult:
"""Serialize the item's footnotes."""
params = self.params.merge_with_patch(patch=kwargs)
results: list[SerializationResult] = []
excluded_refs = self.get_excluded_refs(**kwargs)

if DocItemLabel.FOOTNOTE in params.labels:
for ftn in item.footnotes:
if isinstance(it := ftn.resolve(self.doc), TextItem) and it.self_ref not in excluded_refs:
text_ftn = it.text
text_dir = get_text_direction(text_ftn)
dir_str = f' dir="{text_dir}"' if text_dir == "rtl" else ""
results.append(
create_ser_result(
text=(f'<div class="footnote"{dir_str}>{html.escape(text_ftn)}</div>'),
span_source=it,
)
)

text_res = params.caption_delim.join([r.text for r in results])
return create_ser_result(text=text_res, span_source=results)

def _generate_head(self) -> str:
"""Generate the HTML head section with metadata and styles."""
params = self.params
Expand Down
4 changes: 4 additions & 0 deletions docling_core/transforms/serializer/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,10 @@ def serialize(
if table_text:
res_parts.append(create_ser_result(text=table_text, span_source=item))

ftn_res = doc_serializer.serialize_footnotes(item=item, **kwargs)
if ftn_res.text:
res_parts.append(ftn_res)

text_res = "\n\n".join([r.text for r in res_parts])

return create_ser_result(text=text_res, span_source=res_parts)
Expand Down
33 changes: 33 additions & 0 deletions test/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -1103,3 +1103,36 @@ def test_html_meta_emits_xhtml_compatible_attributes():
assert 'data-meta-name="entities"' in html_out
# Output must be parseable by a strict XML parser.
ET.fromstring(html_out)


def test_table_footnotes_serialized_to_markdown_and_html():
"""Regression test for #496: table footnotes must not be dropped by the
Markdown and HTML serializers (they are delegated to the parent TableItem)."""
doc = DoclingDocument(name="t")

td = TableData(num_rows=0, num_cols=2)
td.add_row(["Header 1", "Header 2"])
td.add_row(["Data 1", "Data 2"])
table = doc.add_table(data=td)

fn1 = doc.add_text(label=DocItemLabel.FOOTNOTE, text="First footnote.")
fn2 = doc.add_text(label=DocItemLabel.FOOTNOTE, text="Second footnote.")
table.footnotes.append(fn1.get_ref())
table.footnotes.append(fn2.get_ref())

# footnotes are stored on the table in the document model ...
assert [fn.resolve(doc).text for fn in table.footnotes] == [
"First footnote.",
"Second footnote.",
]

# ... and must survive Markdown serialization
md_out = doc.export_to_markdown()
assert "First footnote." in md_out
assert "Second footnote." in md_out

# ... and HTML serialization (as escaped footnote divs after the table)
html_out = doc.export_to_html()
assert "First footnote." in html_out
assert "Second footnote." in html_out
assert '<div class="footnote">First footnote.</div>' in html_out
Loading