@sdxc/atom 0.0.0-pre.1
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.md +21 -0
- package/README.md +245 -0
- package/dist/index.d.ts +231 -0
- package/dist/index.js +162 -0
- package/dist/lib/build-document.d.ts +17 -0
- package/dist/lib/build-document.js +195 -0
- package/dist/lib/clone.d.ts +42 -0
- package/dist/lib/clone.js +147 -0
- package/dist/lib/constants.d.ts +17 -0
- package/dist/lib/constants.js +17 -0
- package/dist/lib/content.d.ts +34 -0
- package/dist/lib/content.js +75 -0
- package/dist/lib/extensions.d.ts +23 -0
- package/dist/lib/extensions.js +42 -0
- package/dist/lib/namespaces.d.ts +44 -0
- package/dist/lib/namespaces.js +63 -0
- package/dist/lib/parse-feed.d.ts +20 -0
- package/dist/lib/parse-feed.js +431 -0
- package/dist/lib/text-construct.d.ts +33 -0
- package/dist/lib/text-construct.js +97 -0
- package/dist/lib/utils.d.ts +59 -0
- package/dist/lib/utils.js +99 -0
- package/dist/lib/validate-entry.d.ts +23 -0
- package/dist/lib/validate-entry.js +34 -0
- package/dist/lib/validate-feed.d.ts +15 -0
- package/dist/lib/validate-feed.js +23 -0
- package/dist/lib/xml-base.d.ts +36 -0
- package/dist/lib/xml-base.js +49 -0
- package/package.json +23 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergio Xalambrí
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# @sdxc/atom
|
|
2
|
+
|
|
3
|
+
Atom 1.0 feed parser and builder.
|
|
4
|
+
|
|
5
|
+
It reads [RFC 4287](https://www.rfc-editor.org/rfc/rfc4287) documents into an `Atom`
|
|
6
|
+
instance and serializes that instance back into XML. It is a faithful reader rather than a
|
|
7
|
+
helpful one: every link keeps its relation, dates stay the text the source held, and an entry
|
|
8
|
+
that omits an author is reported as omitting one.
|
|
9
|
+
|
|
10
|
+
That matters because Atom leaves several choices to the consumer. Which link is "the" link,
|
|
11
|
+
whether an entry inherits its author from the feed, and how a date should be interpreted are
|
|
12
|
+
decisions a reader makes differently from an archiver. Both get the same complete picture
|
|
13
|
+
here, and pick from it.
|
|
14
|
+
|
|
15
|
+
Elements outside the Atom namespace are preserved through `extensions`, so a document
|
|
16
|
+
carrying a foreign module survives a read and a write unchanged.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm add @sdxc/atom
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The statics report failures as a `Result` from
|
|
25
|
+
[`@sdxc/result`](https://www.npmjs.com/package/@sdxc/result), and `Atom.fromXML` takes a
|
|
26
|
+
document from [`@sdxc/xml`](https://www.npmjs.com/package/@sdxc/xml). Both install alongside
|
|
27
|
+
this package.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Parse A Feed
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Atom } from "@sdxc/atom";
|
|
35
|
+
import { isFailure } from "@sdxc/result";
|
|
36
|
+
|
|
37
|
+
let result = Atom.parse(xml);
|
|
38
|
+
if (isFailure(result)) throw result.error;
|
|
39
|
+
|
|
40
|
+
let atom = result.data;
|
|
41
|
+
console.log(atom.feed.title);
|
|
42
|
+
for (let entry of atom.entries) console.log(entry.id, entry.updated);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Pass the document's own URL as the second argument, and the references the feed leaves
|
|
46
|
+
relative resolve against it:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
let result = Atom.parse(xml, "https://example.com/feed.xml");
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Fetch A Feed
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
let result = await Atom.fetch(new URL("https://example.com/feed.xml"));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`Atom.fetch` places no constraint on the response's `Content-Type`, because feeds are
|
|
59
|
+
routinely served as `text/xml`, `application/octet-stream`, and worse. Relative references
|
|
60
|
+
resolve against the URL the response finally came from, so a redirected feed still yields
|
|
61
|
+
absolute links.
|
|
62
|
+
|
|
63
|
+
### Build A Feed
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { Atom } from "@sdxc/atom";
|
|
67
|
+
|
|
68
|
+
let atom = new Atom({
|
|
69
|
+
id: "tag:example.com,2026:feed",
|
|
70
|
+
title: "My Blog",
|
|
71
|
+
updated: new Date().toISOString(),
|
|
72
|
+
link: [
|
|
73
|
+
{ href: "https://example.com/", rel: "alternate", type: "text/html" },
|
|
74
|
+
{ href: "https://example.com/feed.xml", rel: "self", type: "application/atom+xml" },
|
|
75
|
+
],
|
|
76
|
+
author: { name: "Ada Lovelace" },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
atom.addEntry({
|
|
80
|
+
id: "tag:example.com,2026:post-1",
|
|
81
|
+
title: "Hello World",
|
|
82
|
+
updated: "2026-04-14T10:30:00Z",
|
|
83
|
+
published: "2026-04-14T09:00:00Z",
|
|
84
|
+
summary: "A short summary",
|
|
85
|
+
content: { type: "html", value: "<p>Full post content</p>" },
|
|
86
|
+
link: { href: "https://example.com/posts/hello", rel: "alternate" },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let xml = atom.toString();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
### `new Atom(feed: Atom.Feed)`
|
|
95
|
+
|
|
96
|
+
Creates a feed from its metadata, with no entries. RFC 4287 §4.1.1 requires `id`, `title`,
|
|
97
|
+
and `updated`, and an `AtomParseError` is thrown when one is missing.
|
|
98
|
+
|
|
99
|
+
### `atom.feed`
|
|
100
|
+
|
|
101
|
+
The feed-level metadata, as a clone. Assigning a new `Atom.Feed` replaces it and leaves the
|
|
102
|
+
entries in place.
|
|
103
|
+
|
|
104
|
+
### `atom.entries`
|
|
105
|
+
|
|
106
|
+
The entries, as clones, in the order they were added.
|
|
107
|
+
|
|
108
|
+
### `atom.addEntry(entry: Atom.Entry)`
|
|
109
|
+
|
|
110
|
+
Appends one entry. Each entry requires `id`, `title`, and `updated`, on the same terms as the
|
|
111
|
+
feed.
|
|
112
|
+
|
|
113
|
+
### `atom.removeEntry(id: string)`
|
|
114
|
+
|
|
115
|
+
Removes the entry carrying an id. RFC 4287 makes entry ids unique within a feed, so at most
|
|
116
|
+
one entry matches.
|
|
117
|
+
|
|
118
|
+
### `atom.toJSON()`
|
|
119
|
+
|
|
120
|
+
Returns `{ feed, entries }` as plain serializable data.
|
|
121
|
+
|
|
122
|
+
### `atom.toString()`
|
|
123
|
+
|
|
124
|
+
Serializes the feed to Atom 1.0 XML.
|
|
125
|
+
|
|
126
|
+
### `Atom.parse(source: string, base?: string): Result<Atom, AtomParseError>`
|
|
127
|
+
|
|
128
|
+
Parses XML text, resolving relative references against `base`.
|
|
129
|
+
|
|
130
|
+
### `Atom.fromXML(xml: XML, base?: string): Result<Atom, AtomParseError>`
|
|
131
|
+
|
|
132
|
+
Reads a feed out of an already-parsed document, for a caller that parsed the text for some
|
|
133
|
+
other purpose first.
|
|
134
|
+
|
|
135
|
+
### `Atom.fetch(input, init?): Promise<Result<Atom, AtomFetchError | AtomParseError>>`
|
|
136
|
+
|
|
137
|
+
Retrieves a document and parses it, so a failed request, an error status, and an unparseable
|
|
138
|
+
body are all values rather than exceptions.
|
|
139
|
+
|
|
140
|
+
### Errors
|
|
141
|
+
|
|
142
|
+
`AtomParseError` reports a document that is not a feed, or metadata missing a required
|
|
143
|
+
field. `AtomFetchError` reports a request that failed or answered with an error status.
|
|
144
|
+
`AtomStringifyError` reports a feed that cannot be written as XML.
|
|
145
|
+
|
|
146
|
+
## Reading The Data
|
|
147
|
+
|
|
148
|
+
### Text Constructs
|
|
149
|
+
|
|
150
|
+
`title`, `subtitle`, `summary`, and `rights` carry a `type` of `text`, `html`, or `xhtml`. A
|
|
151
|
+
construct with no type collapses to a plain string; the other two arrive as
|
|
152
|
+
`{ value, type }`:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
atom.feed.title; // "My Blog"
|
|
156
|
+
entry.summary; // { value: "<em>Recap</em>", type: "html" }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
An `xhtml` construct is serialized back into a markup string while parsing, and the `<div>`
|
|
160
|
+
wrapper RFC 4287 requires is dropped, so reading a title never means walking a second tree.
|
|
161
|
+
|
|
162
|
+
### Links
|
|
163
|
+
|
|
164
|
+
Every `<link>` is kept with its attributes. RFC 4287 defaults an absent `rel` to
|
|
165
|
+
`alternate`, and that default is left absent rather than filled in, because applying it is a
|
|
166
|
+
reader's concern:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
let links = Array.isArray(entry.link) ? entry.link : entry.link ? [entry.link] : [];
|
|
170
|
+
let page = links.find((link) => (link.rel ?? "alternate") === "alternate");
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Dates
|
|
174
|
+
|
|
175
|
+
`updated` and `published` are the raw RFC 3339 strings the document held, so a malformed date
|
|
176
|
+
is visible to you rather than becoming an `Invalid Date` inside the parser. Convert at the
|
|
177
|
+
point of use:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
let publishedAt = new Date(entry.published ?? entry.updated);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Authors
|
|
184
|
+
|
|
185
|
+
RFC 4287 §4.1.2 lets an entry omit its author when the feed supplies one, or when `<source>`
|
|
186
|
+
does. All three positions are recorded and no fallback is applied, so a consumer picks the
|
|
187
|
+
precedence it wants:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
let author = entry.author ?? entry.source?.author ?? atom.feed.author;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Relative References
|
|
194
|
+
|
|
195
|
+
`xml:base` is inherited, and a relative base composes with the one enclosing it. Every
|
|
196
|
+
`href`, `uri`, `src`, `icon`, and `logo` resolves against the base in scope while parsing. A
|
|
197
|
+
reference with no base in scope, or one that cannot form a URL with it, is left as the
|
|
198
|
+
document wrote it.
|
|
199
|
+
|
|
200
|
+
### Namespaces
|
|
201
|
+
|
|
202
|
+
The Atom namespace identifies the format; the prefix a document binds it to does not.
|
|
203
|
+
`<feed xmlns="…">` and `<a:feed xmlns:a="…">` both parse, and an element counts as Atom only
|
|
204
|
+
when its prefix resolves to `http://www.w3.org/2005/Atom`. Anything else is preserved:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
atom.feed.extensions;
|
|
208
|
+
// [{ name: "media:rating", attributes: { scheme: "urn:simple" }, children: ["adult"] }]
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Notes
|
|
212
|
+
|
|
213
|
+
1. An `xhtml` text construct re-serializes as `type="html"`. Its value is a markup string by
|
|
214
|
+
then, and re-parsing it to rebuild a wrapper would fail on any fragment the XML parser
|
|
215
|
+
rejects, so the payload is kept and the typing narrows.
|
|
216
|
+
2. Text is never sanitized. An `html` or `xhtml` construct holds whatever the publisher
|
|
217
|
+
wrote, and escaping it is the responsibility of whatever renders it.
|
|
218
|
+
3. `length` on a link parses like the rest of the package's numbers: a non-numeric value
|
|
219
|
+
reads as `NaN` rather than failing the document.
|
|
220
|
+
|
|
221
|
+
## Versioning
|
|
222
|
+
|
|
223
|
+
Releases are dated rather than semantic. A version is the UTC date it was published, written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one release goes out per day.
|
|
224
|
+
|
|
225
|
+
Those numbers say when, not what: a later date means a later release and carries no compatibility promise. Any release may change or remove an export.
|
|
226
|
+
|
|
227
|
+
Depend on one exact date, and move it when you are ready to take the change:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"dependencies": {
|
|
232
|
+
"@sdxc/atom": "2026.9.4"
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A caret or tilde range reads the date as major, minor and patch, so it accepts every later release in the same year. An exact version keeps the upgrade yours to schedule.
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
|
242
|
+
|
|
243
|
+
## Author
|
|
244
|
+
|
|
245
|
+
[Sergio Xalambrí](https://sergiodxa.com)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atom 1.0 feed parser and builder. Reads a document into the `Atom` class and
|
|
3
|
+
* serializes it back, keeping every link, text construct and foreign element the
|
|
4
|
+
* source carried so a consumer decides what matters rather than the parser.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Result } from "@sdxc/result";
|
|
10
|
+
import { XML } from "@sdxc/xml";
|
|
11
|
+
/** Raised when a document is not a usable Atom feed. */
|
|
12
|
+
export declare class AtomParseError extends Error {
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
/** Raised when a feed cannot be retrieved or does not arrive as XML. */
|
|
16
|
+
export declare class AtomFetchError extends Error {
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
/** Raised when in-memory feed data cannot be serialized to XML. */
|
|
20
|
+
export declare class AtomStringifyError extends Error {
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
export declare namespace Atom {
|
|
24
|
+
/** A foreign element, preserved verbatim so unknown modules round-trip. */
|
|
25
|
+
interface Element {
|
|
26
|
+
name: string;
|
|
27
|
+
attributes?: Record<string, string>;
|
|
28
|
+
children?: Node[];
|
|
29
|
+
}
|
|
30
|
+
/** A child of a foreign element: either text or another element. */
|
|
31
|
+
type Node = string | Element;
|
|
32
|
+
/** How a text construct's payload should be read (RFC 4287 §3.1). */
|
|
33
|
+
type TextType = "text" | "html" | "xhtml";
|
|
34
|
+
/**
|
|
35
|
+
* A human-readable text construct. `value` is always a string: an `xhtml`
|
|
36
|
+
* construct arrives as the serialized markup its wrapper contained, so a
|
|
37
|
+
* consumer never walks a second tree to read a title.
|
|
38
|
+
*/
|
|
39
|
+
interface Text {
|
|
40
|
+
value: string;
|
|
41
|
+
type?: TextType;
|
|
42
|
+
}
|
|
43
|
+
/** A text construct, or the bare string form when it carries no type. */
|
|
44
|
+
type TextInput = string | Text;
|
|
45
|
+
/** A person construct: an author or a contributor (RFC 4287 §3.2). */
|
|
46
|
+
interface Person {
|
|
47
|
+
name: string;
|
|
48
|
+
uri?: string;
|
|
49
|
+
email?: string;
|
|
50
|
+
extensions?: Element[];
|
|
51
|
+
}
|
|
52
|
+
/** One person, or several. */
|
|
53
|
+
type PersonInput = Person | Person[];
|
|
54
|
+
/**
|
|
55
|
+
* A reference away from the feed or entry. `rel` is left as the document
|
|
56
|
+
* spelled it, absent included, because the default is a reader's concern.
|
|
57
|
+
*/
|
|
58
|
+
interface Link {
|
|
59
|
+
href: string;
|
|
60
|
+
rel?: string;
|
|
61
|
+
type?: string;
|
|
62
|
+
hreflang?: string;
|
|
63
|
+
title?: string;
|
|
64
|
+
length?: number;
|
|
65
|
+
attributes?: Record<string, string>;
|
|
66
|
+
extensions?: Element[];
|
|
67
|
+
}
|
|
68
|
+
/** One link, or several. */
|
|
69
|
+
type LinkInput = Link | Link[];
|
|
70
|
+
/** A category, with the scheme that gives its term meaning. */
|
|
71
|
+
interface Category {
|
|
72
|
+
term: string;
|
|
73
|
+
scheme?: string;
|
|
74
|
+
label?: string;
|
|
75
|
+
attributes?: Record<string, string>;
|
|
76
|
+
extensions?: Element[];
|
|
77
|
+
}
|
|
78
|
+
/** A category, or the bare term when it carries nothing else. */
|
|
79
|
+
type CategoryInput = string | Category;
|
|
80
|
+
/** The agent that produced the feed. */
|
|
81
|
+
interface Generator {
|
|
82
|
+
value: string;
|
|
83
|
+
uri?: string;
|
|
84
|
+
version?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* An entry's body. A superset of a text construct: `type` may be any media
|
|
88
|
+
* type, and `src` points at content held out of line, in which case the
|
|
89
|
+
* element itself is empty and `value` is absent.
|
|
90
|
+
*/
|
|
91
|
+
interface Content {
|
|
92
|
+
type?: string;
|
|
93
|
+
src?: string;
|
|
94
|
+
value?: string;
|
|
95
|
+
attributes?: Record<string, string>;
|
|
96
|
+
}
|
|
97
|
+
/** Metadata of the feed an entry was copied from (RFC 4287 §4.2.11). */
|
|
98
|
+
interface Source {
|
|
99
|
+
id?: string;
|
|
100
|
+
title?: TextInput;
|
|
101
|
+
updated?: string;
|
|
102
|
+
subtitle?: TextInput;
|
|
103
|
+
author?: PersonInput;
|
|
104
|
+
link?: LinkInput;
|
|
105
|
+
rights?: TextInput;
|
|
106
|
+
attributes?: Record<string, string>;
|
|
107
|
+
extensions?: Element[];
|
|
108
|
+
}
|
|
109
|
+
/** Feed-level metadata. `updated` stays the raw RFC 3339 text the source held. */
|
|
110
|
+
interface Feed {
|
|
111
|
+
id: string;
|
|
112
|
+
title: TextInput;
|
|
113
|
+
updated: string;
|
|
114
|
+
subtitle?: TextInput;
|
|
115
|
+
rights?: TextInput;
|
|
116
|
+
author?: PersonInput;
|
|
117
|
+
contributor?: PersonInput;
|
|
118
|
+
link?: LinkInput;
|
|
119
|
+
category?: CategoryInput | CategoryInput[];
|
|
120
|
+
generator?: Generator;
|
|
121
|
+
icon?: string;
|
|
122
|
+
logo?: string;
|
|
123
|
+
/** `xml:lang` on the feed element, inherited by everything inside it. */
|
|
124
|
+
lang?: string;
|
|
125
|
+
/** `xml:base` on the feed element, against which its references resolved. */
|
|
126
|
+
base?: string;
|
|
127
|
+
namespaces?: Record<string, string>;
|
|
128
|
+
attributes?: Record<string, string>;
|
|
129
|
+
extensions?: Element[];
|
|
130
|
+
}
|
|
131
|
+
/** One entry. `updated` is required by RFC 4287 §4.1.2; `published` is not. */
|
|
132
|
+
interface Entry {
|
|
133
|
+
id: string;
|
|
134
|
+
title: TextInput;
|
|
135
|
+
updated: string;
|
|
136
|
+
published?: string;
|
|
137
|
+
summary?: TextInput;
|
|
138
|
+
content?: Content;
|
|
139
|
+
author?: PersonInput;
|
|
140
|
+
contributor?: PersonInput;
|
|
141
|
+
link?: LinkInput;
|
|
142
|
+
category?: CategoryInput | CategoryInput[];
|
|
143
|
+
rights?: TextInput;
|
|
144
|
+
source?: Source;
|
|
145
|
+
lang?: string;
|
|
146
|
+
base?: string;
|
|
147
|
+
attributes?: Record<string, string>;
|
|
148
|
+
extensions?: Element[];
|
|
149
|
+
}
|
|
150
|
+
/** A whole document: the feed's metadata and its entries. */
|
|
151
|
+
interface Document {
|
|
152
|
+
feed: Feed;
|
|
153
|
+
entries: Entry[];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* An Atom 1.0 feed, holding feed-level metadata and an ordered list of entries.
|
|
158
|
+
*
|
|
159
|
+
* Reading accessors hand back clones, so a caller cannot reach into the instance
|
|
160
|
+
* by mutating what it returned.
|
|
161
|
+
*/
|
|
162
|
+
export declare class Atom {
|
|
163
|
+
#private;
|
|
164
|
+
/**
|
|
165
|
+
* Builds a feed from its metadata, starting with no entries.
|
|
166
|
+
*
|
|
167
|
+
* @param feed - The feed-level metadata
|
|
168
|
+
* @throws AtomParseError When `id`, `title` or `updated` is missing
|
|
169
|
+
*/
|
|
170
|
+
constructor(feed: Atom.Feed);
|
|
171
|
+
/** The feed-level metadata. */
|
|
172
|
+
get feed(): Atom.Feed;
|
|
173
|
+
/**
|
|
174
|
+
* Replaces the feed-level metadata, leaving the entries in place.
|
|
175
|
+
*
|
|
176
|
+
* @throws AtomParseError When `id`, `title` or `updated` is missing
|
|
177
|
+
*/
|
|
178
|
+
set feed(feed: Atom.Feed);
|
|
179
|
+
/** The entries, in the order they were added. */
|
|
180
|
+
get entries(): Atom.Entry[];
|
|
181
|
+
/**
|
|
182
|
+
* Appends one entry.
|
|
183
|
+
*
|
|
184
|
+
* @param entry - The entry to append
|
|
185
|
+
* @throws AtomParseError When `id`, `title` or `updated` is missing
|
|
186
|
+
*/
|
|
187
|
+
addEntry(entry: Atom.Entry): void;
|
|
188
|
+
/**
|
|
189
|
+
* Removes the first entry carrying an id, which RFC 4287 makes unique within
|
|
190
|
+
* a feed, so at most one entry can match.
|
|
191
|
+
*
|
|
192
|
+
* @param id - The entry id to remove
|
|
193
|
+
*/
|
|
194
|
+
removeEntry(id: string): void;
|
|
195
|
+
/** The feed and its entries as plain, serializable data. */
|
|
196
|
+
toJSON(): Atom.Document;
|
|
197
|
+
/**
|
|
198
|
+
* Serializes the feed into Atom 1.0 XML.
|
|
199
|
+
*
|
|
200
|
+
* @throws AtomStringifyError When the data cannot form a valid document
|
|
201
|
+
*/
|
|
202
|
+
toString(): string;
|
|
203
|
+
/**
|
|
204
|
+
* Reads a feed out of an already-parsed XML document, which is the entry
|
|
205
|
+
* point for a caller that parsed the text for some other purpose first.
|
|
206
|
+
*
|
|
207
|
+
* @param xml - The parsed XML document
|
|
208
|
+
* @param base - Document URI, used to resolve references the feed leaves relative
|
|
209
|
+
* @returns The feed, or the reason the document is not one
|
|
210
|
+
*/
|
|
211
|
+
static fromXML(xml: XML, base?: string): Result<Atom, AtomParseError>;
|
|
212
|
+
/**
|
|
213
|
+
* Parses Atom XML text.
|
|
214
|
+
*
|
|
215
|
+
* @param source - The raw XML text
|
|
216
|
+
* @param base - Document URI, used to resolve references the feed leaves relative
|
|
217
|
+
* @returns The feed, or the reason the text is not one
|
|
218
|
+
*/
|
|
219
|
+
static parse(source: string, base?: string): Result<Atom, AtomParseError>;
|
|
220
|
+
/**
|
|
221
|
+
* Retrieves a feed and parses it.
|
|
222
|
+
*
|
|
223
|
+
* References the feed leaves relative resolve against the URL the response
|
|
224
|
+
* finally came from, so a redirected feed still yields absolute links.
|
|
225
|
+
*
|
|
226
|
+
* @param input - The URL or request to retrieve
|
|
227
|
+
* @param init - Additional request options
|
|
228
|
+
* @returns The feed, or the reason it could not be read
|
|
229
|
+
*/
|
|
230
|
+
static fetch(input: URL | RequestInfo, init?: RequestInit): Promise<Result<Atom, AtomFetchError | AtomParseError>>;
|
|
231
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atom 1.0 feed parser and builder. Reads a document into the `Atom` class and
|
|
3
|
+
* serializes it back, keeping every link, text construct and foreign element the
|
|
4
|
+
* source carried so a consumer decides what matters rather than the parser.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import { failure, isFailure, success } from "@sdxc/result";
|
|
10
|
+
import { XML } from "@sdxc/xml";
|
|
11
|
+
import { buildDocument } from "./lib/build-document.js";
|
|
12
|
+
import { cloneEntry, cloneFeed } from "./lib/clone.js";
|
|
13
|
+
import { parseDocument } from "./lib/parse-feed.js";
|
|
14
|
+
import { validateEntry } from "./lib/validate-entry.js";
|
|
15
|
+
import { validateFeed } from "./lib/validate-feed.js";
|
|
16
|
+
/** Raised when a document is not a usable Atom feed. */
|
|
17
|
+
export class AtomParseError extends Error {
|
|
18
|
+
name = "AtomParseError";
|
|
19
|
+
}
|
|
20
|
+
/** Raised when a feed cannot be retrieved or does not arrive as XML. */
|
|
21
|
+
export class AtomFetchError extends Error {
|
|
22
|
+
name = "AtomFetchError";
|
|
23
|
+
}
|
|
24
|
+
/** Raised when in-memory feed data cannot be serialized to XML. */
|
|
25
|
+
export class AtomStringifyError extends Error {
|
|
26
|
+
name = "AtomStringifyError";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* An Atom 1.0 feed, holding feed-level metadata and an ordered list of entries.
|
|
30
|
+
*
|
|
31
|
+
* Reading accessors hand back clones, so a caller cannot reach into the instance
|
|
32
|
+
* by mutating what it returned.
|
|
33
|
+
*/
|
|
34
|
+
export class Atom {
|
|
35
|
+
#feed;
|
|
36
|
+
#entries = [];
|
|
37
|
+
/**
|
|
38
|
+
* Builds a feed from its metadata, starting with no entries.
|
|
39
|
+
*
|
|
40
|
+
* @param feed - The feed-level metadata
|
|
41
|
+
* @throws AtomParseError When `id`, `title` or `updated` is missing
|
|
42
|
+
*/
|
|
43
|
+
constructor(feed) {
|
|
44
|
+
validateFeed(feed);
|
|
45
|
+
this.#feed = cloneFeed(feed);
|
|
46
|
+
}
|
|
47
|
+
/** The feed-level metadata. */
|
|
48
|
+
get feed() {
|
|
49
|
+
return cloneFeed(this.#feed);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Replaces the feed-level metadata, leaving the entries in place.
|
|
53
|
+
*
|
|
54
|
+
* @throws AtomParseError When `id`, `title` or `updated` is missing
|
|
55
|
+
*/
|
|
56
|
+
set feed(feed) {
|
|
57
|
+
validateFeed(feed);
|
|
58
|
+
this.#feed = cloneFeed(feed);
|
|
59
|
+
}
|
|
60
|
+
/** The entries, in the order they were added. */
|
|
61
|
+
get entries() {
|
|
62
|
+
return this.#entries.map(cloneEntry);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Appends one entry.
|
|
66
|
+
*
|
|
67
|
+
* @param entry - The entry to append
|
|
68
|
+
* @throws AtomParseError When `id`, `title` or `updated` is missing
|
|
69
|
+
*/
|
|
70
|
+
addEntry(entry) {
|
|
71
|
+
validateEntry(entry);
|
|
72
|
+
this.#entries.push(cloneEntry(entry));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Removes the first entry carrying an id, which RFC 4287 makes unique within
|
|
76
|
+
* a feed, so at most one entry can match.
|
|
77
|
+
*
|
|
78
|
+
* @param id - The entry id to remove
|
|
79
|
+
*/
|
|
80
|
+
removeEntry(id) {
|
|
81
|
+
let index = this.#entries.findIndex((entry) => entry.id === id);
|
|
82
|
+
if (index === -1)
|
|
83
|
+
return;
|
|
84
|
+
this.#entries.splice(index, 1);
|
|
85
|
+
}
|
|
86
|
+
/** The feed and its entries as plain, serializable data. */
|
|
87
|
+
toJSON() {
|
|
88
|
+
return { feed: cloneFeed(this.#feed), entries: this.#entries.map(cloneEntry) };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Serializes the feed into Atom 1.0 XML.
|
|
92
|
+
*
|
|
93
|
+
* @throws AtomStringifyError When the data cannot form a valid document
|
|
94
|
+
*/
|
|
95
|
+
toString() {
|
|
96
|
+
let result = XML.stringify(new XML(buildDocument(this.#feed, this.#entries)));
|
|
97
|
+
if (isFailure(result))
|
|
98
|
+
throw new AtomStringifyError(result.error.message);
|
|
99
|
+
return result.data;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reads a feed out of an already-parsed XML document, which is the entry
|
|
103
|
+
* point for a caller that parsed the text for some other purpose first.
|
|
104
|
+
*
|
|
105
|
+
* @param xml - The parsed XML document
|
|
106
|
+
* @param base - Document URI, used to resolve references the feed leaves relative
|
|
107
|
+
* @returns The feed, or the reason the document is not one
|
|
108
|
+
*/
|
|
109
|
+
static fromXML(xml, base) {
|
|
110
|
+
let document = parseDocument(xml, base);
|
|
111
|
+
if (isFailure(document))
|
|
112
|
+
return document;
|
|
113
|
+
let atom = new Atom(document.data.feed);
|
|
114
|
+
for (let entry of document.data.entries)
|
|
115
|
+
atom.addEntry(entry);
|
|
116
|
+
return success(atom);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parses Atom XML text.
|
|
120
|
+
*
|
|
121
|
+
* @param source - The raw XML text
|
|
122
|
+
* @param base - Document URI, used to resolve references the feed leaves relative
|
|
123
|
+
* @returns The feed, or the reason the text is not one
|
|
124
|
+
*/
|
|
125
|
+
static parse(source, base) {
|
|
126
|
+
let parsed = XML.parse(source);
|
|
127
|
+
if (isFailure(parsed))
|
|
128
|
+
return failure(new AtomParseError(parsed.error.message));
|
|
129
|
+
return Atom.fromXML(parsed.data, base);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Retrieves a feed and parses it.
|
|
133
|
+
*
|
|
134
|
+
* References the feed leaves relative resolve against the URL the response
|
|
135
|
+
* finally came from, so a redirected feed still yields absolute links.
|
|
136
|
+
*
|
|
137
|
+
* @param input - The URL or request to retrieve
|
|
138
|
+
* @param init - Additional request options
|
|
139
|
+
* @returns The feed, or the reason it could not be read
|
|
140
|
+
*/
|
|
141
|
+
static async fetch(input, init) {
|
|
142
|
+
let response;
|
|
143
|
+
try {
|
|
144
|
+
response = await fetch(input, init);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return failure(new AtomFetchError(`Failed to fetch Atom feed: ${message(error)}`));
|
|
148
|
+
}
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
return failure(new AtomFetchError(`Failed to fetch Atom feed: ${response.status}`));
|
|
151
|
+
}
|
|
152
|
+
let text = await response.text();
|
|
153
|
+
return Atom.parse(text, response.url || undefined);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Reads a thrown value's message, so a rejected request reports what went wrong
|
|
158
|
+
* whether or not it rejected with an `Error`.
|
|
159
|
+
*/
|
|
160
|
+
function message(error) {
|
|
161
|
+
return error instanceof Error ? error.message : String(error);
|
|
162
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the XML document for a feed, declaring the Atom namespace on the root and
|
|
3
|
+
* writing every construct back in the form the format expects.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import type { XML } from "@sdxc/xml";
|
|
9
|
+
import type { Atom } from "../index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Builds the whole document.
|
|
12
|
+
*
|
|
13
|
+
* @param feed - The feed-level metadata
|
|
14
|
+
* @param entries - The entries to write, in order
|
|
15
|
+
* @returns The document, ready to serialize
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildDocument(feed: Atom.Feed, entries: Atom.Entry[]): XML.Document;
|