@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.
@@ -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 };
@@ -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 };
@@ -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 };