@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,63 @@
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 { ATOM_NAMESPACE } from "./constants.js";
10
+ import { prefixOf } from "./utils.js";
11
+ /**
12
+ * Reads every `xmlns` declaration off an element.
13
+ *
14
+ * @param element - The element whose declarations should be read
15
+ * @returns The declared namespaces, keyed by prefix
16
+ */
17
+ export function readNamespaceDeclarations(element) {
18
+ let declarations = {};
19
+ for (let [name, value] of Object.entries(element.attributes ?? {})) {
20
+ if (name === "xmlns") {
21
+ declarations[""] = value;
22
+ continue;
23
+ }
24
+ if (name.startsWith("xmlns:"))
25
+ declarations[name.slice("xmlns:".length)] = value;
26
+ }
27
+ return declarations;
28
+ }
29
+ /**
30
+ * Extends an inherited scope with the declarations an element adds, so a nested
31
+ * element that rebinds a prefix is read against its own binding.
32
+ *
33
+ * @param scope - The namespaces inherited from ancestors
34
+ * @param element - The element whose declarations should be layered on top
35
+ * @returns The scope in effect inside the element
36
+ */
37
+ export function extendNamespaceScope(scope, element) {
38
+ let declarations = readNamespaceDeclarations(element);
39
+ if (Object.keys(declarations).length === 0)
40
+ return scope;
41
+ return { ...scope, ...declarations };
42
+ }
43
+ /**
44
+ * Resolves the namespace a qualified name belongs to under a scope.
45
+ *
46
+ * @param name - The qualified element name
47
+ * @param scope - The namespaces in effect
48
+ * @returns The namespace URI, or `undefined` when the prefix is unbound
49
+ */
50
+ export function namespaceOf(name, scope) {
51
+ return scope[prefixOf(name)];
52
+ }
53
+ /**
54
+ * Reports whether an element belongs to the Atom namespace, which is what
55
+ * separates an Atom element from a foreign one carrying the same local name.
56
+ *
57
+ * @param name - The qualified element name
58
+ * @param scope - The namespaces in effect
59
+ * @returns `true` when the name resolves to the Atom namespace
60
+ */
61
+ export function isAtomName(name, scope) {
62
+ return namespaceOf(name, scope) === ATOM_NAMESPACE;
63
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Parses an XML tree into Atom feed and entry data, threading the namespace and
3
+ * base-URI scopes down the document so an element is judged by the namespace it
4
+ * resolves to and every relative reference is read against the bases above it.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Result } from "@sdxc/result";
10
+ import type { XML } from "@sdxc/xml";
11
+ import type { Atom } from "../index.js";
12
+ import { AtomParseError } from "../index.js";
13
+ /**
14
+ * Parses a document into feed metadata and entries.
15
+ *
16
+ * @param xml - The parsed XML document
17
+ * @param base - Document URI, seeding the base against which relative references resolve
18
+ * @returns The feed and its entries, or the reason the document is not Atom
19
+ */
20
+ export declare function parseDocument(xml: XML, base?: string): Result<Atom.Document, AtomParseError>;
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Parses an XML tree into Atom feed and entry data, threading the namespace and
3
+ * base-URI scopes down the document so an element is judged by the namespace it
4
+ * resolves to and every relative reference is read against the bases above it.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { failure, success } from "@sdxc/result";
10
+ import { AtomParseError } from "../index.js";
11
+ import { cloneAttributes } from "./clone.js";
12
+ import { parseContent } from "./content.js";
13
+ import { toExtensionElement } from "./extensions.js";
14
+ import { extendNamespaceScope, isAtomName, readNamespaceDeclarations } from "./namespaces.js";
15
+ import { parseText } from "./text-construct.js";
16
+ import { collapseArray, getChildElements, getElementText, localName, parseOptionalNumber, } from "./utils.js";
17
+ import { extendScope, resolveUri } from "./xml-base.js";
18
+ /**
19
+ * Parses a document into feed metadata and entries.
20
+ *
21
+ * @param xml - The parsed XML document
22
+ * @param base - Document URI, seeding the base against which relative references resolve
23
+ * @returns The feed and its entries, or the reason the document is not Atom
24
+ */
25
+ export function parseDocument(xml, base) {
26
+ let root = xml.root;
27
+ let namespaces = readNamespaceDeclarations(root);
28
+ if (localName(root.name) !== "feed") {
29
+ return failure(new AtomParseError(`Expected the root element to be "feed".`));
30
+ }
31
+ if (!isAtomName(root.name, namespaces)) {
32
+ return failure(new AtomParseError("Expected the root element in the Atom namespace."));
33
+ }
34
+ let scope = extendScope({ base }, root);
35
+ let feedResult = parseFeed(root, scope, namespaces);
36
+ if (feedResult.status === "failure")
37
+ return feedResult;
38
+ let entries = [];
39
+ for (let child of getChildElements(root)) {
40
+ if (!isAtomName(child.name, namespaces) || localName(child.name) !== "entry")
41
+ continue;
42
+ let entryResult = parseEntry(child, scope, namespaces);
43
+ if (entryResult.status === "failure")
44
+ return entryResult;
45
+ entries.push(entryResult.data);
46
+ }
47
+ return success({ feed: feedResult.data, entries });
48
+ }
49
+ /**
50
+ * Parses the feed element's own children, skipping the entries, which the caller
51
+ * collects separately so one pass does not have to build both shapes at once.
52
+ */
53
+ function parseFeed(element, scope, namespaces) {
54
+ let authors = [];
55
+ let contributors = [];
56
+ let links = [];
57
+ let categories = [];
58
+ let extensions = [];
59
+ let feed = {
60
+ namespaces: cloneAttributes(namespaces),
61
+ attributes: readOwnAttributes(element),
62
+ lang: scope.lang,
63
+ base: scope.base,
64
+ };
65
+ for (let child of getChildElements(element)) {
66
+ if (!isAtomName(child.name, extendNamespaceScope(namespaces, child))) {
67
+ extensions.push(toExtensionElement(child));
68
+ continue;
69
+ }
70
+ let childScope = extendScope(scope, child);
71
+ switch (localName(child.name)) {
72
+ case "id": {
73
+ feed.id = getElementText(child);
74
+ break;
75
+ }
76
+ case "title": {
77
+ feed.title = parseText(child);
78
+ break;
79
+ }
80
+ case "updated": {
81
+ feed.updated = getElementText(child);
82
+ break;
83
+ }
84
+ case "subtitle": {
85
+ feed.subtitle = parseText(child);
86
+ break;
87
+ }
88
+ case "rights": {
89
+ feed.rights = parseText(child);
90
+ break;
91
+ }
92
+ case "author": {
93
+ authors.push(parsePerson(child, childScope, namespaces));
94
+ break;
95
+ }
96
+ case "contributor": {
97
+ contributors.push(parsePerson(child, childScope, namespaces));
98
+ break;
99
+ }
100
+ case "link": {
101
+ links.push(parseLink(child, childScope, namespaces));
102
+ break;
103
+ }
104
+ case "category": {
105
+ categories.push(parseCategory(child, namespaces));
106
+ break;
107
+ }
108
+ case "generator": {
109
+ feed.generator = parseGenerator(child, childScope);
110
+ break;
111
+ }
112
+ case "icon": {
113
+ feed.icon = resolveUri(childScope, getElementText(child));
114
+ break;
115
+ }
116
+ case "logo": {
117
+ feed.logo = resolveUri(childScope, getElementText(child));
118
+ break;
119
+ }
120
+ /** Collected by the caller, which builds the entry list in its own pass. */
121
+ case "entry": {
122
+ break;
123
+ }
124
+ default: {
125
+ extensions.push(toExtensionElement(child));
126
+ }
127
+ }
128
+ }
129
+ if (!feed.id)
130
+ return failure(new AtomParseError("Feed must include an id."));
131
+ if (feed.title === undefined)
132
+ return failure(new AtomParseError("Feed must include a title."));
133
+ if (!feed.updated) {
134
+ return failure(new AtomParseError("Feed must include an updated timestamp."));
135
+ }
136
+ if (authors.length > 0)
137
+ feed.author = collapseArray(authors);
138
+ if (contributors.length > 0)
139
+ feed.contributor = collapseArray(contributors);
140
+ if (links.length > 0)
141
+ feed.link = collapseArray(links);
142
+ if (categories.length > 0)
143
+ feed.category = collapseArray(categories);
144
+ if (extensions.length > 0)
145
+ feed.extensions = extensions;
146
+ return success(feed);
147
+ }
148
+ /** Parses one entry element. */
149
+ function parseEntry(element, parentScope, namespaces) {
150
+ let scope = extendScope(parentScope, element);
151
+ let authors = [];
152
+ let contributors = [];
153
+ let links = [];
154
+ let categories = [];
155
+ let extensions = [];
156
+ let entry = {
157
+ attributes: readOwnAttributes(element),
158
+ lang: scope.lang === parentScope.lang ? undefined : scope.lang,
159
+ base: scope.base === parentScope.base ? undefined : scope.base,
160
+ };
161
+ for (let child of getChildElements(element)) {
162
+ if (!isAtomName(child.name, extendNamespaceScope(namespaces, child))) {
163
+ extensions.push(toExtensionElement(child));
164
+ continue;
165
+ }
166
+ let childScope = extendScope(scope, child);
167
+ switch (localName(child.name)) {
168
+ case "id": {
169
+ entry.id = getElementText(child);
170
+ break;
171
+ }
172
+ case "title": {
173
+ entry.title = parseText(child);
174
+ break;
175
+ }
176
+ case "updated": {
177
+ entry.updated = getElementText(child);
178
+ break;
179
+ }
180
+ case "published": {
181
+ entry.published = getElementText(child);
182
+ break;
183
+ }
184
+ case "summary": {
185
+ entry.summary = parseText(child);
186
+ break;
187
+ }
188
+ case "content": {
189
+ entry.content = parseContent(child, childScope);
190
+ break;
191
+ }
192
+ case "rights": {
193
+ entry.rights = parseText(child);
194
+ break;
195
+ }
196
+ case "author": {
197
+ authors.push(parsePerson(child, childScope, namespaces));
198
+ break;
199
+ }
200
+ case "contributor": {
201
+ contributors.push(parsePerson(child, childScope, namespaces));
202
+ break;
203
+ }
204
+ case "link": {
205
+ links.push(parseLink(child, childScope, namespaces));
206
+ break;
207
+ }
208
+ case "category": {
209
+ categories.push(parseCategory(child, namespaces));
210
+ break;
211
+ }
212
+ case "source": {
213
+ entry.source = parseSource(child, childScope, namespaces);
214
+ break;
215
+ }
216
+ default: {
217
+ extensions.push(toExtensionElement(child));
218
+ }
219
+ }
220
+ }
221
+ if (!entry.id)
222
+ return failure(new AtomParseError("Entry must include an id."));
223
+ if (entry.title === undefined)
224
+ return failure(new AtomParseError("Entry must include a title."));
225
+ if (!entry.updated) {
226
+ return failure(new AtomParseError("Entry must include an updated timestamp."));
227
+ }
228
+ if (authors.length > 0)
229
+ entry.author = collapseArray(authors);
230
+ if (contributors.length > 0)
231
+ entry.contributor = collapseArray(contributors);
232
+ if (links.length > 0)
233
+ entry.link = collapseArray(links);
234
+ if (categories.length > 0)
235
+ entry.category = collapseArray(categories);
236
+ if (extensions.length > 0)
237
+ entry.extensions = extensions;
238
+ return success(entry);
239
+ }
240
+ /** Parses a person construct: an author or a contributor. */
241
+ function parsePerson(element, scope, namespaces) {
242
+ let person = { name: "" };
243
+ let extensions = [];
244
+ for (let child of getChildElements(element)) {
245
+ if (!isAtomName(child.name, extendNamespaceScope(namespaces, child))) {
246
+ extensions.push(toExtensionElement(child));
247
+ continue;
248
+ }
249
+ switch (localName(child.name)) {
250
+ case "name": {
251
+ person.name = getElementText(child);
252
+ break;
253
+ }
254
+ case "uri": {
255
+ person.uri = resolveUri(extendScope(scope, child), getElementText(child));
256
+ break;
257
+ }
258
+ case "email": {
259
+ person.email = getElementText(child);
260
+ break;
261
+ }
262
+ default: {
263
+ extensions.push(toExtensionElement(child));
264
+ }
265
+ }
266
+ }
267
+ if (extensions.length > 0)
268
+ person.extensions = extensions;
269
+ return person;
270
+ }
271
+ /**
272
+ * Parses a link, resolving `href` against the base in scope. `rel` is left as the
273
+ * document spelled it, absent included, because RFC 4287's `alternate` default is
274
+ * a reader's concern rather than something to bake into stored data.
275
+ */
276
+ function parseLink(element, scope, namespaces) {
277
+ let attributes = { ...element.attributes };
278
+ let link = { href: resolveUri(scope, attributes["href"] ?? "") };
279
+ if (attributes["rel"] !== undefined)
280
+ link.rel = attributes["rel"];
281
+ if (attributes["type"] !== undefined)
282
+ link.type = attributes["type"];
283
+ if (attributes["hreflang"] !== undefined)
284
+ link.hreflang = attributes["hreflang"];
285
+ if (attributes["title"] !== undefined)
286
+ link.title = attributes["title"];
287
+ if (attributes["length"] !== undefined) {
288
+ link.length = parseOptionalNumber(attributes["length"]);
289
+ }
290
+ for (let name of ["href", "rel", "type", "hreflang", "title", "length"])
291
+ delete attributes[name];
292
+ let remaining = cloneAttributes(stripScopeAttributes(attributes));
293
+ if (remaining)
294
+ link.attributes = remaining;
295
+ let extensions = collectForeignChildren(element, namespaces);
296
+ if (extensions.length > 0)
297
+ link.extensions = extensions;
298
+ return link;
299
+ }
300
+ /** Parses a category, collapsing to the bare term when nothing else is present. */
301
+ function parseCategory(element, namespaces) {
302
+ let attributes = { ...element.attributes };
303
+ let term = attributes["term"] ?? "";
304
+ let scheme = attributes["scheme"];
305
+ let label = attributes["label"];
306
+ for (let name of ["term", "scheme", "label"])
307
+ delete attributes[name];
308
+ let remaining = cloneAttributes(stripScopeAttributes(attributes));
309
+ let extensions = collectForeignChildren(element, namespaces);
310
+ if (scheme === undefined && label === undefined && !remaining && extensions.length === 0) {
311
+ return term;
312
+ }
313
+ let category = { term };
314
+ if (scheme !== undefined)
315
+ category.scheme = scheme;
316
+ if (label !== undefined)
317
+ category.label = label;
318
+ if (remaining)
319
+ category.attributes = remaining;
320
+ if (extensions.length > 0)
321
+ category.extensions = extensions;
322
+ return category;
323
+ }
324
+ /** Parses the generator element, whose `uri` resolves against the base in scope. */
325
+ function parseGenerator(element, scope) {
326
+ let attributes = element.attributes ?? {};
327
+ let generator = { value: getElementText(element) };
328
+ if (attributes["uri"] !== undefined)
329
+ generator.uri = resolveUri(scope, attributes["uri"]);
330
+ if (attributes["version"] !== undefined)
331
+ generator.version = attributes["version"];
332
+ return generator;
333
+ }
334
+ /**
335
+ * Parses the source element, which carries a subset of feed metadata describing
336
+ * where a copied entry came from.
337
+ */
338
+ function parseSource(element, scope, namespaces) {
339
+ let source = {};
340
+ let authors = [];
341
+ let links = [];
342
+ let extensions = [];
343
+ for (let child of getChildElements(element)) {
344
+ if (!isAtomName(child.name, extendNamespaceScope(namespaces, child))) {
345
+ extensions.push(toExtensionElement(child));
346
+ continue;
347
+ }
348
+ let childScope = extendScope(scope, child);
349
+ switch (localName(child.name)) {
350
+ case "id": {
351
+ source.id = getElementText(child);
352
+ break;
353
+ }
354
+ case "title": {
355
+ source.title = parseText(child);
356
+ break;
357
+ }
358
+ case "subtitle": {
359
+ source.subtitle = parseText(child);
360
+ break;
361
+ }
362
+ case "updated": {
363
+ source.updated = getElementText(child);
364
+ break;
365
+ }
366
+ case "rights": {
367
+ source.rights = parseText(child);
368
+ break;
369
+ }
370
+ case "author": {
371
+ authors.push(parsePerson(child, childScope, namespaces));
372
+ break;
373
+ }
374
+ case "link": {
375
+ links.push(parseLink(child, childScope, namespaces));
376
+ break;
377
+ }
378
+ default: {
379
+ extensions.push(toExtensionElement(child));
380
+ }
381
+ }
382
+ }
383
+ let attributes = readOwnAttributes(element);
384
+ if (attributes)
385
+ source.attributes = attributes;
386
+ if (authors.length > 0)
387
+ source.author = collapseArray(authors);
388
+ if (links.length > 0)
389
+ source.link = collapseArray(links);
390
+ if (extensions.length > 0)
391
+ source.extensions = extensions;
392
+ return source;
393
+ }
394
+ /** Collects the children of an element that fall outside the Atom namespace. */
395
+ function collectForeignChildren(element, namespaces) {
396
+ let extensions = [];
397
+ for (let child of getChildElements(element)) {
398
+ if (isAtomName(child.name, extendNamespaceScope(namespaces, child)))
399
+ continue;
400
+ extensions.push(toExtensionElement(child));
401
+ }
402
+ return extensions;
403
+ }
404
+ /**
405
+ * Reads the attributes an element carries in its own right, dropping the ones
406
+ * already modelled elsewhere: namespace declarations become `namespaces`, and
407
+ * `xml:base`/`xml:lang` become the scope every reference was resolved against.
408
+ */
409
+ function readOwnAttributes(element) {
410
+ let attributes = {};
411
+ for (let [name, value] of Object.entries(element.attributes ?? {})) {
412
+ if (name === "xmlns" || name.startsWith("xmlns:"))
413
+ continue;
414
+ if (name === "xml:base" || name === "xml:lang")
415
+ continue;
416
+ attributes[name] = value;
417
+ }
418
+ return cloneAttributes(attributes);
419
+ }
420
+ /** Drops the scope-carrying attributes from a record already stripped of its own keys. */
421
+ function stripScopeAttributes(attributes) {
422
+ let remaining = {};
423
+ for (let [name, value] of Object.entries(attributes)) {
424
+ if (name === "xmlns" || name.startsWith("xmlns:"))
425
+ continue;
426
+ if (name === "xml:base" || name === "xml:lang")
427
+ continue;
428
+ remaining[name] = value;
429
+ }
430
+ return remaining;
431
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Reads and writes Atom text constructs, whose `type` decides whether the payload
3
+ * is plain text, a run of HTML, or an XHTML subtree that has to be serialized back
4
+ * into markup before a consumer can use it (RFC 4287 §3.1).
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { XML } from "@sdxc/xml";
10
+ import type { Atom } from "../index.js";
11
+ /**
12
+ * Reads one text construct.
13
+ *
14
+ * An `xhtml` construct collapses to the serialized markup its single `div`
15
+ * wrapper contained — the wrapper itself is structural and is dropped — so every
16
+ * construct hands back a string regardless of how it was written.
17
+ *
18
+ * @param element - The element holding the construct
19
+ * @returns The construct, collapsed to a bare string when it carries no type
20
+ */
21
+ export declare function parseText(element: XML.Element): Atom.TextInput;
22
+ /**
23
+ * Builds the element for one text construct.
24
+ *
25
+ * An `xhtml` construct is written back as `html`: the value is markup in a string
26
+ * by then, and re-parsing it to rebuild a wrapper would fail on any fragment the
27
+ * XML parser rejects. The payload survives; the typing narrows.
28
+ *
29
+ * @param name - The element name to write
30
+ * @param text - The construct to serialize
31
+ * @returns The element, ready to place in a document
32
+ */
33
+ export declare function buildTextElement(name: string, text: Atom.TextInput): XML.Element;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Reads and writes Atom text constructs, whose `type` decides whether the payload
3
+ * is plain text, a run of HTML, or an XHTML subtree that has to be serialized back
4
+ * into markup before a consumer can use it (RFC 4287 §3.1).
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { isFailure } from "@sdxc/result";
10
+ import { XML } from "@sdxc/xml";
11
+ import { DEFAULT_TEXT_TYPE } from "./constants.js";
12
+ import { getChildElements, getElementText, localName } from "./utils.js";
13
+ /** The characters that carry markup meaning when XHTML text is re-serialized. */
14
+ const TEXT_ESCAPES = {
15
+ "<": "&lt;",
16
+ ">": "&gt;",
17
+ "&": "&amp;",
18
+ };
19
+ const TEXT_PATTERN = /[<>&]/g;
20
+ /**
21
+ * Reads one text construct.
22
+ *
23
+ * An `xhtml` construct collapses to the serialized markup its single `div`
24
+ * wrapper contained — the wrapper itself is structural and is dropped — so every
25
+ * construct hands back a string regardless of how it was written.
26
+ *
27
+ * @param element - The element holding the construct
28
+ * @returns The construct, collapsed to a bare string when it carries no type
29
+ */
30
+ export function parseText(element) {
31
+ let type = element.attributes?.type ?? DEFAULT_TEXT_TYPE;
32
+ if (type === "xhtml")
33
+ return { value: readXhtml(element), type: "xhtml" };
34
+ if (type === "html")
35
+ return { value: getElementText(element), type: "html" };
36
+ /**
37
+ * A `text` construct is the default, so the bare string says everything the
38
+ * structured form would and reads better at a call site.
39
+ */
40
+ return getElementText(element);
41
+ }
42
+ /**
43
+ * Serializes the children of an `xhtml` construct's wrapper.
44
+ *
45
+ * The wrapper is the `div` RFC 4287 §3.1.1.3 requires; when a document omits it
46
+ * the element's own children are serialized instead, so malformed input still
47
+ * yields the markup it meant rather than nothing.
48
+ */
49
+ function readXhtml(element) {
50
+ let wrapper = getChildElements(element).find((child) => localName(child.name) === "div");
51
+ let host = wrapper ?? element;
52
+ let markup = "";
53
+ for (let child of host.children ?? []) {
54
+ if (typeof child === "string") {
55
+ markup += escapeText(child);
56
+ continue;
57
+ }
58
+ markup += stringifyElement(child);
59
+ }
60
+ return markup;
61
+ }
62
+ /**
63
+ * Serializes one element of an XHTML construct back into markup.
64
+ *
65
+ * A failure yields the element's text alone, because losing the tags around a
66
+ * paragraph is a better outcome for a reader than losing the paragraph.
67
+ */
68
+ function stringifyElement(element) {
69
+ let result = XML.stringify(element);
70
+ if (isFailure(result))
71
+ return escapeText(getElementText(element));
72
+ return result.data;
73
+ }
74
+ /** Escapes the three characters that would otherwise read as markup. */
75
+ function escapeText(value) {
76
+ return value.replace(TEXT_PATTERN, (character) => TEXT_ESCAPES[character] ?? character);
77
+ }
78
+ /**
79
+ * Builds the element for one text construct.
80
+ *
81
+ * An `xhtml` construct is written back as `html`: the value is markup in a string
82
+ * by then, and re-parsing it to rebuild a wrapper would fail on any fragment the
83
+ * XML parser rejects. The payload survives; the typing narrows.
84
+ *
85
+ * @param name - The element name to write
86
+ * @param text - The construct to serialize
87
+ * @returns The element, ready to place in a document
88
+ */
89
+ export function buildTextElement(name, text) {
90
+ if (typeof text === "string")
91
+ return { name, attributes: {}, children: [text] };
92
+ let type = text.type === "xhtml" ? "html" : text.type;
93
+ let attributes = {};
94
+ if (type && type !== DEFAULT_TEXT_TYPE)
95
+ attributes["type"] = type;
96
+ return { name, attributes, children: [text.value] };
97
+ }