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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ MarkItDown currently supports the conversion from:
- ZIP files (iterates over contents)
- YouTube URLs
- EPubs
- RSS and Atom feeds (including Atom feeds without entries)
- ... and more!

## Why Markdown?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ def _feed_type(self, doc: Document) -> str | None:
if root.tagName == "rss":
return "rss"
if root.localName == "feed" and root.namespaceURI in (None, ATOM_NAMESPACE):
if self._get_children(root, "entry"):
# An Atom feed must have a root element of <feed> and at least one <entry>
# RFC 4287 permits zero entries. Keep the entry heuristic only for
# legacy feeds that do not declare the Atom namespace.
if root.namespaceURI == ATOM_NAMESPACE or self._get_children(root, "entry"):
return "atom"
return None

Expand Down
48 changes: 48 additions & 0 deletions packages/markitdown/tests/test_rss_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,54 @@
from markitdown.converters import RssConverter


@pytest.mark.parametrize("prefix", ["", "a:"])
@pytest.mark.parametrize(
"stream_info",
[
StreamInfo(extension=".xml"),
StreamInfo(extension=".atom"),
StreamInfo(mimetype="application/xml"),
StreamInfo(mimetype="application/atom+xml"),
],
)
def test_atom_without_entries(prefix: str, stream_info: StreamInfo) -> None:
namespace = "xmlns:a" if prefix else "xmlns"
feed = f"""<{prefix}feed {namespace}="http://www.w3.org/2005/Atom">
<{prefix}title>Release updates</{prefix}title>
<{prefix}subtitle>No releases yet.</{prefix}subtitle>
<{prefix}id>urn:example:releases</{prefix}id>
<{prefix}updated>2026-09-13T00:00:00Z</{prefix}updated>
<{prefix}author><{prefix}name>Example project</{prefix}name></{prefix}author>
</{prefix}feed>""".encode(
"utf-8"
)
converter = RssConverter()
stream = io.BytesIO(feed)

assert converter.accepts(stream, stream_info)
assert stream.tell() == 0
result = converter.convert(stream, stream_info)
assert result.title == "Release updates"
assert result.markdown == "# Release updates\nNo releases yet.\n"

converted = MarkItDown().convert_stream(io.BytesIO(feed), stream_info=stream_info)
assert converted.title == result.title
assert converted.markdown == result.markdown


@pytest.mark.parametrize("namespace", ["", "urn:example:other"])
def test_non_atom_feed_without_entries_is_not_accepted(namespace: str) -> None:
feed = f'<feed xmlns="{namespace}"><title>Other feed</title></feed>'.encode()
converter = RssConverter()
stream_info = StreamInfo(extension=".xml")
stream = io.BytesIO(feed)

assert not converter.accepts(stream, stream_info)
assert stream.tell() == 0
with pytest.raises(ValueError, match="Unknown feed type"):
converter.convert(stream, stream_info)


@pytest.mark.parametrize(
"root_prefix, child_prefix",
[("", ""), ("a:", "a:"), ("a:", "b:"), ("a:", ""), ("", "a:")],
Expand Down