@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
|
@@ -0,0 +1,195 @@
|
|
|
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 { ATOM_NAMESPACE } from "./constants.js";
|
|
9
|
+
import { buildContentElement } from "./content.js";
|
|
10
|
+
import { toXMLElement } from "./extensions.js";
|
|
11
|
+
import { buildTextElement } from "./text-construct.js";
|
|
12
|
+
import { normalizeArray } from "./utils.js";
|
|
13
|
+
/**
|
|
14
|
+
* Builds the whole document.
|
|
15
|
+
*
|
|
16
|
+
* @param feed - The feed-level metadata
|
|
17
|
+
* @param entries - The entries to write, in order
|
|
18
|
+
* @returns The document, ready to serialize
|
|
19
|
+
*/
|
|
20
|
+
export function buildDocument(feed, entries) {
|
|
21
|
+
return {
|
|
22
|
+
declaration: { version: "1.0", encoding: "UTF-8" },
|
|
23
|
+
root: {
|
|
24
|
+
name: "feed",
|
|
25
|
+
attributes: buildRootAttributes(feed),
|
|
26
|
+
children: [...buildFeedChildren(feed), ...entries.map(buildEntryElement)],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Builds the root's attributes, declaring the Atom namespace as the default so
|
|
32
|
+
* every element beneath it is unprefixed, alongside any the feed carried.
|
|
33
|
+
*/
|
|
34
|
+
function buildRootAttributes(feed) {
|
|
35
|
+
let attributes = {
|
|
36
|
+
xmlns: ATOM_NAMESPACE,
|
|
37
|
+
...feed.attributes,
|
|
38
|
+
};
|
|
39
|
+
for (let [prefix, uri] of Object.entries(feed.namespaces ?? {})) {
|
|
40
|
+
if (prefix === "")
|
|
41
|
+
continue;
|
|
42
|
+
attributes[`xmlns:${prefix}`] = uri;
|
|
43
|
+
}
|
|
44
|
+
if (feed.base !== undefined)
|
|
45
|
+
attributes["xml:base"] = feed.base;
|
|
46
|
+
if (feed.lang !== undefined)
|
|
47
|
+
attributes["xml:lang"] = feed.lang;
|
|
48
|
+
return attributes;
|
|
49
|
+
}
|
|
50
|
+
/** Builds the feed element's own children, in the order RFC 4287 presents them. */
|
|
51
|
+
function buildFeedChildren(feed) {
|
|
52
|
+
let children = [
|
|
53
|
+
textElement("id", feed.id),
|
|
54
|
+
buildTextElement("title", feed.title),
|
|
55
|
+
textElement("updated", feed.updated),
|
|
56
|
+
];
|
|
57
|
+
if (feed.subtitle !== undefined)
|
|
58
|
+
children.push(buildTextElement("subtitle", feed.subtitle));
|
|
59
|
+
if (feed.rights !== undefined)
|
|
60
|
+
children.push(buildTextElement("rights", feed.rights));
|
|
61
|
+
for (let author of normalizeArray(feed.author))
|
|
62
|
+
children.push(buildPersonElement("author", author));
|
|
63
|
+
for (let contributor of normalizeArray(feed.contributor)) {
|
|
64
|
+
children.push(buildPersonElement("contributor", contributor));
|
|
65
|
+
}
|
|
66
|
+
for (let link of normalizeArray(feed.link))
|
|
67
|
+
children.push(buildLinkElement(link));
|
|
68
|
+
for (let category of normalizeArray(feed.category))
|
|
69
|
+
children.push(buildCategoryElement(category));
|
|
70
|
+
if (feed.generator)
|
|
71
|
+
children.push(buildGeneratorElement(feed.generator));
|
|
72
|
+
if (feed.icon !== undefined)
|
|
73
|
+
children.push(textElement("icon", feed.icon));
|
|
74
|
+
if (feed.logo !== undefined)
|
|
75
|
+
children.push(textElement("logo", feed.logo));
|
|
76
|
+
for (let extension of feed.extensions ?? [])
|
|
77
|
+
children.push(toXMLElement(extension));
|
|
78
|
+
return children;
|
|
79
|
+
}
|
|
80
|
+
/** Builds one entry element. */
|
|
81
|
+
function buildEntryElement(entry) {
|
|
82
|
+
let attributes = { ...entry.attributes };
|
|
83
|
+
if (entry.base !== undefined)
|
|
84
|
+
attributes["xml:base"] = entry.base;
|
|
85
|
+
if (entry.lang !== undefined)
|
|
86
|
+
attributes["xml:lang"] = entry.lang;
|
|
87
|
+
let children = [
|
|
88
|
+
textElement("id", entry.id),
|
|
89
|
+
buildTextElement("title", entry.title),
|
|
90
|
+
textElement("updated", entry.updated),
|
|
91
|
+
];
|
|
92
|
+
if (entry.published !== undefined)
|
|
93
|
+
children.push(textElement("published", entry.published));
|
|
94
|
+
if (entry.summary !== undefined)
|
|
95
|
+
children.push(buildTextElement("summary", entry.summary));
|
|
96
|
+
if (entry.content)
|
|
97
|
+
children.push(buildContentElement(entry.content));
|
|
98
|
+
if (entry.rights !== undefined)
|
|
99
|
+
children.push(buildTextElement("rights", entry.rights));
|
|
100
|
+
for (let author of normalizeArray(entry.author))
|
|
101
|
+
children.push(buildPersonElement("author", author));
|
|
102
|
+
for (let contributor of normalizeArray(entry.contributor)) {
|
|
103
|
+
children.push(buildPersonElement("contributor", contributor));
|
|
104
|
+
}
|
|
105
|
+
for (let link of normalizeArray(entry.link))
|
|
106
|
+
children.push(buildLinkElement(link));
|
|
107
|
+
for (let category of normalizeArray(entry.category))
|
|
108
|
+
children.push(buildCategoryElement(category));
|
|
109
|
+
if (entry.source)
|
|
110
|
+
children.push(buildSourceElement(entry.source));
|
|
111
|
+
for (let extension of entry.extensions ?? [])
|
|
112
|
+
children.push(toXMLElement(extension));
|
|
113
|
+
return { name: "entry", attributes, children };
|
|
114
|
+
}
|
|
115
|
+
/** Builds a person construct. */
|
|
116
|
+
function buildPersonElement(name, person) {
|
|
117
|
+
let children = [textElement("name", person.name)];
|
|
118
|
+
if (person.uri !== undefined)
|
|
119
|
+
children.push(textElement("uri", person.uri));
|
|
120
|
+
if (person.email !== undefined)
|
|
121
|
+
children.push(textElement("email", person.email));
|
|
122
|
+
for (let extension of person.extensions ?? [])
|
|
123
|
+
children.push(toXMLElement(extension));
|
|
124
|
+
return { name, attributes: {}, children };
|
|
125
|
+
}
|
|
126
|
+
/** Builds a link, which is empty and carries everything in its attributes. */
|
|
127
|
+
function buildLinkElement(link) {
|
|
128
|
+
let attributes = { href: link.href, ...link.attributes };
|
|
129
|
+
if (link.rel !== undefined)
|
|
130
|
+
attributes["rel"] = link.rel;
|
|
131
|
+
if (link.type !== undefined)
|
|
132
|
+
attributes["type"] = link.type;
|
|
133
|
+
if (link.hreflang !== undefined)
|
|
134
|
+
attributes["hreflang"] = link.hreflang;
|
|
135
|
+
if (link.title !== undefined)
|
|
136
|
+
attributes["title"] = link.title;
|
|
137
|
+
if (link.length !== undefined && Number.isFinite(link.length)) {
|
|
138
|
+
attributes["length"] = String(link.length);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
name: "link",
|
|
142
|
+
attributes,
|
|
143
|
+
children: (link.extensions ?? []).map(toXMLElement),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Builds a category from either the bare-term or the structured form. */
|
|
147
|
+
function buildCategoryElement(category) {
|
|
148
|
+
if (typeof category === "string") {
|
|
149
|
+
return { name: "category", attributes: { term: category }, children: [] };
|
|
150
|
+
}
|
|
151
|
+
let attributes = { term: category.term, ...category.attributes };
|
|
152
|
+
if (category.scheme !== undefined)
|
|
153
|
+
attributes["scheme"] = category.scheme;
|
|
154
|
+
if (category.label !== undefined)
|
|
155
|
+
attributes["label"] = category.label;
|
|
156
|
+
return {
|
|
157
|
+
name: "category",
|
|
158
|
+
attributes,
|
|
159
|
+
children: (category.extensions ?? []).map(toXMLElement),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Builds the generator element. */
|
|
163
|
+
function buildGeneratorElement(generator) {
|
|
164
|
+
let attributes = {};
|
|
165
|
+
if (generator.uri !== undefined)
|
|
166
|
+
attributes["uri"] = generator.uri;
|
|
167
|
+
if (generator.version !== undefined)
|
|
168
|
+
attributes["version"] = generator.version;
|
|
169
|
+
return { name: "generator", attributes, children: [generator.value] };
|
|
170
|
+
}
|
|
171
|
+
/** Builds the source element an entry copied from another feed carries. */
|
|
172
|
+
function buildSourceElement(source) {
|
|
173
|
+
let children = [];
|
|
174
|
+
if (source.id !== undefined)
|
|
175
|
+
children.push(textElement("id", source.id));
|
|
176
|
+
if (source.title !== undefined)
|
|
177
|
+
children.push(buildTextElement("title", source.title));
|
|
178
|
+
if (source.subtitle !== undefined)
|
|
179
|
+
children.push(buildTextElement("subtitle", source.subtitle));
|
|
180
|
+
if (source.updated !== undefined)
|
|
181
|
+
children.push(textElement("updated", source.updated));
|
|
182
|
+
if (source.rights !== undefined)
|
|
183
|
+
children.push(buildTextElement("rights", source.rights));
|
|
184
|
+
for (let author of normalizeArray(source.author))
|
|
185
|
+
children.push(buildPersonElement("author", author));
|
|
186
|
+
for (let link of normalizeArray(source.link))
|
|
187
|
+
children.push(buildLinkElement(link));
|
|
188
|
+
for (let extension of source.extensions ?? [])
|
|
189
|
+
children.push(toXMLElement(extension));
|
|
190
|
+
return { name: "source", attributes: { ...source.attributes }, children };
|
|
191
|
+
}
|
|
192
|
+
/** Builds an element holding one run of text and no attributes. */
|
|
193
|
+
function textElement(name, value) {
|
|
194
|
+
return { name, attributes: {}, children: [value] };
|
|
195
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-clone helpers for feed, entry, and extension element data, so the class
|
|
3
|
+
* hands out copies and a caller cannot reach its internals through a return value.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import type { Atom } from "../index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Clones an attribute record, reporting an absent or empty one as `undefined` so
|
|
11
|
+
* a document that declared nothing does not grow an empty object.
|
|
12
|
+
*
|
|
13
|
+
* @param attributes - The attributes to clone
|
|
14
|
+
* @returns The cloned attributes, or `undefined`
|
|
15
|
+
*/
|
|
16
|
+
export declare function cloneAttributes(attributes?: Record<string, string>): Record<string, string> | undefined;
|
|
17
|
+
/** Clones a list of extension elements, or reports `undefined` for none. */
|
|
18
|
+
export declare function cloneExtensionElements(elements?: Atom.Element[]): Atom.Element[] | undefined;
|
|
19
|
+
/** Clones a text construct, which is immutable in its string form. */
|
|
20
|
+
export declare function cloneTextInput(text?: Atom.TextInput): Atom.TextInput | undefined;
|
|
21
|
+
/** Clones one person or a list of them, preserving which form was used. */
|
|
22
|
+
export declare function clonePersonInput(person?: Atom.PersonInput): Atom.PersonInput | undefined;
|
|
23
|
+
/** Clones one link or a list of them, preserving which form was used. */
|
|
24
|
+
export declare function cloneLinkInput(link?: Atom.LinkInput): Atom.LinkInput | undefined;
|
|
25
|
+
/** Clones one category or a list of them, preserving which form was used. */
|
|
26
|
+
export declare function cloneCategoryInput(category?: Atom.CategoryInput | Atom.CategoryInput[]): Atom.CategoryInput | Atom.CategoryInput[] | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Clones feed-level metadata deeply enough that nothing reachable from the copy
|
|
29
|
+
* is shared with the original.
|
|
30
|
+
*
|
|
31
|
+
* @param feed - The feed to clone
|
|
32
|
+
* @returns The cloned feed
|
|
33
|
+
*/
|
|
34
|
+
export declare function cloneFeed(feed: Atom.Feed): Atom.Feed;
|
|
35
|
+
/**
|
|
36
|
+
* Clones one entry deeply enough that nothing reachable from the copy is shared
|
|
37
|
+
* with the original.
|
|
38
|
+
*
|
|
39
|
+
* @param entry - The entry to clone
|
|
40
|
+
* @returns The cloned entry
|
|
41
|
+
*/
|
|
42
|
+
export declare function cloneEntry(entry: Atom.Entry): Atom.Entry;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-clone helpers for feed, entry, and extension element data, so the class
|
|
3
|
+
* hands out copies and a caller cannot reach its internals through a return value.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Clones an attribute record, reporting an absent or empty one as `undefined` so
|
|
10
|
+
* a document that declared nothing does not grow an empty object.
|
|
11
|
+
*
|
|
12
|
+
* @param attributes - The attributes to clone
|
|
13
|
+
* @returns The cloned attributes, or `undefined`
|
|
14
|
+
*/
|
|
15
|
+
export function cloneAttributes(attributes) {
|
|
16
|
+
if (!attributes)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (Object.keys(attributes).length === 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
return { ...attributes };
|
|
21
|
+
}
|
|
22
|
+
/** Clones a list of extension elements, or reports `undefined` for none. */
|
|
23
|
+
export function cloneExtensionElements(elements) {
|
|
24
|
+
if (!elements)
|
|
25
|
+
return undefined;
|
|
26
|
+
return elements.map(cloneExtensionElement);
|
|
27
|
+
}
|
|
28
|
+
/** Clones one extension element and everything beneath it. */
|
|
29
|
+
function cloneExtensionElement(element) {
|
|
30
|
+
let children = element.children?.map((child) => typeof child === "string" ? child : cloneExtensionElement(child));
|
|
31
|
+
return { name: element.name, attributes: cloneAttributes(element.attributes), children };
|
|
32
|
+
}
|
|
33
|
+
/** Clones a text construct, which is immutable in its string form. */
|
|
34
|
+
export function cloneTextInput(text) {
|
|
35
|
+
if (text === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (typeof text === "string")
|
|
38
|
+
return text;
|
|
39
|
+
return { ...text };
|
|
40
|
+
}
|
|
41
|
+
/** Clones one person construct. */
|
|
42
|
+
function clonePerson(person) {
|
|
43
|
+
return { ...person, extensions: cloneExtensionElements(person.extensions) };
|
|
44
|
+
}
|
|
45
|
+
/** Clones one person or a list of them, preserving which form was used. */
|
|
46
|
+
export function clonePersonInput(person) {
|
|
47
|
+
if (person === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
if (Array.isArray(person))
|
|
50
|
+
return person.map(clonePerson);
|
|
51
|
+
return clonePerson(person);
|
|
52
|
+
}
|
|
53
|
+
/** Clones one link. */
|
|
54
|
+
function cloneLink(link) {
|
|
55
|
+
return {
|
|
56
|
+
...link,
|
|
57
|
+
attributes: cloneAttributes(link.attributes),
|
|
58
|
+
extensions: cloneExtensionElements(link.extensions),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Clones one link or a list of them, preserving which form was used. */
|
|
62
|
+
export function cloneLinkInput(link) {
|
|
63
|
+
if (link === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (Array.isArray(link))
|
|
66
|
+
return link.map(cloneLink);
|
|
67
|
+
return cloneLink(link);
|
|
68
|
+
}
|
|
69
|
+
/** Clones one category, which stays a bare string when it carried nothing else. */
|
|
70
|
+
function cloneCategory(category) {
|
|
71
|
+
if (typeof category === "string")
|
|
72
|
+
return category;
|
|
73
|
+
return {
|
|
74
|
+
...category,
|
|
75
|
+
attributes: cloneAttributes(category.attributes),
|
|
76
|
+
extensions: cloneExtensionElements(category.extensions),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Clones one category or a list of them, preserving which form was used. */
|
|
80
|
+
export function cloneCategoryInput(category) {
|
|
81
|
+
if (category === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
if (Array.isArray(category))
|
|
84
|
+
return category.map(cloneCategory);
|
|
85
|
+
return cloneCategory(category);
|
|
86
|
+
}
|
|
87
|
+
/** Clones the source feed metadata an entry was copied with. */
|
|
88
|
+
function cloneSource(source) {
|
|
89
|
+
return {
|
|
90
|
+
...source,
|
|
91
|
+
title: cloneTextInput(source.title),
|
|
92
|
+
subtitle: cloneTextInput(source.subtitle),
|
|
93
|
+
rights: cloneTextInput(source.rights),
|
|
94
|
+
author: clonePersonInput(source.author),
|
|
95
|
+
link: cloneLinkInput(source.link),
|
|
96
|
+
attributes: cloneAttributes(source.attributes),
|
|
97
|
+
extensions: cloneExtensionElements(source.extensions),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Clones feed-level metadata deeply enough that nothing reachable from the copy
|
|
102
|
+
* is shared with the original.
|
|
103
|
+
*
|
|
104
|
+
* @param feed - The feed to clone
|
|
105
|
+
* @returns The cloned feed
|
|
106
|
+
*/
|
|
107
|
+
export function cloneFeed(feed) {
|
|
108
|
+
return {
|
|
109
|
+
...feed,
|
|
110
|
+
title: cloneTextInput(feed.title),
|
|
111
|
+
subtitle: cloneTextInput(feed.subtitle),
|
|
112
|
+
rights: cloneTextInput(feed.rights),
|
|
113
|
+
author: clonePersonInput(feed.author),
|
|
114
|
+
contributor: clonePersonInput(feed.contributor),
|
|
115
|
+
link: cloneLinkInput(feed.link),
|
|
116
|
+
category: cloneCategoryInput(feed.category),
|
|
117
|
+
generator: feed.generator ? { ...feed.generator } : undefined,
|
|
118
|
+
namespaces: cloneAttributes(feed.namespaces),
|
|
119
|
+
attributes: cloneAttributes(feed.attributes),
|
|
120
|
+
extensions: cloneExtensionElements(feed.extensions),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Clones one entry deeply enough that nothing reachable from the copy is shared
|
|
125
|
+
* with the original.
|
|
126
|
+
*
|
|
127
|
+
* @param entry - The entry to clone
|
|
128
|
+
* @returns The cloned entry
|
|
129
|
+
*/
|
|
130
|
+
export function cloneEntry(entry) {
|
|
131
|
+
return {
|
|
132
|
+
...entry,
|
|
133
|
+
title: cloneTextInput(entry.title),
|
|
134
|
+
summary: cloneTextInput(entry.summary),
|
|
135
|
+
rights: cloneTextInput(entry.rights),
|
|
136
|
+
content: entry.content
|
|
137
|
+
? { ...entry.content, attributes: cloneAttributes(entry.content.attributes) }
|
|
138
|
+
: undefined,
|
|
139
|
+
author: clonePersonInput(entry.author),
|
|
140
|
+
contributor: clonePersonInput(entry.contributor),
|
|
141
|
+
link: cloneLinkInput(entry.link),
|
|
142
|
+
category: cloneCategoryInput(entry.category),
|
|
143
|
+
source: entry.source ? cloneSource(entry.source) : undefined,
|
|
144
|
+
attributes: cloneAttributes(entry.attributes),
|
|
145
|
+
extensions: cloneExtensionElements(entry.extensions),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The namespace that identifies Atom and the default a text construct assumes,
|
|
3
|
+
* shared across the package.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Stores the Atom 1.0 namespace URI, which identifies the format rather than any
|
|
10
|
+
* prefix a document happens to bind it to.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
|
|
13
|
+
/**
|
|
14
|
+
* Stores the text type assumed when a construct omits its `type` attribute,
|
|
15
|
+
* which RFC 4287 §3.1.1 defines as `text`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_TEXT_TYPE = "text";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The namespace that identifies Atom and the default a text construct assumes,
|
|
3
|
+
* shared across the package.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Stores the Atom 1.0 namespace URI, which identifies the format rather than any
|
|
10
|
+
* prefix a document happens to bind it to.
|
|
11
|
+
*/
|
|
12
|
+
export const ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
|
|
13
|
+
/**
|
|
14
|
+
* Stores the text type assumed when a construct omits its `type` attribute,
|
|
15
|
+
* which RFC 4287 §3.1.1 defines as `text`.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_TEXT_TYPE = "text";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and writes an entry's `content` element, which extends a text construct
|
|
3
|
+
* with any media type and with `src` for a body held out of line (RFC 4287 §4.1.3).
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import { XML } from "@sdxc/xml";
|
|
9
|
+
import type { Atom } from "../index.js";
|
|
10
|
+
import type { Scope } from "./xml-base.js";
|
|
11
|
+
/**
|
|
12
|
+
* Reads the content element.
|
|
13
|
+
*
|
|
14
|
+
* A `src` body is reported without a value, because the element is empty by
|
|
15
|
+
* definition and inventing one would hide that the body lives elsewhere. Text,
|
|
16
|
+
* HTML and XHTML share the text construct's reading; any other media type is
|
|
17
|
+
* handed back as the raw string it was written as, undecoded.
|
|
18
|
+
*
|
|
19
|
+
* @param element - The content element
|
|
20
|
+
* @param scope - The base and language in effect
|
|
21
|
+
* @returns The parsed content
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseContent(element: XML.Element, scope: Scope): Atom.Content;
|
|
24
|
+
/**
|
|
25
|
+
* Builds the content element.
|
|
26
|
+
*
|
|
27
|
+
* An `xhtml` body is written back as `html` for the reason the text constructs
|
|
28
|
+
* are: by this point the markup is a string, and re-parsing it to rebuild a
|
|
29
|
+
* wrapper would fail on any fragment the XML parser rejects.
|
|
30
|
+
*
|
|
31
|
+
* @param content - The content to serialize
|
|
32
|
+
* @returns The element, ready to place in an entry
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildContentElement(content: Atom.Content): XML.Element;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and writes an entry's `content` element, which extends a text construct
|
|
3
|
+
* with any media type and with `src` for a body held out of line (RFC 4287 §4.1.3).
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import { XML } from "@sdxc/xml";
|
|
9
|
+
import { cloneAttributes } from "./clone.js";
|
|
10
|
+
import { parseText } from "./text-construct.js";
|
|
11
|
+
import { getElementText } from "./utils.js";
|
|
12
|
+
import { resolveUri } from "./xml-base.js";
|
|
13
|
+
/**
|
|
14
|
+
* Reads the content element.
|
|
15
|
+
*
|
|
16
|
+
* A `src` body is reported without a value, because the element is empty by
|
|
17
|
+
* definition and inventing one would hide that the body lives elsewhere. Text,
|
|
18
|
+
* HTML and XHTML share the text construct's reading; any other media type is
|
|
19
|
+
* handed back as the raw string it was written as, undecoded.
|
|
20
|
+
*
|
|
21
|
+
* @param element - The content element
|
|
22
|
+
* @param scope - The base and language in effect
|
|
23
|
+
* @returns The parsed content
|
|
24
|
+
*/
|
|
25
|
+
export function parseContent(element, scope) {
|
|
26
|
+
let attributes = { ...element.attributes };
|
|
27
|
+
let type = attributes["type"];
|
|
28
|
+
let src = attributes["src"];
|
|
29
|
+
delete attributes["type"];
|
|
30
|
+
delete attributes["src"];
|
|
31
|
+
let content = {};
|
|
32
|
+
if (type !== undefined)
|
|
33
|
+
content.type = type;
|
|
34
|
+
if (src !== undefined)
|
|
35
|
+
content.src = resolveUri(scope, src);
|
|
36
|
+
if (src === undefined)
|
|
37
|
+
content.value = readValue(element, type);
|
|
38
|
+
let remaining = cloneAttributes(attributes);
|
|
39
|
+
if (remaining)
|
|
40
|
+
content.attributes = remaining;
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Reads an inline body, routing the three text types through the text construct
|
|
45
|
+
* and leaving every other media type as the source text.
|
|
46
|
+
*/
|
|
47
|
+
function readValue(element, type) {
|
|
48
|
+
if (type === undefined || type === "text" || type === "html" || type === "xhtml") {
|
|
49
|
+
let text = parseText(element);
|
|
50
|
+
return typeof text === "string" ? text : text.value;
|
|
51
|
+
}
|
|
52
|
+
return getElementText(element);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds the content element.
|
|
56
|
+
*
|
|
57
|
+
* An `xhtml` body is written back as `html` for the reason the text constructs
|
|
58
|
+
* are: by this point the markup is a string, and re-parsing it to rebuild a
|
|
59
|
+
* wrapper would fail on any fragment the XML parser rejects.
|
|
60
|
+
*
|
|
61
|
+
* @param content - The content to serialize
|
|
62
|
+
* @returns The element, ready to place in an entry
|
|
63
|
+
*/
|
|
64
|
+
export function buildContentElement(content) {
|
|
65
|
+
let attributes = { ...content.attributes };
|
|
66
|
+
if (content.type !== undefined)
|
|
67
|
+
attributes["type"] = content.type === "xhtml" ? "html" : content.type;
|
|
68
|
+
if (content.src !== undefined)
|
|
69
|
+
attributes["src"] = content.src;
|
|
70
|
+
return {
|
|
71
|
+
name: "content",
|
|
72
|
+
attributes,
|
|
73
|
+
children: content.src === undefined && content.value !== undefined ? [content.value] : [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts between XML elements and the package's extension element shape, so a
|
|
3
|
+
* foreign module the parser does not model survives a read and a write unchanged.
|
|
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
|
+
* Converts one XML element into the package extension element shape.
|
|
12
|
+
*
|
|
13
|
+
* @param element - The XML element to convert
|
|
14
|
+
* @returns The package extension element
|
|
15
|
+
*/
|
|
16
|
+
export declare function toExtensionElement(element: XML.Element): Atom.Element;
|
|
17
|
+
/**
|
|
18
|
+
* Converts one package extension element into the XML element shape.
|
|
19
|
+
*
|
|
20
|
+
* @param element - The extension element to convert
|
|
21
|
+
* @returns The XML element representation
|
|
22
|
+
*/
|
|
23
|
+
export declare function toXMLElement(element: Atom.Element): XML.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts between XML elements and the package's extension element shape, so a
|
|
3
|
+
* foreign module the parser does not model survives a read and a write unchanged.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import { cloneAttributes } from "./clone.js";
|
|
9
|
+
/**
|
|
10
|
+
* Converts one XML element into the package extension element shape.
|
|
11
|
+
*
|
|
12
|
+
* @param element - The XML element to convert
|
|
13
|
+
* @returns The package extension element
|
|
14
|
+
*/
|
|
15
|
+
export function toExtensionElement(element) {
|
|
16
|
+
let children = [];
|
|
17
|
+
for (let child of element.children ?? []) {
|
|
18
|
+
if (typeof child === "string") {
|
|
19
|
+
children.push(child);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
children.push(toExtensionElement(child));
|
|
23
|
+
}
|
|
24
|
+
return { name: element.name, attributes: cloneAttributes(element.attributes), children };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Converts one package extension element into the XML element shape.
|
|
28
|
+
*
|
|
29
|
+
* @param element - The extension element to convert
|
|
30
|
+
* @returns The XML element representation
|
|
31
|
+
*/
|
|
32
|
+
export function toXMLElement(element) {
|
|
33
|
+
let children = [];
|
|
34
|
+
for (let child of element.children ?? []) {
|
|
35
|
+
if (typeof child === "string") {
|
|
36
|
+
children.push(child);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
children.push(toXMLElement(child));
|
|
40
|
+
}
|
|
41
|
+
return { name: element.name, attributes: cloneAttributes(element.attributes) ?? {}, children };
|
|
42
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves which prefixes a document binds to which namespaces, so an element can
|
|
3
|
+
* be tested for membership in the Atom namespace rather than for a spelling. The
|
|
4
|
+
* XML layer performs no namespace resolution, so this package does its own.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { XML } from "@sdxc/xml";
|
|
10
|
+
/** The namespace declarations in scope, keyed by prefix with `""` for the default. */
|
|
11
|
+
export type NamespaceScope = Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Reads every `xmlns` declaration off an element.
|
|
14
|
+
*
|
|
15
|
+
* @param element - The element whose declarations should be read
|
|
16
|
+
* @returns The declared namespaces, keyed by prefix
|
|
17
|
+
*/
|
|
18
|
+
export declare function readNamespaceDeclarations(element: XML.Element): NamespaceScope;
|
|
19
|
+
/**
|
|
20
|
+
* Extends an inherited scope with the declarations an element adds, so a nested
|
|
21
|
+
* element that rebinds a prefix is read against its own binding.
|
|
22
|
+
*
|
|
23
|
+
* @param scope - The namespaces inherited from ancestors
|
|
24
|
+
* @param element - The element whose declarations should be layered on top
|
|
25
|
+
* @returns The scope in effect inside the element
|
|
26
|
+
*/
|
|
27
|
+
export declare function extendNamespaceScope(scope: NamespaceScope, element: XML.Element): NamespaceScope;
|
|
28
|
+
/**
|
|
29
|
+
* Resolves the namespace a qualified name belongs to under a scope.
|
|
30
|
+
*
|
|
31
|
+
* @param name - The qualified element name
|
|
32
|
+
* @param scope - The namespaces in effect
|
|
33
|
+
* @returns The namespace URI, or `undefined` when the prefix is unbound
|
|
34
|
+
*/
|
|
35
|
+
export declare function namespaceOf(name: string, scope: NamespaceScope): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Reports whether an element belongs to the Atom namespace, which is what
|
|
38
|
+
* separates an Atom element from a foreign one carrying the same local name.
|
|
39
|
+
*
|
|
40
|
+
* @param name - The qualified element name
|
|
41
|
+
* @param scope - The namespaces in effect
|
|
42
|
+
* @returns `true` when the name resolves to the Atom namespace
|
|
43
|
+
*/
|
|
44
|
+
export declare function isAtomName(name: string, scope: NamespaceScope): boolean;
|