@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 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)
@@ -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;