@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.
- package/LICENSE +15 -0
- package/README.md +92 -0
- package/dist/auth.d.ts +53 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +102 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +102 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/consumer.d.ts +40 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +87 -0
- package/dist/consumer.js.map +1 -0
- package/dist/discovery.d.ts +59 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +190 -0
- package/dist/discovery.js.map +1 -0
- package/dist/fetch.d.ts +28 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +72 -0
- package/dist/fetch.js.map +1 -0
- package/dist/handler.d.ts +24 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +434 -0
- package/dist/handler.js.map +1 -0
- package/dist/hfeed.d.ts +25 -0
- package/dist/hfeed.d.ts.map +1 -0
- package/dist/hfeed.js +252 -0
- package/dist/hfeed.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/jf2.d.ts +69 -0
- package/dist/jf2.d.ts.map +1 -0
- package/dist/jf2.js +295 -0
- package/dist/jf2.js.map +1 -0
- package/dist/log.d.ts +44 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +42 -0
- package/dist/log.js.map +1 -0
- package/dist/poll.d.ts +22 -0
- package/dist/poll.d.ts.map +1 -0
- package/dist/poll.js +39 -0
- package/dist/poll.js.map +1 -0
- package/dist/queue.d.ts +25 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +13 -0
- package/dist/queue.js.map +1 -0
- package/dist/replay.d.ts +34 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +49 -0
- package/dist/replay.js.map +1 -0
- package/dist/safe-fetch.d.ts +86 -0
- package/dist/safe-fetch.d.ts.map +1 -0
- package/dist/safe-fetch.js +311 -0
- package/dist/safe-fetch.js.map +1 -0
- package/dist/store.d.ts +131 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +393 -0
- package/dist/store.js.map +1 -0
- package/dist/xml.d.ts +51 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +196 -0
- package/dist/xml.js.map +1 -0
- package/package.json +49 -0
- package/src/auth.ts +184 -0
- package/src/config.ts +156 -0
- package/src/consumer.ts +140 -0
- package/src/discovery.ts +270 -0
- package/src/fetch.ts +82 -0
- package/src/handler.ts +594 -0
- package/src/hfeed.ts +287 -0
- package/src/index.ts +86 -0
- package/src/jf2.ts +394 -0
- package/src/log.ts +46 -0
- package/src/poll.ts +72 -0
- package/src/queue.ts +26 -0
- package/src/replay.ts +68 -0
- package/src/safe-fetch.ts +346 -0
- package/src/store.ts +644 -0
- package/src/xml.ts +229 -0
package/src/xml.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
|
|
20
|
+
/** A parsed XML element: its lowercased name, attributes, and child nodes. */
|
|
21
|
+
export interface XmlElement {
|
|
22
|
+
readonly type: "element";
|
|
23
|
+
/** Lowercased tag name, prefix included (e.g. `"content:encoded"`). */
|
|
24
|
+
readonly name: string;
|
|
25
|
+
/** Lowercased-key attribute map. */
|
|
26
|
+
readonly attrs: Readonly<Record<string, string>>;
|
|
27
|
+
readonly children: readonly XmlNode[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A run of character data (already entity-decoded). */
|
|
31
|
+
export interface XmlText {
|
|
32
|
+
readonly type: "text";
|
|
33
|
+
readonly value: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A node in the parsed tree. */
|
|
37
|
+
export type XmlNode = XmlElement | XmlText;
|
|
38
|
+
|
|
39
|
+
const NAMED_ENTITIES: Record<string, string> = {
|
|
40
|
+
amp: "&",
|
|
41
|
+
lt: "<",
|
|
42
|
+
gt: ">",
|
|
43
|
+
quot: '"',
|
|
44
|
+
apos: "'",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Decode XML predefined and numeric (`&#nn;` / `&#xnn;`) character references. */
|
|
48
|
+
export function decodeEntities(text: string): string {
|
|
49
|
+
return text.replace(
|
|
50
|
+
/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g,
|
|
51
|
+
(whole, body: string) => {
|
|
52
|
+
if (body[0] === "#") {
|
|
53
|
+
const code =
|
|
54
|
+
body[1] === "x" || body[1] === "X"
|
|
55
|
+
? Number.parseInt(body.slice(2), 16)
|
|
56
|
+
: Number.parseInt(body.slice(1), 10);
|
|
57
|
+
if (!Number.isFinite(code) || code < 0 || code > 0x10ffff) return whole;
|
|
58
|
+
try {
|
|
59
|
+
return String.fromCodePoint(code);
|
|
60
|
+
} catch {
|
|
61
|
+
return whole;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const named = NAMED_ENTITIES[body];
|
|
65
|
+
return named ?? whole;
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface MutableElement {
|
|
71
|
+
type: "element";
|
|
72
|
+
name: string;
|
|
73
|
+
attrs: Record<string, string>;
|
|
74
|
+
children: XmlNode[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Parse attributes out of a start-tag's interior (everything after the name). */
|
|
78
|
+
function parseAttrs(source: string): Record<string, string> {
|
|
79
|
+
const attrs: Record<string, string> = {};
|
|
80
|
+
const re = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
|
|
81
|
+
let match: RegExpExecArray | null;
|
|
82
|
+
while ((match = re.exec(source)) !== null) {
|
|
83
|
+
const name = (match[1] ?? "").toLowerCase();
|
|
84
|
+
const raw = match[3] ?? match[4] ?? match[5] ?? "";
|
|
85
|
+
if (name !== "") attrs[name] = decodeEntities(raw);
|
|
86
|
+
}
|
|
87
|
+
return attrs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse an XML document into a tree, returning the root element (or `null` when
|
|
92
|
+
* the input contains no element). Comments, the XML declaration, processing
|
|
93
|
+
* instructions, and `DOCTYPE` are skipped; `CDATA` sections become literal text.
|
|
94
|
+
*/
|
|
95
|
+
export function parseXml(input: string): XmlElement | null {
|
|
96
|
+
const root: MutableElement = {
|
|
97
|
+
type: "element",
|
|
98
|
+
name: "#root",
|
|
99
|
+
attrs: {},
|
|
100
|
+
children: [],
|
|
101
|
+
};
|
|
102
|
+
const stack: MutableElement[] = [root];
|
|
103
|
+
let i = 0;
|
|
104
|
+
const n = input.length;
|
|
105
|
+
|
|
106
|
+
const pushText = (value: string): void => {
|
|
107
|
+
if (value === "") return;
|
|
108
|
+
const parent = stack[stack.length - 1];
|
|
109
|
+
if (parent) parent.children.push({ type: "text", value });
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
while (i < n) {
|
|
113
|
+
const lt = input.indexOf("<", i);
|
|
114
|
+
if (lt === -1) {
|
|
115
|
+
pushText(decodeEntities(input.slice(i)));
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (lt > i) {
|
|
119
|
+
pushText(decodeEntities(input.slice(i, lt)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Comments, CDATA, DOCTYPE, and declarations all start `<!`.
|
|
123
|
+
if (input.startsWith("<!--", lt)) {
|
|
124
|
+
const end = input.indexOf("-->", lt + 4);
|
|
125
|
+
i = end === -1 ? n : end + 3;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (input.startsWith("<![CDATA[", lt)) {
|
|
129
|
+
const end = input.indexOf("]]>", lt + 9);
|
|
130
|
+
const data = input.slice(lt + 9, end === -1 ? n : end);
|
|
131
|
+
pushText(data); // CDATA is literal — no entity decoding.
|
|
132
|
+
i = end === -1 ? n : end + 3;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (input[lt + 1] === "!") {
|
|
136
|
+
// DOCTYPE or other declaration: skip to the matching `>`.
|
|
137
|
+
const end = input.indexOf(">", lt);
|
|
138
|
+
i = end === -1 ? n : end + 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (input[lt + 1] === "?") {
|
|
142
|
+
// Processing instruction / XML declaration.
|
|
143
|
+
const end = input.indexOf("?>", lt);
|
|
144
|
+
i = end === -1 ? n : end + 2;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const gt = input.indexOf(">", lt);
|
|
149
|
+
if (gt === -1) {
|
|
150
|
+
// Unterminated tag — treat the remainder as text and stop.
|
|
151
|
+
pushText(decodeEntities(input.slice(lt)));
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
let tag = input.slice(lt + 1, gt);
|
|
155
|
+
i = gt + 1;
|
|
156
|
+
|
|
157
|
+
if (tag[0] === "/") {
|
|
158
|
+
// Close tag: pop to the nearest matching open element.
|
|
159
|
+
const name = tag.slice(1).trim().toLowerCase();
|
|
160
|
+
for (let s = stack.length - 1; s >= 1; s--) {
|
|
161
|
+
if (stack[s]?.name === name) {
|
|
162
|
+
stack.length = s;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const selfClosing = tag.endsWith("/");
|
|
170
|
+
if (selfClosing) tag = tag.slice(0, -1);
|
|
171
|
+
|
|
172
|
+
const space = tag.search(/\s/);
|
|
173
|
+
const name = (space === -1 ? tag : tag.slice(0, space)).toLowerCase();
|
|
174
|
+
const attrs = space === -1 ? {} : parseAttrs(tag.slice(space + 1));
|
|
175
|
+
const element: MutableElement = {
|
|
176
|
+
type: "element",
|
|
177
|
+
name,
|
|
178
|
+
attrs,
|
|
179
|
+
children: [],
|
|
180
|
+
};
|
|
181
|
+
const parent = stack[stack.length - 1];
|
|
182
|
+
if (parent) parent.children.push(element);
|
|
183
|
+
if (!selfClosing) stack.push(element);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const first = root.children.find(
|
|
187
|
+
(node): node is XmlElement => node.type === "element",
|
|
188
|
+
);
|
|
189
|
+
return first ?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The first direct child element named `name` (case-insensitive). */
|
|
193
|
+
export function child(el: XmlElement, name: string): XmlElement | null {
|
|
194
|
+
const lower = name.toLowerCase();
|
|
195
|
+
for (const node of el.children) {
|
|
196
|
+
if (node.type === "element" && node.name === lower) return node;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** All direct child elements named `name` (case-insensitive), in order. */
|
|
202
|
+
export function children(el: XmlElement, name: string): XmlElement[] {
|
|
203
|
+
const lower = name.toLowerCase();
|
|
204
|
+
const out: XmlElement[] = [];
|
|
205
|
+
for (const node of el.children) {
|
|
206
|
+
if (node.type === "element" && node.name === lower) out.push(node);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Concatenated text of an element's descendants, trimmed. */
|
|
212
|
+
export function text(el: XmlElement | null): string {
|
|
213
|
+
if (el === null) return "";
|
|
214
|
+
let out = "";
|
|
215
|
+
const walk = (node: XmlNode): void => {
|
|
216
|
+
if (node.type === "text") {
|
|
217
|
+
out += node.value;
|
|
218
|
+
} else {
|
|
219
|
+
for (const c of node.children) walk(c);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
for (const c of el.children) walk(c);
|
|
223
|
+
return out.trim();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Text of the first child element named `name`, or `""`. */
|
|
227
|
+
export function childText(el: XmlElement, name: string): string {
|
|
228
|
+
return text(child(el, name));
|
|
229
|
+
}
|