@cofold/yaml 0.2.0 → 0.3.0
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/README.md +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/stringify.d.ts +3 -0
- package/dist/stringify.d.ts.map +1 -0
- package/dist/stringify.js +138 -0
- package/dist/stringify.js.map +1 -0
- package/dist/yaml.js +4 -2
- package/dist/yaml.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +6 -5
- package/src/load.ts +78 -78
- package/src/stringify.ts +155 -0
- package/src/types/load.ts +11 -11
- package/src/types/parse.ts +8 -8
- package/src/yaml.ts +465 -463
package/src/stringify.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { YamlError } from "./yaml.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The writer of the subset `parseYaml` reads: block mappings and sequences,
|
|
5
|
+
* `{}` and `[]` for empty collections, literal block scalars for multi-line
|
|
6
|
+
* text, and plain or double-quoted scalars. What it writes reads back as the
|
|
7
|
+
* same value.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A literal block scalar's header and its content lines, before indentation. */
|
|
11
|
+
interface Literal {
|
|
12
|
+
header: string;
|
|
13
|
+
lines: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** What a plain scalar may not start with: a YAML indicator or a quote. */
|
|
17
|
+
const INDICATORS = "-?:,[]{}#&*!|>'\"%@`";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Plain text that `scalar`, or another reader of the YAML 1.2 core schema,
|
|
21
|
+
* reads as null, a boolean or a number.
|
|
22
|
+
*/
|
|
23
|
+
const TYPED = /^(?:~|null|Null|NULL|true|True|TRUE|false|False|FALSE|[-+]?\d+|0o[0-7]+|0x[\da-fA-F]+|[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$/u;
|
|
24
|
+
|
|
25
|
+
/** Characters plain text may not hold: controls, line separators, lone surrogates, and flow brackets. */
|
|
26
|
+
const UNPLAIN = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\ud800-\udfff[\]{}]|:\s|:$|\s#|[:-]\s+[|>][1-9+-]*$/u;
|
|
27
|
+
|
|
28
|
+
/** Characters a key may not hold plain, because `keyAt` reads them as quotes or flow brackets. */
|
|
29
|
+
const UNPLAIN_KEY = /["',]/u;
|
|
30
|
+
|
|
31
|
+
/** Characters a literal block scalar cannot carry through `blockScalars`. */
|
|
32
|
+
const UNLITERAL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u2028\u2029\ud800-\udfff]/u;
|
|
33
|
+
|
|
34
|
+
/** Characters a double-quoted scalar writes as an escape. */
|
|
35
|
+
const ESCAPED = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\ud800-\udfff"\\]/gu;
|
|
36
|
+
|
|
37
|
+
const ESCAPES: Readonly<Record<string, string>> = { "\n": "\\n", "\t": "\\t", "\r": "\\r", '"': '\\"', "\\": "\\\\" };
|
|
38
|
+
|
|
39
|
+
/** The value as YAML text that `parseYaml` reads back as the same value. */
|
|
40
|
+
export function stringifyYaml(value: unknown): string {
|
|
41
|
+
const out: string[] = [];
|
|
42
|
+
if (Array.isArray(value) && value.length > 0) sequence(value, 0, out);
|
|
43
|
+
else if (isObject(value) && entries(value).length > 0) mapping(value, 0, out, "");
|
|
44
|
+
else out.push(inline(value, out));
|
|
45
|
+
return `${out.join("\n")}\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
49
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The keys JSON writes: own enumerable keys whose value is not `undefined`. */
|
|
53
|
+
function entries(value: Record<string, unknown>): string[] {
|
|
54
|
+
return Object.keys(value).filter((key) => value[key] !== undefined);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A block sequence whose dashes sit at `indent`. */
|
|
58
|
+
function sequence(items: unknown[], indent: number, out: string[]): void {
|
|
59
|
+
const pad = " ".repeat(indent);
|
|
60
|
+
for (let at = 0; at < items.length; at += 1) {
|
|
61
|
+
const item = items[at];
|
|
62
|
+
/*
|
|
63
|
+
* A mapping opens on the dash's own line unless its first value is a
|
|
64
|
+
* literal block scalar, whose content `blockScalars` measures from the dash.
|
|
65
|
+
*/
|
|
66
|
+
if (isObject(item)) {
|
|
67
|
+
const keys = entries(item);
|
|
68
|
+
if (keys.length > 0 && literalOf(item[keys[0]!]) === undefined) {
|
|
69
|
+
mapping(item, indent + 2, out, `${pad}- `);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
entry(`${pad}-`, item, indent, out);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A block mapping whose keys sit at `indent`; the first key's line starts with `first` when it is not empty. */
|
|
78
|
+
function mapping(value: Record<string, unknown>, indent: number, out: string[], first: string): void {
|
|
79
|
+
const pad = " ".repeat(indent);
|
|
80
|
+
entries(value).forEach((key, at) => {
|
|
81
|
+
entry(`${at === 0 && first !== "" ? first : pad}${keyOf(key)}:`, value[key], indent, out);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** One entry: `lead` is the dash or the key with its colon, already indented to `indent`. */
|
|
86
|
+
function entry(lead: string, value: unknown, indent: number, out: string[]): void {
|
|
87
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
88
|
+
out.push(lead);
|
|
89
|
+
sequence(value, indent + 2, out);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (isObject(value) && entries(value).length > 0) {
|
|
93
|
+
out.push(lead);
|
|
94
|
+
mapping(value, indent + 2, out, "");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const literal = literalOf(value);
|
|
98
|
+
if (literal !== undefined) {
|
|
99
|
+
const pad = " ".repeat(indent + 2);
|
|
100
|
+
out.push(`${lead} ${literal.header}`);
|
|
101
|
+
for (const line of literal.lines) out.push(line === "" ? "" : pad + line);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
out.push(`${lead} ${inline(value, out)}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A value on one line: a scalar, or an empty collection. */
|
|
108
|
+
function inline(value: unknown, out: string[]): string {
|
|
109
|
+
if (value === null) return "null";
|
|
110
|
+
if (typeof value === "boolean") return String(value);
|
|
111
|
+
if (typeof value === "number") {
|
|
112
|
+
if (!Number.isFinite(value)) throw new YamlError(`${value} is not a JSON value`, out.length + 1);
|
|
113
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
114
|
+
}
|
|
115
|
+
if (typeof value === "string") return plain(value) ? value : quoted(value);
|
|
116
|
+
if (Array.isArray(value)) return "[]";
|
|
117
|
+
if (isObject(value)) return "{}";
|
|
118
|
+
throw new YamlError(`${value === undefined ? "undefined" : `A ${typeof value}`} is not a JSON value`, out.length + 1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Text `scalar` reads back unquoted as the same text. */
|
|
122
|
+
function plain(text: string): boolean {
|
|
123
|
+
return text !== ""
|
|
124
|
+
&& text !== "..."
|
|
125
|
+
&& text === text.trim()
|
|
126
|
+
&& !INDICATORS.includes(text[0]!)
|
|
127
|
+
&& !UNPLAIN.test(text)
|
|
128
|
+
&& !TYPED.test(text);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A key as written: plain when it can be, and quoted otherwise, as `<<` always is, because plain it is a merge key. */
|
|
132
|
+
function keyOf(key: string): string {
|
|
133
|
+
return key !== "<<" && plain(key) && !UNPLAIN_KEY.test(key) ? key : quoted(key);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function quoted(text: string): string {
|
|
137
|
+
return `"${text.replace(ESCAPED, (char) => ESCAPES[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`)}"`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Multi-line text as a literal block scalar, or `undefined` when the block
|
|
142
|
+
* cannot carry it: a line of only spaces, which `blockScalars` reads as empty,
|
|
143
|
+
* no line with text, or a character a quoted scalar has to escape.
|
|
144
|
+
*/
|
|
145
|
+
function literalOf(value: unknown): Literal | undefined {
|
|
146
|
+
if (typeof value !== "string" || !value.includes("\n") || UNLITERAL.test(value)) return undefined;
|
|
147
|
+
const trailing = /\n*$/u.exec(value)![0].length;
|
|
148
|
+
const lines = (trailing > 0 ? value.slice(0, -1) : value).split("\n");
|
|
149
|
+
if (lines.some((line) => line !== "" && line.trim() === "")) return undefined;
|
|
150
|
+
const text = lines.find((line) => line !== "");
|
|
151
|
+
if (text === undefined) return undefined;
|
|
152
|
+
const indicator = /^\s/u.test(text) ? "2" : "";
|
|
153
|
+
const chomping = trailing === 0 ? "-" : trailing === 1 ? "" : "+";
|
|
154
|
+
return { header: `|${indicator}${chomping}`, lines };
|
|
155
|
+
}
|
package/src/types/load.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { YamlParseOptions } from "./parse.js";
|
|
2
|
-
|
|
3
|
-
export interface YamlLoadOptions extends YamlParseOptions {
|
|
4
|
-
refs?: { local?: boolean; remote?: boolean; circular?: "error" | "preserve"; unresolved?: "error" | "preserve" };
|
|
5
|
-
readFile?(path: string): Promise<string> | string;
|
|
6
|
-
fetch?: typeof globalThis.fetch;
|
|
7
|
-
timeoutMs?: number;
|
|
8
|
-
maxDocuments?: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface LoadedYaml { data: unknown; sources: string[]; diagnostics: { reference: string; message: string }[] }
|
|
1
|
+
import type { YamlParseOptions } from "./parse.js";
|
|
2
|
+
|
|
3
|
+
export interface YamlLoadOptions extends YamlParseOptions {
|
|
4
|
+
refs?: { local?: boolean; remote?: boolean; circular?: "error" | "preserve"; unresolved?: "error" | "preserve" };
|
|
5
|
+
readFile?(path: string): Promise<string> | string;
|
|
6
|
+
fetch?: typeof globalThis.fetch;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
maxDocuments?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LoadedYaml { data: unknown; sources: string[]; diagnostics: { reference: string; message: string }[] }
|
package/src/types/parse.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export interface YamlParseOptions {
|
|
2
|
-
/** A filename or URL used in diagnostics; the parser does not read it. */
|
|
3
|
-
source?: string;
|
|
4
|
-
/** Bound input size before parsing. Default: 16 MiB. */
|
|
5
|
-
maxBytes?: number;
|
|
6
|
-
/** Bound nesting before recursive parsing. Default: 128. */
|
|
7
|
-
maxDepth?: number;
|
|
8
|
-
}
|
|
1
|
+
export interface YamlParseOptions {
|
|
2
|
+
/** A filename or URL used in diagnostics; the parser does not read it. */
|
|
3
|
+
source?: string;
|
|
4
|
+
/** Bound input size before parsing. Default: 16 MiB. */
|
|
5
|
+
maxBytes?: number;
|
|
6
|
+
/** Bound nesting before recursive parsing. Default: 128. */
|
|
7
|
+
maxDepth?: number;
|
|
8
|
+
}
|