clckernel 1.2.19 → 2.0.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/.agents/skills/clckernel_cli/SKILL.md +23 -6
- package/.agents/skills/clckernel_cli/reference/audit.md +13 -3
- package/.agents/skills/clckernel_cli/reference/commit.md +1 -1
- package/.agents/skills/clckernel_cli/reference/doctor.md +5 -3
- package/.agents/skills/clckernel_cli/reference/feature.md +3 -3
- package/.agents/skills/clckernel_cli/reference/guard.md +48 -12
- package/.agents/skills/clckernel_cli/reference/phase.md +6 -3
- package/.agents/skills/clckernel_cli/reference/start.md +14 -4
- package/.agents/skills/clckernel_cli/reference/test.md +1 -1
- package/.agents/skills/clckernel_cli/reference/update.md +19 -19
- package/CHANGELOG.md +244 -0
- package/LICENSE +21 -0
- package/README.es.md +280 -51
- package/README.md +274 -50
- package/bin/cli.js +8 -0
- package/package.json +12 -2
- package/src/adapters/agent-os.js +161 -84
- package/src/adapters/astro.js +8 -6
- package/src/adapters/base.js +23 -38
- package/src/adapters/django.js +14 -13
- package/src/adapters/fastapi.js +16 -15
- package/src/adapters/go.js +11 -9
- package/src/adapters/laravel.js +12 -10
- package/src/adapters/nextjs.js +7 -5
- package/src/adapters/rails.js +13 -11
- package/src/adapters/rust.js +11 -9
- package/src/catalog.js +58 -49
- package/src/config.js +38 -240
- package/src/detector.js +243 -44
- package/src/doctor.js +18 -9
- package/src/generator.js +734 -302
- package/src/gitignore.js +60 -14
- package/src/index.js +257 -18
- package/src/io.js +409 -0
- package/src/known-hashes.json +309 -0
- package/src/onboarding.js +15 -0
- package/src/technologies/detector.js +1 -1
- package/src/technologies/guards/_shared.js +1 -1
- package/src/ui.js +233 -40
- package/src/updater.js +271 -38
- package/tools/_changes.js +279 -0
- package/tools/_changes.py +233 -0
- package/tools/_config.js +294 -0
- package/tools/_config.py +271 -0
- package/tools/_yaml.py +489 -0
- package/tools/audit.js +511 -111
- package/tools/audit.py +506 -128
- package/tools/celery_guard.py +13 -35
- package/tools/check_a11y.js +8 -28
- package/tools/check_api_contracts.js +13 -39
- package/tools/check_architecture.js +35 -44
- package/tools/check_architecture.py +172 -105
- package/tools/check_custom.js +108 -61
- package/tools/check_custom.py +108 -271
- package/tools/check_data_files.js +88 -0
- package/tools/check_db_efficiency.py +80 -65
- package/tools/check_hitl.js +112 -69
- package/tools/check_links.js +138 -0
- package/tools/check_migrations.py +143 -94
- package/tools/check_performance.js +35 -44
- package/tools/check_placeholders.js +104 -0
- package/tools/check_responsive.js +8 -28
- package/tools/check_scope.py +40 -31
- package/tools/check_seo.js +8 -28
- package/tools/check_skills.js +158 -0
- package/tools/check_storybook.js +8 -21
- package/tools/check_symlinks.js +40 -22
- package/tools/check_ui_reuse.js +17 -29
- package/tools/docker_guard.py +19 -41
- package/tools/postgres_guard.py +15 -36
- package/tools/redis_guard.py +13 -35
- package/tools/scan_secrets.js +134 -67
- package/tools/scan_secrets.py +163 -72
- package/tools/verify_tdd.js +867 -0
- package/tools/verify_tdd.py +407 -165
- package/tools/yaml.js +404 -132
- package/tools/check_domain_data.js +0 -100
- package/tools/check_test_calendar_sync.js +0 -111
package/tools/yaml.js
CHANGED
|
@@ -1,173 +1,445 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLC
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* CLC Kernel — Zero-Dependency strict YAML subset parser.
|
|
3
|
+
* Uses only Node.js built-ins (no npm deps). Twin of `minimal_yaml_parse` in
|
|
4
|
+
* tools/_yaml.py: same functions, same order, same error messages.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Supported subset (anything else throws `Unsupported YAML syntax at line N`):
|
|
7
|
+
* - block mappings and block sequences, including a sequence at the SAME
|
|
8
|
+
* indent as its key (`rules:` / `- name:`) and multi-key mapping items
|
|
9
|
+
* - scalars: plain, 'single' (only the '' escape), "double" (escapes
|
|
10
|
+
* \\ \" \/ \n \t \r \0)
|
|
11
|
+
* - types: int, float, true/false (+ True/TRUE/False/FALSE),
|
|
12
|
+
* null/Null/NULL/~/empty; ambiguous YAML 1.1 forms (yes/no/on/off, octal,
|
|
13
|
+
* hex, 1_000, 1:30, dates, .5, .inf, ...) are errors — quote them
|
|
14
|
+
* - flow `[...]` / `{...}` of scalars on one line, quote-aware
|
|
15
|
+
* - comments: `#` at line start or preceded by a space, outside quotes
|
|
8
16
|
*
|
|
9
|
-
*
|
|
10
|
-
* obj: the object we are building key-value pairs into
|
|
11
|
-
* indent: indent level (children at same indent stay)
|
|
12
|
-
* _parentObj/_parentKey: where to find/create the array for sequences
|
|
17
|
+
* The top level must be a mapping (an empty document parses to {}).
|
|
13
18
|
*
|
|
14
19
|
* @module yaml
|
|
15
20
|
*/
|
|
16
21
|
|
|
22
|
+
const NULL_WORDS = ['', '~', 'null', 'Null', 'NULL'];
|
|
23
|
+
const TRUE_WORDS = ['true', 'True', 'TRUE'];
|
|
24
|
+
const FALSE_WORDS = ['false', 'False', 'FALSE'];
|
|
25
|
+
const YAML11_BOOL_RE = /^(?:yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF)$/;
|
|
26
|
+
const INT_RE = /^[-+]?(?:0|[1-9][0-9]*)$/;
|
|
27
|
+
const FLOAT_RE = /^[-+]?[0-9]+\.[0-9]+$/;
|
|
28
|
+
// Everything PyYAML would resolve to an int/float/timestamp; used to reject
|
|
29
|
+
// forms outside the subset instead of silently reading them as strings.
|
|
30
|
+
const YAML11_INT_RE = /^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$/;
|
|
31
|
+
const YAML11_FLOAT_RE = /^(?:[-+]?[0-9][0-9_]*\.[0-9_]*(?:[eE][-+][0-9]+)?|\.[0-9_]+(?:[eE][-+][0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$/;
|
|
32
|
+
const YAML11_TIMESTAMP_RE = /^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}(?:$|[Tt \t])/;
|
|
33
|
+
const DOUBLE_ESCAPES = { '\\': '\\', '"': '"', '/': '/', n: '\n', t: '\t', r: '\r', 0: '\0' };
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
21
|
-
* @
|
|
36
|
+
* Build the error thrown for any input outside the subset.
|
|
37
|
+
* @param {number} lineNo - 1-based line number.
|
|
38
|
+
* @param {string} reason - What is not supported.
|
|
39
|
+
* @returns {Error}
|
|
22
40
|
*/
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
function yamlError(lineNo, reason) {
|
|
42
|
+
return new Error(`Unsupported YAML syntax at line ${lineNo}: ${reason}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Cursor over the significant lines of the document. */
|
|
46
|
+
class Cursor {
|
|
47
|
+
constructor(lines) {
|
|
48
|
+
this.lines = lines;
|
|
49
|
+
this.index = 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
peek() {
|
|
53
|
+
return this.index < this.lines.length ? this.lines[this.index] : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
advance() {
|
|
57
|
+
this.index += 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Replace the current line (used to re-read `- key: v` as a mapping). */
|
|
61
|
+
replace(line) {
|
|
62
|
+
this.lines[this.index] = line;
|
|
31
63
|
}
|
|
32
64
|
}
|
|
33
65
|
|
|
34
66
|
/**
|
|
35
|
-
* Parse a YAML string into a
|
|
36
|
-
* Throws on anchors/aliases (unsupported).
|
|
67
|
+
* Parse a YAML string (strict subset) into a plain object.
|
|
37
68
|
* @param {string} text - Raw YAML source.
|
|
38
|
-
* @returns {object} Parsed
|
|
69
|
+
* @returns {object} Parsed mapping ({} for an empty document).
|
|
70
|
+
* @throws {Error} `Unsupported YAML syntax at line N: ...` outside the subset.
|
|
39
71
|
*/
|
|
40
72
|
function parseYaml(text) {
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
73
|
+
const cursor = new Cursor(splitLines(text));
|
|
74
|
+
const first = cursor.peek();
|
|
75
|
+
if (first === null) return {};
|
|
76
|
+
if (isSeqItem(first.text)) throw yamlError(first.lineNo, 'the top level must be a mapping');
|
|
77
|
+
const root = parseMapping(cursor, first.indent);
|
|
78
|
+
const rest = cursor.peek();
|
|
79
|
+
if (rest !== null) throw yamlError(rest.lineNo, 'inconsistent indentation');
|
|
80
|
+
return root;
|
|
81
|
+
}
|
|
44
82
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Split the source into significant lines: { indent, text, lineNo }.
|
|
85
|
+
* Drops blank and comment-only lines; rejects tabs in indentation and
|
|
86
|
+
* document markers.
|
|
87
|
+
*/
|
|
88
|
+
function splitLines(text) {
|
|
89
|
+
const lines = [];
|
|
90
|
+
text.split('\n').forEach((raw, i) => {
|
|
91
|
+
const lineNo = i + 1;
|
|
92
|
+
const line = raw.replace(/\r$/, '');
|
|
93
|
+
const content = line.replace(/^[ \t]+/, '');
|
|
94
|
+
if (content === '' || content.startsWith('#')) return;
|
|
95
|
+
const leading = line.slice(0, line.length - content.length);
|
|
96
|
+
if (leading.includes('\t')) throw yamlError(lineNo, 'tabs are not allowed in indentation');
|
|
97
|
+
const body = content.replace(/[ \t]+$/, '');
|
|
98
|
+
if (leading === '' && /^(?:---|\.\.\.)(?:[ \t]|$)/.test(body)) {
|
|
99
|
+
throw yamlError(lineNo, 'multiple documents (---/...) are not supported');
|
|
100
|
+
}
|
|
101
|
+
lines.push({ indent: leading.length, text: body, lineNo });
|
|
102
|
+
});
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
48
105
|
|
|
49
|
-
|
|
50
|
-
|
|
106
|
+
/** `- item` or a bare `-`. */
|
|
107
|
+
function isSeqItem(text) {
|
|
108
|
+
return text === '-' || text.startsWith('- ');
|
|
109
|
+
}
|
|
51
110
|
|
|
52
|
-
|
|
53
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Parse the block that starts at the current line, whose indent is `indent`.
|
|
113
|
+
* @returns {object|Array}
|
|
114
|
+
*/
|
|
115
|
+
function parseBlock(cursor, indent) {
|
|
116
|
+
if (isSeqItem(cursor.peek().text)) return parseSequence(cursor, indent);
|
|
117
|
+
return parseMapping(cursor, indent);
|
|
118
|
+
}
|
|
54
119
|
|
|
55
|
-
|
|
56
|
-
|
|
120
|
+
/** Parse a block mapping whose keys sit at exactly `indent`. */
|
|
121
|
+
function parseMapping(cursor, indent) {
|
|
122
|
+
const obj = {};
|
|
123
|
+
for (let line = cursor.peek(); line !== null; line = cursor.peek()) {
|
|
124
|
+
if (line.indent < indent) break;
|
|
125
|
+
if (line.indent > indent) throw yamlError(line.lineNo, 'inconsistent indentation');
|
|
126
|
+
if (isSeqItem(line.text)) throw yamlError(line.lineNo, 'sequence item where a mapping key was expected');
|
|
127
|
+
const entry = splitMappingEntry(line.text, line.lineNo);
|
|
128
|
+
if (entry === null) {
|
|
129
|
+
assertNodeStart(line.text, 0, line.lineNo, false);
|
|
130
|
+
throw yamlError(line.lineNo, 'expected "key: value"');
|
|
57
131
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// ── Sequence item: "- rest" ──
|
|
61
|
-
if (trimmed.startsWith('- ')) {
|
|
62
|
-
const rest = trimmed.slice(2).trim();
|
|
63
|
-
|
|
64
|
-
if (rest.includes(': ')) {
|
|
65
|
-
// "- key: value" — mapping inside a sequence
|
|
66
|
-
let arr = null;
|
|
67
|
-
// Check if parent key already points to an array
|
|
68
|
-
if (entry._parentObj && entry._parentKey) {
|
|
69
|
-
const pk = entry._parentObj[entry._parentKey];
|
|
70
|
-
if (Array.isArray(pk)) {
|
|
71
|
-
arr = pk;
|
|
72
|
-
} else if (pk && typeof pk === 'object' && Object.keys(pk).length === 0) {
|
|
73
|
-
// Convert empty object to array
|
|
74
|
-
entry._parentObj[entry._parentKey] = [];
|
|
75
|
-
arr = entry._parentObj[entry._parentKey];
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (arr) {
|
|
79
|
-
const obj = {};
|
|
80
|
-
const colonIdx = rest.indexOf(': ');
|
|
81
|
-
obj[rest.slice(0, colonIdx).trim()] = parseScalar(rest.slice(colonIdx + 2).trim());
|
|
82
|
-
arr.push(obj);
|
|
83
|
-
stack.push({ obj, indent, _parentObj: arr, _parentKey: null });
|
|
84
|
-
}
|
|
85
|
-
} else if (entry._parentObj && entry._parentKey) {
|
|
86
|
-
// "- scalar" inside a sequence
|
|
87
|
-
const pk = entry._parentObj[entry._parentKey];
|
|
88
|
-
if (Array.isArray(pk)) {
|
|
89
|
-
pk.push(parseScalar(rest));
|
|
90
|
-
} else if (pk && typeof pk === 'object' && Object.keys(pk).length === 0) {
|
|
91
|
-
// Convert empty object to array (same rule as the mapping-in-sequence
|
|
92
|
-
// branch above) so plain block sequences like
|
|
93
|
-
// active_guards:
|
|
94
|
-
// - check_custom
|
|
95
|
-
// parse into arrays, not empty objects. Additive: previously valid
|
|
96
|
-
// YAML parses identically.
|
|
97
|
-
entry._parentObj[entry._parentKey] = [parseScalar(rest)];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
continue;
|
|
132
|
+
if (Object.prototype.hasOwnProperty.call(obj, entry.key)) {
|
|
133
|
+
throw yamlError(line.lineNo, `duplicate key "${entry.key}"`);
|
|
101
134
|
}
|
|
135
|
+
cursor.advance();
|
|
136
|
+
const inline = parseInlineValue(entry.rest, line.lineNo);
|
|
137
|
+
obj[entry.key] = inline.empty ? parseChild(cursor, indent, true) : inline.value;
|
|
138
|
+
}
|
|
139
|
+
return obj;
|
|
140
|
+
}
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
142
|
+
/** Parse a block sequence whose `-` markers sit at exactly `indent`. */
|
|
143
|
+
function parseSequence(cursor, indent) {
|
|
144
|
+
const arr = [];
|
|
145
|
+
for (let line = cursor.peek(); line !== null; line = cursor.peek()) {
|
|
146
|
+
if (line.indent < indent) break;
|
|
147
|
+
if (line.indent > indent) throw yamlError(line.lineNo, 'inconsistent indentation');
|
|
148
|
+
if (!isSeqItem(line.text)) break;
|
|
149
|
+
const rest = line.text.slice(1).replace(/^ +/, '');
|
|
150
|
+
const column = line.indent + (line.text.length - rest.length);
|
|
151
|
+
if (rest === '' || rest.startsWith('#')) {
|
|
152
|
+
cursor.advance();
|
|
153
|
+
arr.push(parseChild(cursor, indent, false));
|
|
154
|
+
} else if (isSeqItem(rest)) {
|
|
155
|
+
throw yamlError(line.lineNo, 'nested sequences on one line are not supported');
|
|
156
|
+
} else if (splitMappingEntry(rest, line.lineNo) !== null) {
|
|
157
|
+
cursor.replace({ indent: column, text: rest, lineNo: line.lineNo });
|
|
158
|
+
arr.push(parseMapping(cursor, column));
|
|
159
|
+
} else {
|
|
160
|
+
cursor.advance();
|
|
161
|
+
arr.push(parseInlineValue(rest, line.lineNo).value);
|
|
121
162
|
}
|
|
122
163
|
}
|
|
164
|
+
return arr;
|
|
165
|
+
}
|
|
123
166
|
|
|
124
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Value of a `key:` or `-` with nothing after it: a nested block, a sequence
|
|
169
|
+
* at the same indent (only after a key), or null.
|
|
170
|
+
*/
|
|
171
|
+
function parseChild(cursor, parentIndent, allowSameIndentSeq) {
|
|
172
|
+
const next = cursor.peek();
|
|
173
|
+
if (next === null) return null;
|
|
174
|
+
if (next.indent > parentIndent) return parseBlock(cursor, next.indent);
|
|
175
|
+
if (allowSameIndentSeq && next.indent === parentIndent && isSeqItem(next.text)) {
|
|
176
|
+
return parseSequence(cursor, parentIndent);
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
125
179
|
}
|
|
126
180
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Split `key: rest` on the first mapping indicator outside quotes.
|
|
183
|
+
* @returns {{key: string, rest: string}|null} null when the text is not a mapping entry.
|
|
184
|
+
*/
|
|
185
|
+
function splitMappingEntry(text, lineNo) {
|
|
186
|
+
const first = text[0];
|
|
187
|
+
if (first === '[' || first === '{') return null;
|
|
188
|
+
if (first === "'" || first === '"') {
|
|
189
|
+
const [key, end] = parseQuoted(text, 0, lineNo);
|
|
190
|
+
const colon = skipSpaces(text, end);
|
|
191
|
+
if (text[colon] === ':' && isBlankOrEnd(text, colon + 1)) {
|
|
192
|
+
return { key, rest: text.slice(colon + 1) };
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
for (let i = 0; i < text.length; i++) {
|
|
197
|
+
if (text[i] === '#' && i > 0 && text[i - 1] === ' ') return null;
|
|
198
|
+
if (text[i] === ':' && isBlankOrEnd(text, i + 1)) {
|
|
199
|
+
return { key: parsePlainKey(text.slice(0, i).replace(/[ \t]+$/, ''), lineNo), rest: text.slice(i + 1) };
|
|
200
|
+
}
|
|
136
201
|
}
|
|
137
|
-
return
|
|
202
|
+
return null;
|
|
138
203
|
}
|
|
139
204
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (colonIdx === -1) continue;
|
|
148
|
-
const k = pair.slice(0, colonIdx).trim();
|
|
149
|
-
const v = pair.slice(colonIdx + 1).trim();
|
|
150
|
-
obj[k] = parseScalar(v);
|
|
205
|
+
/** Validate a plain (unquoted) mapping key: it must resolve to a string. */
|
|
206
|
+
function parsePlainKey(raw, lineNo) {
|
|
207
|
+
if (raw === '') throw yamlError(lineNo, 'empty mapping key');
|
|
208
|
+
assertNodeStart(raw, 0, lineNo, false);
|
|
209
|
+
if (raw === '<<') throw yamlError(lineNo, 'merge keys (<<) are not supported');
|
|
210
|
+
if (typeof resolvePlain(raw, lineNo) !== 'string') {
|
|
211
|
+
throw yamlError(lineNo, `key "${raw}" is not a string; quote it`);
|
|
151
212
|
}
|
|
152
|
-
return
|
|
213
|
+
return raw;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Parse what follows `key:` or `- ` on the same line.
|
|
218
|
+
* @returns {{empty: boolean, value: *}} empty when only spaces/comment remain.
|
|
219
|
+
*/
|
|
220
|
+
function parseInlineValue(text, lineNo) {
|
|
221
|
+
const start = skipSpaces(text, 0);
|
|
222
|
+
if (start >= text.length || text[start] === '#') return { empty: true, value: null };
|
|
223
|
+
const first = text[start];
|
|
224
|
+
let value;
|
|
225
|
+
let end;
|
|
226
|
+
if (first === '[' || first === '{') {
|
|
227
|
+
[value, end] = parseFlow(text, start, lineNo);
|
|
228
|
+
} else if (first === "'" || first === '"') {
|
|
229
|
+
[value, end] = parseQuoted(text, start, lineNo);
|
|
230
|
+
} else {
|
|
231
|
+
assertNodeStart(text, start, lineNo, false);
|
|
232
|
+
const [raw, stop] = readPlain(text, start, false, lineNo);
|
|
233
|
+
if (raw.includes(': ') || raw.endsWith(':')) {
|
|
234
|
+
throw yamlError(lineNo, 'mapping values are not allowed inside a plain scalar; quote it');
|
|
235
|
+
}
|
|
236
|
+
value = resolvePlain(raw, lineNo);
|
|
237
|
+
end = stop;
|
|
238
|
+
}
|
|
239
|
+
assertEndOfLine(text, end, lineNo);
|
|
240
|
+
return { empty: false, value };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Parse a one-line flow collection of scalars starting at `pos` (`[` or `{`).
|
|
245
|
+
* @returns {[Array|object, number]} value and the index after the closer.
|
|
246
|
+
*/
|
|
247
|
+
function parseFlow(text, pos, lineNo) {
|
|
248
|
+
const isSeq = text[pos] === '[';
|
|
249
|
+
const close = isSeq ? ']' : '}';
|
|
250
|
+
const result = isSeq ? [] : {};
|
|
251
|
+
let i = skipSpaces(text, pos + 1);
|
|
252
|
+
if (text[i] === close) return [result, i + 1];
|
|
253
|
+
for (;;) {
|
|
254
|
+
i = skipSpaces(text, i);
|
|
255
|
+
if (isSeq) {
|
|
256
|
+
let item;
|
|
257
|
+
[item, i] = parseFlowScalar(text, i, lineNo);
|
|
258
|
+
result.push(item);
|
|
259
|
+
} else {
|
|
260
|
+
let key;
|
|
261
|
+
[key, i] = parseFlowScalar(text, i, lineNo, true);
|
|
262
|
+
i = skipSpaces(text, i);
|
|
263
|
+
if (text[i] !== ':') throw yamlError(lineNo, 'expected ":" in flow mapping');
|
|
264
|
+
i = skipSpaces(text, i + 1);
|
|
265
|
+
let value = null;
|
|
266
|
+
if (text[i] !== ',' && text[i] !== close) [value, i] = parseFlowScalar(text, i, lineNo);
|
|
267
|
+
if (Object.prototype.hasOwnProperty.call(result, key)) throw yamlError(lineNo, `duplicate key "${key}"`);
|
|
268
|
+
result[key] = value;
|
|
269
|
+
}
|
|
270
|
+
i = skipSpaces(text, i);
|
|
271
|
+
if (text[i] === ',') {
|
|
272
|
+
i = skipSpaces(text, i + 1);
|
|
273
|
+
if (text[i] === close) return [result, i + 1];
|
|
274
|
+
} else if (text[i] === close) {
|
|
275
|
+
return [result, i + 1];
|
|
276
|
+
} else if (i >= text.length || text[i] === '#') {
|
|
277
|
+
throw yamlError(lineNo, 'unclosed flow collection (flow must fit on one line)');
|
|
278
|
+
} else {
|
|
279
|
+
throw yamlError(lineNo, `expected "," or "${close}" in flow collection`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Parse one scalar inside a flow collection.
|
|
286
|
+
* @returns {[*, number]} value and the index after it.
|
|
287
|
+
*/
|
|
288
|
+
function parseFlowScalar(text, pos, lineNo, isKey = false) {
|
|
289
|
+
if (pos >= text.length) throw yamlError(lineNo, 'unclosed flow collection (flow must fit on one line)');
|
|
290
|
+
const first = text[pos];
|
|
291
|
+
if (first === '[' || first === '{') throw yamlError(lineNo, 'nested flow collections are not supported');
|
|
292
|
+
if (first === "'" || first === '"') return parseQuoted(text, pos, lineNo);
|
|
293
|
+
assertNodeStart(text, pos, lineNo, true);
|
|
294
|
+
const [raw, end] = readPlain(text, pos, true, lineNo);
|
|
295
|
+
if (raw === '') throw yamlError(lineNo, 'empty entry in flow collection');
|
|
296
|
+
return [isKey ? parsePlainKey(raw, lineNo) : resolvePlain(raw, lineNo), end];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Parse a single- or double-quoted scalar starting at `pos`.
|
|
301
|
+
* @returns {[string, number]} value and the index after the closing quote.
|
|
302
|
+
*/
|
|
303
|
+
function parseQuoted(text, pos, lineNo) {
|
|
304
|
+
const quote = text[pos];
|
|
305
|
+
let out = '';
|
|
306
|
+
let i = pos + 1;
|
|
307
|
+
while (i < text.length) {
|
|
308
|
+
const ch = text[i];
|
|
309
|
+
if (quote === "'" && ch === "'") {
|
|
310
|
+
if (text[i + 1] !== "'") return [out, i + 1];
|
|
311
|
+
out += "'";
|
|
312
|
+
i += 2;
|
|
313
|
+
} else if (quote === '"' && ch === '"') {
|
|
314
|
+
return [out, i + 1];
|
|
315
|
+
} else if (quote === '"' && ch === '\\') {
|
|
316
|
+
const esc = text[i + 1];
|
|
317
|
+
if (esc === undefined) throw yamlError(lineNo, 'unclosed double-quoted scalar');
|
|
318
|
+
if (!Object.prototype.hasOwnProperty.call(DOUBLE_ESCAPES, esc)) {
|
|
319
|
+
throw yamlError(lineNo, `unsupported escape "\\${esc}" in double-quoted scalar`);
|
|
320
|
+
}
|
|
321
|
+
out += DOUBLE_ESCAPES[esc];
|
|
322
|
+
i += 2;
|
|
323
|
+
} else {
|
|
324
|
+
out += ch;
|
|
325
|
+
i += 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw yamlError(lineNo, `unclosed ${quote === "'" ? 'single' : 'double'}-quoted scalar`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Read a plain scalar starting at `pos`; stops at a comment and, inside flow
|
|
333
|
+
* collections, at flow indicators and `: `.
|
|
334
|
+
* @returns {[string, number]} trimmed raw text and the index where it stopped.
|
|
335
|
+
*/
|
|
336
|
+
function readPlain(text, pos, inFlow, lineNo) {
|
|
337
|
+
let i = pos;
|
|
338
|
+
while (i < text.length) {
|
|
339
|
+
const ch = text[i];
|
|
340
|
+
if (ch === '#' && text[i - 1] === ' ') break;
|
|
341
|
+
if (ch === '#' && text[i - 1] === '\t') throw yamlError(lineNo, 'tabs are not allowed before comments');
|
|
342
|
+
if (inFlow && ',[]{}?'.includes(ch)) break;
|
|
343
|
+
if (inFlow && ch === ':' && (i + 1 >= text.length || ' \t,[]{}'.includes(text[i + 1]))) break;
|
|
344
|
+
i += 1;
|
|
345
|
+
}
|
|
346
|
+
return [text.slice(pos, i).replace(/[ \t]+$/, ''), i];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Resolve a plain scalar to null / boolean / number / string. */
|
|
350
|
+
function resolvePlain(raw, lineNo) {
|
|
351
|
+
if (NULL_WORDS.includes(raw)) return null;
|
|
352
|
+
if (TRUE_WORDS.includes(raw)) return true;
|
|
353
|
+
if (FALSE_WORDS.includes(raw)) return false;
|
|
354
|
+
if (YAML11_BOOL_RE.test(raw)) throw yamlError(lineNo, `ambiguous boolean "${raw}"; use true/false or quote it`);
|
|
355
|
+
if (INT_RE.test(raw)) return Number(raw) + 0; // + 0 turns -0 into 0
|
|
356
|
+
if (FLOAT_RE.test(raw)) return Number(raw);
|
|
357
|
+
if (YAML11_INT_RE.test(raw) || YAML11_FLOAT_RE.test(raw) || YAML11_TIMESTAMP_RE.test(raw) || raw === '=' || raw === '<<') {
|
|
358
|
+
throw yamlError(lineNo, `ambiguous scalar "${raw}"; quote it`);
|
|
359
|
+
}
|
|
360
|
+
return raw;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Reject indicators that cannot start a node in the subset. */
|
|
364
|
+
function assertNodeStart(text, pos, lineNo, inFlow) {
|
|
365
|
+
const ch = text[pos];
|
|
366
|
+
const spaced = isBlankOrEnd(text, pos + 1);
|
|
367
|
+
if (ch === '&' || ch === '*') throw yamlError(lineNo, 'anchors/aliases are not supported');
|
|
368
|
+
if (ch === '!') throw yamlError(lineNo, 'tags (!) are not supported');
|
|
369
|
+
if (ch === '|' || ch === '>') throw yamlError(lineNo, 'block scalars (| and >) are not supported');
|
|
370
|
+
if (ch === '?' && spaced) throw yamlError(lineNo, 'complex keys (? ) are not supported');
|
|
371
|
+
if (ch === '-' && spaced) throw yamlError(lineNo, 'a sequence is not allowed here');
|
|
372
|
+
if (ch === ':' && spaced) throw yamlError(lineNo, 'unexpected ":"');
|
|
373
|
+
if ('%@`,]}#'.includes(ch)) throw yamlError(lineNo, `a plain scalar cannot start with "${ch}"; quote it`);
|
|
374
|
+
if (inFlow && (ch === '[' || ch === '{')) throw yamlError(lineNo, 'nested flow collections are not supported');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** After a value only spaces and a ` #` comment may follow. */
|
|
378
|
+
function assertEndOfLine(text, pos, lineNo) {
|
|
379
|
+
const i = skipSpaces(text, pos);
|
|
380
|
+
if (i >= text.length) return;
|
|
381
|
+
if (text[i] === '#' && text[i - 1] === ' ') return;
|
|
382
|
+
throw yamlError(lineNo, `unexpected content after value: "${text.slice(i)}"`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function skipSpaces(text, pos) {
|
|
386
|
+
let i = pos;
|
|
387
|
+
while (i < text.length && (text[i] === ' ' || text[i] === '\t')) i += 1;
|
|
388
|
+
return i;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isBlankOrEnd(text, pos) {
|
|
392
|
+
return pos >= text.length || text[pos] === ' ' || text[pos] === '\t';
|
|
153
393
|
}
|
|
154
394
|
|
|
155
395
|
// ═══════════════════════════════════════════════════════════════════════
|
|
156
|
-
// Glob-to-Regex Converter (
|
|
157
|
-
// Handles: **, *, {a,b} alternation
|
|
396
|
+
// Glob-to-Regex Converter (twin of glob_to_regex in tools/_yaml.py)
|
|
397
|
+
// Handles: **/, trailing **, *, ?, {a,b} alternation; everything else literal
|
|
158
398
|
// ═══════════════════════════════════════════════════════════════════════
|
|
159
399
|
|
|
400
|
+
function escapeRegex(str) {
|
|
401
|
+
return str.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Convert a glob pattern to a fully anchored RegExp.
|
|
406
|
+
* - `**\/` matches zero or more leading directories
|
|
407
|
+
* - `**` (trailing or standalone) matches anything, separators included
|
|
408
|
+
* - `*` matches any run of characters except `/`
|
|
409
|
+
* - `?` matches exactly one character except `/`
|
|
410
|
+
* - `{a,b}` matches either alternative (alternatives are literal)
|
|
411
|
+
* - every other character is matched literally
|
|
412
|
+
* @param {string} glob
|
|
413
|
+
* @returns {RegExp}
|
|
414
|
+
*/
|
|
160
415
|
function globToRegex(glob) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
416
|
+
const out = [];
|
|
417
|
+
let i = 0;
|
|
418
|
+
while (i < glob.length) {
|
|
419
|
+
const ch = glob[i];
|
|
420
|
+
if (glob.startsWith('**/', i)) {
|
|
421
|
+
out.push('(?:.+/)?');
|
|
422
|
+
i += 3;
|
|
423
|
+
} else if (glob.startsWith('**', i)) {
|
|
424
|
+
out.push('.*');
|
|
425
|
+
i += 2;
|
|
426
|
+
} else if (ch === '*') {
|
|
427
|
+
out.push('[^/]*');
|
|
428
|
+
i += 1;
|
|
429
|
+
} else if (ch === '?') {
|
|
430
|
+
out.push('[^/]');
|
|
431
|
+
i += 1;
|
|
432
|
+
} else if (ch === '{' && glob.indexOf('}', i + 1) !== -1) {
|
|
433
|
+
const close = glob.indexOf('}', i + 1);
|
|
434
|
+
const alternatives = glob.slice(i + 1, close).split(',');
|
|
435
|
+
out.push('(?:' + alternatives.map(escapeRegex).join('|') + ')');
|
|
436
|
+
i = close + 1;
|
|
437
|
+
} else {
|
|
438
|
+
out.push(escapeRegex(ch));
|
|
439
|
+
i += 1;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return new RegExp('^' + out.join('') + '$');
|
|
171
443
|
}
|
|
172
444
|
|
|
173
445
|
module.exports = { parseYaml, globToRegex };
|