@procedures/json 0.0.0-stage → 0.2.3
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 +21 -0
- package/README.md +104 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/diff.d.ts +18 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +66 -0
- package/dist/diff.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/merge.d.ts +14 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +34 -0
- package/dist/merge.js.map +1 -0
- package/dist/module.d.ts +21 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +192 -0
- package/dist/module.js.map +1 -0
- package/dist/patch.d.ts +23 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +137 -0
- package/dist/patch.js.map +1 -0
- package/dist/path.d.ts +38 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +157 -0
- package/dist/path.js.map +1 -0
- package/dist/pointer.d.ts +22 -0
- package/dist/pointer.d.ts.map +1 -0
- package/dist/pointer.js +70 -0
- package/dist/pointer.js.map +1 -0
- package/dist/procedures/json/diff.d.ts +12 -0
- package/dist/procedures/json/diff.d.ts.map +1 -0
- package/dist/procedures/json/diff.js +17 -0
- package/dist/procedures/json/diff.js.map +1 -0
- package/dist/procedures/json/index.d.ts +6 -0
- package/dist/procedures/json/index.d.ts.map +1 -0
- package/dist/procedures/json/index.js +6 -0
- package/dist/procedures/json/index.js.map +1 -0
- package/dist/procedures/json/merge.d.ts +12 -0
- package/dist/procedures/json/merge.d.ts.map +1 -0
- package/dist/procedures/json/merge.js +17 -0
- package/dist/procedures/json/merge.js.map +1 -0
- package/dist/procedures/json/patch.d.ts +12 -0
- package/dist/procedures/json/patch.d.ts.map +1 -0
- package/dist/procedures/json/patch.js +20 -0
- package/dist/procedures/json/patch.js.map +1 -0
- package/dist/procedures/json/query.d.ts +12 -0
- package/dist/procedures/json/query.d.ts.map +1 -0
- package/dist/procedures/json/query.js +28 -0
- package/dist/procedures/json/query.js.map +1 -0
- package/dist/procedures/json/validate.d.ts +12 -0
- package/dist/procedures/json/validate.d.ts.map +1 -0
- package/dist/procedures/json/validate.js +18 -0
- package/dist/procedures/json/validate.js.map +1 -0
- package/dist/procedures/yaml/index.d.ts +3 -0
- package/dist/procedures/yaml/index.d.ts.map +1 -0
- package/dist/procedures/yaml/index.js +3 -0
- package/dist/procedures/yaml/index.js.map +1 -0
- package/dist/procedures/yaml/parse.d.ts +12 -0
- package/dist/procedures/yaml/parse.d.ts.map +1 -0
- package/dist/procedures/yaml/parse.js +14 -0
- package/dist/procedures/yaml/parse.js.map +1 -0
- package/dist/procedures/yaml/stringify.d.ts +11 -0
- package/dist/procedures/yaml/stringify.d.ts.map +1 -0
- package/dist/procedures/yaml/stringify.js +15 -0
- package/dist/procedures/yaml/stringify.js.map +1 -0
- package/dist/register.d.ts +6 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +8 -0
- package/dist/register.js.map +1 -0
- package/dist/schema.d.ts +38 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +92 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/value.d.ts +61 -0
- package/dist/value.d.ts.map +1 -0
- package/dist/value.js +124 -0
- package/dist/value.js.map +1 -0
- package/dist/yaml.d.ts +35 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +111 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +81 -3
- package/src/diff.ts +58 -0
- package/src/index.ts +14 -0
- package/src/merge.ts +34 -0
- package/src/module.ts +236 -0
- package/src/patch.ts +132 -0
- package/src/path.ts +155 -0
- package/src/pointer.ts +70 -0
- package/src/procedures/json/diff.ts +19 -0
- package/src/procedures/json/index.ts +5 -0
- package/src/procedures/json/merge.ts +19 -0
- package/src/procedures/json/patch.ts +21 -0
- package/src/procedures/json/query.ts +29 -0
- package/src/procedures/json/validate.ts +20 -0
- package/src/procedures/yaml/index.ts +2 -0
- package/src/procedures/yaml/parse.ts +16 -0
- package/src/procedures/yaml/stringify.ts +17 -0
- package/src/register.ts +10 -0
- package/src/schema.ts +104 -0
- package/src/types.ts +127 -0
- package/src/value.ts +143 -0
- package/src/yaml.ts +128 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions of the procedures of this package
|
|
3
|
+
*
|
|
4
|
+
* mark procedure new adds the types of each new procedure here. The zod schemas are in
|
|
5
|
+
* `module.ts`: an exported schema needs a written type (isolatedDeclarations).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PatchOperation } from "./patch.js";
|
|
9
|
+
|
|
10
|
+
export type { JsonValue } from "./value.js";
|
|
11
|
+
export type { PatchOperation } from "./patch.js";
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// json.query Types - Find the values at a JSON Pointer or a path in a JSON value
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export interface JsonQueryInput {
|
|
18
|
+
/** The JSON value to search. */
|
|
19
|
+
value: unknown;
|
|
20
|
+
/** A JSON Pointer (RFC 6901): `/items/0/name`. Give `pointer` or `path`. */
|
|
21
|
+
pointer?: string | undefined;
|
|
22
|
+
/** A path query: `items[*].name`, `$..id`. Give `pointer` or `path`. */
|
|
23
|
+
path?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface JsonQueryOutput {
|
|
27
|
+
/** True when one or more values match. */
|
|
28
|
+
found: boolean;
|
|
29
|
+
/** The value of the first match. */
|
|
30
|
+
value?: unknown;
|
|
31
|
+
/** Each match, in document order: its JSON Pointer and its value. */
|
|
32
|
+
matches: Array<{ pointer: string; value?: unknown }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// json.patch Types - Apply an RFC 6902 JSON Patch to a JSON value
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
export interface JsonPatchInput {
|
|
40
|
+
value: unknown;
|
|
41
|
+
/** The operations, in order. */
|
|
42
|
+
patch: PatchOperation[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface JsonPatchOutput {
|
|
46
|
+
/** The value after the patch. */
|
|
47
|
+
value?: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// json.diff Types - Give the RFC 6902 JSON Patch that changes one JSON value into another
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
export interface JsonDiffInput {
|
|
55
|
+
from: unknown;
|
|
56
|
+
to: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface JsonDiffOutput {
|
|
60
|
+
/** The operations that change `from` into `to`. Equal values give an empty list. */
|
|
61
|
+
patch: PatchOperation[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// json.merge Types - Apply an RFC 7386 JSON merge patch to a JSON value
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
export interface JsonMergeInput {
|
|
69
|
+
/** The value to change. */
|
|
70
|
+
target: unknown;
|
|
71
|
+
/** The merge patch: an object of changes, where null removes a key. */
|
|
72
|
+
patch: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface JsonMergeOutput {
|
|
76
|
+
value?: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// json.validate Types - Examine a JSON value against a JSON Schema
|
|
81
|
+
// =============================================================================
|
|
82
|
+
|
|
83
|
+
export interface JsonValidateInput {
|
|
84
|
+
value: unknown;
|
|
85
|
+
/** The JSON Schema: an object or a boolean. */
|
|
86
|
+
schema: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface JsonValidateOutput {
|
|
90
|
+
valid: boolean;
|
|
91
|
+
/** Each problem: the JSON Pointer of the part of the value, and a message. */
|
|
92
|
+
errors: Array<{ pointer: string; message: string }>;
|
|
93
|
+
/** The keywords of the schema that the check does not examine. Empty: the verdict is complete. */
|
|
94
|
+
unchecked: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// yaml.parse Types - Parse YAML text into a JSON value
|
|
99
|
+
// =============================================================================
|
|
100
|
+
|
|
101
|
+
export interface YamlParseInput {
|
|
102
|
+
text: string;
|
|
103
|
+
/** True: the text can have more than one document, and `value` is the list of documents. */
|
|
104
|
+
all?: boolean | undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface YamlParseOutput {
|
|
108
|
+
value?: unknown;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// yaml.stringify Types - Write a JSON value as YAML text
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
export interface YamlStringifyInput {
|
|
116
|
+
value: unknown;
|
|
117
|
+
/** The spaces of one level of indentation: 1 to 8. The default is 2. */
|
|
118
|
+
indent?: number | undefined;
|
|
119
|
+
/** The longest line before a fold. 0: no fold. The default is 80. */
|
|
120
|
+
lineWidth?: number | undefined;
|
|
121
|
+
/** True: the keys of each map in alphabetical order. */
|
|
122
|
+
sortKeys?: boolean | undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface YamlStringifyOutput {
|
|
126
|
+
text: string;
|
|
127
|
+
}
|
package/src/value.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON values: the check, the copy and the comparison. The procedures of this package take and
|
|
3
|
+
* give only JSON values: null, booleans, finite numbers, strings, arrays and plain objects.
|
|
4
|
+
*
|
|
5
|
+
* Each object that this package makes has own data properties only, so a key `__proto__` of the
|
|
6
|
+
* data stays data: it does not change the prototype of the object (roadmap R6).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A JSON value. */
|
|
10
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
11
|
+
|
|
12
|
+
/** The keys that reach a prototype. A pointer, a path or a patch of this package refuses them. */
|
|
13
|
+
export const PROTOTYPE_KEYS: ReadonlySet<string> = new Set(["__proto__", "constructor", "prototype"]);
|
|
14
|
+
|
|
15
|
+
/** The kinds of `JsonError`. */
|
|
16
|
+
export type JsonErrorCode =
|
|
17
|
+
/** A value is not JSON: `undefined`, `NaN`, a function, a date or a class instance. */
|
|
18
|
+
| "NOT_JSON"
|
|
19
|
+
/** A JSON Pointer or a path query is not valid. */
|
|
20
|
+
| "INVALID_POINTER"
|
|
21
|
+
/** A pointer, a path or a patch names `__proto__`, `constructor` or `prototype`. */
|
|
22
|
+
| "PROTOTYPE_KEY"
|
|
23
|
+
/** An operation of a patch is not valid, or its location does not exist. */
|
|
24
|
+
| "INVALID_PATCH"
|
|
25
|
+
/** A `test` operation of a patch found another value. */
|
|
26
|
+
| "TEST_FAILED"
|
|
27
|
+
/** The YAML text is not valid, or it has a value that JSON does not have. */
|
|
28
|
+
| "INVALID_YAML"
|
|
29
|
+
/** The JSON Schema is not a schema. */
|
|
30
|
+
| "INVALID_SCHEMA";
|
|
31
|
+
|
|
32
|
+
/** An error of a procedure of this package. A server sends it with the code `HANDLER_ERROR`. */
|
|
33
|
+
export class JsonError extends Error {
|
|
34
|
+
override readonly name = "JsonError";
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
readonly code: JsonErrorCode,
|
|
38
|
+
message: string,
|
|
39
|
+
) {
|
|
40
|
+
super(message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True for an object whose prototype is `Object.prototype` or null. */
|
|
45
|
+
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
47
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
48
|
+
return prototype === Object.prototype || prototype === null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** This function sets an own data property: a key `__proto__` stays data. */
|
|
52
|
+
export function put(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
53
|
+
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** True when the object has an own property with this key. */
|
|
57
|
+
export function hasOwn(target: object, key: string): boolean {
|
|
58
|
+
return Object.prototype.hasOwnProperty.call(target, key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The text of one reference token of a JSON Pointer (RFC 6901): `~` and `/` are escaped. */
|
|
62
|
+
export function escapeToken(token: string | number): string {
|
|
63
|
+
return String(token).replace(/~/g, "~0").replace(/\//g, "~1");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The JSON Pointer of a list of tokens. */
|
|
67
|
+
export function formatPointer(tokens: readonly (string | number)[]): string {
|
|
68
|
+
return tokens.map((token) => `/${escapeToken(token)}`).join("");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The description of a value that is not JSON, or undefined for a JSON value. */
|
|
72
|
+
function notJson(value: unknown): string | undefined {
|
|
73
|
+
switch (typeof value) {
|
|
74
|
+
case "string":
|
|
75
|
+
case "boolean":
|
|
76
|
+
return undefined;
|
|
77
|
+
case "number":
|
|
78
|
+
return Number.isFinite(value) ? undefined : `the number ${String(value)}`;
|
|
79
|
+
case "object":
|
|
80
|
+
if (value === null || Array.isArray(value) || isPlainObject(value)) return undefined;
|
|
81
|
+
return `an object of the class ${(value as { constructor?: { name?: string } }).constructor?.name ?? "unknown"}`;
|
|
82
|
+
default:
|
|
83
|
+
return `a value of the type ${typeof value}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* This function throws `NOT_JSON` when a part of the value is not JSON. The message has the JSON
|
|
89
|
+
* Pointer of the part. `label` starts the message: the procedure and the field.
|
|
90
|
+
*/
|
|
91
|
+
export function assertJson(value: unknown, label: string): asserts value is JsonValue {
|
|
92
|
+
const stack: Array<{ value: unknown; tokens: (string | number)[] }> = [{ value, tokens: [] }];
|
|
93
|
+
const seen = new Set<object>();
|
|
94
|
+
while (stack.length > 0) {
|
|
95
|
+
const { value: current, tokens } = stack.pop()!;
|
|
96
|
+
const problem = notJson(current);
|
|
97
|
+
if (problem !== undefined) {
|
|
98
|
+
throw new JsonError("NOT_JSON", `${label}: the value at "${formatPointer(tokens)}" is ${problem}, which JSON does not have`);
|
|
99
|
+
}
|
|
100
|
+
if (current === null || typeof current !== "object") continue;
|
|
101
|
+
if (seen.has(current)) throw new JsonError("NOT_JSON", `${label}: the value at "${formatPointer(tokens)}" refers to itself`);
|
|
102
|
+
seen.add(current);
|
|
103
|
+
if (Array.isArray(current)) {
|
|
104
|
+
for (let index = current.length - 1; index >= 0; index--) stack.push({ value: current[index], tokens: [...tokens, index] });
|
|
105
|
+
} else {
|
|
106
|
+
for (const key of Object.keys(current)) stack.push({ value: (current as Record<string, unknown>)[key], tokens: [...tokens, key] });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A deep copy of a JSON value. Each object of the copy has own data properties only. */
|
|
112
|
+
export function cloneJson<T extends JsonValue>(value: T): T;
|
|
113
|
+
export function cloneJson(value: JsonValue): JsonValue {
|
|
114
|
+
if (value === null || typeof value !== "object") return value;
|
|
115
|
+
if (Array.isArray(value)) return value.map((item) => cloneJson(item));
|
|
116
|
+
const copy: Record<string, JsonValue> = {};
|
|
117
|
+
for (const key of Object.keys(value)) put(copy, key, cloneJson(value[key]!));
|
|
118
|
+
return copy;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* True when two JSON values are equal: the same type and value, arrays with equal items in the
|
|
123
|
+
* same order, and objects with the same keys and equal values in any order. The number 0 and -0
|
|
124
|
+
* are equal, as in JSON.
|
|
125
|
+
*/
|
|
126
|
+
export function jsonEqual(a: JsonValue, b: JsonValue): boolean {
|
|
127
|
+
if (a === b) return true;
|
|
128
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
129
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
130
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => jsonEqual(item, b[index]!));
|
|
131
|
+
}
|
|
132
|
+
const keysA = Object.keys(a);
|
|
133
|
+
if (keysA.length !== Object.keys(b).length) return false;
|
|
134
|
+
return keysA.every((key) => hasOwn(b, key) && jsonEqual(a[key]!, b[key]!));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The JSON type of a value, for a message. */
|
|
138
|
+
export function typeName(value: JsonValue | undefined): string {
|
|
139
|
+
if (value === undefined) return "nothing";
|
|
140
|
+
if (value === null) return "null";
|
|
141
|
+
if (Array.isArray(value)) return "an array";
|
|
142
|
+
return typeof value === "object" ? "an object" : `a ${typeof value}`;
|
|
143
|
+
}
|
package/src/yaml.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML to JSON and back, with the `yaml` package (YAML 1.2, the core schema).
|
|
3
|
+
*
|
|
4
|
+
* - The parse gives only JSON values. A value that JSON does not have (`.inf`, `.nan`) is an
|
|
5
|
+
* error. The explicit YAML 1.1 tags (`!!binary`, `!!timestamp`, `!!set`) give their text.
|
|
6
|
+
* - A key of a map that is a number, a boolean or null becomes its text. A key that is a map or a
|
|
7
|
+
* list is an error.
|
|
8
|
+
* - The parse refuses more than 100 aliases (a resource attack) and a duplicate key.
|
|
9
|
+
* - Each object of the result has own data properties only, so a key `__proto__` stays data.
|
|
10
|
+
* - The text of a value is YAML 1.2 that a YAML 1.1 reader reads as the same value.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Document, parseAllDocuments, parseDocument, Scalar, Schema, visit } from "yaml";
|
|
14
|
+
import { formatPointer, JsonError, put, type JsonValue } from "./value.js";
|
|
15
|
+
|
|
16
|
+
/** The options of the parse. */
|
|
17
|
+
export interface YamlParseOptions {
|
|
18
|
+
/** True: the text can have more than one document, and the result is the list of documents. */
|
|
19
|
+
readonly all?: boolean | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The options of the text. */
|
|
23
|
+
export interface YamlStringifyOptions {
|
|
24
|
+
/** The spaces of one level of indentation: 1 to 8. The default is 2. */
|
|
25
|
+
readonly indent?: number | undefined;
|
|
26
|
+
/** The longest line before a fold. 0: no fold. The default is 80. */
|
|
27
|
+
readonly lineWidth?: number | undefined;
|
|
28
|
+
/** True: the keys of each map in alphabetical order. The default keeps the order of the value. */
|
|
29
|
+
readonly sortKeys?: boolean | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The log level "error" keeps the error of a second document, and prints nothing
|
|
33
|
+
const PARSE_OPTIONS = { version: "1.2", schema: "core", resolveKnownTags: false, uniqueKeys: true, logLevel: "error", prettyErrors: true } as const;
|
|
34
|
+
|
|
35
|
+
/** The byte order mark. */
|
|
36
|
+
const BOM = String.fromCharCode(0xfeff);
|
|
37
|
+
|
|
38
|
+
/** The JSON value of a value of the parse. */
|
|
39
|
+
function toJson(value: unknown, tokens: (string | number)[]): JsonValue {
|
|
40
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
41
|
+
if (typeof value === "number") {
|
|
42
|
+
if (!Number.isFinite(value)) {
|
|
43
|
+
throw new JsonError("INVALID_YAML", `yaml.parse: the value at "${formatPointer(tokens)}" is ${String(value)}, which JSON does not have`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(value)) return value.map((item, index) => toJson(item, [...tokens, index]));
|
|
48
|
+
if (value instanceof Map) {
|
|
49
|
+
const out: Record<string, JsonValue> = {};
|
|
50
|
+
for (const [key, item] of value) {
|
|
51
|
+
if (key !== null && typeof key === "object") {
|
|
52
|
+
throw new JsonError("INVALID_YAML", `yaml.parse: a key at "${formatPointer(tokens)}" is a map or a list, which JSON does not have`);
|
|
53
|
+
}
|
|
54
|
+
const name = key === null ? "null" : String(key);
|
|
55
|
+
put(out, name, toJson(item, [...tokens, name]));
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
throw new JsonError("INVALID_YAML", `yaml.parse: the value at "${formatPointer(tokens)}" is not a JSON value`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The JSON value of one document, or the error of its first problem. */
|
|
63
|
+
function documentValue(document: Document.Parsed, index: number | undefined): JsonValue {
|
|
64
|
+
const label = index === undefined ? "yaml.parse" : `yaml.parse, document ${index}`;
|
|
65
|
+
const problem = document.errors[0];
|
|
66
|
+
if (problem) throw new JsonError("INVALID_YAML", `${label}: ${problem.message}`);
|
|
67
|
+
let value: unknown;
|
|
68
|
+
try {
|
|
69
|
+
value = document.toJS({ mapAsMap: true, maxAliasCount: 100 });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new JsonError("INVALID_YAML", `${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
|
+
}
|
|
73
|
+
return toJson(value === undefined ? null : value, []);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** This function parses YAML text into a JSON value. Bad YAML throws `INVALID_YAML`. */
|
|
77
|
+
export function parseYaml(text: string, options: YamlParseOptions = {}): JsonValue {
|
|
78
|
+
if (options.all) return parseAllDocuments(text, PARSE_OPTIONS).map((document, index) => {
|
|
79
|
+
if (!("errors" in document) || !("toJS" in document)) throw new JsonError("INVALID_YAML", "yaml.parse: the text is not YAML");
|
|
80
|
+
return documentValue(document, index);
|
|
81
|
+
});
|
|
82
|
+
return documentValue(parseDocument(text, PARSE_OPTIONS), undefined);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The plain scalars that a YAML 1.1 reader takes as a boolean, a null, a number, a date, a merge
|
|
87
|
+
* key or a value key. YAML 1.2 reads them as strings, so a 1.2 writer does not quote them.
|
|
88
|
+
*/
|
|
89
|
+
const YAML_1_1_TYPES: readonly RegExp[] = [
|
|
90
|
+
/^(?:y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$/,
|
|
91
|
+
/^(?:~|null|Null|NULL)$/,
|
|
92
|
+
/^[-+]?(?:0b[01_]+|0[0-7_]+|0|[1-9][0-9_]*|0x[0-9a-fA-F_]+|[1-9][0-9_]*(?::[0-5]?[0-9])+)$/,
|
|
93
|
+
/^[-+]?(?:(?:[0-9][0-9_]*)?\.[0-9._]*(?:[eE][-+]?[0-9]+)?|[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*|\.(?:inf|Inf|INF))$/,
|
|
94
|
+
/^\.(?:nan|NaN|NAN)$/,
|
|
95
|
+
/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}/,
|
|
96
|
+
/^(?:<<|=)$/,
|
|
97
|
+
// The YAML 1.1 schema of the yaml package reads more forms: "e0" is a float there
|
|
98
|
+
...new Schema({ schema: "yaml-1.1" }).tags.flatMap((tag) =>
|
|
99
|
+
tag.default === true && tag.tag !== "tag:yaml.org,2002:str" && "test" in tag && tag.test instanceof RegExp ? [tag.test] : [],
|
|
100
|
+
),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* This function writes a JSON value as YAML text. The text is YAML 1.2, and a YAML 1.1 reader
|
|
105
|
+
* reads the same value: a string that YAML 1.1 takes as another type (`yes`, `no`, `on`, a
|
|
106
|
+
* date, `0777`, `1_000`) is in double quotes.
|
|
107
|
+
*/
|
|
108
|
+
export function stringifyYaml(value: JsonValue, options: YamlStringifyOptions = {}): string {
|
|
109
|
+
const document = new Document(value, {
|
|
110
|
+
version: "1.2",
|
|
111
|
+
schema: "core",
|
|
112
|
+
sortMapEntries: options.sortKeys ?? false,
|
|
113
|
+
aliasDuplicateObjects: false,
|
|
114
|
+
});
|
|
115
|
+
visit(document, {
|
|
116
|
+
Scalar(_key, node) {
|
|
117
|
+
const text = node.value;
|
|
118
|
+
if (typeof text !== "string") return;
|
|
119
|
+
// A byte order mark at the start of a line is removed by a parser: it must be an escape.
|
|
120
|
+
// yaml 2.9.1 writes a block scalar whose first line has only spaces wrongly: " \n" came
|
|
121
|
+
// back as "\n", and " \nb" was not valid YAML. Double quotes keep such a text.
|
|
122
|
+
const blockRisk = /[\n\r]/.test(text) && /(^|[\n\r])[ \t]/.test(text);
|
|
123
|
+
if (blockRisk || text.includes(BOM) || YAML_1_1_TYPES.some((type) => type.test(text))) node.type = Scalar.QUOTE_DOUBLE;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
// Each byte order mark is in a string in double quotes, where the escape has the same value
|
|
127
|
+
return document.toString({ indent: options.indent ?? 2, lineWidth: options.lineWidth ?? 80 }).replaceAll(BOM, "\\uFEFF");
|
|
128
|
+
}
|