@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/LICENSE +115 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/escape.d.ts +66 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/escape.js +122 -0
- package/dist/handler.d.ts +117 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +128 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/serializeAtom1.d.ts +52 -0
- package/dist/serializeAtom1.d.ts.map +1 -0
- package/dist/serializeAtom1.js +176 -0
- package/dist/serializeFeed.d.ts +24 -0
- package/dist/serializeFeed.d.ts.map +1 -0
- package/dist/serializeFeed.js +35 -0
- package/dist/serializeJsonFeed.d.ts +43 -0
- package/dist/serializeJsonFeed.d.ts.map +1 -0
- package/dist/serializeJsonFeed.js +128 -0
- package/dist/serializeRss2.d.ts +48 -0
- package/dist/serializeRss2.d.ts.map +1 -0
- package/dist/serializeRss2.js +231 -0
- package/dist/types.d.ts +209 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/utilities.d.ts +63 -0
- package/dist/utilities.d.ts.map +1 -0
- package/dist/utilities.js +158 -0
- package/package.json +39 -0
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"}
|