@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,59 @@
1
+ /**
2
+ * Shared helpers for reading XML elements, splitting qualified names, and
3
+ * normalizing the package's scalar-or-array data shapes.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ import type { XML } from "@sdxc/xml";
9
+ /**
10
+ * Returns the direct child elements of an XML element.
11
+ *
12
+ * @param element - The element whose child elements should be returned
13
+ * @returns The direct child elements in source order
14
+ */
15
+ export declare function getChildElements(element: XML.Element): XML.Element[];
16
+ /**
17
+ * Reads the concatenated text content of one XML element.
18
+ *
19
+ * @param element - The element whose text content should be collected
20
+ * @returns The concatenated text content
21
+ */
22
+ export declare function getElementText(element: XML.Element): string;
23
+ /**
24
+ * Splits the prefix off a qualified name.
25
+ *
26
+ * @param name - The qualified element or attribute name
27
+ * @returns The prefix, or an empty string for an unprefixed name
28
+ */
29
+ export declare function prefixOf(name: string): string;
30
+ /**
31
+ * Strips the prefix from a qualified name, which is how this package compares
32
+ * element names: the XML layer resolves no namespaces, so the prefix a document
33
+ * binds Atom to carries no meaning on its own.
34
+ *
35
+ * @param name - The qualified element or attribute name
36
+ * @returns The name without its prefix
37
+ */
38
+ export declare function localName(name: string): string;
39
+ /**
40
+ * Parses one optional numeric text value.
41
+ *
42
+ * @param value - The raw string to parse
43
+ * @returns The parsed number or `NaN`
44
+ */
45
+ export declare function parseOptionalNumber(value: string): number;
46
+ /**
47
+ * Collapses one-item arrays back to the package's scalar-friendly API shape.
48
+ *
49
+ * @param values - The values to collapse
50
+ * @returns One value or the full array
51
+ */
52
+ export declare function collapseArray<T>(values: T[]): T | T[];
53
+ /**
54
+ * Normalizes undefined, scalar, and array values into a flat array.
55
+ *
56
+ * @param value - The value to normalize
57
+ * @returns A flat array representation
58
+ */
59
+ export declare function normalizeArray<T>(value?: T | T[]): T[];
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Shared helpers for reading XML elements, splitting qualified names, and
3
+ * normalizing the package's scalar-or-array data shapes.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ /**
9
+ * Returns the direct child elements of an XML element.
10
+ *
11
+ * @param element - The element whose child elements should be returned
12
+ * @returns The direct child elements in source order
13
+ */
14
+ export function getChildElements(element) {
15
+ let children = [];
16
+ for (let child of element.children ?? []) {
17
+ if (typeof child === "string")
18
+ continue;
19
+ children.push(child);
20
+ }
21
+ return children;
22
+ }
23
+ /**
24
+ * Reads the concatenated text content of one XML element.
25
+ *
26
+ * @param element - The element whose text content should be collected
27
+ * @returns The concatenated text content
28
+ */
29
+ export function getElementText(element) {
30
+ let content = "";
31
+ for (let child of element.children ?? []) {
32
+ if (typeof child !== "string")
33
+ continue;
34
+ content += child;
35
+ }
36
+ return content;
37
+ }
38
+ /**
39
+ * Splits the prefix off a qualified name.
40
+ *
41
+ * @param name - The qualified element or attribute name
42
+ * @returns The prefix, or an empty string for an unprefixed name
43
+ */
44
+ export function prefixOf(name) {
45
+ let separator = name.indexOf(":");
46
+ if (separator === -1)
47
+ return "";
48
+ return name.slice(0, separator);
49
+ }
50
+ /**
51
+ * Strips the prefix from a qualified name, which is how this package compares
52
+ * element names: the XML layer resolves no namespaces, so the prefix a document
53
+ * binds Atom to carries no meaning on its own.
54
+ *
55
+ * @param name - The qualified element or attribute name
56
+ * @returns The name without its prefix
57
+ */
58
+ export function localName(name) {
59
+ let separator = name.indexOf(":");
60
+ if (separator === -1)
61
+ return name;
62
+ return name.slice(separator + 1);
63
+ }
64
+ /**
65
+ * Parses one optional numeric text value.
66
+ *
67
+ * @param value - The raw string to parse
68
+ * @returns The parsed number or `NaN`
69
+ */
70
+ export function parseOptionalNumber(value) {
71
+ let number = Number(value);
72
+ if (Number.isFinite(number))
73
+ return number;
74
+ return Number.NaN;
75
+ }
76
+ /**
77
+ * Collapses one-item arrays back to the package's scalar-friendly API shape.
78
+ *
79
+ * @param values - The values to collapse
80
+ * @returns One value or the full array
81
+ */
82
+ export function collapseArray(values) {
83
+ if (values.length === 1)
84
+ return values[0];
85
+ return values;
86
+ }
87
+ /**
88
+ * Normalizes undefined, scalar, and array values into a flat array.
89
+ *
90
+ * @param value - The value to normalize
91
+ * @returns A flat array representation
92
+ */
93
+ export function normalizeArray(value) {
94
+ if (value === undefined)
95
+ return [];
96
+ if (Array.isArray(value))
97
+ return value;
98
+ return [value];
99
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Guards the three elements RFC 4287 §4.1.2 requires of every entry, so an
3
+ * instance can never hold data that would serialize into an invalid document.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ import type { Atom } from "../index.js";
9
+ /**
10
+ * Reads the string behind either text construct form, so a caller testing for
11
+ * presence does not branch on which form the value took.
12
+ *
13
+ * @param text - The construct to read
14
+ * @returns The text, or an empty string when there is none
15
+ */
16
+ export declare function readTextValue(text?: Atom.TextInput): string;
17
+ /**
18
+ * Checks that an entry carries an id, a title and an updated timestamp.
19
+ *
20
+ * @param entry - The entry to check
21
+ * @throws AtomParseError Naming the first required element that is missing
22
+ */
23
+ export declare function validateEntry(entry: Atom.Entry): void;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Guards the three elements RFC 4287 §4.1.2 requires of every entry, so an
3
+ * instance can never hold data that would serialize into an invalid document.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ import { AtomParseError } from "../index.js";
9
+ /**
10
+ * Reads the string behind either text construct form, so a caller testing for
11
+ * presence does not branch on which form the value took.
12
+ *
13
+ * @param text - The construct to read
14
+ * @returns The text, or an empty string when there is none
15
+ */
16
+ export function readTextValue(text) {
17
+ if (text === undefined)
18
+ return "";
19
+ return typeof text === "string" ? text : text.value;
20
+ }
21
+ /**
22
+ * Checks that an entry carries an id, a title and an updated timestamp.
23
+ *
24
+ * @param entry - The entry to check
25
+ * @throws AtomParseError Naming the first required element that is missing
26
+ */
27
+ export function validateEntry(entry) {
28
+ if (!entry.id)
29
+ throw new AtomParseError("Entry must include an id.");
30
+ if (!readTextValue(entry.title))
31
+ throw new AtomParseError("Entry must include a title.");
32
+ if (!entry.updated)
33
+ throw new AtomParseError("Entry must include an updated timestamp.");
34
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Guards the three elements RFC 4287 §4.1.1 requires of every feed, so an
3
+ * instance can never hold data that would serialize into an invalid document.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ import type { Atom } from "../index.js";
9
+ /**
10
+ * Checks that a feed carries an id, a title and an updated timestamp.
11
+ *
12
+ * @param feed - The feed to check
13
+ * @throws AtomParseError Naming the first required element that is missing
14
+ */
15
+ export declare function validateFeed(feed: Atom.Feed): void;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Guards the three elements RFC 4287 §4.1.1 requires of every feed, so an
3
+ * instance can never hold data that would serialize into an invalid document.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ import { AtomParseError } from "../index.js";
9
+ import { readTextValue } from "./validate-entry.js";
10
+ /**
11
+ * Checks that a feed carries an id, a title and an updated timestamp.
12
+ *
13
+ * @param feed - The feed to check
14
+ * @throws AtomParseError Naming the first required element that is missing
15
+ */
16
+ export function validateFeed(feed) {
17
+ if (!feed.id)
18
+ throw new AtomParseError("Feed must include an id.");
19
+ if (!readTextValue(feed.title))
20
+ throw new AtomParseError("Feed must include a title.");
21
+ if (!feed.updated)
22
+ throw new AtomParseError("Feed must include an updated timestamp.");
23
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Tracks the `xml:base` and `xml:lang` in effect at a point in the tree, so a
3
+ * relative reference resolves against the bases that enclose it. RFC 4287 §4.1.1
4
+ * makes both inherited, and makes a relative base compose with the one above it.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { XML } from "@sdxc/xml";
10
+ /** The base URI and language inherited at one point in the document. */
11
+ export interface Scope {
12
+ base?: string;
13
+ lang?: string;
14
+ }
15
+ /**
16
+ * Layers an element's own `xml:base` and `xml:lang` over the inherited scope. A
17
+ * relative `xml:base` is resolved against the enclosing one, so nested bases
18
+ * accumulate rather than replace.
19
+ *
20
+ * @param scope - The scope inherited from ancestors
21
+ * @param element - The element whose attributes should be layered on top
22
+ * @returns The scope in effect inside the element
23
+ */
24
+ export declare function extendScope(scope: Scope, element: XML.Element): Scope;
25
+ /**
26
+ * Resolves a reference against the base in scope.
27
+ *
28
+ * The reference is returned unchanged when no base applies or when the pair does
29
+ * not form a URL, because a feed's own text is more useful to a consumer than a
30
+ * value this package invented or dropped.
31
+ *
32
+ * @param scope - The scope supplying the base URI
33
+ * @param reference - The possibly-relative reference to resolve
34
+ * @returns The absolute reference, or the original when it cannot be resolved
35
+ */
36
+ export declare function resolveUri(scope: Scope, reference: string): string;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tracks the `xml:base` and `xml:lang` in effect at a point in the tree, so a
3
+ * relative reference resolves against the bases that enclose it. RFC 4287 §4.1.1
4
+ * makes both inherited, and makes a relative base compose with the one above it.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Layers an element's own `xml:base` and `xml:lang` over the inherited scope. A
11
+ * relative `xml:base` is resolved against the enclosing one, so nested bases
12
+ * accumulate rather than replace.
13
+ *
14
+ * @param scope - The scope inherited from ancestors
15
+ * @param element - The element whose attributes should be layered on top
16
+ * @returns The scope in effect inside the element
17
+ */
18
+ export function extendScope(scope, element) {
19
+ let attributes = element.attributes ?? {};
20
+ let base = attributes["xml:base"];
21
+ let lang = attributes["xml:lang"];
22
+ if (base === undefined && lang === undefined)
23
+ return scope;
24
+ return {
25
+ base: base === undefined ? scope.base : resolveUri(scope, base),
26
+ lang: lang ?? scope.lang,
27
+ };
28
+ }
29
+ /**
30
+ * Resolves a reference against the base in scope.
31
+ *
32
+ * The reference is returned unchanged when no base applies or when the pair does
33
+ * not form a URL, because a feed's own text is more useful to a consumer than a
34
+ * value this package invented or dropped.
35
+ *
36
+ * @param scope - The scope supplying the base URI
37
+ * @param reference - The possibly-relative reference to resolve
38
+ * @returns The absolute reference, or the original when it cannot be resolved
39
+ */
40
+ export function resolveUri(scope, reference) {
41
+ if (!scope.base)
42
+ return reference;
43
+ try {
44
+ return new URL(reference, scope.base).toString();
45
+ }
46
+ catch {
47
+ return reference;
48
+ }
49
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@sdxc/atom",
3
+ "version": "0.0.0-pre.1",
4
+ "description": "Atom 1.0 feed parser and builder",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "dependencies": {
11
+ "@sdxc/result": "2026.9.5",
12
+ "@sdxc/xml": "0.0.0-pre.1"
13
+ },
14
+ "gitHead": "6b352367c6853be4019c3a3ab761185df8ab5ab8",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/sergiodxa/monorepo.git",
21
+ "directory": "packages/atom"
22
+ }
23
+ }