@dwk/microsub 0.1.0-beta.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.
Files changed (83) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +92 -0
  3. package/dist/auth.d.ts +53 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +102 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/config.d.ts +102 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +64 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/consumer.d.ts +40 -0
  12. package/dist/consumer.d.ts.map +1 -0
  13. package/dist/consumer.js +87 -0
  14. package/dist/consumer.js.map +1 -0
  15. package/dist/discovery.d.ts +59 -0
  16. package/dist/discovery.d.ts.map +1 -0
  17. package/dist/discovery.js +190 -0
  18. package/dist/discovery.js.map +1 -0
  19. package/dist/fetch.d.ts +28 -0
  20. package/dist/fetch.d.ts.map +1 -0
  21. package/dist/fetch.js +72 -0
  22. package/dist/fetch.js.map +1 -0
  23. package/dist/handler.d.ts +24 -0
  24. package/dist/handler.d.ts.map +1 -0
  25. package/dist/handler.js +434 -0
  26. package/dist/handler.js.map +1 -0
  27. package/dist/hfeed.d.ts +25 -0
  28. package/dist/hfeed.d.ts.map +1 -0
  29. package/dist/hfeed.js +252 -0
  30. package/dist/hfeed.js.map +1 -0
  31. package/dist/index.d.ts +39 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +32 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/jf2.d.ts +69 -0
  36. package/dist/jf2.d.ts.map +1 -0
  37. package/dist/jf2.js +295 -0
  38. package/dist/jf2.js.map +1 -0
  39. package/dist/log.d.ts +44 -0
  40. package/dist/log.d.ts.map +1 -0
  41. package/dist/log.js +42 -0
  42. package/dist/log.js.map +1 -0
  43. package/dist/poll.d.ts +22 -0
  44. package/dist/poll.d.ts.map +1 -0
  45. package/dist/poll.js +39 -0
  46. package/dist/poll.js.map +1 -0
  47. package/dist/queue.d.ts +25 -0
  48. package/dist/queue.d.ts.map +1 -0
  49. package/dist/queue.js +13 -0
  50. package/dist/queue.js.map +1 -0
  51. package/dist/replay.d.ts +34 -0
  52. package/dist/replay.d.ts.map +1 -0
  53. package/dist/replay.js +49 -0
  54. package/dist/replay.js.map +1 -0
  55. package/dist/safe-fetch.d.ts +86 -0
  56. package/dist/safe-fetch.d.ts.map +1 -0
  57. package/dist/safe-fetch.js +311 -0
  58. package/dist/safe-fetch.js.map +1 -0
  59. package/dist/store.d.ts +131 -0
  60. package/dist/store.d.ts.map +1 -0
  61. package/dist/store.js +393 -0
  62. package/dist/store.js.map +1 -0
  63. package/dist/xml.d.ts +51 -0
  64. package/dist/xml.d.ts.map +1 -0
  65. package/dist/xml.js +196 -0
  66. package/dist/xml.js.map +1 -0
  67. package/package.json +49 -0
  68. package/src/auth.ts +184 -0
  69. package/src/config.ts +156 -0
  70. package/src/consumer.ts +140 -0
  71. package/src/discovery.ts +270 -0
  72. package/src/fetch.ts +82 -0
  73. package/src/handler.ts +594 -0
  74. package/src/hfeed.ts +287 -0
  75. package/src/index.ts +86 -0
  76. package/src/jf2.ts +394 -0
  77. package/src/log.ts +46 -0
  78. package/src/poll.ts +72 -0
  79. package/src/queue.ts +26 -0
  80. package/src/replay.ts +68 -0
  81. package/src/safe-fetch.ts +346 -0
  82. package/src/store.ts +644 -0
  83. package/src/xml.ts +229 -0
package/dist/store.js ADDED
@@ -0,0 +1,393 @@
1
+ /**
2
+ * `@dwk/microsub` — D1-backed authoritative state.
3
+ *
4
+ * Channels, the feeds each channel follows, the normalised JF2 timeline items,
5
+ * their per-item read flags, and a small per-feed poll cache (`ETag` /
6
+ * `Last-Modified`) all live here. This is correctness-sensitive state — a lost
7
+ * subscription or a dropped read flag is a bug, not a stale cache — so it lives
8
+ * in D1, a strongly-consistent store, and **never** KV (see
9
+ * `spec/non-functional-requirements.md`). The schema is created lazily on first
10
+ * use.
11
+ *
12
+ * Timeline paging uses a monotonic `seq` (an autoincrement rowid): the opaque
13
+ * `before` / `after` cursors a Microsub client round-trips are just `seq`
14
+ * values, so paging is stable under concurrent inserts and deletes.
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+ /** The reserved channel that always exists and cannot be deleted or renamed. */
19
+ export const NOTIFICATIONS_CHANNEL = "notifications";
20
+ const SCHEMA = [
21
+ `CREATE TABLE IF NOT EXISTS microsub_channels (
22
+ uid TEXT PRIMARY KEY,
23
+ name TEXT NOT NULL,
24
+ position INTEGER NOT NULL,
25
+ created_at INTEGER NOT NULL
26
+ )`,
27
+ `CREATE TABLE IF NOT EXISTS microsub_follows (
28
+ channel TEXT NOT NULL,
29
+ feed_url TEXT NOT NULL,
30
+ page_url TEXT NOT NULL,
31
+ created_at INTEGER NOT NULL,
32
+ PRIMARY KEY (channel, feed_url)
33
+ )`,
34
+ `CREATE TABLE IF NOT EXISTS microsub_items (
35
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ channel TEXT NOT NULL,
37
+ entry_id TEXT NOT NULL,
38
+ feed_url TEXT NOT NULL,
39
+ published INTEGER,
40
+ is_read INTEGER NOT NULL DEFAULT 0,
41
+ data TEXT NOT NULL,
42
+ created_at INTEGER NOT NULL,
43
+ UNIQUE (channel, entry_id)
44
+ )`,
45
+ `CREATE INDEX IF NOT EXISTS microsub_items_channel_seq
46
+ ON microsub_items (channel, seq)`,
47
+ `CREATE TABLE IF NOT EXISTS microsub_feeds (
48
+ feed_url TEXT PRIMARY KEY,
49
+ etag TEXT,
50
+ last_modified TEXT,
51
+ last_polled INTEGER
52
+ )`,
53
+ ];
54
+ function channelFromRow(row) {
55
+ return {
56
+ uid: row.uid,
57
+ name: row.name,
58
+ position: row.position,
59
+ createdAt: row.created_at,
60
+ };
61
+ }
62
+ function itemFromRow(row) {
63
+ const entry = JSON.parse(row.data);
64
+ return {
65
+ seq: row.seq,
66
+ channel: row.channel,
67
+ entryId: row.entry_id,
68
+ feedUrl: row.feed_url,
69
+ isRead: row.is_read !== 0,
70
+ entry: { ...entry, _is_read: row.is_read !== 0 },
71
+ };
72
+ }
73
+ /** Parse a `published` ISO string to an epoch-seconds sort key, or `null`. */
74
+ function publishedToEpoch(entry) {
75
+ if (!entry.published)
76
+ return null;
77
+ const ms = Date.parse(entry.published);
78
+ return Number.isFinite(ms) ? Math.floor(ms / 1000) : null;
79
+ }
80
+ /** A short, URL-safe channel uid: base36 timestamp plus random suffix. */
81
+ function generateUid() {
82
+ const time = Date.now().toString(36);
83
+ const rand = Math.floor(Math.random() * 36 ** 5)
84
+ .toString(36)
85
+ .padStart(5, "0");
86
+ return `${time}${rand}`;
87
+ }
88
+ /**
89
+ * Create the D1-backed {@link MicrosubStore}. Fails loudly if the required
90
+ * `MICROSUB_DB` binding is missing — no silent degradation (composition
91
+ * contract).
92
+ */
93
+ export function createMicrosubStore(env) {
94
+ if (!env.MICROSUB_DB) {
95
+ throw new Error("@dwk/microsub: missing required D1 binding `MICROSUB_DB`");
96
+ }
97
+ const db = env.MICROSUB_DB;
98
+ // The init promise is cached per store instance (not module-globally): the
99
+ // vitest-pool-workers test model resets D1 between tests while keeping one
100
+ // `env` singleton, so a global cache would skip re-creating the wiped tables.
101
+ // This matches `@dwk/websub`'s store. The reserved `notifications` channel is
102
+ // materialised as part of the same batch, so it always exists after any store
103
+ // operation — a client may hit `timeline`/`unread` for it on a fresh DB
104
+ // without first listing channels.
105
+ let ready = null;
106
+ const ensureSchema = () => {
107
+ // Clear the cached promise on failure so a transient D1 error during the
108
+ // first call doesn't permanently wedge the store.
109
+ ready ??= db
110
+ .batch([
111
+ ...SCHEMA.map((ddl) => db.prepare(ddl)),
112
+ db
113
+ .prepare(`INSERT OR IGNORE INTO microsub_channels (uid, name, position, created_at)
114
+ VALUES (?1, ?2, -1, ?3)`)
115
+ .bind(NOTIFICATIONS_CHANNEL, "Notifications", Math.floor(Date.now() / 1000)),
116
+ ])
117
+ .then(() => undefined)
118
+ .catch((err) => {
119
+ ready = null;
120
+ throw err;
121
+ });
122
+ return ready;
123
+ };
124
+ const seqOf = async (channel, entryId) => {
125
+ const row = await db
126
+ .prepare(`SELECT seq FROM microsub_items WHERE channel = ?1 AND entry_id = ?2`)
127
+ .bind(channel, entryId)
128
+ .first();
129
+ return row?.seq ?? null;
130
+ };
131
+ return {
132
+ async init() {
133
+ await ensureSchema();
134
+ },
135
+ // Channels ------------------------------------------------------------
136
+ async listChannels() {
137
+ await ensureSchema();
138
+ const { results } = await db
139
+ .prepare(`SELECT uid, name, position, created_at FROM microsub_channels
140
+ ORDER BY position ASC, created_at ASC`)
141
+ .all();
142
+ return results.map(channelFromRow);
143
+ },
144
+ async channelExists(uid) {
145
+ await ensureSchema();
146
+ const row = await db
147
+ .prepare(`SELECT 1 AS present FROM microsub_channels WHERE uid = ?1`)
148
+ .bind(uid)
149
+ .first();
150
+ return row !== null;
151
+ },
152
+ async createChannel(name, now) {
153
+ await ensureSchema();
154
+ const max = await db
155
+ .prepare(`SELECT MAX(position) AS maxPos FROM microsub_channels`)
156
+ .first();
157
+ const position = (max?.maxPos ?? -1) + 1;
158
+ const uid = generateUid();
159
+ await db
160
+ .prepare(`INSERT INTO microsub_channels (uid, name, position, created_at)
161
+ VALUES (?1, ?2, ?3, ?4)`)
162
+ .bind(uid, name, position, now)
163
+ .run();
164
+ return { uid, name, position, createdAt: now };
165
+ },
166
+ async renameChannel(uid, name) {
167
+ await ensureSchema();
168
+ if (uid === NOTIFICATIONS_CHANNEL)
169
+ return null;
170
+ const result = await db
171
+ .prepare(`UPDATE microsub_channels SET name = ?2 WHERE uid = ?1`)
172
+ .bind(uid, name)
173
+ .run();
174
+ if (result.meta.changes === 0)
175
+ return null;
176
+ const row = await db
177
+ .prepare(`SELECT uid, name, position, created_at FROM microsub_channels WHERE uid = ?1`)
178
+ .bind(uid)
179
+ .first();
180
+ return row ? channelFromRow(row) : null;
181
+ },
182
+ async deleteChannel(uid) {
183
+ await ensureSchema();
184
+ if (uid === NOTIFICATIONS_CHANNEL)
185
+ return false;
186
+ const result = await db.batch([
187
+ db.prepare(`DELETE FROM microsub_channels WHERE uid = ?1`).bind(uid),
188
+ db.prepare(`DELETE FROM microsub_follows WHERE channel = ?1`).bind(uid),
189
+ db.prepare(`DELETE FROM microsub_items WHERE channel = ?1`).bind(uid),
190
+ ]);
191
+ return (result[0]?.meta.changes ?? 0) > 0;
192
+ },
193
+ async reorderChannels(order) {
194
+ await ensureSchema();
195
+ const statements = order.map((uid, index) => db
196
+ .prepare(`UPDATE microsub_channels SET position = ?2 WHERE uid = ?1`)
197
+ .bind(uid, index));
198
+ if (statements.length > 0)
199
+ await db.batch(statements);
200
+ },
201
+ // Follows -------------------------------------------------------------
202
+ async listFollows(channel) {
203
+ await ensureSchema();
204
+ const { results } = await db
205
+ .prepare(`SELECT feed_url, page_url, created_at FROM microsub_follows
206
+ WHERE channel = ?1 ORDER BY created_at ASC`)
207
+ .bind(channel)
208
+ .all();
209
+ return results.map((row) => ({
210
+ feedUrl: row.feed_url,
211
+ pageUrl: row.page_url,
212
+ createdAt: row.created_at,
213
+ }));
214
+ },
215
+ async addFollow(channel, feedUrl, pageUrl, now) {
216
+ await ensureSchema();
217
+ await db
218
+ .prepare(`INSERT INTO microsub_follows (channel, feed_url, page_url, created_at)
219
+ VALUES (?1, ?2, ?3, ?4)
220
+ ON CONFLICT (channel, feed_url) DO UPDATE SET page_url = excluded.page_url`)
221
+ .bind(channel, feedUrl, pageUrl, now)
222
+ .run();
223
+ },
224
+ async removeFollow(channel, url) {
225
+ await ensureSchema();
226
+ const result = await db
227
+ .prepare(`DELETE FROM microsub_follows
228
+ WHERE channel = ?1 AND (feed_url = ?2 OR page_url = ?2)`)
229
+ .bind(channel, url)
230
+ .run();
231
+ return (result.meta.changes ?? 0) > 0;
232
+ },
233
+ async distinctFeedUrls() {
234
+ await ensureSchema();
235
+ const { results } = await db
236
+ .prepare(`SELECT DISTINCT feed_url FROM microsub_follows`)
237
+ .all();
238
+ return results.map((row) => row.feed_url);
239
+ },
240
+ async channelsForFeed(feedUrl) {
241
+ await ensureSchema();
242
+ const { results } = await db
243
+ .prepare(`SELECT channel FROM microsub_follows WHERE feed_url = ?1`)
244
+ .bind(feedUrl)
245
+ .all();
246
+ return results.map((row) => row.channel);
247
+ },
248
+ // Items ---------------------------------------------------------------
249
+ async insertItems(channel, feedUrl, entries, now, maxItems) {
250
+ await ensureSchema();
251
+ if (entries.length === 0)
252
+ return 0;
253
+ const statements = entries.map((entry) => db
254
+ .prepare(`INSERT OR IGNORE INTO microsub_items
255
+ (channel, entry_id, feed_url, published, is_read, data, created_at)
256
+ VALUES (?1, ?2, ?3, ?4, 0, ?5, ?6)`)
257
+ .bind(channel, entry._id, feedUrl, publishedToEpoch(entry), JSON.stringify(entry), now));
258
+ const results = await db.batch(statements);
259
+ const added = results.reduce((sum, r) => sum + (r.meta.changes ?? 0), 0);
260
+ // Retention: reap the oldest items past the ceiling so a runaway feed
261
+ // can't fill the store unbounded.
262
+ if (added > 0) {
263
+ await db
264
+ .prepare(`DELETE FROM microsub_items
265
+ WHERE channel = ?1 AND seq NOT IN (
266
+ SELECT seq FROM microsub_items WHERE channel = ?1
267
+ ORDER BY seq DESC LIMIT ?2
268
+ )`)
269
+ .bind(channel, maxItems)
270
+ .run();
271
+ }
272
+ return added;
273
+ },
274
+ async listItems(channel, options) {
275
+ await ensureSchema();
276
+ const limit = Math.max(1, options.limit);
277
+ let rows;
278
+ if (options.after !== undefined) {
279
+ const after = Number.parseInt(options.after, 10);
280
+ const { results } = await db
281
+ .prepare(`SELECT seq, channel, entry_id, feed_url, is_read, data
282
+ FROM microsub_items WHERE channel = ?1 AND seq > ?2
283
+ ORDER BY seq ASC LIMIT ?3`)
284
+ .bind(channel, Number.isFinite(after) ? after : 0, limit)
285
+ .all();
286
+ rows = results.slice().reverse();
287
+ }
288
+ else {
289
+ const before = options.before !== undefined
290
+ ? Number.parseInt(options.before, 10)
291
+ : Number.MAX_SAFE_INTEGER;
292
+ const { results } = await db
293
+ .prepare(`SELECT seq, channel, entry_id, feed_url, is_read, data
294
+ FROM microsub_items WHERE channel = ?1 AND seq < ?2
295
+ ORDER BY seq DESC LIMIT ?3`)
296
+ .bind(channel, Number.isFinite(before) ? before : Number.MAX_SAFE_INTEGER, limit)
297
+ .all();
298
+ rows = results;
299
+ }
300
+ const items = rows.map(itemFromRow);
301
+ const page = {
302
+ items,
303
+ };
304
+ if (items.length > 0) {
305
+ const last = items[items.length - 1];
306
+ const first = items[0];
307
+ // `before` advances to older entries. Offer it when the page filled
308
+ // (there may be more below), and always when paginating with `after`:
309
+ // even a partial newer-page sits above the entries we paged up from, so
310
+ // the client must be able to navigate back down. `after` always points
311
+ // past the newest seen.
312
+ if ((items.length === limit || options.after !== undefined) && last) {
313
+ page.before = String(last.seq);
314
+ }
315
+ if (first)
316
+ page.after = String(first.seq);
317
+ }
318
+ return page;
319
+ },
320
+ async unreadCount(channel) {
321
+ await ensureSchema();
322
+ const row = await db
323
+ .prepare(`SELECT COUNT(*) AS n FROM microsub_items WHERE channel = ?1 AND is_read = 0`)
324
+ .bind(channel)
325
+ .first();
326
+ return row?.n ?? 0;
327
+ },
328
+ async markRead(channel, entryIds, read) {
329
+ await ensureSchema();
330
+ if (entryIds.length === 0)
331
+ return;
332
+ const flag = read ? 1 : 0;
333
+ const statements = entryIds.map((id) => db
334
+ .prepare(`UPDATE microsub_items SET is_read = ?3 WHERE channel = ?1 AND entry_id = ?2`)
335
+ .bind(channel, id, flag));
336
+ await db.batch(statements);
337
+ },
338
+ async markReadThrough(channel, entryId) {
339
+ await ensureSchema();
340
+ const seq = await seqOf(channel, entryId);
341
+ if (seq === null)
342
+ return;
343
+ await db
344
+ .prepare(`UPDATE microsub_items SET is_read = 1 WHERE channel = ?1 AND seq <= ?2`)
345
+ .bind(channel, seq)
346
+ .run();
347
+ },
348
+ async removeItem(channel, entryId) {
349
+ await ensureSchema();
350
+ const result = await db
351
+ .prepare(`DELETE FROM microsub_items WHERE channel = ?1 AND entry_id = ?2`)
352
+ .bind(channel, entryId)
353
+ .run();
354
+ return (result.meta.changes ?? 0) > 0;
355
+ },
356
+ async searchItems(channel, query, limit) {
357
+ await ensureSchema();
358
+ const like = `%${query.replace(/[%_\\]/g, (ch) => `\\${ch}`)}%`;
359
+ const { results } = await db
360
+ .prepare(`SELECT seq, channel, entry_id, feed_url, is_read, data
361
+ FROM microsub_items
362
+ WHERE channel = ?1 AND data LIKE ?2 ESCAPE '\\'
363
+ ORDER BY seq DESC LIMIT ?3`)
364
+ .bind(channel, like, Math.max(1, limit))
365
+ .all();
366
+ return results.map(itemFromRow);
367
+ },
368
+ // Feed poll cache -----------------------------------------------------
369
+ async getFeedCache(feedUrl) {
370
+ await ensureSchema();
371
+ const row = await db
372
+ .prepare(`SELECT etag, last_modified FROM microsub_feeds WHERE feed_url = ?1`)
373
+ .bind(feedUrl)
374
+ .first();
375
+ if (row === null)
376
+ return null;
377
+ return { etag: row.etag, lastModified: row.last_modified };
378
+ },
379
+ async setFeedCache(feedUrl, etag, lastModified, now) {
380
+ await ensureSchema();
381
+ await db
382
+ .prepare(`INSERT INTO microsub_feeds (feed_url, etag, last_modified, last_polled)
383
+ VALUES (?1, ?2, ?3, ?4)
384
+ ON CONFLICT (feed_url) DO UPDATE SET
385
+ etag = excluded.etag,
386
+ last_modified = excluded.last_modified,
387
+ last_polled = excluded.last_polled`)
388
+ .bind(feedUrl, etag, lastModified, now)
389
+ .run();
390
+ },
391
+ };
392
+ }
393
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/dist/xml.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `@dwk/microsub` — a minimal, dependency-free XML reader for feed parsing.
3
+ *
4
+ * Atom and RSS are XML; the Workers runtime exposes `HTMLRewriter` (an HTML
5
+ * tokenizer) but no XML parser, and the non-functional rules forbid pulling a
6
+ * heavy parser into the bundle. This module is a small recursive-descent reader
7
+ * tailored to feeds: elements, attributes, text, `CDATA`, comments, and the
8
+ * predefined/numeric entities — enough to turn a feed document into a tree the
9
+ * JF2 mapper walks. It is **pure** (plain string in, plain tree out) so it
10
+ * unit-tests without a Workers runtime.
11
+ *
12
+ * It is deliberately lenient (feeds in the wild are not always well-formed) and
13
+ * namespace-prefix-preserving but case-folding: element names are lowercased so
14
+ * `content:encoded`, `dc:creator`, and `media:content` match regardless of the
15
+ * source's casing.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ /** A parsed XML element: its lowercased name, attributes, and child nodes. */
20
+ export interface XmlElement {
21
+ readonly type: "element";
22
+ /** Lowercased tag name, prefix included (e.g. `"content:encoded"`). */
23
+ readonly name: string;
24
+ /** Lowercased-key attribute map. */
25
+ readonly attrs: Readonly<Record<string, string>>;
26
+ readonly children: readonly XmlNode[];
27
+ }
28
+ /** A run of character data (already entity-decoded). */
29
+ export interface XmlText {
30
+ readonly type: "text";
31
+ readonly value: string;
32
+ }
33
+ /** A node in the parsed tree. */
34
+ export type XmlNode = XmlElement | XmlText;
35
+ /** Decode XML predefined and numeric (`&#nn;` / `&#xnn;`) character references. */
36
+ export declare function decodeEntities(text: string): string;
37
+ /**
38
+ * Parse an XML document into a tree, returning the root element (or `null` when
39
+ * the input contains no element). Comments, the XML declaration, processing
40
+ * instructions, and `DOCTYPE` are skipped; `CDATA` sections become literal text.
41
+ */
42
+ export declare function parseXml(input: string): XmlElement | null;
43
+ /** The first direct child element named `name` (case-insensitive). */
44
+ export declare function child(el: XmlElement, name: string): XmlElement | null;
45
+ /** All direct child elements named `name` (case-insensitive), in order. */
46
+ export declare function children(el: XmlElement, name: string): XmlElement[];
47
+ /** Concatenated text of an element's descendants, trimmed. */
48
+ export declare function text(el: XmlElement | null): string;
49
+ /** Text of the first child element named `name`, or `""`. */
50
+ export declare function childText(el: XmlElement, name: string): string;
51
+ //# sourceMappingURL=xml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../src/xml.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;CACvC;AAED,wDAAwD;AACxD,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,iCAAiC;AACjC,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;AAU3C,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBnD;AAsBD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA+FzD;AAED,sEAAsE;AACtE,wBAAgB,KAAK,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAMrE;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAOnE;AAED,8DAA8D;AAC9D,wBAAgB,IAAI,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,GAAG,MAAM,CAYlD;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9D"}
package/dist/xml.js ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * `@dwk/microsub` — a minimal, dependency-free XML reader for feed parsing.
3
+ *
4
+ * Atom and RSS are XML; the Workers runtime exposes `HTMLRewriter` (an HTML
5
+ * tokenizer) but no XML parser, and the non-functional rules forbid pulling a
6
+ * heavy parser into the bundle. This module is a small recursive-descent reader
7
+ * tailored to feeds: elements, attributes, text, `CDATA`, comments, and the
8
+ * predefined/numeric entities — enough to turn a feed document into a tree the
9
+ * JF2 mapper walks. It is **pure** (plain string in, plain tree out) so it
10
+ * unit-tests without a Workers runtime.
11
+ *
12
+ * It is deliberately lenient (feeds in the wild are not always well-formed) and
13
+ * namespace-prefix-preserving but case-folding: element names are lowercased so
14
+ * `content:encoded`, `dc:creator`, and `media:content` match regardless of the
15
+ * source's casing.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ const NAMED_ENTITIES = {
20
+ amp: "&",
21
+ lt: "<",
22
+ gt: ">",
23
+ quot: '"',
24
+ apos: "'",
25
+ };
26
+ /** Decode XML predefined and numeric (`&#nn;` / `&#xnn;`) character references. */
27
+ export function decodeEntities(text) {
28
+ return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (whole, body) => {
29
+ if (body[0] === "#") {
30
+ const code = body[1] === "x" || body[1] === "X"
31
+ ? Number.parseInt(body.slice(2), 16)
32
+ : Number.parseInt(body.slice(1), 10);
33
+ if (!Number.isFinite(code) || code < 0 || code > 0x10ffff)
34
+ return whole;
35
+ try {
36
+ return String.fromCodePoint(code);
37
+ }
38
+ catch {
39
+ return whole;
40
+ }
41
+ }
42
+ const named = NAMED_ENTITIES[body];
43
+ return named ?? whole;
44
+ });
45
+ }
46
+ /** Parse attributes out of a start-tag's interior (everything after the name). */
47
+ function parseAttrs(source) {
48
+ const attrs = {};
49
+ const re = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
50
+ let match;
51
+ while ((match = re.exec(source)) !== null) {
52
+ const name = (match[1] ?? "").toLowerCase();
53
+ const raw = match[3] ?? match[4] ?? match[5] ?? "";
54
+ if (name !== "")
55
+ attrs[name] = decodeEntities(raw);
56
+ }
57
+ return attrs;
58
+ }
59
+ /**
60
+ * Parse an XML document into a tree, returning the root element (or `null` when
61
+ * the input contains no element). Comments, the XML declaration, processing
62
+ * instructions, and `DOCTYPE` are skipped; `CDATA` sections become literal text.
63
+ */
64
+ export function parseXml(input) {
65
+ const root = {
66
+ type: "element",
67
+ name: "#root",
68
+ attrs: {},
69
+ children: [],
70
+ };
71
+ const stack = [root];
72
+ let i = 0;
73
+ const n = input.length;
74
+ const pushText = (value) => {
75
+ if (value === "")
76
+ return;
77
+ const parent = stack[stack.length - 1];
78
+ if (parent)
79
+ parent.children.push({ type: "text", value });
80
+ };
81
+ while (i < n) {
82
+ const lt = input.indexOf("<", i);
83
+ if (lt === -1) {
84
+ pushText(decodeEntities(input.slice(i)));
85
+ break;
86
+ }
87
+ if (lt > i) {
88
+ pushText(decodeEntities(input.slice(i, lt)));
89
+ }
90
+ // Comments, CDATA, DOCTYPE, and declarations all start `<!`.
91
+ if (input.startsWith("<!--", lt)) {
92
+ const end = input.indexOf("-->", lt + 4);
93
+ i = end === -1 ? n : end + 3;
94
+ continue;
95
+ }
96
+ if (input.startsWith("<![CDATA[", lt)) {
97
+ const end = input.indexOf("]]>", lt + 9);
98
+ const data = input.slice(lt + 9, end === -1 ? n : end);
99
+ pushText(data); // CDATA is literal — no entity decoding.
100
+ i = end === -1 ? n : end + 3;
101
+ continue;
102
+ }
103
+ if (input[lt + 1] === "!") {
104
+ // DOCTYPE or other declaration: skip to the matching `>`.
105
+ const end = input.indexOf(">", lt);
106
+ i = end === -1 ? n : end + 1;
107
+ continue;
108
+ }
109
+ if (input[lt + 1] === "?") {
110
+ // Processing instruction / XML declaration.
111
+ const end = input.indexOf("?>", lt);
112
+ i = end === -1 ? n : end + 2;
113
+ continue;
114
+ }
115
+ const gt = input.indexOf(">", lt);
116
+ if (gt === -1) {
117
+ // Unterminated tag — treat the remainder as text and stop.
118
+ pushText(decodeEntities(input.slice(lt)));
119
+ break;
120
+ }
121
+ let tag = input.slice(lt + 1, gt);
122
+ i = gt + 1;
123
+ if (tag[0] === "/") {
124
+ // Close tag: pop to the nearest matching open element.
125
+ const name = tag.slice(1).trim().toLowerCase();
126
+ for (let s = stack.length - 1; s >= 1; s--) {
127
+ if (stack[s]?.name === name) {
128
+ stack.length = s;
129
+ break;
130
+ }
131
+ }
132
+ continue;
133
+ }
134
+ const selfClosing = tag.endsWith("/");
135
+ if (selfClosing)
136
+ tag = tag.slice(0, -1);
137
+ const space = tag.search(/\s/);
138
+ const name = (space === -1 ? tag : tag.slice(0, space)).toLowerCase();
139
+ const attrs = space === -1 ? {} : parseAttrs(tag.slice(space + 1));
140
+ const element = {
141
+ type: "element",
142
+ name,
143
+ attrs,
144
+ children: [],
145
+ };
146
+ const parent = stack[stack.length - 1];
147
+ if (parent)
148
+ parent.children.push(element);
149
+ if (!selfClosing)
150
+ stack.push(element);
151
+ }
152
+ const first = root.children.find((node) => node.type === "element");
153
+ return first ?? null;
154
+ }
155
+ /** The first direct child element named `name` (case-insensitive). */
156
+ export function child(el, name) {
157
+ const lower = name.toLowerCase();
158
+ for (const node of el.children) {
159
+ if (node.type === "element" && node.name === lower)
160
+ return node;
161
+ }
162
+ return null;
163
+ }
164
+ /** All direct child elements named `name` (case-insensitive), in order. */
165
+ export function children(el, name) {
166
+ const lower = name.toLowerCase();
167
+ const out = [];
168
+ for (const node of el.children) {
169
+ if (node.type === "element" && node.name === lower)
170
+ out.push(node);
171
+ }
172
+ return out;
173
+ }
174
+ /** Concatenated text of an element's descendants, trimmed. */
175
+ export function text(el) {
176
+ if (el === null)
177
+ return "";
178
+ let out = "";
179
+ const walk = (node) => {
180
+ if (node.type === "text") {
181
+ out += node.value;
182
+ }
183
+ else {
184
+ for (const c of node.children)
185
+ walk(c);
186
+ }
187
+ };
188
+ for (const c of el.children)
189
+ walk(c);
190
+ return out.trim();
191
+ }
192
+ /** Text of the first child element named `name`, or `""`. */
193
+ export function childText(el, name) {
194
+ return text(child(el, name));
195
+ }
196
+ //# sourceMappingURL=xml.js.map