@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.
Files changed (2) hide show
  1. package/README.md +318 -0
  2. 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.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
  },