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
28 changes: 24 additions & 4 deletions docling_core/transforms/serializer/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,13 @@ class HTMLParams(CommonParams):
image_mode: ImageRefMode = ImageRefMode.PLACEHOLDER

# HTML document properties
html_lang: str = "en"
html_lang: str = Field(
default="en",
description=(
"Language declared on the <html> root (BCP 47). Text items whose "
"meta.language differs get their own lang attribute."
),
)
html_head: str | None = None

css_styles: str | None = None
Expand All @@ -134,6 +140,19 @@ class HTMLParams(CommonParams):
show_original_list_item_marker: bool = True


def _lang_attrs(item: NodeItem, html_lang: str) -> dict[str, str] | None:
"""`lang` attribute for an item whose meta.language differs from the document's.

WCAG 3.1.2 (Language of Parts): the root <html lang> covers the document
language, so only a differing element language needs its own attribute.
"""
language = item.meta.language if item.meta is not None else None
if language is None:
return None
code = language.code.value
return {"lang": code} if code != html_lang else None


class HTMLTextSerializer(BaseModel, BaseTextSerializer):
"""HTML-specific text item serializer."""

Expand Down Expand Up @@ -169,9 +188,10 @@ def serialize(
text = text.replace("\n", "<br>")

# Prepare the HTML based on item type
lang_attrs = _lang_attrs(item=item, html_lang=params.html_lang)
if isinstance(item, TitleItem | SectionHeaderItem):
section_level = min(item.level + 1, 6) if isinstance(item, SectionHeaderItem) else 1
text = get_html_tag_with_text_direction(html_tag=f"h{section_level}", text=text)
text = get_html_tag_with_text_direction(html_tag=f"h{section_level}", text=text, attrs=lang_attrs)

elif isinstance(item, FormulaItem):
text = self._process_formula(
Expand Down Expand Up @@ -230,7 +250,7 @@ def serialize(

elif not is_inline_scope:
# Regular text item
text = get_html_tag_with_text_direction(html_tag="p", text=text)
text = get_html_tag_with_text_direction(html_tag="p", text=text, attrs=lang_attrs)

# Apply formatting and hyperlinks to the parent's own text+tag.
if not post_processed:
Expand Down Expand Up @@ -1147,7 +1167,7 @@ def _serialize_page_img(page_img: Image):
# Create HTML structure
html_parts = [
"<!DOCTYPE html>",
"<html>",
f'<html lang="{html.escape(self.params.html_lang)}">',
self._generate_head(),
"<body>",
]
Expand Down
8 changes: 8 additions & 0 deletions docling_core/transforms/serializer/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
InlineGroup,
KeyValueItem,
KeywordsMetaField,
LanguageMetaField,
ListGroup,
ListItem,
MoleculeMetaField,
Expand Down Expand Up @@ -537,6 +538,13 @@ def _serialize_meta_field(
txt = ", ".join(field_val.values)
elif isinstance(field_val, DescriptionMetaField):
txt = field_val.text
elif isinstance(field_val, LanguageMetaField):
# A language code is metadata, not prose: a bare "de" paragraph
# is indistinguishable from content, so it only renders when
# meta sections are marked ("[Language] de").
if not mark_meta:
return None
txt = field_val.code.value
elif isinstance(field_val, PictureClassificationMetaField):
if not include_picture_classification:
return None
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/2206.01062.yaml.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>2206.01062v1</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/2408.09869_p1_split.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>2408.09869_p1</title>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head></head>
<body>
<div class='page'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head></head>
<body>
<div class='page'>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/2408.09869v3_enriched_p2_p3_p5.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>2408.09869v3</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/2408.09869v3_enriched_split.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>2408.09869v3</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/2408.09869v3_enriched_split_p2.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>2408.09869v3</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/activities.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>activities</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/activities_p1.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>activities</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/activities_p2.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>activities</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/barchart.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Document</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/concatenated.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>2501.17887v1 + Untitled 1 + 2311.18481v1</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/constructed_doc.embedded.html.gt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Untitled 1</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/constructed_doc.placeholder.html.gt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Untitled 1</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/constructed_doc.referenced.html.gt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Untitled 1</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/constructed_document.yaml.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Untitled 1</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/constructed_orig_false.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Untitled 1</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/constructed_orig_true.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Untitled 1</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/dummy_doc.yaml.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>dummy_doc</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/inline_and_formatting.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>inline_and_formatting</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/polymers.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>report on task: `Write me a document on polymers in food-packaging.`</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/doc/rich_table.gt.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Docling Document</title>
Expand Down
2 changes: 1 addition & 1 deletion tests/data/docling_document/export/formula_mathml.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">

<body>
<div class='page'>
Expand Down
47 changes: 47 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,50 @@ def test_keywords_topics_required_values() -> None:
TopicsMetaField(values=34)
with pytest.raises(ValidationError, match="valid string"):
TopicsMetaField(values=[34])


def _doc_with_language_meta() -> DoclingDocument:
doc = DoclingDocument(name="lang-meta")
english = doc.add_text(label=DocItemLabel.TEXT, text="This paragraph is in English.")
english.meta = BaseMeta(language=LanguageMetaField(code=HumanLanguageLabel.EN))
german = doc.add_text(label=DocItemLabel.TEXT, text="Dieser Absatz ist auf Deutsch.")
german.meta = BaseMeta(
language=LanguageMetaField(code=HumanLanguageLabel.DE, confidence=0.99, created_by="detector")
)
doc.add_heading(text="Überschrift", level=1).meta = BaseMeta(language=LanguageMetaField(code=HumanLanguageLabel.DE))
return doc


def test_md_and_text_language_meta_is_not_rendered_as_prose() -> None:
doc = _doc_with_language_meta()
for rendered in (doc.export_to_markdown(), doc.export_to_text()):
assert "HumanLanguageLabel" not in rendered
assert "created_by" not in rendered
assert "Dieser Absatz ist auf Deutsch." in rendered
# Neither the bare code nor its repr appears as a paragraph.
assert "\nde\n" not in f"\n{rendered}\n"
marked = doc.export_to_markdown(mark_meta=True)
assert "[Language] de" in marked
assert "[Language] en" in marked


def test_html_declares_document_and_element_languages() -> None:
doc = _doc_with_language_meta()
default_html = HTMLDocSerializer(doc=doc, params=HTMLParams()).serialize().text
assert '<html lang="en">' in default_html
assert "<p>This paragraph is in English.</p>" in default_html # same as document language
assert '<p lang="de">Dieser Absatz ist auf Deutsch.</p>' in default_html
assert '<h2 lang="de">Überschrift</h2>' in default_html

german_html = HTMLDocSerializer(doc=doc, params=HTMLParams(html_lang="de")).serialize().text
assert '<html lang="de">' in german_html
assert '<p lang="en">This paragraph is in English.</p>' in german_html
assert "<p>Dieser Absatz ist auf Deutsch.</p>" in german_html


def test_html_lang_attribute_is_quote_safe() -> None:
doc = DoclingDocument(name="lang-escape")
doc.add_text(label=DocItemLabel.TEXT, text="x")
html_out = HTMLDocSerializer(doc=doc, params=HTMLParams(html_lang='en" onload="x')).serialize().text
assert '<html lang="en&quot; onload=&quot;x">' in html_out
assert 'lang="en" onload=' not in html_out
Loading