Skip to content

Commit ee6fe25

Browse files
weivwangafourney
andauthored
fix(rss): preserve Atom XHTML content (#2297)
* fix(rss): preserve Atom XHTML content * Support xhtml namespace prefixes. --------- Co-authored-by: afourney <adamfo@microsoft.com>
1 parent bebea8b commit ee6fe25

2 files changed

Lines changed: 110 additions & 2 deletions

File tree

packages/markitdown/src/markitdown/converters/_rss_converter.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import warnings
22

33
from defusedxml import minidom
4+
from xml.dom import Node
45
from xml.dom.minidom import Document, Element
56
from typing import BinaryIO, Any, Union
67
from bs4 import BeautifulSoup
@@ -27,6 +28,8 @@
2728
".xml",
2829
]
2930

31+
XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
32+
3033

3134
class RssConverter(DocumentConverter):
3235
"""Convert RSS / Atom type to markdown"""
@@ -119,9 +122,9 @@ def _parse_atom_type(
119122
md_text += f"{subtitle}\n"
120123
for entry in entries:
121124
entry_title = self._get_data_by_tag_name(entry, "title")
122-
entry_summary = self._get_data_by_tag_name(entry, "summary")
125+
entry_summary = self._get_atom_content(entry, "summary")
123126
entry_updated = self._get_data_by_tag_name(entry, "updated")
124-
entry_content = self._get_data_by_tag_name(entry, "content")
127+
entry_content = self._get_atom_content(entry, "content")
125128

126129
if entry_title:
127130
md_text += f"\n## {entry_title}\n"
@@ -137,6 +140,37 @@ def _parse_atom_type(
137140
title=title,
138141
)
139142

143+
def _get_atom_content(self, entry: Element, tag_name: str) -> Union[str, None]:
144+
nodes = entry.getElementsByTagName(tag_name)
145+
if not nodes:
146+
return None
147+
148+
node = nodes[0]
149+
if node.getAttribute("type").lower() != "xhtml":
150+
return self._get_data_by_tag_name(entry, tag_name)
151+
152+
return "".join(
153+
self._localize_xhtml_names(child.cloneNode(True)).toxml()
154+
for child in node.childNodes
155+
if child.nodeType == Node.ELEMENT_NODE
156+
)
157+
158+
def _localize_xhtml_names(self, node: Node) -> Node:
159+
"""Rewrite prefixed XHTML element names to their local HTML names.
160+
161+
Atom permits XHTML content to be namespace-prefixed (e.g. ``x:strong``).
162+
The downstream HTML converter dispatches on HTML tag names, so the
163+
prefix has to be dropped or the element is treated as an unknown tag
164+
and its formatting is lost.
165+
"""
166+
if node.nodeType == Node.ELEMENT_NODE:
167+
if node.prefix and node.namespaceURI == XHTML_NAMESPACE:
168+
node.tagName = node.nodeName = node.localName
169+
node.prefix = None
170+
for child in node.childNodes:
171+
self._localize_xhtml_names(child)
172+
return node
173+
140174
def _parse_rss_type(
141175
self, doc: Document, *, strict: bool = False
142176
) -> DocumentConverterResult:
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import io
2+
3+
from markitdown import StreamInfo
4+
from markitdown.converters import RssConverter
5+
6+
7+
def test_atom_xhtml_content_is_preserved() -> None:
8+
feed = b"""<?xml version="1.0" encoding="utf-8"?>
9+
<feed xmlns="http://www.w3.org/2005/Atom">
10+
<title>Example feed</title>
11+
<entry>
12+
<title>Example entry</title>
13+
<content type="xhtml">
14+
<div xmlns="http://www.w3.org/1999/xhtml">
15+
<p>Read the <strong>important details</strong>.</p>
16+
</div>
17+
</content>
18+
</entry>
19+
</feed>
20+
"""
21+
22+
result = RssConverter().convert(
23+
io.BytesIO(feed), StreamInfo(mimetype="application/atom+xml")
24+
)
25+
26+
assert "Read the **important details**." in result.markdown
27+
28+
29+
def test_atom_xhtml_summary_is_preserved() -> None:
30+
feed = b"""<?xml version="1.0" encoding="utf-8"?>
31+
<feed xmlns="http://www.w3.org/2005/Atom">
32+
<title>Example feed</title>
33+
<entry>
34+
<title>Example entry</title>
35+
<summary type="xhtml">
36+
<div xmlns="http://www.w3.org/1999/xhtml">
37+
<p>A <em>structured</em> summary.</p>
38+
</div>
39+
</summary>
40+
<content type="text">Plain text content.</content>
41+
</entry>
42+
</feed>
43+
"""
44+
45+
result = RssConverter().convert(
46+
io.BytesIO(feed), StreamInfo(mimetype="application/atom+xml")
47+
)
48+
49+
assert "A *structured* summary." in result.markdown
50+
assert "Plain text content." in result.markdown
51+
52+
53+
def test_atom_prefixed_xhtml_content_is_preserved() -> None:
54+
feed = b"""<?xml version="1.0" encoding="utf-8"?>
55+
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:x="http://www.w3.org/1999/xhtml">
56+
<title>Example feed</title>
57+
<entry>
58+
<title>Example entry</title>
59+
<content type="xhtml">
60+
<x:div>
61+
<x:p>Hello <x:strong>bold</x:strong> and <x:em>italic</x:em>.</x:p>
62+
<x:a href="https://example.com">link</x:a>
63+
</x:div>
64+
</content>
65+
</entry>
66+
</feed>
67+
"""
68+
69+
result = RssConverter().convert(
70+
io.BytesIO(feed), StreamInfo(mimetype="application/atom+xml")
71+
)
72+
73+
assert "Hello **bold** and *italic*." in result.markdown
74+
assert "[link](https://example.com)" in result.markdown

0 commit comments

Comments
 (0)