Skip to content

Commit d25f494

Browse files
author
Lukas Geiger
committed
Fix CDATA parsing in feeds
1 parent 7292e70 commit d25f494

4 files changed

Lines changed: 63 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010

1111
### Fixed
1212
- Fixed alarm updates for manual-only feeds when global interval is disabled but other feeds define per-feed intervals.
13+
- Fixed RSS and Atom text parsing so CDATA wrappers are removed from feed titles, item titles, links, and Atom dates.
1314
- Normalized the German locale file to real UTF-8 Umlaute instead of escaped code points.
1415

1516
### Changed
1617
- Added repository line-ending rules, explicitly ignored local pytest caches, and updated the README Edge packaging note.
1718

1819
### Verified
19-
- `npm test` now covers 28 dependency-free Node tests, including theme, service-worker scheduling, and package-content coverage.
20+
- `npm test` now covers 29 dependency-free Node tests, including CDATA cleanup, theme, service-worker scheduling, and package-content coverage.
2021
- `npm run package` creates `dist/RSS-BOOK-v1.1.2-edge.zip` with the Manifest V3 runtime files plus license/privacy docs.
2122

2223
## [1.1.2] — 2026-04-30

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Your feeds live in your bookmarks — accessible everywhere your browser syncs,
2424

2525
- **Manifest V3 native** — built for modern Chromium browsers
2626
- **RSS 2.0 + Atom** — both formats supported
27+
- **CDATA-safe parsing** — titles, links, and Atom dates are cleaned before bookmark creation
2728
- **ETag/304 caching** — bandwidth-efficient, respects server cache headers
2829
- **Per-feed intervals** — each feed can have its own update schedule
2930
- **Retention** — auto-remove bookmarks older than N days
@@ -59,7 +60,7 @@ Create the upload-ready ZIP with `npm run package`. The generated archive stays
5960

6061
## Development
6162

62-
RSS-BOOK has no bundling step. The repository includes 28 dependency-free Node tests for parser behavior, OPML, storage, bookmark cleanup, feed discovery, folder export, store assets, service-worker scheduling, light/dark theme CSS coverage, and Edge package contents:
63+
RSS-BOOK has no bundling step. The repository includes 29 dependency-free Node tests for parser behavior, CDATA cleanup, OPML, storage, bookmark cleanup, feed discovery, folder export, store assets, service-worker scheduling, light/dark theme CSS coverage, and Edge package contents:
6364

6465
```bash
6566
npm test

lib/rss.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ function extractTextContent(xml, tag) {
7575
const re = new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)</${tag}>`, "i");
7676
const m = xml.match(re);
7777
if (!m) return "";
78-
return decodeXmlEntities(m[1].trim());
78+
return decodeXmlEntities(stripCdata(m[1].trim()));
79+
}
80+
81+
function stripCdata(text) {
82+
return text.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/gi, "$1");
7983
}
8084

8185
function extractAttr(xml, tag, attr) {

tests/rss.test.mjs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,60 @@ test("parses RSS 2.0 feeds and sends cache validators", async () => {
6363
]);
6464
});
6565

66+
test("strips CDATA wrappers from RSS and Atom text content", async () => {
67+
globalThis.fetch = async () => new Response(
68+
`<?xml version="1.0"?>
69+
<rss version="2.0">
70+
<channel>
71+
<title><![CDATA[Example & News]]></title>
72+
<item>
73+
<title><![CDATA[First <Post>]]></title>
74+
<link><![CDATA[https://example.test/first]]></link>
75+
<guid>item-1</guid>
76+
</item>
77+
</channel>
78+
</rss>`,
79+
{ status: 200 }
80+
);
81+
82+
const rssParsed = await fetchFeedAndParse("https://example.test/feed.xml");
83+
84+
assert.equal(rssParsed.title, "Example & News");
85+
assert.deepEqual(rssParsed.items, [
86+
{
87+
title: "First <Post>",
88+
link: "https://example.test/first",
89+
guid: "item-1",
90+
published: ""
91+
}
92+
]);
93+
94+
globalThis.fetch = async () => new Response(
95+
`<feed xmlns="http://www.w3.org/2005/Atom">
96+
<title><![CDATA[Atom Feed]]></title>
97+
<entry>
98+
<title><![CDATA[Atom & One]]></title>
99+
<id>urn:post:1</id>
100+
<updated><![CDATA[2026-04-30T10:00:00Z]]></updated>
101+
<link href="https://example.test/one" rel="alternate" />
102+
</entry>
103+
</feed>`,
104+
{ status: 200 }
105+
);
106+
107+
const atomParsed = await fetchFeedAndParse("https://example.test/atom.xml");
108+
109+
assert.equal(atomParsed.title, "Atom Feed");
110+
assert.deepEqual(atomParsed.items, [
111+
{
112+
title: "Atom & One",
113+
link: "https://example.test/one",
114+
guid: "urn:post:1",
115+
published: "2026-04-30T10:00:00Z"
116+
}
117+
]);
118+
});
119+
66120
test("parses Atom entries and prefers rel alternate links regardless of attribute order", async () => {
67121
globalThis.fetch = async () => new Response(
68122
`<feed xmlns="http://www.w3.org/2005/Atom">

0 commit comments

Comments
 (0)