@molecule/api-feed-rss 1.0.0

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/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * RSS / Atom / JSON Feed **producer** for molecule.dev — pure-function
3
+ * serializers that turn a single normalized {@link Feed} structure into
4
+ * a valid RSS 2.0, Atom 1.0, or JSON Feed 1.1 document.
5
+ *
6
+ * Companion to `@molecule/api-feed-rss-parser` (the consumer side). Both
7
+ * packages share a similar normalized shape, so apps can ingest external
8
+ * feeds, store the items, and re-publish them through this package
9
+ * without intermediate translation layers.
10
+ *
11
+ * Apps that use this:
12
+ * - `blog` — emit `/feed.rss`, `/feed.atom`, `/feed.json` for posts.
13
+ * - `news-aggregator` — re-publish curated cross-source feeds.
14
+ * - `podcast` — RSS 2.0 with the iTunes namespace (set `feed.itunes`).
15
+ *
16
+ * @example One serializer per call
17
+ * ```ts
18
+ * import { serializeRss2, serializeAtom1, serializeJsonFeed } from '@molecule/api-feed-rss'
19
+ *
20
+ * const feed = {
21
+ * title: 'My Blog',
22
+ * link: 'https://example.com',
23
+ * description: 'Posts about software',
24
+ * items: [{ id: '1', title: 'Hello', link: 'https://example.com/p/1' }],
25
+ * }
26
+ *
27
+ * const rss = serializeRss2(feed)
28
+ * const atom = serializeAtom1(feed)
29
+ * const json = serializeJsonFeed(feed)
30
+ * ```
31
+ *
32
+ * @example HTTP handler with caching — Express adapter
33
+ * ```ts
34
+ * import { createFeedHandler, type Feed } from '@molecule/api-feed-rss'
35
+ *
36
+ * const loadFromDb = async (): Promise<Feed> => ({ title: 'My Feed', link: 'https://example.com', description: 'Latest posts', items: [] }) // your DB lookup
37
+ * const app = { get(_path: string, _fn: (req: any, res: any) => void): void {} } // your Express app
38
+ *
39
+ * const handle = createFeedHandler({
40
+ * loadFeed: async () => loadFromDb(),
41
+ * cacheTtlMs: 60_000,
42
+ * })
43
+ *
44
+ * // Serves GET /feed.rss, /feed.atom, /feed.json (keyed by extension).
45
+ * // The handler returns { status, headers, body } — map those onto any
46
+ * // framework's response (Fastify/Hono/raw http look the same).
47
+ * app.get('/feed.:ext', async (req, res) => {
48
+ * const result = await handle({
49
+ * extension: req.params.ext,
50
+ * ifNoneMatch: req.get('If-None-Match'),
51
+ * })
52
+ * res.status(result.status).set(result.headers).send(result.body)
53
+ * })
54
+ * ```
55
+ *
56
+ * **Security**: every user-supplied field is escaped before serialization.
57
+ * RSS / Atom XML use {@link escapeXml} for text nodes, {@link escapeAttr}
58
+ * for attributes, {@link escapeUrl} for URL attributes (which neutralizes
59
+ * `javascript:` URIs), and {@link wrapCdata} for HTML content (with
60
+ * `]]>` splitting). JSON Feed relies on `JSON.stringify` for escaping.
61
+ *
62
+ * @module
63
+ */
64
+ export * from './browser-guard.js';
65
+ export * from './escape.js';
66
+ export * from './handler.js';
67
+ export * from './serializeAtom1.js';
68
+ export * from './serializeFeed.js';
69
+ export * from './serializeJsonFeed.js';
70
+ export * from './serializeRss2.js';
71
+ export * from './types.js';
72
+ export * from './utilities.js';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Atom 1.0 serializer — accepts a {@link Feed} structure and returns a
3
+ * complete Atom 1.0 XML document string.
4
+ *
5
+ * @module
6
+ */
7
+ import type { Feed, SerializeAtom1Options } from './types.js';
8
+ /**
9
+ * Serialize a {@link Feed} to a complete Atom 1.0 XML document.
10
+ *
11
+ * Conforms to RFC 4287:
12
+ * - Declares `xmlns="http://www.w3.org/2005/Atom"` on the root.
13
+ * - Always emits required `<id>`, `<title>`, `<updated>` elements.
14
+ * - Each entry has `<id>`, `<title>`, `<updated>`.
15
+ * - HTML content is wrapped in CDATA inside `<content type="html">` /
16
+ * `<summary type="html">`. Plain-text content uses `type="text"` with
17
+ * raw escaping.
18
+ *
19
+ * **All user-supplied content is escaped** — text fields run through
20
+ * {@link escapeXml}, attribute values through {@link escapeAttr}, URLs
21
+ * through {@link escapeUrl} (which neutralizes `javascript:` URIs), and
22
+ * HTML-bearing fields are CDATA-wrapped via {@link wrapCdata}.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { serializeAtom1 } from '@molecule/api-feed-rss'
27
+ *
28
+ * const xml = serializeAtom1({
29
+ * title: 'My Blog',
30
+ * link: 'https://example.com',
31
+ * description: 'Posts about software',
32
+ * feedUrl: 'https://example.com/feed.atom',
33
+ * updatedAt: new Date(),
34
+ * items: [
35
+ * {
36
+ * id: 'urn:uuid:123',
37
+ * title: 'Hello, world!',
38
+ * link: 'https://example.com/posts/hello',
39
+ * publishedAt: new Date(),
40
+ * content: '<p>First post.</p>',
41
+ * },
42
+ * ],
43
+ * })
44
+ * ```
45
+ *
46
+ * @param feed - The feed structure to serialize.
47
+ * @param options - See {@link SerializeAtom1Options}.
48
+ * @returns Complete Atom 1.0 XML document string.
49
+ * @throws {FeedSerializeError} when required fields are missing.
50
+ */
51
+ export declare function serializeAtom1(feed: Feed, options?: SerializeAtom1Options): string;
52
+ //# sourceMappingURL=serializeAtom1.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializeAtom1.d.ts","sourceRoot":"","sources":["../src/serializeAtom1.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAwB,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAyFnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,qBAA0B,GAAG,MAAM,CA6CtF"}
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Atom 1.0 serializer — accepts a {@link Feed} structure and returns a
3
+ * complete Atom 1.0 XML document string.
4
+ *
5
+ * @module
6
+ */
7
+ import { escapeAttr, escapeUrl, escapeXml, wrapCdata } from './escape.js';
8
+ import { assertFeedShape, indent, toIso } from './utilities.js';
9
+ const ATOM_NS = 'http://www.w3.org/2005/Atom';
10
+ /**
11
+ * Render an `<author>` block. Atom requires `<name>`; `<email>` and
12
+ * `<uri>` are optional. The `name` is supplied by callers — empty names
13
+ * fall back to `'Anonymous'` to keep the document well-formed.
14
+ *
15
+ * @param author - The author record.
16
+ * @param pretty - Whether to pretty-print children.
17
+ * @returns `<author>...</author>` XML string.
18
+ */
19
+ function renderAuthor(author, pretty) {
20
+ const name = author.name && author.name !== '' ? author.name : 'Anonymous';
21
+ const parts = [`<name>${escapeXml(name)}</name>`];
22
+ if (author.email)
23
+ parts.push(`<email>${escapeXml(author.email)}</email>`);
24
+ if (author.url)
25
+ parts.push(`<uri>${escapeUrl(author.url)}</uri>`);
26
+ if (!pretty)
27
+ return `<author>${parts.join('')}</author>`;
28
+ return `<author>\n${indent(parts.join('\n'), 2)}\n</author>`;
29
+ }
30
+ /**
31
+ * Render a single `<entry>`.
32
+ *
33
+ * @param item - Feed item.
34
+ * @param feedUpdated - ISO timestamp used as `<updated>` fallback.
35
+ * @param pretty - Whether to pretty-print children.
36
+ * @returns `<entry>...</entry>` XML string.
37
+ */
38
+ function renderEntry(item, feedUpdated, pretty) {
39
+ const lines = [];
40
+ lines.push(`<id>${escapeXml(item.id)}</id>`);
41
+ lines.push(`<title>${escapeXml(item.title)}</title>`);
42
+ if (item.link) {
43
+ lines.push(`<link rel="alternate" href="${escapeAttr(item.link)}"/>`);
44
+ }
45
+ // Atom requires <updated>. Fall back: item.updatedAt → publishedAt → feed updated.
46
+ const updated = toIso(item.updatedAt) ?? toIso(item.publishedAt) ?? feedUpdated;
47
+ lines.push(`<updated>${escapeXml(updated)}</updated>`);
48
+ const published = toIso(item.publishedAt);
49
+ if (published)
50
+ lines.push(`<published>${escapeXml(published)}</published>`);
51
+ if (item.authors) {
52
+ for (const author of item.authors) {
53
+ lines.push(renderAuthor(author, pretty));
54
+ }
55
+ }
56
+ if (item.categories) {
57
+ for (const cat of item.categories) {
58
+ lines.push(`<category term="${escapeAttr(cat)}"/>`);
59
+ }
60
+ }
61
+ if (item.summary !== undefined) {
62
+ lines.push(`<summary type="html">${wrapCdata(item.summary)}</summary>`);
63
+ }
64
+ if (item.content !== undefined) {
65
+ const isHtml = item.contentIsHtml !== false;
66
+ if (isHtml) {
67
+ lines.push(`<content type="html">${wrapCdata(item.content)}</content>`);
68
+ }
69
+ else {
70
+ lines.push(`<content type="text">${escapeXml(item.content)}</content>`);
71
+ }
72
+ }
73
+ if (item.enclosures) {
74
+ for (const enc of item.enclosures) {
75
+ const attrs = [
76
+ `rel="enclosure"`,
77
+ `href="${escapeAttr(enc.url)}"`,
78
+ `type="${escapeAttr(enc.type ?? 'application/octet-stream')}"`,
79
+ ];
80
+ if (Number.isFinite(enc.length))
81
+ attrs.push(`length="${escapeAttr(String(enc.length))}"`);
82
+ if (enc.title)
83
+ attrs.push(`title="${escapeAttr(enc.title)}"`);
84
+ lines.push(`<link ${attrs.join(' ')}/>`);
85
+ }
86
+ }
87
+ if (!pretty)
88
+ return `<entry>${lines.join('')}</entry>`;
89
+ return `<entry>\n${indent(lines.join('\n'), 2)}\n</entry>`;
90
+ }
91
+ /**
92
+ * Serialize a {@link Feed} to a complete Atom 1.0 XML document.
93
+ *
94
+ * Conforms to RFC 4287:
95
+ * - Declares `xmlns="http://www.w3.org/2005/Atom"` on the root.
96
+ * - Always emits required `<id>`, `<title>`, `<updated>` elements.
97
+ * - Each entry has `<id>`, `<title>`, `<updated>`.
98
+ * - HTML content is wrapped in CDATA inside `<content type="html">` /
99
+ * `<summary type="html">`. Plain-text content uses `type="text"` with
100
+ * raw escaping.
101
+ *
102
+ * **All user-supplied content is escaped** — text fields run through
103
+ * {@link escapeXml}, attribute values through {@link escapeAttr}, URLs
104
+ * through {@link escapeUrl} (which neutralizes `javascript:` URIs), and
105
+ * HTML-bearing fields are CDATA-wrapped via {@link wrapCdata}.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { serializeAtom1 } from '@molecule/api-feed-rss'
110
+ *
111
+ * const xml = serializeAtom1({
112
+ * title: 'My Blog',
113
+ * link: 'https://example.com',
114
+ * description: 'Posts about software',
115
+ * feedUrl: 'https://example.com/feed.atom',
116
+ * updatedAt: new Date(),
117
+ * items: [
118
+ * {
119
+ * id: 'urn:uuid:123',
120
+ * title: 'Hello, world!',
121
+ * link: 'https://example.com/posts/hello',
122
+ * publishedAt: new Date(),
123
+ * content: '<p>First post.</p>',
124
+ * },
125
+ * ],
126
+ * })
127
+ * ```
128
+ *
129
+ * @param feed - The feed structure to serialize.
130
+ * @param options - See {@link SerializeAtom1Options}.
131
+ * @returns Complete Atom 1.0 XML document string.
132
+ * @throws {FeedSerializeError} when required fields are missing.
133
+ */
134
+ export function serializeAtom1(feed, options = {}) {
135
+ assertFeedShape(feed, 'atom-1.0');
136
+ const pretty = options.pretty !== false;
137
+ const xmlDecl = options.xmlDeclaration ?? '<?xml version="1.0" encoding="UTF-8"?>';
138
+ const feedUpdated = toIso(feed.updatedAt) ?? new Date().toISOString();
139
+ const root = [];
140
+ // <id> for the feed: the feedUrl if set, else the link.
141
+ const feedId = feed.feedUrl ?? feed.link;
142
+ root.push(`<id>${escapeXml(feedId)}</id>`);
143
+ root.push(`<title>${escapeXml(feed.title)}</title>`);
144
+ if (feed.description) {
145
+ root.push(`<subtitle>${escapeXml(feed.description)}</subtitle>`);
146
+ }
147
+ root.push(`<updated>${escapeXml(feedUpdated)}</updated>`);
148
+ root.push(`<link rel="alternate" href="${escapeAttr(feed.link)}"/>`);
149
+ if (feed.feedUrl) {
150
+ root.push(`<link rel="self" href="${escapeAttr(feed.feedUrl)}" type="application/atom+xml"/>`);
151
+ }
152
+ if (feed.language) {
153
+ // Atom uses xml:lang on the root, but we also expose it as an element
154
+ // for convenience. The xml:lang attribute is added on the root open tag below.
155
+ }
156
+ if (feed.imageUrl) {
157
+ root.push(`<logo>${escapeUrl(feed.imageUrl)}</logo>`);
158
+ root.push(`<icon>${escapeUrl(feed.imageUrl)}</icon>`);
159
+ }
160
+ if (feed.copyright) {
161
+ root.push(`<rights>${escapeXml(feed.copyright)}</rights>`);
162
+ }
163
+ if (feed.authors) {
164
+ for (const author of feed.authors) {
165
+ root.push(renderAuthor(author, pretty));
166
+ }
167
+ }
168
+ for (const item of feed.items) {
169
+ root.push(renderEntry(item, feedUpdated, pretty));
170
+ }
171
+ const langAttr = feed.language ? ` xml:lang="${escapeAttr(feed.language)}"` : '';
172
+ if (!pretty) {
173
+ return `${xmlDecl}<feed xmlns="${ATOM_NS}"${langAttr}>${root.join('')}</feed>`;
174
+ }
175
+ return `${xmlDecl}\n<feed xmlns="${ATOM_NS}"${langAttr}>\n${indent(root.join('\n'), 2)}\n</feed>`;
176
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Format-discriminated dispatcher used by the HTTP handler factory.
3
+ *
4
+ * @module
5
+ */
6
+ import type { Feed, FeedOutputFormat } from './types.js';
7
+ /**
8
+ * The MIME type each format is served with.
9
+ */
10
+ export declare const FEED_CONTENT_TYPES: Record<FeedOutputFormat, string>;
11
+ /**
12
+ * Serialize a {@link Feed} to the body string for a given output format.
13
+ *
14
+ * Delegates to {@link serializeRss2} / {@link serializeAtom1} /
15
+ * {@link serializeJsonFeed}. Useful when callers want a single entry
16
+ * point keyed by a runtime-determined format (e.g. inside a route
17
+ * handler dispatching on the URL extension).
18
+ *
19
+ * @param feed - The feed structure.
20
+ * @param format - Target output format.
21
+ * @returns Serialized body string.
22
+ */
23
+ export declare function serializeFeed(feed: Feed, format: FeedOutputFormat): string;
24
+ //# sourceMappingURL=serializeFeed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializeFeed.d.ts","sourceRoot":"","sources":["../src/serializeFeed.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAExD;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAI/D,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAI1E"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Format-discriminated dispatcher used by the HTTP handler factory.
3
+ *
4
+ * @module
5
+ */
6
+ import { serializeAtom1 } from './serializeAtom1.js';
7
+ import { serializeJsonFeed } from './serializeJsonFeed.js';
8
+ import { serializeRss2 } from './serializeRss2.js';
9
+ /**
10
+ * The MIME type each format is served with.
11
+ */
12
+ export const FEED_CONTENT_TYPES = {
13
+ 'rss-2.0': 'application/rss+xml; charset=utf-8',
14
+ 'atom-1.0': 'application/atom+xml; charset=utf-8',
15
+ 'json-feed': 'application/feed+json; charset=utf-8',
16
+ };
17
+ /**
18
+ * Serialize a {@link Feed} to the body string for a given output format.
19
+ *
20
+ * Delegates to {@link serializeRss2} / {@link serializeAtom1} /
21
+ * {@link serializeJsonFeed}. Useful when callers want a single entry
22
+ * point keyed by a runtime-determined format (e.g. inside a route
23
+ * handler dispatching on the URL extension).
24
+ *
25
+ * @param feed - The feed structure.
26
+ * @param format - Target output format.
27
+ * @returns Serialized body string.
28
+ */
29
+ export function serializeFeed(feed, format) {
30
+ if (format === 'rss-2.0')
31
+ return serializeRss2(feed);
32
+ if (format === 'atom-1.0')
33
+ return serializeAtom1(feed);
34
+ return serializeJsonFeed(feed);
35
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * JSON Feed 1.1 serializer — accepts a {@link Feed} structure and returns
3
+ * a complete JSON Feed document string.
4
+ *
5
+ * @module
6
+ */
7
+ import type { Feed, SerializeJsonFeedOptions } from './types.js';
8
+ /**
9
+ * Serialize a {@link Feed} to a complete JSON Feed 1.1 document.
10
+ *
11
+ * The output conforms to https://www.jsonfeed.org/version/1.1/ — the
12
+ * entire payload is built as plain JS objects then `JSON.stringify`-ed,
13
+ * so all string content is naturally JSON-escaped. No CDATA / XML
14
+ * escaping is applicable.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { serializeJsonFeed } from '@molecule/api-feed-rss'
19
+ *
20
+ * const json = serializeJsonFeed({
21
+ * title: 'My Blog',
22
+ * link: 'https://example.com',
23
+ * description: 'Posts about software',
24
+ * feedUrl: 'https://example.com/feed.json',
25
+ * items: [
26
+ * {
27
+ * id: 'https://example.com/posts/hello',
28
+ * title: 'Hello, world!',
29
+ * link: 'https://example.com/posts/hello',
30
+ * publishedAt: new Date(),
31
+ * content: '<p>First post.</p>',
32
+ * },
33
+ * ],
34
+ * }, { pretty: true })
35
+ * ```
36
+ *
37
+ * @param feed - The feed structure to serialize.
38
+ * @param options - See {@link SerializeJsonFeedOptions}.
39
+ * @returns Complete JSON Feed 1.1 document string.
40
+ * @throws {FeedSerializeError} when required fields are missing.
41
+ */
42
+ export declare function serializeJsonFeed(feed: Feed, options?: SerializeJsonFeedOptions): string;
43
+ //# sourceMappingURL=serializeJsonFeed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializeJsonFeed.d.ts","sourceRoot":"","sources":["../src/serializeJsonFeed.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAc,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAmB5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,wBAA6B,GAAG,MAAM,CAsE5F"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * JSON Feed 1.1 serializer — accepts a {@link Feed} structure and returns
3
+ * a complete JSON Feed document string.
4
+ *
5
+ * @module
6
+ */
7
+ import { assertFeedShape, toIso } from './utilities.js';
8
+ const JSON_FEED_VERSION = 'https://jsonfeed.org/version/1.1';
9
+ /**
10
+ * Map a {@link FeedAuthor} to the JSON Feed 1.1 author shape.
11
+ *
12
+ * @param author - The author record.
13
+ * @returns Object containing the populated subset of name / url / avatar.
14
+ */
15
+ function jsonAuthor(author) {
16
+ const out = {};
17
+ if (author.name)
18
+ out.name = author.name;
19
+ if (author.url)
20
+ out.url = author.url;
21
+ if (author.avatarUrl)
22
+ out.avatar = author.avatarUrl;
23
+ return out;
24
+ }
25
+ /**
26
+ * Serialize a {@link Feed} to a complete JSON Feed 1.1 document.
27
+ *
28
+ * The output conforms to https://www.jsonfeed.org/version/1.1/ — the
29
+ * entire payload is built as plain JS objects then `JSON.stringify`-ed,
30
+ * so all string content is naturally JSON-escaped. No CDATA / XML
31
+ * escaping is applicable.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { serializeJsonFeed } from '@molecule/api-feed-rss'
36
+ *
37
+ * const json = serializeJsonFeed({
38
+ * title: 'My Blog',
39
+ * link: 'https://example.com',
40
+ * description: 'Posts about software',
41
+ * feedUrl: 'https://example.com/feed.json',
42
+ * items: [
43
+ * {
44
+ * id: 'https://example.com/posts/hello',
45
+ * title: 'Hello, world!',
46
+ * link: 'https://example.com/posts/hello',
47
+ * publishedAt: new Date(),
48
+ * content: '<p>First post.</p>',
49
+ * },
50
+ * ],
51
+ * }, { pretty: true })
52
+ * ```
53
+ *
54
+ * @param feed - The feed structure to serialize.
55
+ * @param options - See {@link SerializeJsonFeedOptions}.
56
+ * @returns Complete JSON Feed 1.1 document string.
57
+ * @throws {FeedSerializeError} when required fields are missing.
58
+ */
59
+ export function serializeJsonFeed(feed, options = {}) {
60
+ assertFeedShape(feed, 'json-feed');
61
+ const out = {
62
+ version: JSON_FEED_VERSION,
63
+ title: feed.title,
64
+ home_page_url: feed.link,
65
+ };
66
+ if (feed.feedUrl)
67
+ out.feed_url = feed.feedUrl;
68
+ if (feed.description)
69
+ out.description = feed.description;
70
+ if (feed.language)
71
+ out.language = feed.language;
72
+ if (feed.imageUrl) {
73
+ out.icon = feed.imageUrl;
74
+ out.favicon = feed.imageUrl;
75
+ }
76
+ if (feed.authors && feed.authors.length > 0) {
77
+ out.authors = feed.authors.map(jsonAuthor);
78
+ }
79
+ out.items = feed.items.map((item) => {
80
+ const obj = {
81
+ id: item.id,
82
+ title: item.title,
83
+ };
84
+ if (item.link) {
85
+ obj.url = item.link;
86
+ obj.external_url = item.link;
87
+ }
88
+ if (item.summary !== undefined)
89
+ obj.summary = item.summary;
90
+ if (item.content !== undefined) {
91
+ const isHtml = item.contentIsHtml !== false;
92
+ if (isHtml)
93
+ obj.content_html = item.content;
94
+ else
95
+ obj.content_text = item.content;
96
+ }
97
+ const datePublished = toIso(item.publishedAt);
98
+ if (datePublished)
99
+ obj.date_published = datePublished;
100
+ const dateModified = toIso(item.updatedAt);
101
+ if (dateModified)
102
+ obj.date_modified = dateModified;
103
+ if (item.authors && item.authors.length > 0) {
104
+ obj.authors = item.authors.map(jsonAuthor);
105
+ }
106
+ if (item.categories && item.categories.length > 0) {
107
+ obj.tags = [...item.categories];
108
+ }
109
+ if (item.enclosures && item.enclosures.length > 0) {
110
+ obj.attachments = item.enclosures.map((enc) => {
111
+ const att = {
112
+ url: enc.url,
113
+ mime_type: enc.type ?? 'application/octet-stream',
114
+ };
115
+ if (Number.isFinite(enc.length))
116
+ att.size_in_bytes = enc.length;
117
+ if (Number.isFinite(enc.durationSeconds)) {
118
+ att.duration_in_seconds = enc.durationSeconds;
119
+ }
120
+ if (enc.title)
121
+ att.title = enc.title;
122
+ return att;
123
+ });
124
+ }
125
+ return obj;
126
+ });
127
+ return options.pretty ? JSON.stringify(out, null, 2) : JSON.stringify(out);
128
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * RSS 2.0 serializer — accepts a {@link Feed} structure and returns a
3
+ * complete RSS 2.0 XML document string.
4
+ *
5
+ * @module
6
+ */
7
+ import type { Feed, SerializeRss2Options } from './types.js';
8
+ /**
9
+ * Serialize a {@link Feed} to a complete RSS 2.0 XML document.
10
+ *
11
+ * The output is valid RSS 2.0 — declares the `dc`, `content`, and `atom`
12
+ * namespaces always, plus `itunes` when {@link Feed.itunes} is present.
13
+ *
14
+ * **All user-supplied content is escaped** — text fields run through
15
+ * {@link escapeXml}, attribute values through {@link escapeAttr}, URLs
16
+ * through {@link escapeUrl} (which neutralizes `javascript:` URIs), and
17
+ * HTML-bearing fields (`description`, `content:encoded`,
18
+ * `itunes:summary`) are CDATA-wrapped via {@link wrapCdata} with
19
+ * `]]>` splitting.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { serializeRss2 } from '@molecule/api-feed-rss'
24
+ *
25
+ * const xml = serializeRss2({
26
+ * title: 'My Blog',
27
+ * link: 'https://example.com',
28
+ * description: 'Posts about software',
29
+ * feedUrl: 'https://example.com/feed.xml',
30
+ * items: [
31
+ * {
32
+ * id: 'https://example.com/posts/hello',
33
+ * title: 'Hello, world!',
34
+ * link: 'https://example.com/posts/hello',
35
+ * publishedAt: new Date(),
36
+ * content: '<p>First post.</p>',
37
+ * },
38
+ * ],
39
+ * })
40
+ * ```
41
+ *
42
+ * @param feed - The feed structure to serialize.
43
+ * @param options - See {@link SerializeRss2Options}.
44
+ * @returns Complete RSS 2.0 XML document string.
45
+ * @throws {FeedSerializeError} when required fields are missing.
46
+ */
47
+ export declare function serializeRss2(feed: Feed, options?: SerializeRss2Options): string;
48
+ //# sourceMappingURL=serializeRss2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializeRss2.d.ts","sourceRoot":"","sources":["../src/serializeRss2.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAY,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAyGtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAiGpF"}