@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,117 @@
|
|
|
1
|
+
//#region src/internal/composer/state.ts
|
|
2
|
+
let lineStartsText;
|
|
3
|
+
let lineStartsCache = [];
|
|
4
|
+
function getLineStarts(text) {
|
|
5
|
+
if (lineStartsText === text) return lineStartsCache;
|
|
6
|
+
const starts = [0];
|
|
7
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
|
|
8
|
+
lineStartsText = text;
|
|
9
|
+
lineStartsCache = starts;
|
|
10
|
+
return starts;
|
|
11
|
+
}
|
|
12
|
+
function lineCol(text, offset) {
|
|
13
|
+
const starts = getLineStarts(text);
|
|
14
|
+
const pos = Math.min(Math.max(offset, 0), text.length);
|
|
15
|
+
let lo = 0;
|
|
16
|
+
let hi = starts.length - 1;
|
|
17
|
+
while (lo < hi) {
|
|
18
|
+
const mid = lo + hi + 1 >> 1;
|
|
19
|
+
if (starts[mid] <= pos) lo = mid;
|
|
20
|
+
else hi = mid - 1;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
line: lo,
|
|
24
|
+
column: pos - starts[lo]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns true if offsetA and offsetB are on the same source line (no newline between them).
|
|
29
|
+
*/
|
|
30
|
+
function sameLine(text, offsetA, offsetB) {
|
|
31
|
+
const lo = Math.min(offsetA, offsetB);
|
|
32
|
+
const hi = Math.max(offsetA, offsetB);
|
|
33
|
+
for (let i = lo; i < hi && i < text.length; i++) if (text[i] === "\n") return false;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
/** Returns true if there is non-whitespace content before `offset` on the same line. */
|
|
37
|
+
function hasNonWhitespaceBeforeOnLine(text, offset) {
|
|
38
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
39
|
+
const ch = text[i];
|
|
40
|
+
if (ch === "\n" || ch === "\r") return false;
|
|
41
|
+
if (ch !== " " && ch !== " ") return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns the column of the first non-whitespace character on the line
|
|
47
|
+
* containing the given offset. Used to compute the "effective" indent of a
|
|
48
|
+
* line when properties (tag/anchor) precede the actual content scalar —
|
|
49
|
+
* the indent is the leftmost column on the line, not the scalar's column.
|
|
50
|
+
*/
|
|
51
|
+
function lineIndentColumn(text, offset) {
|
|
52
|
+
let lineStart = offset;
|
|
53
|
+
while (lineStart > 0 && text[lineStart - 1] !== "\n") lineStart--;
|
|
54
|
+
let i = lineStart;
|
|
55
|
+
while (i < text.length && (text[i] === " " || text[i] === " ")) i++;
|
|
56
|
+
return i - lineStart;
|
|
57
|
+
}
|
|
58
|
+
function hasMeta(m) {
|
|
59
|
+
return m.anchor !== void 0 || m.tag !== void 0 || m.comment !== void 0;
|
|
60
|
+
}
|
|
61
|
+
function clearMeta(m) {
|
|
62
|
+
delete m.anchor;
|
|
63
|
+
delete m.tag;
|
|
64
|
+
delete m.comment;
|
|
65
|
+
}
|
|
66
|
+
function createState(text, flow, options) {
|
|
67
|
+
return {
|
|
68
|
+
text,
|
|
69
|
+
anchors: /* @__PURE__ */ new Map(),
|
|
70
|
+
aliasCount: 0,
|
|
71
|
+
errors: [],
|
|
72
|
+
warnings: [],
|
|
73
|
+
options: {
|
|
74
|
+
strict: options?.strict ?? true,
|
|
75
|
+
maxAliasCount: options?.maxAliasCount ?? 100,
|
|
76
|
+
uniqueKeys: options?.uniqueKeys ?? true
|
|
77
|
+
},
|
|
78
|
+
tagMap: /* @__PURE__ */ new Map(),
|
|
79
|
+
flow,
|
|
80
|
+
depth: 0
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Maximum collection-nesting depth the composer will recurse into. The
|
|
85
|
+
* composer (and every downstream tree walker: value extraction, stringify,
|
|
86
|
+
* the visitor) recurses per node, so unbounded nesting is a stack-overflow
|
|
87
|
+
* denial-of-service vector. 256 is far beyond any real document and leaves
|
|
88
|
+
* a wide margin under the observed overflow point (~900 nesting levels with
|
|
89
|
+
* the composer's multi-frame recursion chain per level).
|
|
90
|
+
*/
|
|
91
|
+
const MAX_NESTING_DEPTH = 256;
|
|
92
|
+
/**
|
|
93
|
+
* Enter one collection-nesting level. Returns `false` — after recording a
|
|
94
|
+
* single fatal `NestingDepthExceeded` diagnostic — when the depth budget is
|
|
95
|
+
* exhausted; the caller must then return a leaf placeholder instead of
|
|
96
|
+
* recursing. Balance every `true` return with {@link exitNesting}.
|
|
97
|
+
*/
|
|
98
|
+
function enterNesting(state, cst) {
|
|
99
|
+
if (state.depth >= 256) {
|
|
100
|
+
if (!state.errors.some((e) => e.code === "NestingDepthExceeded")) state.errors.push({
|
|
101
|
+
code: "NestingDepthExceeded",
|
|
102
|
+
message: `Nesting depth exceeded maximum of ${256}`,
|
|
103
|
+
offset: cst.offset,
|
|
104
|
+
length: 1
|
|
105
|
+
});
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
state.depth++;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
/** Leave one collection-nesting level. */
|
|
112
|
+
function exitNesting(state) {
|
|
113
|
+
state.depth--;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
export { clearMeta, createState, enterNesting, exitNesting, getLineStarts, hasMeta, hasNonWhitespaceBeforeOnLine, lineCol, lineIndentColumn, sameLine };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//#region src/internal/composer/tags.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a tag shorthand using the document's %TAG directives.
|
|
4
|
+
* For example, with `%TAG !! tag:example.com,2000:app/`, the tag `!!int`
|
|
5
|
+
* resolves to `tag:example.com,2000:app/int`.
|
|
6
|
+
*
|
|
7
|
+
* Returns the resolved tag URI, or the original tag if no directive matches.
|
|
8
|
+
*/
|
|
9
|
+
function resolveTagHandle(tag, state) {
|
|
10
|
+
if (tag.startsWith("!<") && tag.endsWith(">")) return tag.slice(2, -1);
|
|
11
|
+
if (tag.startsWith("!!")) {
|
|
12
|
+
const prefix = state.tagMap.get("!!");
|
|
13
|
+
if (prefix) return prefix + tag.slice(2);
|
|
14
|
+
return `tag:yaml.org,2002:${tag.slice(2)}`;
|
|
15
|
+
}
|
|
16
|
+
const namedMatch = tag.match(/^(![\w-]*!)(.*)$/);
|
|
17
|
+
if (namedMatch) {
|
|
18
|
+
const handle = namedMatch[1];
|
|
19
|
+
const suffix = namedMatch[2];
|
|
20
|
+
if (handle) {
|
|
21
|
+
const prefix = state.tagMap.get(handle);
|
|
22
|
+
if (prefix) return prefix + (suffix ?? "");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (tag.startsWith("!") && tag.length > 1 && !tag.startsWith("!!")) {
|
|
26
|
+
const prefix = state.tagMap.get("!");
|
|
27
|
+
if (prefix) return prefix + tag.slice(1);
|
|
28
|
+
return tag;
|
|
29
|
+
}
|
|
30
|
+
return tag;
|
|
31
|
+
}
|
|
32
|
+
function parseDirective(source) {
|
|
33
|
+
const trimmed = source.trim();
|
|
34
|
+
if (!trimmed.startsWith("%")) return null;
|
|
35
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
36
|
+
const name = parts[0];
|
|
37
|
+
if (!name) return null;
|
|
38
|
+
const parameters = [];
|
|
39
|
+
for (const p of parts.slice(1)) {
|
|
40
|
+
if (p.startsWith("#")) break;
|
|
41
|
+
parameters.push(p);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
parameters
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* QLJ7: validate that any `!handle!suffix` tag reference in this document
|
|
50
|
+
* is declared by a `%TAG` directive in the SAME document. %TAG directives
|
|
51
|
+
* are local to a single document and do not leak across `---` boundaries.
|
|
52
|
+
* The `!!` shorthand and the primary `!` handle are always available.
|
|
53
|
+
*/
|
|
54
|
+
function validateTagHandlesInDocument(docCst, state) {
|
|
55
|
+
const children = docCst.children ?? [];
|
|
56
|
+
const localHandles = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const child of children) {
|
|
58
|
+
if (child.type !== "directive") continue;
|
|
59
|
+
const directive = parseDirective(child.source);
|
|
60
|
+
if (directive && directive.name === "TAG" && directive.parameters.length >= 2) {
|
|
61
|
+
const handle = directive.parameters[0];
|
|
62
|
+
if (handle) localHandles.add(handle);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const stack = [docCst];
|
|
66
|
+
while (stack.length > 0) {
|
|
67
|
+
const node = stack.pop();
|
|
68
|
+
if (!node) continue;
|
|
69
|
+
if (node.type === "tag") {
|
|
70
|
+
const src = node.source;
|
|
71
|
+
if (src.startsWith("!<") || src.startsWith("!!") || src === "!") continue;
|
|
72
|
+
const m = src.match(/^(![\w-]*!)/);
|
|
73
|
+
if (m) {
|
|
74
|
+
const handle = m[1];
|
|
75
|
+
if (handle && !localHandles.has(handle)) state.errors.push({
|
|
76
|
+
code: "UnresolvedTag",
|
|
77
|
+
message: `Tag handle ${handle} is not declared in this document`,
|
|
78
|
+
offset: node.offset,
|
|
79
|
+
length: node.length
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (node.children) for (const c of node.children) stack.push(c);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
export { parseDirective, resolveTagHandle, validateTagHandlesInDocument };
|