@raoh/core 0.9.0-dev.15.20261004151401.ge5893e0cc07d

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
+ import type { Issue } from "./issue.ts";
2
+ /** Writes the sentence for an issue that carries no message of its own. */
3
+ export interface MessageResolver {
4
+ resolve(issue: Issue): string;
5
+ }
6
+ /** The message key of an issue for text that is not JSON, which the specification leaves outside its input model. */
7
+ export declare const INVALID_FORMAT_JSON = "invalid_format.json";
8
+ /**
9
+ * A catalogue of message templates keyed by message key or code, over the catalogue it falls
10
+ * back to.
11
+ *
12
+ * A catalogue is a stack of layers, most specific first, as Raoh for Java reads a locale's
13
+ * `.properties` file before its parent's. An issue is looked up one layer at a time, by its
14
+ * message key and then by its code, and only when neither has a template does the next layer down
15
+ * get asked. So a layer that translates just `invalid_format` wins over a refined key such as
16
+ * `invalid_format.email` beneath it. A template's `{name}` placeholders are filled with the
17
+ * message forms of the issue's metadata; a placeholder naming an entry the metadata lacks stays as
18
+ * it is written. Where no layer has a template, the sentence is what the catalogue's fallback
19
+ * writes, `validation failed: <code>` unless one is given with {@link withFallback}.
20
+ */
21
+ export declare class Messages implements MessageResolver {
22
+ #private;
23
+ private constructor();
24
+ /** The English catalogue of the Raoh Specification. Every issue's default message comes from it. */
25
+ static readonly english: Messages;
26
+ /** The Japanese catalogue of the Raoh Specification, over the English one. */
27
+ static readonly japanese: Messages;
28
+ /** A catalogue with no templates, in which every issue reads `validation failed: <code>`. */
29
+ static empty(): Messages;
30
+ /**
31
+ * The catalogue a `.properties` text holds, read as Java's `Properties.load` reads one, such as
32
+ * the `messages*.properties` of Raoh for Java: one `raoh.<key>=<template>` per entry, `\uXXXX`
33
+ * escapes and continued lines included. The `raoh.` prefix is optional. The catalogue falls
34
+ * back to nothing; give it one with {@link fallingBackTo}.
35
+ *
36
+ * @throws {SyntaxError} where a `\u` escape is malformed
37
+ */
38
+ static fromProperties(text: string): Messages;
39
+ /**
40
+ * The catalogue for a language tag, `ja` or `ja-JP` among them: Japanese for Japanese, and
41
+ * English for any other language.
42
+ */
43
+ static forLocale(locale: string): Messages;
44
+ /** This catalogue with `parent` beneath its last layer, as a locale's file sits over its parent's. */
45
+ fallingBackTo(parent: Messages): Messages;
46
+ /**
47
+ * This catalogue, writing what `fallback` writes for an issue no layer has a template for. It is
48
+ * how a host language gives the issues of its own, which no catalogue of Raoh's knows, the
49
+ * sentence it has for them, and still lets a catalogue that does have a template for one win.
50
+ */
51
+ withFallback(fallback: (issue: Issue) => string): Messages;
52
+ /** A layer of `overrides`, keyed by message key or code, over this catalogue. */
53
+ withOverrides(overrides: Readonly<Record<string, string>>): Messages;
54
+ /** The template the most specific layer holding `key` has, if one does. */
55
+ template(key: string): string | undefined;
56
+ /** Every key, and the template the most specific layer holding it has. */
57
+ templates(): Map<string, string>;
58
+ resolve(issue: Issue): string;
59
+ }
@@ -0,0 +1,225 @@
1
+ // Writing an issue's message in a person's language.
2
+ import { CATALOG } from "./catalog.js";
3
+ import { messageForm } from "./meta.js";
4
+ /** The message key of an issue for text that is not JSON, which the specification leaves outside its input model. */
5
+ export const INVALID_FORMAT_JSON = "invalid_format.json";
6
+ /**
7
+ * A catalogue of message templates keyed by message key or code, over the catalogue it falls
8
+ * back to.
9
+ *
10
+ * A catalogue is a stack of layers, most specific first, as Raoh for Java reads a locale's
11
+ * `.properties` file before its parent's. An issue is looked up one layer at a time, by its
12
+ * message key and then by its code, and only when neither has a template does the next layer down
13
+ * get asked. So a layer that translates just `invalid_format` wins over a refined key such as
14
+ * `invalid_format.email` beneath it. A template's `{name}` placeholders are filled with the
15
+ * message forms of the issue's metadata; a placeholder naming an entry the metadata lacks stays as
16
+ * it is written. Where no layer has a template, the sentence is what the catalogue's fallback
17
+ * writes, `validation failed: <code>` unless one is given with {@link withFallback}.
18
+ */
19
+ export class Messages {
20
+ #templates;
21
+ #parent;
22
+ #fallback;
23
+ constructor(templates, parent, fallback = undefined) {
24
+ this.#templates = templates;
25
+ this.#parent = parent;
26
+ this.#fallback = fallback;
27
+ }
28
+ /** The English catalogue of the Raoh Specification. Every issue's default message comes from it. */
29
+ static english = Messages.fromProperties(CATALOG.en).withOverrides({
30
+ [INVALID_FORMAT_JSON]: "not valid JSON",
31
+ });
32
+ /** The Japanese catalogue of the Raoh Specification, over the English one. */
33
+ static japanese = Messages.fromProperties(CATALOG.ja)
34
+ .withOverrides({ [INVALID_FORMAT_JSON]: "JSONとして読めません" })
35
+ .fallingBackTo(Messages.english);
36
+ /** A catalogue with no templates, in which every issue reads `validation failed: <code>`. */
37
+ static empty() {
38
+ return new Messages(new Map(), undefined);
39
+ }
40
+ /**
41
+ * The catalogue a `.properties` text holds, read as Java's `Properties.load` reads one, such as
42
+ * the `messages*.properties` of Raoh for Java: one `raoh.<key>=<template>` per entry, `\uXXXX`
43
+ * escapes and continued lines included. The `raoh.` prefix is optional. The catalogue falls
44
+ * back to nothing; give it one with {@link fallingBackTo}.
45
+ *
46
+ * @throws {SyntaxError} where a `\u` escape is malformed
47
+ */
48
+ static fromProperties(text) {
49
+ const templates = new Map();
50
+ for (const [key, template] of loadProperties(text)) {
51
+ templates.set(key.startsWith("raoh.") ? key.slice("raoh.".length) : key, template);
52
+ }
53
+ return new Messages(templates, undefined);
54
+ }
55
+ /**
56
+ * The catalogue for a language tag, `ja` or `ja-JP` among them: Japanese for Japanese, and
57
+ * English for any other language.
58
+ */
59
+ static forLocale(locale) {
60
+ const language = locale.split(/[-_]/)[0]?.toLowerCase();
61
+ return language === "ja" ? Messages.japanese : Messages.english;
62
+ }
63
+ /** This catalogue with `parent` beneath its last layer, as a locale's file sits over its parent's. */
64
+ fallingBackTo(parent) {
65
+ return new Messages(this.#templates, this.#parent === undefined ? parent : this.#parent.fallingBackTo(parent), this.#fallback);
66
+ }
67
+ /**
68
+ * This catalogue, writing what `fallback` writes for an issue no layer has a template for. It is
69
+ * how a host language gives the issues of its own, which no catalogue of Raoh's knows, the
70
+ * sentence it has for them, and still lets a catalogue that does have a template for one win.
71
+ */
72
+ withFallback(fallback) {
73
+ return new Messages(this.#templates, this.#parent, fallback);
74
+ }
75
+ /** A layer of `overrides`, keyed by message key or code, over this catalogue. */
76
+ withOverrides(overrides) {
77
+ return new Messages(new Map(Object.entries(overrides)), this);
78
+ }
79
+ /** The template the most specific layer holding `key` has, if one does. */
80
+ template(key) {
81
+ for (let layer = this; layer !== undefined; layer = layer.#parent) {
82
+ const template = layer.#templates.get(key);
83
+ if (template !== undefined) {
84
+ return template;
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+ /** Every key, and the template the most specific layer holding it has. */
90
+ templates() {
91
+ const out = new Map();
92
+ for (let layer = this; layer !== undefined; layer = layer.#parent) {
93
+ for (const [key, template] of layer.#templates) {
94
+ if (!out.has(key)) {
95
+ out.set(key, template);
96
+ }
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ resolve(issue) {
102
+ let fallback;
103
+ for (let layer = this; layer !== undefined; layer = layer.#parent) {
104
+ const template = layer.#templates.get(issue.messageKey) ?? layer.#templates.get(issue.code);
105
+ if (template !== undefined) {
106
+ return fill(template, issue.meta);
107
+ }
108
+ fallback ??= layer.#fallback;
109
+ }
110
+ return fallback === undefined ? `validation failed: ${issue.code}` : fallback(issue);
111
+ }
112
+ }
113
+ const PLACEHOLDER = /\{([A-Za-z_][A-Za-z0-9_.-]*)\}/g;
114
+ /** `template` with each `{name}` replaced by the message form of the entry `name`, or left as written where there is none. */
115
+ function fill(template, meta) {
116
+ return template.replace(PLACEHOLDER, (written, name) => Object.prototype.hasOwnProperty.call(meta, name) ? messageForm(meta[name]) : written);
117
+ }
118
+ /**
119
+ * `Properties.load`: the key and value pairs of a `.properties` text, in order.
120
+ *
121
+ * A logical line continues onto the next when it ends in an odd number of backslashes, and the
122
+ * next line's leading whitespace is skipped. Lines that are blank or whose first character other
123
+ * than whitespace is `#` or `!` are comments. A key ends at the first `=`, `:` or whitespace not
124
+ * escaped; whitespace around the separator is skipped. Both key and value undo the escapes `\t`,
125
+ * `\n`, `\r`, `\f` and `\uXXXX`, and a backslash before any other character stands for that
126
+ * character.
127
+ */
128
+ function loadProperties(text) {
129
+ const lines = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
130
+ const pairs = [];
131
+ let logical = "";
132
+ let start = 0;
133
+ let continuing = false;
134
+ lines.forEach((raw, index) => {
135
+ let line;
136
+ if (continuing) {
137
+ line = raw.replace(/^[ \t\f]+/, "");
138
+ }
139
+ else {
140
+ line = raw.replace(/^[ \t\f]+/, "");
141
+ if (line === "" || line.startsWith("#") || line.startsWith("!")) {
142
+ return;
143
+ }
144
+ start = index + 1;
145
+ }
146
+ const trailing = line.length - line.replace(/\\+$/, "").length;
147
+ if (trailing % 2 === 1) {
148
+ logical += line.slice(0, -1);
149
+ continuing = true;
150
+ }
151
+ else {
152
+ logical += line;
153
+ continuing = false;
154
+ pairs.push(entry(logical, start));
155
+ logical = "";
156
+ }
157
+ });
158
+ if (continuing) {
159
+ pairs.push(entry(logical, start));
160
+ }
161
+ return pairs;
162
+ }
163
+ function entry(line, number) {
164
+ let keyEnd = line.length;
165
+ let escaped = false;
166
+ for (let i = 0; i < line.length; i += 1) {
167
+ const c = line[i];
168
+ if (escaped) {
169
+ escaped = false;
170
+ }
171
+ else if (c === "\\") {
172
+ escaped = true;
173
+ }
174
+ else if (c === "=" || c === ":" || c === " " || c === "\t" || c === "\f") {
175
+ keyEnd = i;
176
+ break;
177
+ }
178
+ }
179
+ let rest = line.slice(keyEnd).replace(/^[ \t\f]+/, "");
180
+ if (rest.startsWith("=") || rest.startsWith(":")) {
181
+ rest = rest.slice(1);
182
+ }
183
+ rest = rest.replace(/^[ \t\f]+/, "");
184
+ return [unescape(line.slice(0, keyEnd), number), unescape(rest, number)];
185
+ }
186
+ function unescape(raw, number) {
187
+ let out = "";
188
+ for (let i = 0; i < raw.length; i += 1) {
189
+ const c = raw[i];
190
+ if (c !== "\\") {
191
+ out += c;
192
+ continue;
193
+ }
194
+ i += 1;
195
+ const next = raw[i];
196
+ switch (next) {
197
+ case undefined:
198
+ break;
199
+ case "t":
200
+ out += "\t";
201
+ break;
202
+ case "n":
203
+ out += "\n";
204
+ break;
205
+ case "r":
206
+ out += "\r";
207
+ break;
208
+ case "f":
209
+ out += "\f";
210
+ break;
211
+ case "u": {
212
+ const hex = raw.slice(i + 1, i + 5);
213
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
214
+ throw new SyntaxError(`line ${number}: malformed \\uXXXX encoding`);
215
+ }
216
+ out += String.fromCharCode(Number.parseInt(hex, 16));
217
+ i += 4;
218
+ break;
219
+ }
220
+ default:
221
+ out += next;
222
+ }
223
+ }
224
+ return out;
225
+ }
package/dist/meta.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Whether the two are the same value of the value model: floats as `Object.is` compares them (+0
3
+ * and -0 differ, NaN is NaN), a decimal by coefficient and scale, lists element by element, sets
4
+ * and maps in any order.
5
+ */
6
+ export declare function same(a: unknown, b: unknown): boolean;
7
+ /**
8
+ * A key two values share exactly when they are the same value, for the scalars that have one;
9
+ * `undefined` for a value that has to be compared with {@link same}.
10
+ */
11
+ export declare function keyOf(value: unknown): string | undefined;
12
+ /** Whether `value` occurs in `values`, compared as the value model compares them. */
13
+ export declare function includesSame(values: readonly unknown[], value: unknown): boolean;
14
+ /** -1, 0 or 1 as `a` comes before, with or after `b`, for the values the bounding operations order. */
15
+ export declare function compareValues(a: unknown, b: unknown): number;
16
+ /** -1, 0 or 1 as `a` comes before, with or after `b` in code point order, which UTF-16 order is not. */
17
+ export declare function compareCodePoints(a: string, b: string): number;
18
+ /**
19
+ * How a metadata value is written in a message: a boolean, an integer or a string as it is, a
20
+ * float and a decimal in their message forms, a list as `[a, b]`, and a temporal value as its
21
+ * `toString` writes it.
22
+ */
23
+ export declare function messageForm(value: unknown): string;
package/dist/meta.js ADDED
@@ -0,0 +1,128 @@
1
+ // Values of the value model: how they are compared, ordered and written in a message.
2
+ import { Decimal } from "./decimal.js";
3
+ import { Float, compareFloats } from "./float.js";
4
+ import { ofThisCopy } from "./copy.js";
5
+ /**
6
+ * Whether the two are the same value of the value model: floats as `Object.is` compares them (+0
7
+ * and -0 differ, NaN is NaN), a decimal by coefficient and scale, lists element by element, sets
8
+ * and maps in any order.
9
+ */
10
+ export function same(a, b) {
11
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
12
+ return Object.is(a, b);
13
+ }
14
+ if (ofThisCopy(a, Decimal, "Decimal")) {
15
+ return ofThisCopy(b, Decimal, "Decimal") && a.equals(b);
16
+ }
17
+ if (ofThisCopy(a, Float, "Float")) {
18
+ return ofThisCopy(b, Float, "Float") && a.width === b.width && Object.is(a.value, b.value);
19
+ }
20
+ if (Array.isArray(a)) {
21
+ return Array.isArray(b) && a.length === b.length && a.every((item, i) => same(item, b[i]));
22
+ }
23
+ if (a instanceof Set) {
24
+ return b instanceof Set && a.size === b.size && [...a].every((item) => [...b].some((other) => same(item, other)));
25
+ }
26
+ if (a instanceof Map) {
27
+ return (b instanceof Map &&
28
+ a.size === b.size &&
29
+ [...a].every(([key, value]) => b.has(key) && same(value, b.get(key))));
30
+ }
31
+ const equals = a.equals;
32
+ if (typeof equals === "function") {
33
+ return equals.call(a, b);
34
+ }
35
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
36
+ return false;
37
+ }
38
+ const keys = Object.keys(a);
39
+ return (keys.length === Object.keys(b).length &&
40
+ keys.every((key) => Object.prototype.hasOwnProperty.call(b, key) &&
41
+ same(a[key], b[key])));
42
+ }
43
+ /**
44
+ * A key two values share exactly when they are the same value, for the scalars that have one;
45
+ * `undefined` for a value that has to be compared with {@link same}.
46
+ */
47
+ export function keyOf(value) {
48
+ switch (typeof value) {
49
+ case "string":
50
+ return `s${value}`;
51
+ case "boolean":
52
+ return value ? "t" : "f";
53
+ case "bigint":
54
+ return `i${value}`;
55
+ case "number":
56
+ return Object.is(value, -0) ? "n-0" : `n${value}`;
57
+ default:
58
+ if (value === null) {
59
+ return "z";
60
+ }
61
+ if (ofThisCopy(value, Decimal, "Decimal")) {
62
+ return `d${value.coefficient}:${value.scale}`;
63
+ }
64
+ if (ofThisCopy(value, Float, "Float")) {
65
+ return `f${value.width}:${Object.is(value.value, -0) ? "-0" : value.value}`;
66
+ }
67
+ return undefined;
68
+ }
69
+ }
70
+ /** Whether `value` occurs in `values`, compared as the value model compares them. */
71
+ export function includesSame(values, value) {
72
+ return values.some((each) => same(each, value));
73
+ }
74
+ /** -1, 0 or 1 as `a` comes before, with or after `b`, for the values the bounding operations order. */
75
+ export function compareValues(a, b) {
76
+ if (typeof a === "string" && typeof b === "string") {
77
+ return compareCodePoints(a, b);
78
+ }
79
+ if (typeof a === "bigint" && typeof b === "bigint") {
80
+ return a < b ? -1 : a > b ? 1 : 0;
81
+ }
82
+ if (typeof a === "number" && typeof b === "number") {
83
+ return compareFloats(a, b);
84
+ }
85
+ if (ofThisCopy(a, Float, "Float") && ofThisCopy(b, Float, "Float")) {
86
+ return compareFloats(a.value, b.value);
87
+ }
88
+ if (ofThisCopy(a, Decimal, "Decimal") && ofThisCopy(b, Decimal, "Decimal")) {
89
+ return a.compare(b);
90
+ }
91
+ const compare = a?.compare;
92
+ if (typeof compare === "function") {
93
+ return compare.call(a, b);
94
+ }
95
+ throw new TypeError(`${String(a)} and ${String(b)} have no order`);
96
+ }
97
+ /** -1, 0 or 1 as `a` comes before, with or after `b` in code point order, which UTF-16 order is not. */
98
+ export function compareCodePoints(a, b) {
99
+ const length = Math.min(a.length, b.length);
100
+ for (let i = 0; i < length; i += 1) {
101
+ const x = a.codePointAt(i);
102
+ const y = b.codePointAt(i);
103
+ if (x !== y) {
104
+ return x < y ? -1 : 1;
105
+ }
106
+ if (x > 0xffff) {
107
+ i += 1;
108
+ }
109
+ }
110
+ return a.length - b.length < 0 ? -1 : a.length > b.length ? 1 : 0;
111
+ }
112
+ /**
113
+ * How a metadata value is written in a message: a boolean, an integer or a string as it is, a
114
+ * float and a decimal in their message forms, a list as `[a, b]`, and a temporal value as its
115
+ * `toString` writes it.
116
+ */
117
+ export function messageForm(value) {
118
+ if (Array.isArray(value)) {
119
+ return `[${value.map(messageForm).join(", ")}]`;
120
+ }
121
+ if (typeof value === "object" && value !== null && !hasOwnToString(value)) {
122
+ return JSON.stringify(value);
123
+ }
124
+ return String(value);
125
+ }
126
+ function hasOwnToString(value) {
127
+ return Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null;
128
+ }
package/dist/path.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * A step into the input: a reference token of a JSON Pointer (RFC 6901). It is the name of an
3
+ * object's member or the index of an array's element, written in decimal; which one is for the
4
+ * value it is applied to to say, as RFC 6901 has it, so a path holds the text and nothing more.
5
+ */
6
+ export type Segment = string;
7
+ /**
8
+ * A path into the input, kept as its segments and written as a JSON Pointer only when it is
9
+ * reported, so that a segment is escaped exactly once. A path holds what its pointer writes and
10
+ * no more, so reading back what it writes gives the same path.
11
+ *
12
+ * A path is immutable. Walking down the input makes a child that points at its parent, so a
13
+ * successful decode copies no segments.
14
+ */
15
+ export declare class Path {
16
+ #private;
17
+ /** The input itself. */
18
+ static readonly ROOT: Path;
19
+ private constructor();
20
+ get [Symbol.toStringTag](): string;
21
+ /** The path of the given segments, from the root; an index is its decimal text. */
22
+ static of(...segments: readonly (string | number)[]): Path;
23
+ /**
24
+ * The path a JSON Pointer writes, each reference token unescaped and kept as its text.
25
+ *
26
+ * @throws {SyntaxError} when the text is not a JSON Pointer
27
+ */
28
+ static parse(pointer: string): Path;
29
+ /**
30
+ * The path one step below this one. An index is held as its decimal text, the token a
31
+ * pointer writes for it.
32
+ *
33
+ * @throws {RangeError} for a number that is not an index: negative, fractional, or beyond
34
+ * the integers a JavaScript number holds exactly
35
+ */
36
+ child(segment: string | number): Path;
37
+ /** `relative`, read as starting where this path ends. */
38
+ concat(relative: Path): Path;
39
+ /** Whether this is the input itself. */
40
+ get isRoot(): boolean;
41
+ /** The segments from the root. */
42
+ segments(): Segment[];
43
+ /** Whether the two paths have the same segments. */
44
+ equals(other: Path): boolean;
45
+ /** The path as a JSON Pointer: `""` for the root, `/items/0/name` below it. */
46
+ toString(): string;
47
+ toJSON(): string;
48
+ }
package/dist/path.js ADDED
@@ -0,0 +1,118 @@
1
+ // Where in the input an issue is.
2
+ import { ofThisCopy, tagOf } from "./copy.js";
3
+ /**
4
+ * A path into the input, kept as its segments and written as a JSON Pointer only when it is
5
+ * reported, so that a segment is escaped exactly once. A path holds what its pointer writes and
6
+ * no more, so reading back what it writes gives the same path.
7
+ *
8
+ * A path is immutable. Walking down the input makes a child that points at its parent, so a
9
+ * successful decode copies no segments.
10
+ */
11
+ export class Path {
12
+ /** The input itself. */
13
+ static ROOT = new Path(undefined, undefined);
14
+ #parent;
15
+ #segment;
16
+ constructor(parent, segment) {
17
+ this.#parent = parent;
18
+ this.#segment = segment;
19
+ }
20
+ get [Symbol.toStringTag]() {
21
+ return tagOf("Path");
22
+ }
23
+ /** The path of the given segments, from the root; an index is its decimal text. */
24
+ static of(...segments) {
25
+ let path = Path.ROOT;
26
+ for (const segment of segments) {
27
+ path = path.child(segment);
28
+ }
29
+ return path;
30
+ }
31
+ /**
32
+ * The path a JSON Pointer writes, each reference token unescaped and kept as its text.
33
+ *
34
+ * @throws {SyntaxError} when the text is not a JSON Pointer
35
+ */
36
+ static parse(pointer) {
37
+ if (pointer === "") {
38
+ return Path.ROOT;
39
+ }
40
+ if (!pointer.startsWith("/")) {
41
+ throw new SyntaxError(`${JSON.stringify(pointer)} is not a JSON Pointer`);
42
+ }
43
+ let path = Path.ROOT;
44
+ for (const written of pointer.slice(1).split("/")) {
45
+ if (/~(?![01])/.test(written)) {
46
+ throw new SyntaxError(`${JSON.stringify(pointer)} has a ~ that is not ~0 or ~1`);
47
+ }
48
+ path = path.child(written.replaceAll("~1", "/").replaceAll("~0", "~"));
49
+ }
50
+ return path;
51
+ }
52
+ /**
53
+ * The path one step below this one. An index is held as its decimal text, the token a
54
+ * pointer writes for it.
55
+ *
56
+ * @throws {RangeError} for a number that is not an index: negative, fractional, or beyond
57
+ * the integers a JavaScript number holds exactly
58
+ */
59
+ child(segment) {
60
+ if (typeof segment === "number" && !(Number.isSafeInteger(segment) && segment >= 0)) {
61
+ throw new RangeError(`${segment} is not an array index`);
62
+ }
63
+ return new Path(this, String(segment));
64
+ }
65
+ /** `relative`, read as starting where this path ends. */
66
+ concat(relative) {
67
+ thisCopys(relative);
68
+ let path = this;
69
+ for (const segment of relative.segments()) {
70
+ path = path.child(segment);
71
+ }
72
+ return path;
73
+ }
74
+ /** Whether this is the input itself. */
75
+ get isRoot() {
76
+ return this.#parent === undefined;
77
+ }
78
+ /** The segments from the root. */
79
+ segments() {
80
+ const out = [];
81
+ for (let at = this; at.#parent !== undefined; at = at.#parent) {
82
+ out.push(at.#segment);
83
+ }
84
+ return out.reverse();
85
+ }
86
+ /** Whether the two paths have the same segments. */
87
+ equals(other) {
88
+ thisCopys(other);
89
+ let a = this;
90
+ let b = other;
91
+ while (a !== undefined && b !== undefined) {
92
+ if (a === b) {
93
+ return true;
94
+ }
95
+ if (a.#segment !== b.#segment) {
96
+ return false;
97
+ }
98
+ a = a.#parent;
99
+ b = b.#parent;
100
+ }
101
+ return a === b;
102
+ }
103
+ /** The path as a JSON Pointer: `""` for the root, `/items/0/name` below it. */
104
+ toString() {
105
+ return this.segments()
106
+ .map((segment) => `/${segment.replaceAll("~", "~0").replaceAll("/", "~1")}`)
107
+ .join("");
108
+ }
109
+ toJSON() {
110
+ return this.toString();
111
+ }
112
+ }
113
+ /** `path`, refused where another copy of the library made it. */
114
+ function thisCopys(path) {
115
+ if (!ofThisCopy(path, Path, "Path")) {
116
+ throw new TypeError(`${String(path)} is not a Path`);
117
+ }
118
+ }