@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.
@@ -0,0 +1,231 @@
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 { escapeAttr, escapeUrl, escapeXml, wrapCdata } from './escape.js';
8
+ import { assertFeedShape, indent, looksLikeHttpUrl, toHhMmSs, toRfc822 } from './utilities.js';
9
+ const ATOM_NS = 'http://www.w3.org/2005/Atom';
10
+ const CONTENT_NS = 'http://purl.org/rss/1.0/modules/content/';
11
+ const ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
12
+ /**
13
+ * Serialize an item to RSS 2.0 `<item>` XML.
14
+ *
15
+ * @param item - Feed item.
16
+ * @param podcastMode - Whether channel-level iTunes metadata is present.
17
+ * @param pretty - Whether to pretty-print children.
18
+ * @returns `<item>...</item>` XML string.
19
+ */
20
+ function renderItem(item, podcastMode, pretty) {
21
+ const lines = [];
22
+ lines.push(`<title>${escapeXml(item.title)}</title>`);
23
+ if (item.link) {
24
+ lines.push(`<link>${escapeUrl(item.link)}</link>`);
25
+ }
26
+ // <guid isPermaLink="true|false">id</guid>
27
+ const isPermalink = looksLikeHttpUrl(item.id);
28
+ lines.push(`<guid isPermaLink="${isPermalink ? 'true' : 'false'}">${escapeXml(item.id)}</guid>`);
29
+ const pubDate = toRfc822(item.publishedAt);
30
+ if (pubDate) {
31
+ lines.push(`<pubDate>${escapeXml(pubDate)}</pubDate>`);
32
+ }
33
+ // RSS 2.0 <author> per spec is `email@example.com (Display Name)`.
34
+ if (item.authors && item.authors.length > 0) {
35
+ const author = item.authors[0];
36
+ if (author.email && author.name) {
37
+ lines.push(`<author>${escapeXml(`${author.email} (${author.name})`)}</author>`);
38
+ }
39
+ else if (author.email) {
40
+ lines.push(`<author>${escapeXml(author.email)}</author>`);
41
+ }
42
+ else if (author.name) {
43
+ // No email — fall back to dc:creator below; RSS spec disallows
44
+ // <author> without an email but consumers tolerate it. We emit
45
+ // dc:creator instead to stay spec-clean.
46
+ lines.push(`<dc:creator>${escapeXml(author.name)}</dc:creator>`);
47
+ }
48
+ }
49
+ if (item.categories) {
50
+ for (const cat of item.categories) {
51
+ lines.push(`<category>${escapeXml(cat)}</category>`);
52
+ }
53
+ }
54
+ if (item.summary !== undefined) {
55
+ // Summary is HTML-or-text; CDATA-wrap to preserve markup safely.
56
+ lines.push(`<description>${wrapCdata(item.summary)}</description>`);
57
+ }
58
+ if (item.content !== undefined) {
59
+ if (item.contentIsHtml === false) {
60
+ // Plain text — escape and put in description-equivalent slot.
61
+ // RSS 2.0 has no `<content>` element; use <content:encoded> with text
62
+ // wrapped in CDATA so reader UIs treat it as preformatted.
63
+ lines.push(`<content:encoded>${wrapCdata(item.content)}</content:encoded>`);
64
+ }
65
+ else {
66
+ lines.push(`<content:encoded>${wrapCdata(item.content)}</content:encoded>`);
67
+ }
68
+ }
69
+ if (item.enclosures) {
70
+ for (const enc of item.enclosures) {
71
+ const url = escapeAttr(enc.url);
72
+ const type = escapeAttr(enc.type ?? 'application/octet-stream');
73
+ const length = Number.isFinite(enc.length) ? String(enc.length) : '0';
74
+ lines.push(`<enclosure url="${url}" length="${escapeAttr(length)}" type="${type}"/>`);
75
+ }
76
+ }
77
+ if (podcastMode && item.itunes) {
78
+ const it = item.itunes;
79
+ if (it.author)
80
+ lines.push(`<itunes:author>${escapeXml(it.author)}</itunes:author>`);
81
+ const dur = toHhMmSs(it.durationSeconds);
82
+ if (dur)
83
+ lines.push(`<itunes:duration>${escapeXml(dur)}</itunes:duration>`);
84
+ if (it.explicit !== undefined) {
85
+ lines.push(`<itunes:explicit>${it.explicit ? 'true' : 'false'}</itunes:explicit>`);
86
+ }
87
+ if (it.imageUrl) {
88
+ lines.push(`<itunes:image href="${escapeAttr(it.imageUrl)}"/>`);
89
+ }
90
+ if (it.episode !== undefined && Number.isFinite(it.episode)) {
91
+ lines.push(`<itunes:episode>${escapeXml(String(it.episode))}</itunes:episode>`);
92
+ }
93
+ if (it.season !== undefined && Number.isFinite(it.season)) {
94
+ lines.push(`<itunes:season>${escapeXml(String(it.season))}</itunes:season>`);
95
+ }
96
+ if (it.summary) {
97
+ lines.push(`<itunes:summary>${wrapCdata(it.summary)}</itunes:summary>`);
98
+ }
99
+ }
100
+ const inner = lines.join(pretty ? '\n' : '');
101
+ if (!pretty)
102
+ return `<item>${inner}</item>`;
103
+ return `<item>\n${indent(inner, 2)}\n</item>`;
104
+ }
105
+ /**
106
+ * Serialize a {@link Feed} to a complete RSS 2.0 XML document.
107
+ *
108
+ * The output is valid RSS 2.0 — declares the `dc`, `content`, and `atom`
109
+ * namespaces always, plus `itunes` when {@link Feed.itunes} is present.
110
+ *
111
+ * **All user-supplied content is escaped** — text fields run through
112
+ * {@link escapeXml}, attribute values through {@link escapeAttr}, URLs
113
+ * through {@link escapeUrl} (which neutralizes `javascript:` URIs), and
114
+ * HTML-bearing fields (`description`, `content:encoded`,
115
+ * `itunes:summary`) are CDATA-wrapped via {@link wrapCdata} with
116
+ * `]]>` splitting.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * import { serializeRss2 } from '@molecule/api-feed-rss'
121
+ *
122
+ * const xml = serializeRss2({
123
+ * title: 'My Blog',
124
+ * link: 'https://example.com',
125
+ * description: 'Posts about software',
126
+ * feedUrl: 'https://example.com/feed.xml',
127
+ * items: [
128
+ * {
129
+ * id: 'https://example.com/posts/hello',
130
+ * title: 'Hello, world!',
131
+ * link: 'https://example.com/posts/hello',
132
+ * publishedAt: new Date(),
133
+ * content: '<p>First post.</p>',
134
+ * },
135
+ * ],
136
+ * })
137
+ * ```
138
+ *
139
+ * @param feed - The feed structure to serialize.
140
+ * @param options - See {@link SerializeRss2Options}.
141
+ * @returns Complete RSS 2.0 XML document string.
142
+ * @throws {FeedSerializeError} when required fields are missing.
143
+ */
144
+ export function serializeRss2(feed, options = {}) {
145
+ assertFeedShape(feed, 'rss-2.0');
146
+ const pretty = options.pretty !== false;
147
+ const xmlDecl = options.xmlDeclaration ?? '<?xml version="1.0" encoding="UTF-8"?>';
148
+ const podcastMode = feed.itunes !== undefined;
149
+ const nsAttrs = [
150
+ `version="2.0"`,
151
+ `xmlns:dc="http://purl.org/dc/elements/1.1/"`,
152
+ `xmlns:content="${CONTENT_NS}"`,
153
+ `xmlns:atom="${ATOM_NS}"`,
154
+ ];
155
+ if (podcastMode)
156
+ nsAttrs.push(`xmlns:itunes="${ITUNES_NS}"`);
157
+ const channel = [];
158
+ channel.push(`<title>${escapeXml(feed.title)}</title>`);
159
+ channel.push(`<link>${escapeUrl(feed.link)}</link>`);
160
+ channel.push(`<description>${escapeXml(feed.description)}</description>`);
161
+ if (feed.language)
162
+ channel.push(`<language>${escapeXml(feed.language)}</language>`);
163
+ if (feed.copyright)
164
+ channel.push(`<copyright>${escapeXml(feed.copyright)}</copyright>`);
165
+ if (feed.authors && feed.authors.length > 0) {
166
+ const a = feed.authors[0];
167
+ if (a.email && a.name) {
168
+ channel.push(`<managingEditor>${escapeXml(`${a.email} (${a.name})`)}</managingEditor>`);
169
+ }
170
+ else if (a.email) {
171
+ channel.push(`<managingEditor>${escapeXml(a.email)}</managingEditor>`);
172
+ }
173
+ }
174
+ const lastBuild = toRfc822(feed.updatedAt);
175
+ if (lastBuild)
176
+ channel.push(`<lastBuildDate>${escapeXml(lastBuild)}</lastBuildDate>`);
177
+ if (feed.feedUrl) {
178
+ channel.push(`<atom:link href="${escapeAttr(feed.feedUrl)}" rel="self" type="application/rss+xml"/>`);
179
+ }
180
+ if (feed.imageUrl) {
181
+ if (pretty) {
182
+ channel.push(`<image>\n <url>${escapeUrl(feed.imageUrl)}</url>\n <title>${escapeXml(feed.title)}</title>\n <link>${escapeUrl(feed.link)}</link>\n</image>`);
183
+ }
184
+ else {
185
+ channel.push(`<image><url>${escapeUrl(feed.imageUrl)}</url><title>${escapeXml(feed.title)}</title><link>${escapeUrl(feed.link)}</link></image>`);
186
+ }
187
+ }
188
+ if (feed.categories) {
189
+ for (const cat of feed.categories) {
190
+ channel.push(`<category>${escapeXml(cat)}</category>`);
191
+ }
192
+ }
193
+ if (podcastMode && feed.itunes) {
194
+ const it = feed.itunes;
195
+ if (it.author)
196
+ channel.push(`<itunes:author>${escapeXml(it.author)}</itunes:author>`);
197
+ if (it.summary)
198
+ channel.push(`<itunes:summary>${wrapCdata(it.summary)}</itunes:summary>`);
199
+ if (it.explicit !== undefined) {
200
+ channel.push(`<itunes:explicit>${it.explicit ? 'true' : 'false'}</itunes:explicit>`);
201
+ }
202
+ if (it.imageUrl) {
203
+ channel.push(`<itunes:image href="${escapeAttr(it.imageUrl)}"/>`);
204
+ }
205
+ if (it.type) {
206
+ channel.push(`<itunes:type>${escapeXml(it.type)}</itunes:type>`);
207
+ }
208
+ if (it.owner) {
209
+ const ownerInner = `<itunes:name>${escapeXml(it.owner.name)}</itunes:name><itunes:email>${escapeXml(it.owner.email)}</itunes:email>`;
210
+ if (pretty) {
211
+ channel.push(`<itunes:owner>\n <itunes:name>${escapeXml(it.owner.name)}</itunes:name>\n <itunes:email>${escapeXml(it.owner.email)}</itunes:email>\n</itunes:owner>`);
212
+ }
213
+ else {
214
+ channel.push(`<itunes:owner>${ownerInner}</itunes:owner>`);
215
+ }
216
+ }
217
+ if (it.categories) {
218
+ for (const cat of it.categories) {
219
+ channel.push(`<itunes:category text="${escapeAttr(cat)}"/>`);
220
+ }
221
+ }
222
+ }
223
+ for (const item of feed.items) {
224
+ channel.push(renderItem(item, podcastMode, pretty));
225
+ }
226
+ if (!pretty) {
227
+ return `${xmlDecl}<rss ${nsAttrs.join(' ')}><channel>${channel.join('')}</channel></rss>`;
228
+ }
229
+ const channelBody = channel.join('\n');
230
+ return `${xmlDecl}\n<rss ${nsAttrs.join(' ')}>\n <channel>\n${indent(channelBody, 4)}\n </channel>\n</rss>`;
231
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Output format discriminator for {@link serializeFeed} and the
3
+ * {@link createFeedHandler} HTTP handler.
4
+ *
5
+ * - `rss-2.0` — RSS 2.0 XML, served as `application/rss+xml`.
6
+ * - `atom-1.0` — Atom 1.0 XML, served as `application/atom+xml`.
7
+ * - `json-feed` — JSON Feed 1.1, served as `application/feed+json`.
8
+ */
9
+ export type FeedOutputFormat = 'rss-2.0' | 'atom-1.0' | 'json-feed';
10
+ /**
11
+ * Optional iTunes podcast metadata for an item.
12
+ *
13
+ * When supplied AND the channel-level {@link Feed.itunes} block is also
14
+ * present, the RSS 2.0 serializer emits the iTunes podcast namespace
15
+ * (`xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"`) and per-item
16
+ * `<itunes:duration>`, `<itunes:author>`, `<itunes:explicit>`,
17
+ * `<itunes:image>`, `<itunes:episode>`, `<itunes:season>`, `<itunes:summary>`
18
+ * elements.
19
+ */
20
+ export interface FeedItemItunes {
21
+ /** Episode duration in seconds. Serialized as HH:MM:SS. */
22
+ durationSeconds?: number;
23
+ /** Episode artwork URL. Emitted as `<itunes:image href="...">`. */
24
+ imageUrl?: string;
25
+ /** Author display name (overrides item-level `<itunes:author>`). */
26
+ author?: string;
27
+ /** Whether the episode contains explicit material. */
28
+ explicit?: boolean;
29
+ /** Episode number within the season / series. */
30
+ episode?: number;
31
+ /** Season number. */
32
+ season?: number;
33
+ /** Plain-text episode summary (`<itunes:summary>`). */
34
+ summary?: string;
35
+ }
36
+ /**
37
+ * Channel-level iTunes podcast metadata.
38
+ *
39
+ * Trigger condition: when present (even as `{}`), the RSS 2.0 serializer
40
+ * declares the iTunes namespace and emits the configured `<itunes:*>`
41
+ * channel elements.
42
+ */
43
+ export interface FeedChannelItunes {
44
+ /** Show author / host name (`<itunes:author>`). */
45
+ author?: string;
46
+ /** Show owner contact info — both fields required by Apple Podcasts. */
47
+ owner?: {
48
+ /** Owner's display name. */
49
+ name: string;
50
+ /** Owner's email address. */
51
+ email: string;
52
+ };
53
+ /** Show artwork URL (`<itunes:image href="...">`). 1400x1400+ recommended. */
54
+ imageUrl?: string;
55
+ /** Whether the show contains explicit material. */
56
+ explicit?: boolean;
57
+ /**
58
+ * iTunes show categories — top-level only (subcategories not currently
59
+ * supported by this serializer).
60
+ */
61
+ categories?: string[];
62
+ /** Long-form show summary. */
63
+ summary?: string;
64
+ /** Show type — `episodic` (default) or `serial`. */
65
+ type?: 'episodic' | 'serial';
66
+ }
67
+ /**
68
+ * Media enclosure (podcast audio, video, image, etc.).
69
+ */
70
+ export interface FeedItemEnclosure {
71
+ /** Direct URL to the media resource. Required. */
72
+ url: string;
73
+ /**
74
+ * MIME type. Required by RSS 2.0 spec — defaults to
75
+ * `application/octet-stream` when omitted.
76
+ */
77
+ type?: string;
78
+ /** Length in bytes. Required by RSS 2.0 spec — defaults to `0` when omitted. */
79
+ length?: number;
80
+ /** Display title or alt text. JSON Feed only. */
81
+ title?: string;
82
+ /** Duration in seconds. Used by JSON Feed `attachments[].duration_in_seconds`. */
83
+ durationSeconds?: number;
84
+ }
85
+ /**
86
+ * Author of a feed or item.
87
+ */
88
+ export interface FeedAuthor {
89
+ /** Display name. Required for Atom; optional everywhere else. */
90
+ name: string;
91
+ /** Author email address. Optional. */
92
+ email?: string;
93
+ /** Author profile / homepage URL. Optional. */
94
+ url?: string;
95
+ /** Avatar URL (JSON Feed only). */
96
+ avatarUrl?: string;
97
+ }
98
+ /**
99
+ * A single feed item / entry / post.
100
+ */
101
+ export interface FeedItem {
102
+ /**
103
+ * Stable, opaque identifier. Required.
104
+ *
105
+ * RSS 2.0 emits this as `<guid>`; Atom as `<id>`; JSON Feed as `id`.
106
+ * If {@link FeedItem.link} is set and `id` looks like an HTTP(S) URL,
107
+ * RSS will mark `<guid isPermaLink="true">`.
108
+ */
109
+ id: string;
110
+ /** Item title. Required. */
111
+ title: string;
112
+ /** Canonical URL the item points to. */
113
+ link?: string;
114
+ /** Short summary / excerpt. Plain text or HTML. */
115
+ summary?: string;
116
+ /**
117
+ * Full content body. HTML is allowed; the serializer wraps it in CDATA
118
+ * for RSS / Atom and escapes it for JSON Feed.
119
+ */
120
+ content?: string;
121
+ /**
122
+ * Whether {@link FeedItem.content} is HTML (`true`, default) or plain text.
123
+ * Drives JSON Feed `content_html` vs `content_text` selection and Atom
124
+ * `<content type="html">` vs `<content type="text">`.
125
+ */
126
+ contentIsHtml?: boolean;
127
+ /** Item author(s). RSS uses the first entry; Atom and JSON Feed list all. */
128
+ authors?: FeedAuthor[];
129
+ /** Publication timestamp. ISO 8601 string or `Date`. */
130
+ publishedAt?: string | Date;
131
+ /** Last modification timestamp. ISO 8601 string or `Date`. */
132
+ updatedAt?: string | Date;
133
+ /** Tags / categories / keywords. */
134
+ categories?: string[];
135
+ /** Media enclosures (podcast audio, attachments, etc.). */
136
+ enclosures?: FeedItemEnclosure[];
137
+ /** Per-item iTunes podcast metadata. See {@link FeedItemItunes}. */
138
+ itunes?: FeedItemItunes;
139
+ }
140
+ /**
141
+ * Feed-level metadata + items, format-agnostic input shape consumed by
142
+ * every serializer.
143
+ */
144
+ export interface Feed {
145
+ /** Channel / feed title. Required. */
146
+ title: string;
147
+ /** Canonical website URL (NOT the feed self-link). Required. */
148
+ link: string;
149
+ /** Long-form description / subtitle / tagline. Required by RSS 2.0. */
150
+ description: string;
151
+ /** Self-referential URL of the feed document. Recommended for Atom + JSON Feed. */
152
+ feedUrl?: string;
153
+ /** RFC 5646 / BCP 47 language tag (e.g. `en`, `en-US`). */
154
+ language?: string;
155
+ /** ISO 8601 / `Date` of the feed's last update. Defaults to "now" for Atom. */
156
+ updatedAt?: string | Date;
157
+ /** Channel author(s). */
158
+ authors?: FeedAuthor[];
159
+ /** Copyright / rights statement. */
160
+ copyright?: string;
161
+ /** Channel image / logo / icon URL. */
162
+ imageUrl?: string;
163
+ /** Channel-level categories (RSS only). */
164
+ categories?: string[];
165
+ /**
166
+ * Channel-level iTunes podcast metadata. Presence of this field switches
167
+ * the RSS 2.0 serializer into "podcast mode" — it declares
168
+ * `xmlns:itunes` and emits per-item `<itunes:*>` elements.
169
+ */
170
+ itunes?: FeedChannelItunes;
171
+ /** Items in display order. The serializer does not re-sort by date. */
172
+ items: FeedItem[];
173
+ }
174
+ /**
175
+ * Options controlling {@link serializeRss2}.
176
+ */
177
+ export interface SerializeRss2Options {
178
+ /** XML declaration. Defaults to `<?xml version="1.0" encoding="UTF-8"?>`. */
179
+ xmlDeclaration?: string;
180
+ /** Pretty-print with 2-space indentation. Defaults to `true`. */
181
+ pretty?: boolean;
182
+ }
183
+ /**
184
+ * Options controlling {@link serializeAtom1}.
185
+ */
186
+ export interface SerializeAtom1Options {
187
+ /** XML declaration. Defaults to `<?xml version="1.0" encoding="UTF-8"?>`. */
188
+ xmlDeclaration?: string;
189
+ /** Pretty-print with 2-space indentation. Defaults to `true`. */
190
+ pretty?: boolean;
191
+ }
192
+ /**
193
+ * Options controlling {@link serializeJsonFeed}.
194
+ */
195
+ export interface SerializeJsonFeedOptions {
196
+ /** Pretty-print with 2-space indentation. Defaults to `false`. */
197
+ pretty?: boolean;
198
+ }
199
+ /**
200
+ * Error raised when the {@link Feed} input fails minimum-viability checks
201
+ * (missing required `title` / `link` / `description`, missing item `id` or
202
+ * `title`, etc.).
203
+ */
204
+ export declare class FeedSerializeError extends Error {
205
+ /** Output format hint at the point of failure, if known. */
206
+ format?: FeedOutputFormat;
207
+ constructor(message: string, format?: FeedOutputFormat);
208
+ }
209
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,CAAA;AAEnE;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,qBAAqB;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,wEAAwE;IACxE,KAAK,CAAC,EAAE;QACN,4BAA4B;QAC5B,IAAI,EAAE,MAAM,CAAA;QACZ,6BAA6B;QAC7B,KAAK,EAAE,MAAM,CAAA;KACd,CAAA;IAED,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IAErB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,oDAAoD;IACpD,IAAI,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAA;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAA;IAEX;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd,kFAAkF;IAClF,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAA;IAEZ,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAA;IAEZ,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB;;;;;;OAMG;IACH,EAAE,EAAE,MAAM,CAAA;IAEV,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAA;IAEb,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;IAEtB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAE3B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAEzB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IAErB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAEhC,oEAAoE;IACpE,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,IAAI;IACnB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAA;IAEb,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAA;IAEZ,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAA;IAEnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAEzB,yBAAyB;IACzB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;IAEtB,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IAErB;;;;OAIG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAE1B,uEAAuE;IACvE,KAAK,EAAE,QAAQ,EAAE,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,4DAA4D;IAC5D,MAAM,CAAC,EAAE,gBAAgB,CAAA;gBAEb,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,gBAAgB;CAKvD"}
package/dist/types.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Error raised when the {@link Feed} input fails minimum-viability checks
3
+ * (missing required `title` / `link` / `description`, missing item `id` or
4
+ * `title`, etc.).
5
+ */
6
+ export class FeedSerializeError extends Error {
7
+ /** Output format hint at the point of failure, if known. */
8
+ format;
9
+ constructor(message, format) {
10
+ super(message);
11
+ this.name = 'FeedSerializeError';
12
+ this.format = format;
13
+ }
14
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Internal helpers shared by every serializer.
3
+ *
4
+ * @module
5
+ */
6
+ import type { Feed, FeedOutputFormat } from './types.js';
7
+ /**
8
+ * Coerce a `Date` / ISO string / arbitrary string into an ISO 8601 string.
9
+ *
10
+ * Returns `undefined` if the input is missing or unparseable. Unparseable
11
+ * arbitrary strings fall back to `undefined` rather than throwing — the
12
+ * serializer simply omits the timestamp element rather than emitting
13
+ * `Invalid Date`.
14
+ *
15
+ * @param value - Raw `Date`, ISO string, or other date-like string.
16
+ * @returns ISO 8601 timestamp (`2026-04-28T15:00:00.000Z`) or `undefined`.
17
+ */
18
+ export declare function toIso(value: string | Date | undefined | null): string | undefined;
19
+ /**
20
+ * Coerce a `Date` / ISO string into an RFC 822 date string suitable for
21
+ * RSS 2.0 `<pubDate>` / `<lastBuildDate>` fields.
22
+ *
23
+ * Always returns GMT / `+0000`. Returns `undefined` for unparseable input.
24
+ *
25
+ * @param value - Raw `Date`, ISO string, or RFC 822 string.
26
+ * @returns RFC 822 timestamp (`Mon, 28 Apr 2026 15:00:00 GMT`) or `undefined`.
27
+ */
28
+ export declare function toRfc822(value: string | Date | undefined | null): string | undefined;
29
+ /**
30
+ * Format a duration in seconds as `HH:MM:SS`, used for `<itunes:duration>`.
31
+ *
32
+ * Returns `undefined` for non-finite or negative input.
33
+ *
34
+ * @param seconds - Whole or fractional seconds.
35
+ * @returns `HH:MM:SS` string or `undefined`.
36
+ */
37
+ export declare function toHhMmSs(seconds: number | undefined | null): string | undefined;
38
+ /**
39
+ * Validate {@link Feed} input has the minimum required fields for the
40
+ * given output format.
41
+ *
42
+ * @param feed - Feed input.
43
+ * @param format - Target serialization format.
44
+ * @throws {FeedSerializeError} if a required field is missing.
45
+ */
46
+ export declare function assertFeedShape(feed: Feed, format: FeedOutputFormat): void;
47
+ /**
48
+ * Indent each newline of `body` by `pad` spaces. Used for pretty-printing.
49
+ *
50
+ * @param body - Multi-line text. Empty input returns empty string.
51
+ * @param pad - Number of spaces to prepend to each line.
52
+ * @returns Indented text.
53
+ */
54
+ export declare function indent(body: string, pad: number): string;
55
+ /**
56
+ * Whether a string looks like an HTTP(S) URL — used to set
57
+ * `<guid isPermaLink>` and to fall back to `link` for `<id>` synthesis.
58
+ *
59
+ * @param value - Candidate string.
60
+ * @returns `true` if the value parses as `http:` or `https:` URL.
61
+ */
62
+ export declare function looksLikeHttpUrl(value: string | undefined | null): boolean;
63
+ //# sourceMappingURL=utilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAGxD;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAUjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAgCpF;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAS/E;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CA2B1E;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAOxD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAG1E"}