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