@molecule/api-feed-rss-parser 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -0
- package/package.json +8 -2
package/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-08-04T00:36:30.547Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-feed-rss-parser
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
Pure-function feed parser for molecule.dev — RSS 2.0, Atom 1.0, RSS 1.0
|
|
16
|
+
(RDF), and JSON Feed 1.0 / 1.1, normalized into a single `{ feed, items[] }`
|
|
17
|
+
shape. Used by `news-aggregator`, `blog`, and `podcast` apps for ingesting
|
|
18
|
+
external feeds and dedup'ing items by stable id.
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { parseFeed } from '@molecule/api-feed-rss-parser'
|
|
24
|
+
|
|
25
|
+
const res = await fetch('https://example.com/feed.xml')
|
|
26
|
+
const { feed, items } = parseFeed(await res.text(), {
|
|
27
|
+
contentType: res.headers.get('content-type') ?? undefined,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
for (const item of items) {
|
|
31
|
+
console.log(item.id, item.title, item.publishedAt)
|
|
32
|
+
for (const enclosure of item.enclosures ?? []) {
|
|
33
|
+
console.log(' ↳', enclosure.url, enclosure.type, enclosure.durationSeconds)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Type
|
|
39
|
+
|
|
40
|
+
`utility`
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @molecule/api-feed-rss-parser fast-xml-parser
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### Interfaces
|
|
51
|
+
|
|
52
|
+
#### `FeedEnclosure`
|
|
53
|
+
|
|
54
|
+
A normalized media enclosure attached to a feed item (podcast audio,
|
|
55
|
+
video, image, etc.). Source: RSS `<enclosure>`, Atom `<link rel="enclosure">`,
|
|
56
|
+
JSON Feed `attachments[]`.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
interface FeedEnclosure {
|
|
60
|
+
/** Direct URL to the media resource. Required. */
|
|
61
|
+
url: string
|
|
62
|
+
|
|
63
|
+
/** MIME type (e.g. `audio/mpeg`, `video/mp4`, `image/png`) when known. */
|
|
64
|
+
type?: string
|
|
65
|
+
|
|
66
|
+
/** Length in bytes when reported by the source feed. */
|
|
67
|
+
length?: number
|
|
68
|
+
|
|
69
|
+
/** Display title or alt text for the enclosure when supplied. */
|
|
70
|
+
title?: string
|
|
71
|
+
|
|
72
|
+
/** Duration in seconds — derived from iTunes `<itunes:duration>` for podcasts. */
|
|
73
|
+
durationSeconds?: number
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### `NormalizedFeed`
|
|
78
|
+
|
|
79
|
+
Normalized feed-level metadata. The exact subset of fields populated
|
|
80
|
+
depends on what the source feed publishes; only `title` is guaranteed.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
interface NormalizedFeed {
|
|
84
|
+
/** Feed/channel title. Empty string if the source omitted a title. */
|
|
85
|
+
title: string
|
|
86
|
+
|
|
87
|
+
/** Long-form description / subtitle / tagline. */
|
|
88
|
+
description?: string
|
|
89
|
+
|
|
90
|
+
/** Canonical website URL (NOT the feed self-link). */
|
|
91
|
+
link?: string
|
|
92
|
+
|
|
93
|
+
/** Self-referential URL of the feed document, when supplied. */
|
|
94
|
+
feedUrl?: string
|
|
95
|
+
|
|
96
|
+
/** RFC 5646 / BCP 47 language tag (e.g. `en`, `en-US`). */
|
|
97
|
+
language?: string
|
|
98
|
+
|
|
99
|
+
/** ISO 8601 timestamp of the feed's last update. */
|
|
100
|
+
updatedAt?: string
|
|
101
|
+
|
|
102
|
+
/** Author / managing editor name reported by the feed. */
|
|
103
|
+
author?: string
|
|
104
|
+
|
|
105
|
+
/** Copyright notice / rights statement. */
|
|
106
|
+
copyright?: string
|
|
107
|
+
|
|
108
|
+
/** Feed-level image / logo / icon URL. */
|
|
109
|
+
imageUrl?: string
|
|
110
|
+
|
|
111
|
+
/** Detected source format. */
|
|
112
|
+
format: FeedFormat
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### `NormalizedFeedItem`
|
|
117
|
+
|
|
118
|
+
Normalized feed item. The exact subset of fields populated depends on
|
|
119
|
+
what the source feed publishes; only `id` is guaranteed (synthesized from
|
|
120
|
+
`link` or content hash if no GUID is present).
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
interface NormalizedFeedItem {
|
|
124
|
+
/**
|
|
125
|
+
* Stable, opaque identifier for deduplication.
|
|
126
|
+
* Source-of-truth order: `<guid>` / `<id>` / JSON Feed `id` →
|
|
127
|
+
* `link` URL → SHA-1 hash of `title + publishedAt`. Always non-empty.
|
|
128
|
+
*/
|
|
129
|
+
id: string
|
|
130
|
+
|
|
131
|
+
/** Item title. Empty string if the source omitted a title. */
|
|
132
|
+
title: string
|
|
133
|
+
|
|
134
|
+
/** Canonical URL the item points to. */
|
|
135
|
+
link?: string
|
|
136
|
+
|
|
137
|
+
/** Short summary / description / excerpt — sanitized HTML or plain text. */
|
|
138
|
+
summary?: string
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Full content body — sanitized HTML (`<script>` tags stripped at minimum).
|
|
142
|
+
* Source: `<content:encoded>`, Atom `<content>`, JSON Feed `content_html`/`content_text`.
|
|
143
|
+
*/
|
|
144
|
+
content?: string
|
|
145
|
+
|
|
146
|
+
/** Author display name reported by the item. */
|
|
147
|
+
author?: string
|
|
148
|
+
|
|
149
|
+
/** ISO 8601 timestamp of publication. */
|
|
150
|
+
publishedAt?: string
|
|
151
|
+
|
|
152
|
+
/** ISO 8601 timestamp of the last update / modification. */
|
|
153
|
+
updatedAt?: string
|
|
154
|
+
|
|
155
|
+
/** Tags / categories / keywords assigned to the item. */
|
|
156
|
+
categories?: string[]
|
|
157
|
+
|
|
158
|
+
/** Attached media (podcast audio, embedded images, etc.). */
|
|
159
|
+
enclosures?: FeedEnclosure[]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### `ParsedFeed`
|
|
164
|
+
|
|
165
|
+
Parsed feed result returned by {@link parseFeed}.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
interface ParsedFeed {
|
|
169
|
+
/** Normalized feed-level metadata. */
|
|
170
|
+
feed: NormalizedFeed
|
|
171
|
+
|
|
172
|
+
/** Items in source order (NOT re-sorted by date). */
|
|
173
|
+
items: NormalizedFeedItem[]
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### `ParseFeedOptions`
|
|
178
|
+
|
|
179
|
+
Options controlling {@link parseFeed} behavior.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
interface ParseFeedOptions {
|
|
183
|
+
/**
|
|
184
|
+
* Hint for the source format. When omitted, format detection runs:
|
|
185
|
+
* 1. If `contentType` includes `json` or body trims to `{` / `[` → JSON Feed.
|
|
186
|
+
* 2. XML is parsed and the root element selects `rss-2.0` / `atom-1.0` / `rdf-1.0`.
|
|
187
|
+
*
|
|
188
|
+
* Pass `contentType` from the HTTP response when available; it short-circuits
|
|
189
|
+
* the JSON vs. XML guess on ambiguous payloads.
|
|
190
|
+
*/
|
|
191
|
+
contentType?: string
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Optional explicit format override. Skips detection entirely.
|
|
195
|
+
*/
|
|
196
|
+
format?: FeedFormat
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* When `true` (default), strips `<script>` blocks from any HTML content
|
|
200
|
+
* fields (`summary`, `content`). Set to `false` only when the caller
|
|
201
|
+
* performs sanitization downstream.
|
|
202
|
+
*/
|
|
203
|
+
sanitizeHtml?: boolean
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Types
|
|
208
|
+
|
|
209
|
+
#### `FeedFormat`
|
|
210
|
+
|
|
211
|
+
Discriminator for the source feed format detected by {@link parseFeed}.
|
|
212
|
+
|
|
213
|
+
- `rss-2.0` — RSS 2.0 (`<rss version="2.0"><channel>...`)
|
|
214
|
+
- `atom-1.0` — Atom 1.0 (`<feed xmlns="http://www.w3.org/2005/Atom">...`)
|
|
215
|
+
- `rdf-1.0` — RSS 1.0 / RDF (`<rdf:RDF ...><channel>...<item>`)
|
|
216
|
+
- `json-feed` — JSON Feed 1.0 / 1.1 (https://www.jsonfeed.org/version/1.1/)
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
type FeedFormat = 'rss-2.0' | 'atom-1.0' | 'rdf-1.0' | 'json-feed'
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Classes
|
|
223
|
+
|
|
224
|
+
#### `FeedParseError`
|
|
225
|
+
|
|
226
|
+
Error raised when {@link parseFeed} cannot determine the source format
|
|
227
|
+
or the body is malformed.
|
|
228
|
+
|
|
229
|
+
### Functions
|
|
230
|
+
|
|
231
|
+
#### `detectFeedFormat(body, contentType)`
|
|
232
|
+
|
|
233
|
+
Detect the source format of a feed body.
|
|
234
|
+
|
|
235
|
+
Detection runs in this order:
|
|
236
|
+
|
|
237
|
+
1. If `contentType` includes `json`, OR the trimmed body begins with `{`/`[`,
|
|
238
|
+
return `'json-feed'` without parsing XML.
|
|
239
|
+
2. Otherwise the XML root element selects the format:
|
|
240
|
+
- `rss` → `'rss-2.0'`
|
|
241
|
+
- `feed` → `'atom-1.0'`
|
|
242
|
+
- `rdf:RDF` (or `RDF`) → `'rdf-1.0'`
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
function detectFeedFormat(body: string, contentType: string | undefined): FeedFormat | undefined
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
- `body` — Raw body string.
|
|
249
|
+
- `contentType` — Optional `Content-Type` header value from the HTTP response.
|
|
250
|
+
|
|
251
|
+
**Returns:** Detected format, or `undefined` when the body cannot be classified.
|
|
252
|
+
|
|
253
|
+
#### `parseFeed(body, options)`
|
|
254
|
+
|
|
255
|
+
Parse an RSS / Atom / RDF / JSON Feed body into a normalized
|
|
256
|
+
`{ feed, items[] }` shape.
|
|
257
|
+
|
|
258
|
+
Pure function — no I/O, no state. Pass the response body and (optionally)
|
|
259
|
+
the HTTP `Content-Type` header. Format detection is automatic; pass
|
|
260
|
+
`options.format` to override.
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
function parseFeed(body: string, options?: ParseFeedOptions): ParsedFeed
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
- `body` — The full HTTP response body. Required, must be non-empty.
|
|
267
|
+
- `options` — Parser options. See {@link ParseFeedOptions}.
|
|
268
|
+
|
|
269
|
+
**Returns:** Normalized feed + items.
|
|
270
|
+
|
|
271
|
+
#### `sanitizeHtml(html)`
|
|
272
|
+
|
|
273
|
+
Strip dangerous content from an HTML string. Returns plain text unchanged.
|
|
274
|
+
|
|
275
|
+
Removes:
|
|
276
|
+
|
|
277
|
+
- Complete `<script>...</script>` and `<style>...</style>` blocks.
|
|
278
|
+
- Stray opening/closing tags of those elements (in malformed HTML).
|
|
279
|
+
- Inline event handlers (`onclick="..."`, `onerror="..."`, etc.).
|
|
280
|
+
- `javascript:` URLs in `href` / `src` / `xlink:href` / `(form)action`.
|
|
281
|
+
|
|
282
|
+
Idempotent: `sanitizeHtml(sanitizeHtml(x)) === sanitizeHtml(x)`.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
function sanitizeHtml(html: string | null | undefined): string | undefined
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
- `html` — HTML or plain-text string. `undefined` and `null` short-circuit to `undefined` so callers can chain safely.
|
|
289
|
+
|
|
290
|
+
**Returns:** Sanitized HTML string, or `undefined` when the input was nullish.
|
|
291
|
+
|
|
292
|
+
## Injection Notes
|
|
293
|
+
|
|
294
|
+
### Runtime Dependencies
|
|
295
|
+
|
|
296
|
+
- `fast-xml-parser`
|
|
297
|
+
|
|
298
|
+
The parser is a pure function — it accepts the response body string plus
|
|
299
|
+
optional `contentType` / `format` hints and returns a normalized result.
|
|
300
|
+
No fetch, no state, no caching: callers fetch the document themselves
|
|
301
|
+
(native `fetch` or the bonded `@molecule/api-http` provider) and can
|
|
302
|
+
layer `@molecule/api-cache` on top for TTL caching.
|
|
303
|
+
|
|
304
|
+
iTunes podcast namespace fields (`<itunes:duration>`, `<itunes:author>`,
|
|
305
|
+
`<itunes:image>`, `<itunes:category>`) are extracted into the normalized
|
|
306
|
+
shape — duration ends up on the matching `<enclosure>` as
|
|
307
|
+
`enclosure.durationSeconds`. Dublin Core (`dc:creator`, `dc:date`,
|
|
308
|
+
`dc:subject`) is also recognized for RSS 1.0 / RDF feeds.
|
|
309
|
+
|
|
310
|
+
Item content fields are sanitized by default — `<script>` blocks, inline
|
|
311
|
+
event handlers, and `javascript:` URLs are stripped. Pass
|
|
312
|
+
`sanitizeHtml: false` to opt out (e.g. when sanitization happens further
|
|
313
|
+
down the pipeline).
|
|
314
|
+
|
|
315
|
+
Item identifiers are derived in priority order: explicit GUID / Atom id /
|
|
316
|
+
JSON Feed id → item link URL → SHA-1 of `title + publishedAt`. The result
|
|
317
|
+
is always non-empty and stable across re-parses, so deduplication on
|
|
318
|
+
`item.id` is safe.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-feed-rss-parser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "RSS/Atom/JSON Feed parser — normalizes RSS 2.0, Atom 1.0, RDF 1.0, and JSON Feed into a uniform { feed, items[] } shape.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"molecule",
|
|
@@ -29,6 +30,11 @@
|
|
|
29
30
|
"parser"
|
|
30
31
|
],
|
|
31
32
|
"license": "Apache-2.0",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/molecule-dev/molecule.git",
|
|
36
|
+
"directory": "packages/api/utilities/feed-rss-parser"
|
|
37
|
+
},
|
|
32
38
|
"dependencies": {
|
|
33
39
|
"fast-xml-parser": "5.10.1"
|
|
34
40
|
},
|