@effected/yaml 0.1.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/LICENSE +21 -0
- package/README.md +111 -0
- package/Yaml.js +414 -0
- package/YamlDiagnostic.js +126 -0
- package/YamlDocument.js +181 -0
- package/YamlEdit.js +45 -0
- package/YamlFormat.js +309 -0
- package/YamlNode.js +396 -0
- package/YamlVisitor.js +172 -0
- package/index.d.ts +1331 -0
- package/index.js +9 -0
- package/internal/composer/anchors.js +97 -0
- package/internal/composer/block.js +1155 -0
- package/internal/composer/document.js +677 -0
- package/internal/composer/flow.js +400 -0
- package/internal/composer/scalars.js +885 -0
- package/internal/composer/state.js +117 -0
- package/internal/composer/tags.js +88 -0
- package/internal/cst-parser.js +716 -0
- package/internal/diagnostics.js +80 -0
- package/internal/diff.js +56 -0
- package/internal/fold.js +153 -0
- package/internal/lexer.js +959 -0
- package/internal/stringifier.js +797 -0
- package/package.json +47 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/internal/diagnostics.ts
|
|
2
|
+
/** Error codes emitted by the lexer stage. */
|
|
3
|
+
const YAML_LEX_ERROR_CODES = [
|
|
4
|
+
"UnexpectedCharacter",
|
|
5
|
+
"UnterminatedString",
|
|
6
|
+
"InvalidEscapeSequence",
|
|
7
|
+
"InvalidUnicode",
|
|
8
|
+
"UnterminatedBlockScalar",
|
|
9
|
+
"UnterminatedFlowCollection",
|
|
10
|
+
"InvalidDirective",
|
|
11
|
+
"InvalidTagHandle",
|
|
12
|
+
"InvalidAnchorName",
|
|
13
|
+
"UnexpectedByteOrderMark"
|
|
14
|
+
];
|
|
15
|
+
/** Error codes emitted by the CST-parser stage. */
|
|
16
|
+
const YAML_PARSE_ERROR_CODES = [
|
|
17
|
+
"InvalidIndentation",
|
|
18
|
+
"DuplicateKey",
|
|
19
|
+
"UnexpectedToken",
|
|
20
|
+
"MissingValue",
|
|
21
|
+
"MissingKey",
|
|
22
|
+
"TabIndentation",
|
|
23
|
+
"InvalidBlockStructure",
|
|
24
|
+
"MalformedFlowCollection",
|
|
25
|
+
"NestingDepthExceeded"
|
|
26
|
+
];
|
|
27
|
+
/** Error codes emitted by the composer stage. */
|
|
28
|
+
const YAML_COMPOSE_ERROR_CODES = [
|
|
29
|
+
"UndefinedAlias",
|
|
30
|
+
"DuplicateAnchor",
|
|
31
|
+
"CircularAlias",
|
|
32
|
+
"UnresolvedTag",
|
|
33
|
+
"InvalidTagValue",
|
|
34
|
+
"AliasCountExceeded",
|
|
35
|
+
"InvalidDirective"
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Error codes for the stringifier stage. The engine's only deliberate
|
|
39
|
+
* stringify failure is the circular-reference guard (thrown as
|
|
40
|
+
* `StringifyFailure`); the facade materializes it under this code so
|
|
41
|
+
* `YamlStringifyError` carries structured diagnostics rather than a
|
|
42
|
+
* `reason` string.
|
|
43
|
+
*/
|
|
44
|
+
const YAML_STRINGIFY_ERROR_CODES = ["CircularReference"];
|
|
45
|
+
/**
|
|
46
|
+
* Error codes for the modify stage (`YamlFormat.modify`'s path navigation
|
|
47
|
+
* against an already-composed AST). Not raised by the parser/composer.
|
|
48
|
+
*/
|
|
49
|
+
const YAML_MODIFY_ERROR_CODES = [
|
|
50
|
+
"EmptyDocument",
|
|
51
|
+
"PathNotFound",
|
|
52
|
+
"InvalidIndex",
|
|
53
|
+
"NotNavigable"
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* The single source of truth for which diagnostic codes are fatal to a
|
|
57
|
+
* parse (vs. recoverable warnings-as-data). Replaces the v3 source's three
|
|
58
|
+
* subtly-differing inline fatal lists with their union: fatality is a
|
|
59
|
+
* property of the code, declared once.
|
|
60
|
+
*/
|
|
61
|
+
const FATAL_CODES = /* @__PURE__ */ new Set([
|
|
62
|
+
"UndefinedAlias",
|
|
63
|
+
"DuplicateAnchor",
|
|
64
|
+
"AliasCountExceeded",
|
|
65
|
+
"UnexpectedToken",
|
|
66
|
+
"InvalidDirective",
|
|
67
|
+
"MalformedFlowCollection",
|
|
68
|
+
"InvalidIndentation",
|
|
69
|
+
"TabIndentation",
|
|
70
|
+
"UnresolvedTag",
|
|
71
|
+
"UnexpectedCharacter",
|
|
72
|
+
"NestingDepthExceeded"
|
|
73
|
+
]);
|
|
74
|
+
/** Whether `code` is fatal to a parse. */
|
|
75
|
+
function isFatalCode(code) {
|
|
76
|
+
return FATAL_CODES.has(code);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { FATAL_CODES, YAML_COMPOSE_ERROR_CODES, YAML_LEX_ERROR_CODES, YAML_MODIFY_ERROR_CODES, YAML_PARSE_ERROR_CODES, YAML_STRINGIFY_ERROR_CODES, isFatalCode };
|
package/internal/diff.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/internal/diff.ts
|
|
2
|
+
/**
|
|
3
|
+
* Compute edits by diffing two strings character by character.
|
|
4
|
+
*
|
|
5
|
+
* Walks both strings from each end inward to find the common prefix and
|
|
6
|
+
* suffix, then emits a single edit covering the changed region in the
|
|
7
|
+
* middle. This is sufficient because both strings derive from the same AST
|
|
8
|
+
* and share structural skeleton — typically only whitespace and values differ.
|
|
9
|
+
*
|
|
10
|
+
* For more granular edits (multiple disjoint changes), a line-level pass
|
|
11
|
+
* splits the middle region into per-line edits when possible.
|
|
12
|
+
*
|
|
13
|
+
* This relies on the assumption that both strings share an identical
|
|
14
|
+
* structural skeleton (they were produced from the same AST); a simple
|
|
15
|
+
* prefix/suffix match is sufficient and a full Myers diff is unnecessary.
|
|
16
|
+
*/
|
|
17
|
+
function computeEdits(original, modified) {
|
|
18
|
+
if (original === modified) return [];
|
|
19
|
+
let prefixLen = 0;
|
|
20
|
+
const minLen = Math.min(original.length, modified.length);
|
|
21
|
+
while (prefixLen < minLen && original[prefixLen] === modified[prefixLen]) prefixLen++;
|
|
22
|
+
let suffixLen = 0;
|
|
23
|
+
while (suffixLen < minLen - prefixLen && original[original.length - 1 - suffixLen] === modified[modified.length - 1 - suffixLen]) suffixLen++;
|
|
24
|
+
const origStart = prefixLen;
|
|
25
|
+
const origEnd = original.length - suffixLen;
|
|
26
|
+
const modStart = prefixLen;
|
|
27
|
+
const modEnd = modified.length - suffixLen;
|
|
28
|
+
if (origStart >= origEnd && modStart >= modEnd) return [];
|
|
29
|
+
const origMiddle = original.substring(origStart, origEnd);
|
|
30
|
+
const modMiddle = modified.substring(modStart, modEnd);
|
|
31
|
+
const origLines = origMiddle.split("\n");
|
|
32
|
+
const modLines = modMiddle.split("\n");
|
|
33
|
+
if (origLines.length === modLines.length && origLines.length > 1) {
|
|
34
|
+
const edits = [];
|
|
35
|
+
let offset = origStart;
|
|
36
|
+
for (let i = 0; i < origLines.length; i++) {
|
|
37
|
+
const origLine = origLines[i] ?? "";
|
|
38
|
+
const modLine = modLines[i] ?? "";
|
|
39
|
+
if (origLine !== modLine) edits.push({
|
|
40
|
+
offset,
|
|
41
|
+
length: origLine.length,
|
|
42
|
+
content: modLine
|
|
43
|
+
});
|
|
44
|
+
offset += origLine.length + 1;
|
|
45
|
+
}
|
|
46
|
+
return edits;
|
|
47
|
+
}
|
|
48
|
+
return [{
|
|
49
|
+
offset: origStart,
|
|
50
|
+
length: origEnd - origStart,
|
|
51
|
+
content: modified.substring(modStart, modEnd)
|
|
52
|
+
}];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { computeEdits };
|
package/internal/fold.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
//#region src/internal/fold.ts
|
|
2
|
+
/**
|
|
3
|
+
* C0 control characters (except TAB) that must be escaped in double-quoted scalars.
|
|
4
|
+
*/
|
|
5
|
+
function isControlChar(code) {
|
|
6
|
+
return code >= 0 && code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Returns true when the value has whitespace immediately before a newline AND
|
|
10
|
+
* the value contains a non-trailing newline. Equivalent to the regex pair
|
|
11
|
+
* `/[\t ]\n/.test(s) && s.replace(/\n+$/, "").includes("\n")` but uses linear
|
|
12
|
+
* imperative scans to avoid polynomial-time regex behaviour on adversarial
|
|
13
|
+
* inputs containing many trailing newlines.
|
|
14
|
+
*/
|
|
15
|
+
function hasInteriorTrailingWhitespace(s) {
|
|
16
|
+
let firstWsBeforeNl = -1;
|
|
17
|
+
for (let i = 1; i < s.length; i++) if (s.charCodeAt(i) === 10) {
|
|
18
|
+
const prev = s.charCodeAt(i - 1);
|
|
19
|
+
if (prev === 32 || prev === 9) {
|
|
20
|
+
firstWsBeforeNl = i;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (firstWsBeforeNl < 0) return false;
|
|
25
|
+
let trailingStart = s.length;
|
|
26
|
+
while (trailingStart > 0 && s.charCodeAt(trailingStart - 1) === 10) trailingStart--;
|
|
27
|
+
for (let i = 0; i < trailingStart; i++) if (s.charCodeAt(i) === 10) return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns true when the value contains a newline followed by one or more
|
|
32
|
+
* spaces and then a tab — mixed leading whitespace on a continuation line
|
|
33
|
+
* that block style cannot represent unambiguously.
|
|
34
|
+
*/
|
|
35
|
+
function hasNewlineSpacesTab(s) {
|
|
36
|
+
for (let i = 0; i < s.length - 2; i++) {
|
|
37
|
+
if (s.charCodeAt(i) !== 10) continue;
|
|
38
|
+
let j = i + 1;
|
|
39
|
+
while (j < s.length && s.charCodeAt(j) === 32) j++;
|
|
40
|
+
if (j > i + 1 && j < s.length && s.charCodeAt(j) === 9) return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Renders a multi-line value as a single-quoted scalar with proper fold encoding.
|
|
46
|
+
*
|
|
47
|
+
* Single-quoted scalars use line folding rules (YAML 1.2 §7.4): bare newlines
|
|
48
|
+
* between non-empty lines fold to a space; empty lines preserve as literal
|
|
49
|
+
* newlines. To round-trip a value with N consecutive literal newlines, the
|
|
50
|
+
* source needs N+1 consecutive source newlines (i.e., one extra to account
|
|
51
|
+
* for the bare newline that would otherwise fold to a space).
|
|
52
|
+
*
|
|
53
|
+
* Continuation lines are prefixed with the given indent. Leading whitespace
|
|
54
|
+
* on continuation lines after the indent is preserved as part of the content
|
|
55
|
+
* because empty lines precede them, suppressing the fold-to-space rule.
|
|
56
|
+
*
|
|
57
|
+
* Returns null if the content cannot safely be represented as single-quoted
|
|
58
|
+
* (carriage returns or non-tab control characters).
|
|
59
|
+
*/
|
|
60
|
+
function renderSingleQuotedMultiline(s, indent) {
|
|
61
|
+
for (let i = 0; i < s.length; i++) {
|
|
62
|
+
const code = s.charCodeAt(i);
|
|
63
|
+
if (code === 13 || isControlChar(code)) return null;
|
|
64
|
+
}
|
|
65
|
+
const escaped = s.replace(/'/g, "''");
|
|
66
|
+
let result = "";
|
|
67
|
+
let i = 0;
|
|
68
|
+
let firstSegment = true;
|
|
69
|
+
while (i < escaped.length) {
|
|
70
|
+
let segEnd = i;
|
|
71
|
+
while (segEnd < escaped.length && escaped[segEnd] !== "\n") segEnd++;
|
|
72
|
+
const segment = escaped.slice(i, segEnd);
|
|
73
|
+
if (firstSegment) {
|
|
74
|
+
result += segment;
|
|
75
|
+
firstSegment = false;
|
|
76
|
+
} else result += `${indent}${segment}`;
|
|
77
|
+
i = segEnd;
|
|
78
|
+
let nlEnd = i;
|
|
79
|
+
while (nlEnd < escaped.length && escaped[nlEnd] === "\n") nlEnd++;
|
|
80
|
+
const nlCount = nlEnd - i;
|
|
81
|
+
if (nlCount > 0) result += "\n".repeat(nlCount + 1);
|
|
82
|
+
i = nlEnd;
|
|
83
|
+
}
|
|
84
|
+
return `'${result}'`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Renders a string scalar using block literal style (pipe `|`).
|
|
88
|
+
*
|
|
89
|
+
* @param explicitChomp - Original chomp indicator from the AST, when known.
|
|
90
|
+
* `keep` (`+`) and `strip` (`-`) preserve trailing-newline semantics that
|
|
91
|
+
* cannot be inferred from the resolved value alone.
|
|
92
|
+
*/
|
|
93
|
+
function renderBlockLiteral(s, indent, explicitChomp, parentPosition) {
|
|
94
|
+
let chomp = "";
|
|
95
|
+
const onlyNewlines = s.length > 0 && /^\n+$/.test(s);
|
|
96
|
+
if (s.endsWith("\n\n") || onlyNewlines && explicitChomp === "keep") chomp = "+";
|
|
97
|
+
else if (!s.endsWith("\n")) chomp = "-";
|
|
98
|
+
const lines = s.split("\n");
|
|
99
|
+
if (s.endsWith("\n")) lines.pop();
|
|
100
|
+
let indentIndicator = "";
|
|
101
|
+
const firstContent = lines.find((l) => l !== "");
|
|
102
|
+
const hasContent = firstContent !== void 0;
|
|
103
|
+
if (firstContent?.startsWith(" ") || lines.length > 0 && lines[0] === "" && hasContent) indentIndicator = String(indent.length);
|
|
104
|
+
else if (!hasContent && chomp === "+" && parentPosition === "block-map-value" && indent.length > 0) indentIndicator = String(indent.length);
|
|
105
|
+
return `|${indentIndicator}${chomp}\n${lines.map((l) => l === "" ? "" : `${indent}${l}`).join("\n")}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Renders a string scalar using block folded style (greater-than `>`).
|
|
109
|
+
*
|
|
110
|
+
* In folded block scalars, a single newline between content lines is folded
|
|
111
|
+
* into a space by the reader. To preserve a literal newline in the value,
|
|
112
|
+
* the output must contain an empty line (double newline). Each empty line
|
|
113
|
+
* in the value already produces the correct number of blank lines.
|
|
114
|
+
*/
|
|
115
|
+
function renderBlockFolded(s, indent) {
|
|
116
|
+
let chomp = "";
|
|
117
|
+
if (s.endsWith("\n\n")) chomp = "+";
|
|
118
|
+
else if (!s.endsWith("\n")) chomp = "-";
|
|
119
|
+
const valueLines = s.split("\n");
|
|
120
|
+
if (s.endsWith("\n")) valueLines.pop();
|
|
121
|
+
let indentIndicator = "";
|
|
122
|
+
const firstContent = valueLines.find((l) => l !== "");
|
|
123
|
+
if (firstContent?.startsWith(" ") || valueLines.length >= 2 && valueLines[0] === "" && valueLines[1] === "" && firstContent !== void 0) indentIndicator = String(indent.length);
|
|
124
|
+
const outputLines = [];
|
|
125
|
+
let prevNonEmpty = false;
|
|
126
|
+
let prevMoreIndented = false;
|
|
127
|
+
for (let i = 0; i < valueLines.length; i++) {
|
|
128
|
+
const line = valueLines[i];
|
|
129
|
+
if (line === "") {
|
|
130
|
+
if (prevNonEmpty && !prevMoreIndented) {
|
|
131
|
+
let nextContentMoreIndented = false;
|
|
132
|
+
for (let j = i + 1; j < valueLines.length; j++) if (valueLines[j] !== "") {
|
|
133
|
+
nextContentMoreIndented = valueLines[j].startsWith(" ") || valueLines[j].startsWith(" ");
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if (!nextContentMoreIndented) outputLines.push("");
|
|
137
|
+
}
|
|
138
|
+
outputLines.push("");
|
|
139
|
+
prevNonEmpty = false;
|
|
140
|
+
prevMoreIndented = false;
|
|
141
|
+
} else {
|
|
142
|
+
const isMoreIndented = line.startsWith(" ") || line.startsWith(" ");
|
|
143
|
+
if (prevNonEmpty && !isMoreIndented) outputLines.push("");
|
|
144
|
+
outputLines.push(`${indent}${line}`);
|
|
145
|
+
prevNonEmpty = true;
|
|
146
|
+
prevMoreIndented = isMoreIndented;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return `>${indentIndicator}${chomp}\n${outputLines.join("\n")}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
//#endregion
|
|
153
|
+
export { hasInteriorTrailingWhitespace, hasNewlineSpacesTab, isControlChar, renderBlockFolded, renderBlockLiteral, renderSingleQuotedMultiline };
|