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
45 changes: 44 additions & 1 deletion docling_core/transforms/serializer/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pathlib import Path
from typing import Annotated, Any, Optional, Union

from pydantic import AnyUrl, BaseModel, Field, PositiveInt
from pydantic import AnyUrl, BaseModel, Field, PositiveInt, model_validator
from tabulate import _column_type, tabulate
from typing_extensions import override

Expand Down Expand Up @@ -144,6 +144,14 @@ class OrigListItemMarkerMode(str, Enum):
AUTO = "auto"


class FurnitureMode(str, Enum):
"""Display mode for document furniture (headers/footers)."""

NONE = "none"
ALL = "all"
DISTINCT = "distinct"


class MarkdownParams(CommonParams):
"""Markdown-specific serialization parameters."""

Expand Down Expand Up @@ -183,6 +191,17 @@ class MarkdownParams(CommonParams):
)
),
] = False
furniture_mode: FurnitureMode = Field(
default=FurnitureMode.NONE,
description="Control whether headers and footers are serialized (none, all, or distinct).",
)

@model_validator(mode="after")
def _adjust_layers_for_furniture(self) -> "MarkdownParams":
"""Automatically include FURNITURE layer if mode is not NONE."""
if self.furniture_mode != FurnitureMode.NONE:
self.layers.add(ContentLayer.FURNITURE)
return self


class MarkdownTextSerializer(BaseModel, BaseTextSerializer):
Expand Down Expand Up @@ -966,6 +985,30 @@ def serialize(
**kwargs: Any,
) -> SerializationResult:
"""Serialize a given node."""

if item is not None and getattr(item, "content_layer", None) == ContentLayer.FURNITURE:
# 'none' mode: skip completely
if self.params.furniture_mode == FurnitureMode.NONE:
return create_ser_result()

# 'distinct' mode: deduplicate based on text
if self.params.furniture_mode == FurnitureMode.DISTINCT:
# Initialize the tracking set on the serializer instance if it doesn't exist
if not hasattr(self, "_seen_furniture"):
self._seen_furniture: set[str] = set()

# Extract text for comparison (fallback to ref if no text exists)
item_text = getattr(item, "text", None)
if not item_text:
item_text = item.self_ref

# If we've seen this exact header/footer text before, skip it
if item_text in self._seen_furniture:
return create_ser_result()

# Otherwise, mark it as seen and proceed
self._seen_furniture.add(item_text)

return super().serialize(
item=item,
list_level=list_level,
Expand Down
40 changes: 40 additions & 0 deletions test/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
HTMLTableSerializer,
)
from docling_core.transforms.serializer.markdown import (
FurnitureMode,
MarkdownDocSerializer,
MarkdownParams,
MarkdownTableSerializer,
Expand All @@ -29,6 +30,7 @@
from docling_core.types.doc.document import (
BaseMeta,
CharSpan,
ContentLayer,
DescriptionAnnotation,
EntitiesMetaField,
EntityMention,
Expand Down Expand Up @@ -612,6 +614,44 @@ def test_md_traverse_pictures():
assert "<!-- image -->" in result_with_traverse


def test_md_furniture_modes():
"""Test that furniture_mode correctly filters and deduplicates headers/footers."""
doc = DoclingDocument(name="Furniture Test")

# 1. Add standard body text
doc.add_text(label=DocItemLabel.PARAGRAPH, text="Main body paragraph text.")

# 2. Add headers/footers and tag them as FURNITURE layer items
h1 = doc.add_text(label=DocItemLabel.TEXT, text="Document Header Title")
h1.content_layer = ContentLayer.FURNITURE

h2 = doc.add_text(label=DocItemLabel.TEXT, text="Document Header Title") # Duplicate text
h2.content_layer = ContentLayer.FURNITURE

h3 = doc.add_text(label=DocItemLabel.TEXT, text="Different Footer Notice")
h3.content_layer = ContentLayer.FURNITURE

# Test Mode 1: NONE (Default) -> Should completely drop furniture items
ser_none = MarkdownDocSerializer(doc=doc, params=MarkdownParams(furniture_mode=FurnitureMode.NONE))
text_none = ser_none.serialize().text
assert "Main body paragraph text." in text_none
assert "Document Header Title" not in text_none

# Test Mode 2: ALL -> Should render every furniture item, including duplicates
ser_all = MarkdownDocSerializer(doc=doc, params=MarkdownParams(furniture_mode=FurnitureMode.ALL))
text_all = ser_all.serialize().text
assert "Main body paragraph text." in text_all
assert text_all.count("Document Header Title") == 2
assert "Different Footer Notice" in text_all

# Test Mode 3: DISTINCT -> Should render unique text items exactly once
ser_distinct = MarkdownDocSerializer(doc=doc, params=MarkdownParams(furniture_mode=FurnitureMode.DISTINCT))
text_distinct = ser_distinct.serialize().text
assert "Main body paragraph text." in text_distinct
assert text_distinct.count("Document Header Title") == 1
assert "Different Footer Notice" in text_distinct


# ===============================
# HTML tests
# ===============================
Expand Down
Loading