@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,158 @@
1
+ /**
2
+ * Internal helpers shared by every serializer.
3
+ *
4
+ * @module
5
+ */
6
+ import { FeedSerializeError } 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 function toIso(value) {
19
+ if (value === undefined || value === null)
20
+ return undefined;
21
+ if (value instanceof Date) {
22
+ return Number.isFinite(value.getTime()) ? value.toISOString() : undefined;
23
+ }
24
+ if (typeof value === 'string' && value.trim() !== '') {
25
+ const ms = Date.parse(value);
26
+ if (Number.isFinite(ms))
27
+ return new Date(ms).toISOString();
28
+ }
29
+ return undefined;
30
+ }
31
+ /**
32
+ * Coerce a `Date` / ISO string into an RFC 822 date string suitable for
33
+ * RSS 2.0 `<pubDate>` / `<lastBuildDate>` fields.
34
+ *
35
+ * Always returns GMT / `+0000`. Returns `undefined` for unparseable input.
36
+ *
37
+ * @param value - Raw `Date`, ISO string, or RFC 822 string.
38
+ * @returns RFC 822 timestamp (`Mon, 28 Apr 2026 15:00:00 GMT`) or `undefined`.
39
+ */
40
+ export function toRfc822(value) {
41
+ if (value === undefined || value === null)
42
+ return undefined;
43
+ let date;
44
+ if (value instanceof Date) {
45
+ if (Number.isFinite(value.getTime()))
46
+ date = value;
47
+ }
48
+ else if (typeof value === 'string' && value.trim() !== '') {
49
+ const ms = Date.parse(value);
50
+ if (Number.isFinite(ms))
51
+ date = new Date(ms);
52
+ }
53
+ if (!date)
54
+ return undefined;
55
+ const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
56
+ const months = [
57
+ 'Jan',
58
+ 'Feb',
59
+ 'Mar',
60
+ 'Apr',
61
+ 'May',
62
+ 'Jun',
63
+ 'Jul',
64
+ 'Aug',
65
+ 'Sep',
66
+ 'Oct',
67
+ 'Nov',
68
+ 'Dec',
69
+ ];
70
+ const pad = (n) => (n < 10 ? `0${n}` : `${n}`);
71
+ return (`${days[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${months[date.getUTCMonth()]} ` +
72
+ `${date.getUTCFullYear()} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:` +
73
+ `${pad(date.getUTCSeconds())} GMT`);
74
+ }
75
+ /**
76
+ * Format a duration in seconds as `HH:MM:SS`, used for `<itunes:duration>`.
77
+ *
78
+ * Returns `undefined` for non-finite or negative input.
79
+ *
80
+ * @param seconds - Whole or fractional seconds.
81
+ * @returns `HH:MM:SS` string or `undefined`.
82
+ */
83
+ export function toHhMmSs(seconds) {
84
+ if (seconds === undefined || seconds === null)
85
+ return undefined;
86
+ if (!Number.isFinite(seconds) || seconds < 0)
87
+ return undefined;
88
+ const total = Math.floor(seconds);
89
+ const h = Math.floor(total / 3600);
90
+ const m = Math.floor((total % 3600) / 60);
91
+ const s = total % 60;
92
+ const pad = (n) => (n < 10 ? `0${n}` : `${n}`);
93
+ return `${pad(h)}:${pad(m)}:${pad(s)}`;
94
+ }
95
+ /**
96
+ * Validate {@link Feed} input has the minimum required fields for the
97
+ * given output format.
98
+ *
99
+ * @param feed - Feed input.
100
+ * @param format - Target serialization format.
101
+ * @throws {FeedSerializeError} if a required field is missing.
102
+ */
103
+ export function assertFeedShape(feed, format) {
104
+ if (!feed || typeof feed !== 'object') {
105
+ throw new FeedSerializeError('feed must be an object', format);
106
+ }
107
+ if (typeof feed.title !== 'string' || feed.title === '') {
108
+ throw new FeedSerializeError('feed.title is required', format);
109
+ }
110
+ if (typeof feed.link !== 'string' || feed.link === '') {
111
+ throw new FeedSerializeError('feed.link is required', format);
112
+ }
113
+ if (format === 'rss-2.0' && (typeof feed.description !== 'string' || feed.description === '')) {
114
+ throw new FeedSerializeError('feed.description is required for RSS 2.0', format);
115
+ }
116
+ if (!Array.isArray(feed.items)) {
117
+ throw new FeedSerializeError('feed.items must be an array', format);
118
+ }
119
+ for (const [i, item] of feed.items.entries()) {
120
+ if (!item || typeof item !== 'object') {
121
+ throw new FeedSerializeError(`feed.items[${i}] must be an object`, format);
122
+ }
123
+ if (typeof item.id !== 'string' || item.id === '') {
124
+ throw new FeedSerializeError(`feed.items[${i}].id is required`, format);
125
+ }
126
+ if (typeof item.title !== 'string') {
127
+ throw new FeedSerializeError(`feed.items[${i}].title must be a string`, format);
128
+ }
129
+ }
130
+ }
131
+ /**
132
+ * Indent each newline of `body` by `pad` spaces. Used for pretty-printing.
133
+ *
134
+ * @param body - Multi-line text. Empty input returns empty string.
135
+ * @param pad - Number of spaces to prepend to each line.
136
+ * @returns Indented text.
137
+ */
138
+ export function indent(body, pad) {
139
+ if (body === '')
140
+ return '';
141
+ const prefix = ' '.repeat(pad);
142
+ return body
143
+ .split('\n')
144
+ .map((line) => (line === '' ? line : prefix + line))
145
+ .join('\n');
146
+ }
147
+ /**
148
+ * Whether a string looks like an HTTP(S) URL — used to set
149
+ * `<guid isPermaLink>` and to fall back to `link` for `<id>` synthesis.
150
+ *
151
+ * @param value - Candidate string.
152
+ * @returns `true` if the value parses as `http:` or `https:` URL.
153
+ */
154
+ export function looksLikeHttpUrl(value) {
155
+ if (typeof value !== 'string' || value === '')
156
+ return false;
157
+ return /^https?:\/\//i.test(value.trim());
158
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@molecule/api-feed-rss",
3
+ "version": "1.0.0",
4
+ "description": "RSS/Atom/JSON Feed producer — pure-function serializers for RSS 2.0, Atom 1.0, JSON Feed 1.1; emits valid XML/JSON with full XML escaping and CDATA wrapping. Includes optional HTTP handler factory.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "molecule",
24
+ "feed",
25
+ "rss",
26
+ "atom",
27
+ "json-feed",
28
+ "podcast",
29
+ "producer",
30
+ "serializer"
31
+ ],
32
+ "license": "Apache-2.0",
33
+ "devDependencies": {
34
+ "@molecule/api-feed-rss-parser": "1.0.0",
35
+ "@types/node": "26.1.2",
36
+ "typescript": "6.0.3",
37
+ "vitest": "4.1.10"
38
+ }
39
+ }