@limaformat/lima 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/CHANGELOG.md +29 -0
- package/LICENSE +15 -0
- package/README.md +70 -0
- package/dist/block-cursor.d.ts +24 -0
- package/dist/block-cursor.js +71 -0
- package/dist/block.d.ts +18 -0
- package/dist/block.js +305 -0
- package/dist/builder.d.ts +41 -0
- package/dist/builder.js +1 -0
- package/dist/chars.d.ts +2 -0
- package/dist/chars.js +4 -0
- package/dist/core.d.ts +73 -0
- package/dist/core.js +439 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +24 -0
- package/dist/flow.d.ts +7 -0
- package/dist/flow.js +162 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/normalize.d.ts +41 -0
- package/dist/normalize.js +67 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.js +522 -0
- package/dist/scalars.d.ts +89 -0
- package/dist/scalars.js +407 -0
- package/dist/scanner.d.ts +82 -0
- package/dist/scanner.js +204 -0
- package/dist/value.d.ts +95 -0
- package/dist/value.js +185 -0
- package/package.json +41 -0
package/dist/scalars.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core scalar grammar: the annotated `PositionedValue` tree type, dates,
|
|
3
|
+
* numbers/type coercion, quoting/escaping, and the shared quoted-or-typed
|
|
4
|
+
* scalar parser every value position (inline values, flow items, block
|
|
5
|
+
* array/map items) builds on.
|
|
6
|
+
*/
|
|
7
|
+
import { LNull, LBool, LFloat, LInt, LString, LInstant } from './value.js';
|
|
8
|
+
import { checkStringLimit } from './normalize.js';
|
|
9
|
+
import { LimaError } from './errors.js';
|
|
10
|
+
/** The `ValueBuilder<PositionedValue>` — reconstructs today's annotated tree exactly, for References. */
|
|
11
|
+
export const positionedBuilder = {
|
|
12
|
+
null: (line) => ({ kind: 'null', line }),
|
|
13
|
+
bool: (value, line) => ({ kind: 'bool', value, line }),
|
|
14
|
+
int: (value, line) => ({ kind: 'int', value, line }),
|
|
15
|
+
float: (value, line) => ({ kind: 'float', value, line }),
|
|
16
|
+
string: (value, line, quoted) => ({ kind: 'string', value, line, quoted }),
|
|
17
|
+
instant: (value, line) => ({ kind: 'instant', value, line }),
|
|
18
|
+
array: (items, line) => ({ kind: 'array', items, line }),
|
|
19
|
+
createMapping: () => new Map(),
|
|
20
|
+
createMappingWith: (key, value) => new Map([[key, value]]),
|
|
21
|
+
hasMappingKey: (entries, key) => entries.has(key),
|
|
22
|
+
setMapping: (entries, key, value) => { entries.set(key, value); },
|
|
23
|
+
mappingMaxDepth: (entries, depthOf) => {
|
|
24
|
+
let max = 0;
|
|
25
|
+
for (const value of entries.values()) {
|
|
26
|
+
if (value.kind === 'array' || value.kind === 'mapping')
|
|
27
|
+
max = Math.max(max, depthOf(value));
|
|
28
|
+
}
|
|
29
|
+
return max;
|
|
30
|
+
},
|
|
31
|
+
mapping: (entries, line) => ({ kind: 'mapping', entries, line }),
|
|
32
|
+
};
|
|
33
|
+
/** Strips position/quoted-origin annotations, recursively — the public parseCore() projection. */
|
|
34
|
+
export const toPlainValue = (v) => {
|
|
35
|
+
switch (v.kind) {
|
|
36
|
+
case 'null': return LNull;
|
|
37
|
+
case 'bool': return LBool(v.value);
|
|
38
|
+
case 'int': return LInt(v.value);
|
|
39
|
+
case 'float': return LFloat(v.value);
|
|
40
|
+
case 'string': return LString(v.value);
|
|
41
|
+
case 'instant': return LInstant(v.value);
|
|
42
|
+
case 'array': return { kind: 'array', items: v.items.map(toPlainValue) };
|
|
43
|
+
case 'mapping': {
|
|
44
|
+
const entries = new Map();
|
|
45
|
+
for (const [k, c] of v.entries)
|
|
46
|
+
entries.set(k, toPlainValue(c));
|
|
47
|
+
return { kind: 'mapping', entries };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
// ─── Dates ──────────────────────────────────────────────────────────────────
|
|
52
|
+
const ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})?)?$/;
|
|
53
|
+
const GERMAN_DATE_RE = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?: (\d{2}):(\d{2})(?::(\d{2}))?)?$/;
|
|
54
|
+
const SLASH_DATE_RE = /^(\d{4})\/(\d{2})\/(\d{2})(?: (\d{2}):(\d{2})(?::(\d{2}))?)?$/;
|
|
55
|
+
const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
56
|
+
const isLeapYear = (y) => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
|
|
57
|
+
const daysInMonth = (y, m) => (m === 2 && isLeapYear(y)) ? 29 : DAYS_IN_MONTH[m - 1];
|
|
58
|
+
/** Shared exact ISO-date recognizer over either a complete string or a source span. */
|
|
59
|
+
const parseExactIsoSpan = (source, start, end, strict, line) => {
|
|
60
|
+
const length = end - start;
|
|
61
|
+
const dateOnly = length === 10 && source.charCodeAt(start + 4) === 45 && source.charCodeAt(start + 7) === 45;
|
|
62
|
+
const instant = length === 20 && source.charCodeAt(start + 4) === 45 && source.charCodeAt(start + 7) === 45 &&
|
|
63
|
+
source.charCodeAt(start + 10) === 84 && source.charCodeAt(start + 13) === 58 &&
|
|
64
|
+
source.charCodeAt(start + 16) === 58 && source.charCodeAt(start + 19) === 90;
|
|
65
|
+
if (!dateOnly && !instant)
|
|
66
|
+
return null;
|
|
67
|
+
const digitAt = (offset) => source.charCodeAt(start + offset) - 48;
|
|
68
|
+
const d0 = digitAt(0), d1 = digitAt(1), d2 = digitAt(2), d3 = digitAt(3);
|
|
69
|
+
const d5 = digitAt(5), d6 = digitAt(6), d8 = digitAt(8), d9 = digitAt(9);
|
|
70
|
+
if (d0 < 0 || d0 > 9 || d1 < 0 || d1 > 9 || d2 < 0 || d2 > 9 || d3 < 0 || d3 > 9 ||
|
|
71
|
+
d5 < 0 || d5 > 9 || d6 < 0 || d6 > 9 || d8 < 0 || d8 > 9 || d9 < 0 || d9 > 9)
|
|
72
|
+
return null;
|
|
73
|
+
const year = d0 * 1000 + d1 * 100 + d2 * 10 + d3;
|
|
74
|
+
const month = d5 * 10 + d6, day = d8 * 10 + d9;
|
|
75
|
+
let hour = 0, minute = 0, second = 0;
|
|
76
|
+
if (instant) {
|
|
77
|
+
const d11 = digitAt(11), d12 = digitAt(12), d14 = digitAt(14), d15 = digitAt(15);
|
|
78
|
+
const d17 = digitAt(17), d18 = digitAt(18);
|
|
79
|
+
if (d11 < 0 || d11 > 9 || d12 < 0 || d12 > 9 || d14 < 0 || d14 > 9 ||
|
|
80
|
+
d15 < 0 || d15 > 9 || d17 < 0 || d17 > 9 || d18 < 0 || d18 > 9)
|
|
81
|
+
return null;
|
|
82
|
+
hour = d11 * 10 + d12;
|
|
83
|
+
minute = d14 * 10 + d15;
|
|
84
|
+
second = d17 * 10 + d18;
|
|
85
|
+
}
|
|
86
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month) ||
|
|
87
|
+
hour > 23 || minute > 59 || second > 59) {
|
|
88
|
+
if (strict) {
|
|
89
|
+
const raw = source.slice(start, end);
|
|
90
|
+
throw new LimaError({ code: 'INVALID_DATE', line, message: `LIMA: invalid date "${raw}" at line ${line}` });
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (year >= 100)
|
|
95
|
+
return new Date(Date.UTC(year, month - 1, day, hour, minute, second));
|
|
96
|
+
const result = new Date(0);
|
|
97
|
+
result.setUTCFullYear(year, month - 1, day);
|
|
98
|
+
if (instant)
|
|
99
|
+
result.setUTCHours(hour, minute, second, 0);
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
102
|
+
const parseDateUTC = (str, strict = false, line = 0) => {
|
|
103
|
+
const invalid = () => {
|
|
104
|
+
if (strict)
|
|
105
|
+
throw new LimaError({ code: 'INVALID_DATE', line, message: `LIMA: invalid date "${str}" at line ${line}` });
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
const exact = parseExactIsoSpan(str, 0, str.length, strict, line);
|
|
109
|
+
if (exact !== null)
|
|
110
|
+
return exact;
|
|
111
|
+
let y, mo, d, h = 0, mi = 0, s = 0, offsetMin = 0;
|
|
112
|
+
const iso = ISO_DATE_RE.exec(str);
|
|
113
|
+
const german = !iso ? GERMAN_DATE_RE.exec(str) : null;
|
|
114
|
+
const slash = !iso && !german ? SLASH_DATE_RE.exec(str) : null;
|
|
115
|
+
if (iso) {
|
|
116
|
+
y = +iso[1];
|
|
117
|
+
mo = +iso[2];
|
|
118
|
+
d = +iso[3];
|
|
119
|
+
h = iso[4] !== undefined ? +iso[4] : 0;
|
|
120
|
+
mi = iso[5] !== undefined ? +iso[5] : 0;
|
|
121
|
+
s = iso[6] !== undefined ? +iso[6] : 0;
|
|
122
|
+
const offsetStr = iso[7];
|
|
123
|
+
if (offsetStr && offsetStr !== 'Z') {
|
|
124
|
+
const sign = offsetStr.charCodeAt(0) === 45 ? -1 : 1;
|
|
125
|
+
const oh = +offsetStr.slice(1, 3);
|
|
126
|
+
const om = +offsetStr.slice(4, 6);
|
|
127
|
+
if (oh > 14 || om > 59 || (oh === 14 && om !== 0))
|
|
128
|
+
return invalid();
|
|
129
|
+
offsetMin = sign * (oh * 60 + om);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (german) {
|
|
133
|
+
d = +german[1];
|
|
134
|
+
mo = +german[2];
|
|
135
|
+
y = +german[3];
|
|
136
|
+
h = german[4] !== undefined ? +german[4] : 0;
|
|
137
|
+
mi = german[5] !== undefined ? +german[5] : 0;
|
|
138
|
+
s = german[6] !== undefined ? +german[6] : 0;
|
|
139
|
+
}
|
|
140
|
+
else if (slash) {
|
|
141
|
+
y = +slash[1];
|
|
142
|
+
mo = +slash[2];
|
|
143
|
+
d = +slash[3];
|
|
144
|
+
h = slash[4] !== undefined ? +slash[4] : 0;
|
|
145
|
+
mi = slash[5] !== undefined ? +slash[5] : 0;
|
|
146
|
+
s = slash[6] !== undefined ? +slash[6] : 0;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (y < 1 || y > 9999 || mo < 1 || mo > 12 || d < 1 || d > daysInMonth(y, mo) ||
|
|
152
|
+
h > 23 || mi > 59 || s > 59)
|
|
153
|
+
return invalid();
|
|
154
|
+
const base = new Date(0);
|
|
155
|
+
base.setUTCFullYear(y, mo - 1, d);
|
|
156
|
+
base.setUTCHours(h, mi, s, 0);
|
|
157
|
+
const result = new Date(base.getTime() - offsetMin * 60000);
|
|
158
|
+
const utcYear = result.getUTCFullYear();
|
|
159
|
+
if (utcYear < 1 || utcYear > 9999)
|
|
160
|
+
return invalid();
|
|
161
|
+
return result;
|
|
162
|
+
};
|
|
163
|
+
// ─── Numbers ────────────────────────────────────────────────────────────────
|
|
164
|
+
const NUMBER_RE = /^-?(?:(?:0|[1-9][0-9]*)(?:\.[0-9]+)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
|
|
165
|
+
const DATE_PRE_RE = /\d[\d\-:.\/a-zA-Z]{4,}/;
|
|
166
|
+
const isFloatForm = (str) => str.includes('.') || str.includes('e') || str.includes('E');
|
|
167
|
+
const isZeroLiteral = (str) => /^0+(\.0+)?$/.test(str.replace(/^-/, '').split(/[eE]/)[0]);
|
|
168
|
+
export const NO_SPAN_VALUE = Symbol('NO_SPAN_VALUE');
|
|
169
|
+
/**
|
|
170
|
+
* Parses allocation-free scalar forms whose complete grammar can be proven
|
|
171
|
+
* directly from one source span. Everything else returns a sentinel and is
|
|
172
|
+
* handled by the full string-based scalar/flow grammar.
|
|
173
|
+
*/
|
|
174
|
+
export const parseSimpleScalarSpan = (source, start, end, line, strict, builder) => {
|
|
175
|
+
const length = end - start;
|
|
176
|
+
if (length === 10 || length === 20) {
|
|
177
|
+
const exact = parseExactIsoSpan(source, start, end, strict, line);
|
|
178
|
+
if (exact !== null)
|
|
179
|
+
return builder.instant(exact, line);
|
|
180
|
+
}
|
|
181
|
+
if (length === 1 && source.charCodeAt(start) === 126)
|
|
182
|
+
return builder.null(line);
|
|
183
|
+
if (length === 4) {
|
|
184
|
+
const a = source.charCodeAt(start), b = source.charCodeAt(start + 1);
|
|
185
|
+
const c = source.charCodeAt(start + 2), d = source.charCodeAt(start + 3);
|
|
186
|
+
if (a === 116 && b === 114 && c === 117 && d === 101)
|
|
187
|
+
return builder.bool(true, line);
|
|
188
|
+
if (a === 110 && b === 117 && c === 108 && d === 108)
|
|
189
|
+
return builder.null(line);
|
|
190
|
+
}
|
|
191
|
+
if (length === 5 && source.charCodeAt(start) === 102 && source.charCodeAt(start + 1) === 97 &&
|
|
192
|
+
source.charCodeAt(start + 2) === 108 && source.charCodeAt(start + 3) === 115 &&
|
|
193
|
+
source.charCodeAt(start + 4) === 101)
|
|
194
|
+
return builder.bool(false, line);
|
|
195
|
+
if (length === 0 || length > 15)
|
|
196
|
+
return NO_SPAN_VALUE;
|
|
197
|
+
const first = source.charCodeAt(start);
|
|
198
|
+
if (first === 48 && length !== 1)
|
|
199
|
+
return NO_SPAN_VALUE;
|
|
200
|
+
let value = 0;
|
|
201
|
+
for (let pos = start; pos < end; pos++) {
|
|
202
|
+
const digit = source.charCodeAt(pos) - 48;
|
|
203
|
+
if (digit < 0 || digit > 9)
|
|
204
|
+
return NO_SPAN_VALUE;
|
|
205
|
+
value = value * 10 + digit;
|
|
206
|
+
}
|
|
207
|
+
return value <= Number.MAX_SAFE_INTEGER ? builder.int(value, line) : NO_SPAN_VALUE;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Classifies a raw token and constructs its final builder value directly,
|
|
211
|
+
* per Core §6.4.1's explicit number
|
|
212
|
+
* grammar (never `Number()`/`parseFloat()`, which accept far more than Lima
|
|
213
|
+
* does) and the three §6.5.1 date shapes. Reference-shaped text (`($key)`,
|
|
214
|
+
* `(%key)`) matches none of these and falls through to a plain string,
|
|
215
|
+
* unrecognised and unresolved — Core has no concept of it at all. Avoiding
|
|
216
|
+
* an intermediate tagged `LimaValue` keeps both builders on one grammar while
|
|
217
|
+
* eliminating an allocate-then-switch conversion for every scalar.
|
|
218
|
+
*/
|
|
219
|
+
const buildTyped = (str, strict, line, builder) => {
|
|
220
|
+
if (str === '' || str === 'null' || str === '~')
|
|
221
|
+
return builder.null(line);
|
|
222
|
+
if (str === 'true')
|
|
223
|
+
return builder.bool(true, line);
|
|
224
|
+
if (str === 'false')
|
|
225
|
+
return builder.bool(false, line);
|
|
226
|
+
const first = str.charCodeAt(0);
|
|
227
|
+
// Every number and every supported date form starts with a digit, '-'
|
|
228
|
+
// or '.'. Once the null/boolean literals above are excluded, any other
|
|
229
|
+
// leading character is unconditionally a string; avoid both regexes and
|
|
230
|
+
// the email/date prechecks for ordinary words, URLs and identifiers.
|
|
231
|
+
if (!((first >= 48 && first <= 57) || first === 45 || first === 46)) {
|
|
232
|
+
checkStringLimit(str, line);
|
|
233
|
+
return builder.string(str, line, false);
|
|
234
|
+
}
|
|
235
|
+
// Hex (0x/0X), octal (0o/0O), binary (0b/0B) — kept as strings (YAML 1.2 compatible).
|
|
236
|
+
if (str.length > 2 && str.charCodeAt(0) === 48 &&
|
|
237
|
+
(str.charCodeAt(1) === 120 || str.charCodeAt(1) === 88 ||
|
|
238
|
+
str.charCodeAt(1) === 111 || str.charCodeAt(1) === 79 ||
|
|
239
|
+
str.charCodeAt(1) === 98 || str.charCodeAt(1) === 66)) {
|
|
240
|
+
checkStringLimit(str, line);
|
|
241
|
+
return builder.string(str, line, false);
|
|
242
|
+
}
|
|
243
|
+
const exactIsoShape = (str.length === 10 && str.charCodeAt(4) === 45 && str.charCodeAt(7) === 45) ||
|
|
244
|
+
(str.length === 20 && str.charCodeAt(4) === 45 && str.charCodeAt(7) === 45 &&
|
|
245
|
+
str.charCodeAt(10) === 84 && str.charCodeAt(13) === 58 &&
|
|
246
|
+
str.charCodeAt(16) === 58 && str.charCodeAt(19) === 90);
|
|
247
|
+
if (exactIsoShape) {
|
|
248
|
+
const date = parseDateUTC(str, strict, line);
|
|
249
|
+
if (date !== null)
|
|
250
|
+
return builder.instant(date, line);
|
|
251
|
+
checkStringLimit(str, line);
|
|
252
|
+
return builder.string(str, line, false);
|
|
253
|
+
}
|
|
254
|
+
if (NUMBER_RE.test(str)) {
|
|
255
|
+
const n = Number(str);
|
|
256
|
+
if (isFloatForm(str)) {
|
|
257
|
+
if (!Number.isFinite(n)) {
|
|
258
|
+
if (strict)
|
|
259
|
+
throw new LimaError({
|
|
260
|
+
code: 'INVALID_NUMBER', line,
|
|
261
|
+
message: `LIMA: float value overflows to a non-finite value at line ${line}: "${str}"`,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else if (n === 0 && !isZeroLiteral(str)) {
|
|
265
|
+
if (strict)
|
|
266
|
+
throw new LimaError({
|
|
267
|
+
code: 'INVALID_NUMBER', line,
|
|
268
|
+
message: `LIMA: non-zero float value underflows to zero at line ${line}: "${str}"`,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
return builder.float(n === 0 ? 0 : n, line);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else if (Math.abs(n) <= Number.MAX_SAFE_INTEGER) {
|
|
276
|
+
return builder.int(n === 0 ? 0 : n, line);
|
|
277
|
+
}
|
|
278
|
+
// Outside the safe integer range, or overflow/underflow already
|
|
279
|
+
// handled above in non-strict mode: fall through to string.
|
|
280
|
+
}
|
|
281
|
+
if (!str.includes('@') && DATE_PRE_RE.test(str)) {
|
|
282
|
+
const date = parseDateUTC(str, strict, line);
|
|
283
|
+
if (date !== null)
|
|
284
|
+
return builder.instant(date, line);
|
|
285
|
+
}
|
|
286
|
+
checkStringLimit(str, line);
|
|
287
|
+
return builder.string(str, line, false);
|
|
288
|
+
};
|
|
289
|
+
// ─── Scalar / quoting ──────────────────────────────────────────────────────
|
|
290
|
+
const ESCAPED_HASH_RE = /\\#/g;
|
|
291
|
+
const ANY_ESCAPE_RE = /\\(u[0-9a-fA-F]{0,4}|U[0-9a-fA-F]{0,8}|x[0-9a-fA-F]{0,2}|.)/gs;
|
|
292
|
+
const SINGLE_CHAR_ESCAPES = '"\\/bfnrt'; // deliberately excludes '0' — Core Appendix A: \0 is unknown, not a null shorthand.
|
|
293
|
+
const U_ESCAPE_RE = /^u([0-9a-fA-F]{4})$/;
|
|
294
|
+
const CAP_U_ESCAPE_RE = /^U([0-9a-fA-F]{8})$/;
|
|
295
|
+
const X_ESCAPE_RE = /^x([0-9a-fA-F]{2})$/;
|
|
296
|
+
const isValidEscape = (escape) => {
|
|
297
|
+
if (escape.length === 1)
|
|
298
|
+
return SINGLE_CHAR_ESCAPES.includes(escape);
|
|
299
|
+
const u = escape.match(U_ESCAPE_RE);
|
|
300
|
+
if (u) {
|
|
301
|
+
const cp = parseInt(u[1], 16);
|
|
302
|
+
return cp < 0xd800 || cp > 0xdfff;
|
|
303
|
+
}
|
|
304
|
+
const bigU = escape.match(CAP_U_ESCAPE_RE);
|
|
305
|
+
if (bigU)
|
|
306
|
+
return parseInt(bigU[1], 16) <= 0x10ffff;
|
|
307
|
+
return X_ESCAPE_RE.test(escape);
|
|
308
|
+
};
|
|
309
|
+
export const unescapeDQ = (s, strict = false, line = 0) => {
|
|
310
|
+
if (!s.includes('\\'))
|
|
311
|
+
return s;
|
|
312
|
+
if (strict) {
|
|
313
|
+
for (const m of s.matchAll(ANY_ESCAPE_RE)) {
|
|
314
|
+
if (!isValidEscape(m[0].slice(1))) {
|
|
315
|
+
throw new LimaError({ code: 'INVALID_ESCAPE', line, message: `LIMA: unknown escape sequence "${m[0]}" at line ${line}` });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return s.replace(ANY_ESCAPE_RE, (full) => {
|
|
320
|
+
const e = full.slice(1);
|
|
321
|
+
if (!isValidEscape(e))
|
|
322
|
+
return full;
|
|
323
|
+
switch (e[0]) {
|
|
324
|
+
case '"': return '"';
|
|
325
|
+
case '\\': return '\\';
|
|
326
|
+
case '/': return '/';
|
|
327
|
+
case 'b': return '\b';
|
|
328
|
+
case 'f': return '\f';
|
|
329
|
+
case 'n': return '\n';
|
|
330
|
+
case 'r': return '\r';
|
|
331
|
+
case 't': return '\t';
|
|
332
|
+
case 'u': return String.fromCharCode(parseInt(e.slice(1), 16));
|
|
333
|
+
case 'U': return String.fromCodePoint(parseInt(e.slice(1), 16));
|
|
334
|
+
case 'x': return String.fromCharCode(parseInt(e.slice(1), 16));
|
|
335
|
+
default: return full;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
export const stripComment = (val) => {
|
|
340
|
+
let quote = 0;
|
|
341
|
+
for (let i = 0; i < val.length; i++) {
|
|
342
|
+
const cc = val.charCodeAt(i);
|
|
343
|
+
if (quote) {
|
|
344
|
+
if (cc === 92)
|
|
345
|
+
i++;
|
|
346
|
+
else if (cc === quote)
|
|
347
|
+
quote = 0;
|
|
348
|
+
}
|
|
349
|
+
else if (cc === 34 || cc === 39) {
|
|
350
|
+
quote = cc;
|
|
351
|
+
}
|
|
352
|
+
else if (cc === 92 && val.charCodeAt(i + 1) === 35) {
|
|
353
|
+
i++;
|
|
354
|
+
}
|
|
355
|
+
else if (cc === 35) {
|
|
356
|
+
return val.slice(0, i).trimEnd().replace(ESCAPED_HASH_RE, '#');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return val.replace(ESCAPED_HASH_RE, '#');
|
|
360
|
+
};
|
|
361
|
+
/** Strips a key's surrounding quotes (unescaping double-quoted keys), or returns it unchanged. */
|
|
362
|
+
export const stripKeyQuotes = (s) => {
|
|
363
|
+
const f = s.charCodeAt(0);
|
|
364
|
+
if (f === 39 && s.charCodeAt(s.length - 1) === 39)
|
|
365
|
+
return s.slice(1, -1);
|
|
366
|
+
if (f === 34 && s.charCodeAt(s.length - 1) === 34)
|
|
367
|
+
return unescapeDQ(s.slice(1, -1));
|
|
368
|
+
return s;
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Quoted-or-typed scalar, shared by every value position (top-level inline
|
|
372
|
+
* values, flow-sequence/flow-mapping items, block-array scalar items).
|
|
373
|
+
* `topLevel` gates two checks that only apply at the outermost resolveValue
|
|
374
|
+
* call site in the legacy parser and are deliberately not extended to flow
|
|
375
|
+
* items here, to keep this a faithful behavioral port: the "unclosed flow
|
|
376
|
+
* bracket" throw and the "non-whitespace after closing quote" strict throw.
|
|
377
|
+
*/
|
|
378
|
+
export const parseQuotedOrTyped = (raw, ctx, line, topLevel, builder) => {
|
|
379
|
+
const first = raw.charCodeAt(0);
|
|
380
|
+
if (first === 34 || first === 39) {
|
|
381
|
+
if (raw.charCodeAt(raw.length - 1) === first) {
|
|
382
|
+
const unquoted = raw.slice(1, -1);
|
|
383
|
+
const value = first === 34 ? unescapeDQ(unquoted, ctx.strict, line) : unquoted.replace(/\\'/g, "'");
|
|
384
|
+
checkStringLimit(value, line);
|
|
385
|
+
return builder.string(value, line, true);
|
|
386
|
+
}
|
|
387
|
+
if (topLevel && ctx.strict) {
|
|
388
|
+
throw new LimaError({ code: 'INVALID_QUOTE', line, message: `LIMA: non-whitespace content after closing quote at line ${line}` });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (raw !== '' && raw !== 'null' && raw !== '~' && raw !== 'true' && raw !== 'false' &&
|
|
392
|
+
!((first >= 48 && first <= 57) || first === 45 || first === 46)) {
|
|
393
|
+
checkStringLimit(raw, line);
|
|
394
|
+
return builder.string(raw, line, false);
|
|
395
|
+
}
|
|
396
|
+
return buildTyped(raw, ctx.strict, line, builder);
|
|
397
|
+
};
|
|
398
|
+
export const parseScalarValue = (raw, ctx, line, builder) => {
|
|
399
|
+
const first = raw.charCodeAt(0);
|
|
400
|
+
if (ctx.strict && (first === 91 || first === 123)) {
|
|
401
|
+
throw new LimaError({
|
|
402
|
+
code: 'INVALID_FLOW_SYNTAX', line,
|
|
403
|
+
message: `LIMA: unclosed flow ${first === 91 ? 'sequence' : 'mapping'} at line ${line}`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return parseQuotedOrTyped(raw, ctx, line, true, builder);
|
|
407
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level key tokenization — a hand-written scanner, not a regex, for
|
|
3
|
+
* two reasons: it's measurably faster (the regex-split approach it
|
|
4
|
+
* replaced re-scanned the whole document with a single large alternation
|
|
5
|
+
* regex on every parse), and it removes the parser's only remaining
|
|
6
|
+
* dependency on genuine backtracking-dependent regex semantics, keeping
|
|
7
|
+
* the implementation representable by an RE2-family (linear-time,
|
|
8
|
+
* non-backtracking) engine — relevant for a future Rust port, where the
|
|
9
|
+
* standard `regex` crate is itself RE2-derived and cannot express
|
|
10
|
+
* backtracking-dependent patterns at all.
|
|
11
|
+
*
|
|
12
|
+
* Reproduces (verified by extensive differential testing against the
|
|
13
|
+
* regex it replaced — over 100,000 fuzzed and structured cases, the full
|
|
14
|
+
* conformance corpus, and hand-built adversarial edge cases, zero
|
|
15
|
+
* divergences) the exact matching behavior of:
|
|
16
|
+
*
|
|
17
|
+
* /^(?:([a-zA-Z\d_][a-zA-Z\d_:-]*)|'([^']*)'|"((?:[^"\\]|\\.)*)"):( *\n| )/gm
|
|
18
|
+
*
|
|
19
|
+
* including subtle backtracking-dependent cases that are easy to get
|
|
20
|
+
* wrong without empirical verification against the real parser:
|
|
21
|
+
* - `a:b: value` → key "a:b" (colon is a legal mid-key character; the
|
|
22
|
+
* mandatory separator-introducing `:` is whichever colon within the
|
|
23
|
+
* greedily-matched run is the RIGHTMOST one for which a valid
|
|
24
|
+
* separator follows it — matching real regex backtracking, which
|
|
25
|
+
* always tries the longest capture first and shrinks it only when the
|
|
26
|
+
* rest of the pattern fails to match).
|
|
27
|
+
* - `key: value` (two spaces) → the separator consumes exactly one
|
|
28
|
+
* space; the second space becomes part of the raw value.
|
|
29
|
+
* - `key:` at end of input with no following space or `\n` → no match
|
|
30
|
+
* at all (the separator alternative `( *\n| )` requires an actual
|
|
31
|
+
* trailing newline or a literal space; end-of-string alone satisfies
|
|
32
|
+
* neither).
|
|
33
|
+
* - Quoted keys (`'...'`, `"..."`) may contain a literal `\n` — the
|
|
34
|
+
* character classes inside quotes don't exclude it.
|
|
35
|
+
* - A backslash directly followed by a line terminator inside a
|
|
36
|
+
* double-quoted key is NOT a valid `\\.` escape — the source regex's
|
|
37
|
+
* `.` never matches a line terminator without the `s` flag (not set),
|
|
38
|
+
* so this fails the match entirely rather than consuming the pair.
|
|
39
|
+
*/
|
|
40
|
+
export interface KeyMatch {
|
|
41
|
+
/** One-based source line of the key. */
|
|
42
|
+
line: number;
|
|
43
|
+
/** Start of the whole match (a line-start position). */
|
|
44
|
+
matchStart: number;
|
|
45
|
+
/** Position right after the mandatory `:` — start of the separator. */
|
|
46
|
+
sepStart: number;
|
|
47
|
+
/** Position right after the separator — start of this key's raw content. */
|
|
48
|
+
rawStart: number;
|
|
49
|
+
/** True for the `( *\n)` block-form separator, false for the single-space inline form. */
|
|
50
|
+
isBlock: boolean;
|
|
51
|
+
/** End of the first physical value line, populated for inline matches. */
|
|
52
|
+
inlineEnd?: number;
|
|
53
|
+
unquoted?: string;
|
|
54
|
+
singleQuoted?: string;
|
|
55
|
+
doubleQuotedRaw?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* All top-level key matches, in document order. Only line-start positions
|
|
59
|
+
* (index 0, and every position right after a `\n`) are attempted,
|
|
60
|
+
* mirroring `^` with the multiline flag — content between one match's raw
|
|
61
|
+
* start and the next match's start (or end of document) is that key's raw
|
|
62
|
+
* value text, exactly as the regex-split's discarded inter-match segments
|
|
63
|
+
* were.
|
|
64
|
+
*/
|
|
65
|
+
export declare const scanKeys: (frontMatter: string) => KeyMatch[];
|
|
66
|
+
/** Stateful top-level token cursor; `next()` performs no text slicing. */
|
|
67
|
+
export declare class KeyCursor {
|
|
68
|
+
private readonly source;
|
|
69
|
+
tokenLine: number;
|
|
70
|
+
matchStart: number;
|
|
71
|
+
sepStart: number;
|
|
72
|
+
rawStart: number;
|
|
73
|
+
inlineEnd: number;
|
|
74
|
+
keyStart: number;
|
|
75
|
+
keyEnd: number;
|
|
76
|
+
keyKind: number;
|
|
77
|
+
isBlock: boolean;
|
|
78
|
+
private pos;
|
|
79
|
+
private line;
|
|
80
|
+
constructor(source: string);
|
|
81
|
+
next(): boolean;
|
|
82
|
+
}
|