@openpresentation/opf-editor 0.26.3 → 0.26.5

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,623 @@
1
+ // The YAML text adapter of the source editor (artifacts/source-editor/SPEC.md, section 3.2): what a position in a YAML draft means. A line-and-indent
2
+ // reader, not a parser, so it works while the text is half typed: it walks the lines once, keeping the stack of mappings and lists the lines open
3
+ // (the column their entries start at), and answers for the caret the same things the JSON adapter does: the token being typed, whether a key, a value
4
+ // or a list item belongs there, the path of the node in the document, the keys already there and a skeleton of the document that the schema reads.
5
+ // It handles block style, flow collections on one line, quoted keys and values, block scalars and comments; whatever it cannot place (a flow collection
6
+ // over several lines, text in a block scalar, a comment) gives null, so no list opens there. Plain functions, no DOM.
7
+ import { parse as parseYaml } from "yaml";
8
+
9
+ const KEY_WORD = /[A-Za-z0-9_.$+-]/;
10
+ const BLOCK_HEADER = /^[|>][+-]?\d*[+-]?\s*(?:#.*)?$/;
11
+ const isSpace = (character) => character === " " || character === "\t";
12
+
13
+ // ---- one line ----------------------------------------------------------------------------------------------------------------------
14
+ /** The `- ` markers a line starts with (their columns) and the text after them: `{ indent, markers, rest, restStart }`. */
15
+ export function readMarkers(line) {
16
+ let index = 0;
17
+ while (isSpace(line[index])) index++;
18
+ const indent = index;
19
+ const markers = [];
20
+ while (line[index] === "-" && (index + 1 === line.length || isSpace(line[index + 1]))) {
21
+ markers.push(index);
22
+ index++;
23
+ while (isSpace(line[index])) index++;
24
+ }
25
+ return { indent, markers, rest: line.slice(index), restStart: index };
26
+ }
27
+
28
+ /**
29
+ * The `key:` a text starts with, or null: `{ key, keyStart, keyEnd, colon, valueStart }` (columns in `rest`). A plain key runs to the first colon that
30
+ * is followed by a space or the end (so `top+middle:left` is one key); a quoted key to its closing quote.
31
+ */
32
+ export function readEntry(rest) {
33
+ if (rest === "" || /^[[{|>&*!%@`#]/.test(rest)) return null;
34
+ let keyEnd;
35
+ let key;
36
+ let keyStart = 0;
37
+ if (rest[0] === '"' || rest[0] === "'") {
38
+ const quote = rest[0];
39
+ let index = 1;
40
+ for (; index < rest.length; index++) {
41
+ if (quote === '"' && rest[index] === "\\") { index++; continue; }
42
+ if (rest[index] === quote) {
43
+ if (quote === "'" && rest[index + 1] === "'") { index++; continue; }
44
+ break;
45
+ }
46
+ }
47
+ if (index >= rest.length) return null;
48
+ keyEnd = index + 1;
49
+ key = quote === '"' ? safeJson(rest.slice(0, keyEnd)) : rest.slice(1, index).replaceAll("''", "'");
50
+ keyStart = 0;
51
+ let after = keyEnd;
52
+ while (isSpace(rest[after])) after++;
53
+ if (rest[after] !== ":" || !(after + 1 === rest.length || isSpace(rest[after + 1]))) return null;
54
+ return { key, keyStart, keyEnd, colon: after, valueStart: after + 1, quoted: true };
55
+ }
56
+ for (let index = 0; index < rest.length; index++) {
57
+ if (rest[index] === "#" && index > 0 && isSpace(rest[index - 1])) return null;
58
+ if (rest[index] === ":" && (index + 1 === rest.length || isSpace(rest[index + 1]))) {
59
+ key = rest.slice(0, index).trimEnd();
60
+ if (key === "") return null;
61
+ return { key, keyStart, keyEnd: key.length, colon: index, valueStart: index + 1, quoted: false };
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function safeJson(text) {
68
+ try { return JSON.parse(text); } catch { return text.slice(1, -1); }
69
+ }
70
+
71
+ /** A plain or quoted scalar as the value it stands for, for the skeleton the schema reads. */
72
+ export function scalarValue(text) {
73
+ const value = text.trim();
74
+ if (value === "" || value === "~" || value === "null") return null;
75
+ if (value === "true") return true;
76
+ if (value === "false") return false;
77
+ if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) return Number(value);
78
+ if (value[0] === '"') return safeJson(value.length > 1 && value.endsWith('"') ? value : `${value}"`);
79
+ if (value[0] === "'") return value.slice(1, value.endsWith("'") && value.length > 1 ? -1 : undefined).replaceAll("''", "'");
80
+ return value.replace(/\s+#.*$/, "");
81
+ }
82
+
83
+ /** The change in flow-collection depth over a text (brackets outside quotes and comments), and the depth the text ends at, starting from `depth`. */
84
+ export function flowDepth(text, depth = 0) {
85
+ let quote = null;
86
+ for (let index = 0; index < text.length; index++) {
87
+ const character = text[index];
88
+ if (quote) {
89
+ if (quote === '"' && character === "\\") index++;
90
+ else if (character === quote) quote = null;
91
+ } else if (character === '"' || character === "'") quote = character;
92
+ else if (character === "#" && (index === 0 || isSpace(text[index - 1]))) break;
93
+ else if (character === "[" || character === "{") depth++;
94
+ else if ((character === "]" || character === "}") && depth > 0) depth--;
95
+ }
96
+ return depth;
97
+ }
98
+
99
+ // ---- the stack of open collections -------------------------------------------------------------------------------------------------
100
+ class Frame {
101
+ constructor(kind, col, parent, via) {
102
+ this.kind = kind;
103
+ this.col = col;
104
+ this.parent = parent;
105
+ this.via = via;
106
+ this.keys = new Map();
107
+ this.lastKey = undefined;
108
+ this.count = 0;
109
+ this.items = [];
110
+ }
111
+ get path() {
112
+ const path = [];
113
+ for (let frame = this; frame?.parent; frame = frame.parent) path.unshift(frame.via);
114
+ return path;
115
+ }
116
+ get open() { return this.kind === "map" && this.lastKey !== undefined && this.keys.get(this.lastKey)?.open === true; }
117
+ }
118
+
119
+ class Stack {
120
+ constructor() { this.frames = []; }
121
+ get top() { return this.frames.at(-1); }
122
+ pop() { return this.frames.pop(); }
123
+ popAbove(col) { while (this.top && this.top.col > col) this.pop(); }
124
+ push(kind, col) {
125
+ const parent = this.top;
126
+ const via = !parent ? null : parent.kind === "map" ? parent.lastKey : parent.count - 1;
127
+ const frame = new Frame(kind, col, parent ?? null, via);
128
+ this.frames.push(frame);
129
+ return frame;
130
+ }
131
+ /** The mapping whose keys start at `col`: the open one, or a new one under the key or list item that opened it. */
132
+ mapAt(col) {
133
+ this.popAbove(col);
134
+ while (this.top?.kind === "seq" && this.top.col === col) this.pop();
135
+ if (this.top?.kind === "map" && this.top.col === col) return this.top;
136
+ return this.push("map", col);
137
+ }
138
+ /** The list whose dashes are at `col`, with a new item counted. */
139
+ seqAt(col) {
140
+ this.popAbove(col);
141
+ if (this.top?.kind === "seq" && this.top.col === col) {
142
+ this.top.count++;
143
+ return this.top;
144
+ }
145
+ const frame = this.push("seq", col);
146
+ frame.count++;
147
+ return frame;
148
+ }
149
+ }
150
+
151
+ function shapeOfValue(value) {
152
+ const text = value.trim();
153
+ if (text === "" || text.startsWith("#")) return { open: true };
154
+ if (BLOCK_HEADER.test(text)) return { block: true };
155
+ if (text[0] === "[") return { flow: "seq" };
156
+ if (text[0] === "{") return { flow: "map" };
157
+ if (text[0] === "&" || text[0] === "*" || text[0] === "!") return { open: true };
158
+ return { scalar: scalarValue(text) };
159
+ }
160
+
161
+ /** Apply the entries of one line to the stack: its dashes, then its key (when it has one). Returns `{ seq, map }`: the list and the mapping the line is in. */
162
+ function enter(stack, parsed, entry, shape) {
163
+ let frame = null;
164
+ for (const col of parsed.markers) {
165
+ frame = stack.seqAt(col);
166
+ frame.items[frame.count - 1] = { open: true };
167
+ }
168
+ let mapFrame = null;
169
+ if (entry) {
170
+ mapFrame = stack.mapAt(parsed.restStart + entry.keyStart);
171
+ mapFrame.keys.set(entry.key, shape);
172
+ mapFrame.lastKey = entry.key;
173
+ if (frame?.kind === "seq") frame.items[frame.count - 1] = { map: mapFrame };
174
+ } else if (frame && shape && "scalar" in shape) frame.items[frame.count - 1] = shape;
175
+ return { seq: frame, map: mapFrame };
176
+ }
177
+
178
+ // ---- the lines ---------------------------------------------------------------------------------------------------------------------
179
+ let lastLines = null;
180
+
181
+ /** The lines of a text, `[{ start, end, text }]` (a line break is `\n`, `\r\n` or `\r`), remembered for the last text asked about. */
182
+ export function linesOf(text) {
183
+ if (lastLines?.text === text) return lastLines.lines;
184
+ const lines = [];
185
+ let start = 0;
186
+ for (let index = 0; index <= text.length; index++) {
187
+ const code = index < text.length ? text.charCodeAt(index) : -1;
188
+ if (index === text.length || code === 10 || code === 13) {
189
+ lines.push({ start, end: index, text: text.slice(start, index) });
190
+ if (code === 13 && text.charCodeAt(index + 1) === 10) index++;
191
+ start = index + 1;
192
+ }
193
+ }
194
+ lastLines = { text, lines };
195
+ return lines;
196
+ }
197
+
198
+ const lineIndex = (lines, position) => {
199
+ let low = 0, high = lines.length - 1;
200
+ while (low < high) {
201
+ const middle = (low + high + 1) >> 1;
202
+ if (lines[middle].start <= position) low = middle; else high = middle - 1;
203
+ }
204
+ return low;
205
+ };
206
+
207
+ /** Whether `column` of `line` is inside a comment (a `#` at the start or after a space, outside quotes). */
208
+ function inComment(line, column) {
209
+ let quote = null;
210
+ for (let index = 0; index < column && index < line.length; index++) {
211
+ const character = line[index];
212
+ if (quote) {
213
+ if (quote === '"' && character === "\\") index++;
214
+ else if (character === quote) quote = null;
215
+ } else if (character === '"' || character === "'") {
216
+ // A quote opens a string only where a value or key can start.
217
+ if (index === 0 || /[\s[{,:-]/.test(line[index - 1])) quote = character;
218
+ } else if (character === "#" && (index === 0 || isSpace(line[index - 1]))) return true;
219
+ }
220
+ return false;
221
+ }
222
+
223
+ /** The quote character of a quoted scalar that `value` opens and does not close, or null (a quoted scalar may run over several lines). */
224
+ export function openQuote(value) {
225
+ const text = value.trimStart();
226
+ const quote = text[0];
227
+ if (quote !== '"' && quote !== "'") return null;
228
+ return closesQuote(text.slice(1), quote) ? null : quote;
229
+ }
230
+
231
+ /** Whether `text` (the rest of a quoted scalar) holds its closing quote. */
232
+ function closesQuote(text, quote) {
233
+ for (let index = 0; index < text.length; index++) {
234
+ if (quote === '"' && text[index] === "\\") { index++; continue; }
235
+ if (text[index] === quote) {
236
+ if (quote === "'" && text[index + 1] === "'") { index++; continue; }
237
+ return true;
238
+ }
239
+ }
240
+ return false;
241
+ }
242
+
243
+ /**
244
+ * Walk the lines before `stop` (a line index), applying each to `stack`. Lines inside a block scalar, a flow collection or a quoted scalar that span
245
+ * lines, blank lines, comments and document markers change nothing. Returns `{ block, flow, quote, open }`: the column a block scalar's text must be
246
+ * indented past (or -1), the depth of an unclosed flow collection (0 when none), the quote of an unclosed quoted scalar (or null) at line `stop`, so
247
+ * the caller knows whether that line is inside any of them, and for a flow collection `open`: the line it began on and where it sits in the stack.
248
+ */
249
+ function walk(lines, stop, stack) {
250
+ let block = -1;
251
+ let flow = 0;
252
+ let quote = null;
253
+ let open = null;
254
+ for (let number = 0; number < stop; number++) {
255
+ const text = lines[number].text;
256
+ if (quote) { if (closesQuote(text, quote)) quote = null; continue; }
257
+ if (text.trim() === "") continue;
258
+ const indent = text.length - text.trimStart().length;
259
+ if (block >= 0) {
260
+ if (indent > block) continue;
261
+ block = -1;
262
+ }
263
+ if (flow > 0) { flow = flowDepth(text, flow); continue; }
264
+ const trimmed = text.trimStart();
265
+ if (trimmed.startsWith("#") || trimmed === "---" || trimmed === "..." || trimmed.startsWith("%")) continue;
266
+ const parsed = readMarkers(text);
267
+ const entry = readEntry(parsed.rest);
268
+ const value = entry ? parsed.rest.slice(entry.valueStart) : parsed.rest;
269
+ const shape = entry ? shapeOfValue(value) : (parsed.markers.length ? shapeOfValue(value) : undefined);
270
+ if (!entry && !parsed.markers.length) continue;
271
+ const placed = enter(stack, parsed, entry, shape);
272
+ if (shape?.block) block = entry ? parsed.restStart + entry.keyStart : parsed.markers.at(-1);
273
+ else if (shape?.flow) {
274
+ flow = flowDepth(value, 0);
275
+ open = flow > 0 ? { number, parsed, entry, value, placed } : null;
276
+ } else if (shape && "scalar" in shape) quote = openQuote(value);
277
+ }
278
+ return { block, flow, quote, open };
279
+ }
280
+
281
+ // ---- flow collections on one line --------------------------------------------------------------------------------------------------
282
+ /**
283
+ * The place of the caret inside flow collections that began on this line: the containers open at `column` (outermost first), each with the keys and
284
+ * the number of items before the caret. `null` when the caret is not in a flow collection.
285
+ */
286
+ function flowAt(rest, column) {
287
+ const containers = [];
288
+ let quote = null;
289
+ for (let index = 0; index < column; index++) {
290
+ const character = rest[index];
291
+ if (quote) {
292
+ if (quote === '"' && character === "\\") { index++; continue; }
293
+ if (character === quote) quote = null;
294
+ continue;
295
+ }
296
+ if (character === '"' || character === "'") { quote = character; continue; }
297
+ if (character === "[" || character === "{") {
298
+ containers.push({ kind: character === "[" ? "seq" : "map", count: 0, keys: [], key: undefined, expect: character === "[" ? "item" : "key", start: index });
299
+ } else if ((character === "]" || character === "}") && containers.length) {
300
+ containers.pop();
301
+ const parent = containers.at(-1);
302
+ if (parent) { parent.expect = parent.kind === "seq" ? "item" : "after"; }
303
+ } else if (character === ",") {
304
+ const top = containers.at(-1);
305
+ if (top) { top.count++; top.expect = top.kind === "seq" ? "item" : "key"; top.key = undefined; }
306
+ } else if (character === ":" && containers.at(-1)?.kind === "map" && (index + 1 >= rest.length || /[\s,}\]]/.test(rest[index + 1]) || containers.at(-1).expect === "key")) {
307
+ const top = containers.at(-1);
308
+ if (top.expect === "key") {
309
+ const before = rest.slice(top.start + 1, index);
310
+ const word = before.slice(before.lastIndexOf(",") + 1).trim().replace(/^["']|["']$/g, "");
311
+ top.key = word;
312
+ top.keys.push(word);
313
+ top.expect = "value";
314
+ }
315
+ }
316
+ }
317
+ return containers.length ? containers : null;
318
+ }
319
+
320
+ // ---- the context at a caret --------------------------------------------------------------------------------------------------------
321
+ // The document the schema reads: every collection on the path to the caret in full (with the scalar values of its other keys), the others cut to
322
+ // their type. `catalogs` is parsed in full so embedded records are offered as values.
323
+ function skeletonOf(frame, lines) {
324
+ const chain = [];
325
+ for (let current = frame; current; current = current.parent) chain.unshift(current);
326
+ const shallow = (shape) => {
327
+ if (!shape) return {};
328
+ if ("scalar" in shape) return shape.scalar;
329
+ if (shape.flow === "seq") return [];
330
+ if (shape.flow === "map") return {};
331
+ if (shape.block) return "";
332
+ return {};
333
+ };
334
+ const build = (index) => {
335
+ const current = chain[index];
336
+ const next = chain[index + 1];
337
+ const onPath = (via) => next !== undefined && next.parent === current && next.via === via;
338
+ if (current.kind === "map") {
339
+ const out = {};
340
+ for (const [key, shape] of current.keys) out[key] = onPath(key) ? build(index + 1) : shallow(shape);
341
+ if (next && next.parent === current && typeof next.via === "string" && !(next.via in out)) out[next.via] = build(index + 1);
342
+ return out;
343
+ }
344
+ const out = new Array(current.count);
345
+ for (let position = 0; position < current.count; position++) {
346
+ const item = current.items[position];
347
+ out[position] = onPath(position) ? build(index + 1) : item && "scalar" in item ? item.scalar : {};
348
+ }
349
+ return out;
350
+ };
351
+ const root = chain.length ? build(0) : {};
352
+ const catalogs = catalogsBlock(lines);
353
+ if (catalogs !== undefined && root && typeof root === "object" && !Array.isArray(root)) {
354
+ try { const value = parseYaml(catalogs); if (value?.catalogs) root.catalogs = value.catalogs; } catch { /* A broken catalogs block offers no embedded records. */ }
355
+ }
356
+ return root;
357
+ }
358
+
359
+ // The text of the top-level `catalogs:` block (the key and the lines indented under it).
360
+ function catalogsBlock(lines) {
361
+ for (let number = 0; number < lines.length; number++) {
362
+ if (!/^catalogs\s*:/.test(lines[number].text)) continue;
363
+ let end = number + 1;
364
+ while (end < lines.length && (lines[end].text.trim() === "" || /^\s/.test(lines[end].text) || lines[end].text.startsWith("- "))) end++;
365
+ return lines.slice(number, end).map((line) => line.text).join("\n");
366
+ }
367
+ return undefined;
368
+ }
369
+
370
+ // Wrap the skeleton in the path the text is the body of (YAML in a fence of a Markdown slide).
371
+ function wrapped(document, prefix) {
372
+ let root = document;
373
+ for (let index = prefix.length - 1; index >= 0; index--) {
374
+ const wrapper = typeof prefix[index] === "number" ? [] : {};
375
+ wrapper[prefix[index]] = root;
376
+ root = wrapper;
377
+ }
378
+ return root;
379
+ }
380
+
381
+ /** Where the scalar value that begins at `from` ends, on a line ending at `lineEnd`: `{ quoted, closed, start, end, from, to }` (a quoted token has its quotes in `start..end`). */
382
+ function valueToken(text, from, position, lineEnd, flow) {
383
+ const first = text[from];
384
+ if (first === '"' || first === "'") {
385
+ let close = -1;
386
+ for (let index = from + 1; index < lineEnd; index++) {
387
+ if (first === '"' && text[index] === "\\") { index++; continue; }
388
+ if (text[index] === first) {
389
+ if (first === "'" && text[index + 1] === "'") { index++; continue; }
390
+ close = index;
391
+ break;
392
+ }
393
+ }
394
+ if (close >= 0 && position > close) return null;
395
+ const to = close >= 0 ? close : Math.max(position, from + 1);
396
+ return { quoted: true, closed: close >= 0, start: from, end: close >= 0 ? close + 1 : to, from: from + 1, to };
397
+ }
398
+ let end = from;
399
+ while (end < lineEnd) {
400
+ if (flow ? /[,\]}]/.test(text[end]) : isSpace(text[end]) && text[end + 1] === "#") break;
401
+ end++;
402
+ }
403
+ while (end > from && isSpace(text[end - 1])) end--;
404
+ end = Math.max(end, position);
405
+ return { quoted: false, closed: false, start: from, end, from, to: end };
406
+ }
407
+
408
+ /** The word (or quoted key) around the caret where a key goes: `{ quoted, closed, start, end, from, to }`. */
409
+ function keyToken(text, line, restStart, position) {
410
+ const start = line.start + restStart;
411
+ const first = text[start];
412
+ if ((first === '"' || first === "'") && start < position) {
413
+ let close = -1;
414
+ for (let index = position; index < line.end; index++) if (text[index] === first) { close = index; break; }
415
+ return { quoted: true, closed: close >= 0, start, end: close >= 0 ? close + 1 : position, from: start + 1, to: close >= 0 ? close : position };
416
+ }
417
+ let end = position;
418
+ while (end < line.end && (KEY_WORD.test(text[end]) || (text[end] === ":" && end + 1 < line.end && KEY_WORD.test(text[end + 1])))) end++;
419
+ return { quoted: false, closed: false, start, end, from: start, to: end };
420
+ }
421
+
422
+ /**
423
+ * What the draft means at `position`, or null: `{ format: "yaml", mode, token, path, key, existing, document, ... }`.
424
+ * `mode` is "key" (a property name goes here; `path` is the mapping), "value" (the value of the property at `path`; `key` is its name) or "item" (an
425
+ * entry of a flow list, which ends with the entry's index). `existing` holds the other property names of the mapping. `document` is the skeleton that
426
+ * `describeField` reads. A key after a list dash has `seq`: `{ path, index }` of the list item, `itemStart` when nothing else is on the item yet.
427
+ * `column` is the column of the entry (where a key goes), `indent` the indentation of its line, `needsSpace` that a value place has no space after the
428
+ * colon yet. Options: `explicit` (Ctrl+Space also takes an empty place), `prefix` (the path of the node the text is the body of, for YAML in a fence of
429
+ * a Markdown slide), `exclude` (keys never offered, the `slides` of front matter).
430
+ */
431
+ export function yamlContextAt(text, position, options = {}) {
432
+ const context = readContext(text, position, options);
433
+ // The text the context was read from, and the path it is the body of: the references the value lists offer are looked up in it.
434
+ return context ? { ...context, sourceText: text, prefix: options.prefix ?? [] } : null;
435
+ }
436
+
437
+ function readContext(text, position, { explicit = false, prefix = [], exclude = [] }) {
438
+ const lines = linesOf(text);
439
+ const number = lineIndex(lines, position);
440
+ const line = lines[number];
441
+ const column = position - line.start;
442
+ const lineText = line.text;
443
+ if (inComment(lineText, column)) return null;
444
+ const trimmedLine = lineText.trim();
445
+ if (trimmedLine === "---" || trimmedLine === "...") return null;
446
+ const stack = new Stack();
447
+ const state = walk(lines, number, stack);
448
+ const lineIndent = lineText.length - lineText.trimStart().length;
449
+ if (state.block >= 0 && (trimmedLine === "" ? column > state.block : lineIndent > state.block)) return null;
450
+ const before = lineText.slice(0, column);
451
+ // A line inside a quoted scalar that began above: text, in which only `{{` of a variable is completed.
452
+ if (state.quote) return continuationContext({ text, line, position, before, explicit });
453
+ if (state.flow > 0) return flowContinuation({ text, lines, number, line, position, column, explicit, prefix, exclude, open: state.open });
454
+ const parsed = readMarkers(before);
455
+ // The key is read from the whole line: a colon after the caret that ends the key (`top:le|ft: x`) is not the colon before it.
456
+ const whole = readMarkers(lineText);
457
+ const found = readEntry(whole.rest);
458
+ const entry = found && column > whole.restStart + found.colon ? readEntry(parsed.rest) : null;
459
+ const shared = { text, lines, number, line, position, parsed, explicit, exclude, prefix, stack, lineIndent };
460
+ return entry ? valueContext({ ...shared, entry }) : keyOrItemContext(shared);
461
+ }
462
+
463
+ const VARIABLE_OPEN = /\{\{[A-Za-z0-9._-]*$/;
464
+
465
+ /**
466
+ * A line of text that continues a scalar begun on a line above (a quoted scalar left open, or a sentence that runs on): nothing to complete in it
467
+ * except the name of a variable after `{{`. The context is a value in the text sense (`multiline`), with no path.
468
+ */
469
+ function continuationContext({ text, line, position, before, explicit }) {
470
+ if (!VARIABLE_OPEN.test(before)) return null;
471
+ let end = position;
472
+ while (end < line.end && /[A-Za-z0-9._-]/.test(text[end])) end++;
473
+ const start = line.start + (line.text.length - line.text.trimStart().length);
474
+ return { format: "yaml", mode: "value", multiline: true, token: { quoted: true, closed: false, start, end, from: start, to: end }, path: [], key: undefined, existing: new Set(), document: {}, itemStart: false, flow: false, needsSpace: false, column: 0, indent: 0, explicit };
475
+ }
476
+
477
+ // The text of a line without its comment (a `#` after a space, outside quotes).
478
+ function withoutComment(text) {
479
+ let quote = null;
480
+ for (let index = 0; index < text.length; index++) {
481
+ const character = text[index];
482
+ if (quote) {
483
+ if (quote === '"' && character === "\\") index++;
484
+ else if (character === quote) quote = null;
485
+ } else if (character === '"' || character === "'") {
486
+ if (index === 0 || /[\s[{,:-]/.test(text[index - 1])) quote = character;
487
+ } else if (character === "#" && (index === 0 || isSpace(text[index - 1]))) return text.slice(0, index);
488
+ }
489
+ return text;
490
+ }
491
+
492
+ /**
493
+ * A caret on a line inside a flow collection that began on an earlier line (`tags: [a,` / ` b, |]`, `{theme: minimal,` / ` |}`): the lines from
494
+ * the opening one to the caret are read as one text, so the containers open at the caret and the keys and items before it are known as on one line.
495
+ */
496
+ function flowContinuation({ text, lines, number, line, position, column, explicit, prefix, exclude, open }) {
497
+ if (!open) return null;
498
+ const first = lines[open.number].text;
499
+ const start = open.entry ? open.parsed.restStart + open.entry.valueStart : open.parsed.restStart;
500
+ const middle = lines.slice(open.number + 1, number).map((item) => withoutComment(item.text).trim());
501
+ const joined = [withoutComment(first.slice(start)).trimStart(), ...middle, line.text.slice(0, column).trimStart()].join(" ");
502
+ const flow = flowAt(joined, joined.length);
503
+ if (!flow) return null;
504
+ const { map, seq } = open.placed;
505
+ const frame = open.entry ? map : seq;
506
+ const base = open.entry ? [...map.path, open.entry.key] : [...seq.path, seq.count - 1];
507
+ return flowContext({ text, line, position, flow, base, frame, lines, explicit, prefix, exclude });
508
+ }
509
+
510
+ function valueContext({ text, lines, line, position, parsed, entry, explicit, prefix, stack, lineIndent, exclude }) {
511
+ const lineText = line.text;
512
+ let valueFrom = line.start + parsed.restStart + entry.valueStart;
513
+ let spaces = 0;
514
+ while (valueFrom + spaces < position && isSpace(text[valueFrom + spaces])) spaces++;
515
+ const needsSpace = spaces === 0;
516
+ valueFrom += spaces;
517
+ if (position < valueFrom) return null;
518
+ const valueText = lineText.slice(valueFrom - line.start);
519
+ // A block scalar header, an anchor, an alias or a tag: no suggestions.
520
+ if (/^[|>&*!]/.test(valueText)) return null;
521
+ const place = enter(stack, parsed, entry, shapeOfValue(valueText));
522
+ const map = place.map;
523
+ const base = [...map.path, entry.key];
524
+ const rest = lineText.slice(parsed.restStart);
525
+ const flow = valueText[0] === "[" || valueText[0] === "{" ? flowAt(rest, position - line.start - parsed.restStart) : null;
526
+ if (flow) return flowContext({ text, line, position, flow, base, frame: map, lines, explicit, prefix, exclude });
527
+ const token = valueToken(text, valueFrom, position, line.end, false);
528
+ if (!token) return null;
529
+ // Right after the colon (no space typed) a value place opens only when asked for.
530
+ if (token.from === position && needsSpace && !explicit) return null;
531
+ return {
532
+ format: "yaml", mode: "value", token, path: [...prefix, ...base], key: entry.key, existing: new Set(), document: wrapped(skeletonOf(map, lines), prefix),
533
+ itemStart: false, flow: false, needsSpace, column: parsed.restStart + entry.keyStart, indent: lineIndent,
534
+ };
535
+ }
536
+
537
+ function keyOrItemContext({ text, lines, number, line, position, parsed, explicit, exclude, prefix, stack, lineIndent }) {
538
+ const lineText = line.text;
539
+ const rest = parsed.rest;
540
+ // A flow collection that starts after a dash: a list item of its own.
541
+ if (rest[0] === "[" || rest[0] === "{") {
542
+ const flow = flowAt(rest, rest.length);
543
+ if (!flow || !parsed.markers.length) return null;
544
+ let seq = null;
545
+ for (const col of parsed.markers) { seq = stack.seqAt(col); seq.items[seq.count - 1] = { open: true }; }
546
+ return flowContext({ text, line, position, flow, base: [...seq.path, seq.count - 1], frame: seq, lines, explicit, prefix, exclude });
547
+ }
548
+ const quoteOpen = rest[0] === '"' || rest[0] === "'";
549
+ // The word before the caret is a plain key (or the start of a scalar item) or an unfinished quoted key; anything else (a sentence) is prose.
550
+ if (!quoteOpen && rest !== "" && !/^[A-Za-z0-9_.$+:-]+$/.test(rest)) {
551
+ // A sentence that runs on from a scalar above: only a variable (`{{`) is completed in it.
552
+ return parsed.markers.length ? null : continuationContext({ text, line, position, before: lineText.slice(0, position - line.start), explicit });
553
+ }
554
+ let seq = null;
555
+ for (const col of parsed.markers) { seq = stack.seqAt(col); seq.items[seq.count - 1] = { open: true }; }
556
+ const frame = stack.mapAt(parsed.restStart);
557
+ const token = keyToken(text, line, parsed.restStart, position);
558
+ // An empty place after nothing is not worth a list unless asked for; after a dash it is.
559
+ if (token.from === position && token.to === position && !explicit && !parsed.markers.length && rest === "") return null;
560
+ const full = readMarkers(lineText);
561
+ const fullEntry = readEntry(full.rest);
562
+ const inKey = fullEntry && position - line.start >= full.restStart && position - line.start <= full.restStart + fullEntry.keyEnd;
563
+ const path = frame.path;
564
+ const seqInfo = seq ? { path: [...prefix, ...seq.path], index: seq.count - 1 } : null;
565
+ // The other keys of this mapping: the ones above are in the frame, the ones below the caret's line are read now.
566
+ collectSiblings(lines, number, frame, stack, new Set());
567
+ const existing = new Set(frame.keys.keys());
568
+ if (inKey) existing.delete(fullEntry.key);
569
+ for (const key of exclude) existing.add(key);
570
+ const itemStart = parsed.markers.length > 0 && [...frame.keys.keys()].every((key) => key === fullEntry?.key) ;
571
+ return {
572
+ format: "yaml", mode: "key", token, path: [...prefix, ...path], key: undefined, existing, document: wrapped(skeletonOf(frame, lines), prefix),
573
+ itemStart, flow: false, needsSpace: false, column: parsed.restStart, indent: lineIndent, seq: seqInfo,
574
+ };
575
+ }
576
+
577
+ // The keys of `frame` on the lines after line `number`, until the mapping ends; they are added to the frame (and to `into`).
578
+ function collectSiblings(lines, number, frame, stack, into) {
579
+ let block = -1;
580
+ let flow = 0;
581
+ for (let index = number + 1; index < lines.length; index++) {
582
+ const text = lines[index].text;
583
+ if (text.trim() === "") continue;
584
+ const indent = text.length - text.trimStart().length;
585
+ if (block >= 0) { if (indent > block) continue; block = -1; }
586
+ if (flow > 0) { flow = flowDepth(text, flow); continue; }
587
+ const trimmed = text.trimStart();
588
+ if (trimmed.startsWith("#") || trimmed === "---" || trimmed === "...") continue;
589
+ const parsed = readMarkers(text);
590
+ const entry = readEntry(parsed.rest);
591
+ if (!entry && !parsed.markers.length) continue;
592
+ const value = entry ? parsed.rest.slice(entry.valueStart) : parsed.rest;
593
+ const shape = shapeOfValue(value);
594
+ const placed = enter(stack, parsed, entry, shape);
595
+ if (!stack.frames.includes(frame)) return;
596
+ if (entry && placed.map === frame) into.add(entry.key);
597
+ if (shape.block) block = entry ? parsed.restStart + entry.keyStart : parsed.markers.at(-1);
598
+ else if (shape.flow) flow = flowDepth(value, 0);
599
+ }
600
+ }
601
+
602
+ // A caret inside a flow collection written on one line (`audience: [a, |]`, `{ theme: x, | }`).
603
+ function flowContext({ text, line, position, flow, base, frame, lines, explicit, prefix, exclude }) {
604
+ // The outer containers lead to the innermost: a list item is its position, a mapping value its key.
605
+ let path = base;
606
+ for (let level = 0; level < flow.length - 1; level++) {
607
+ const container = flow[level];
608
+ if (container.kind === "seq") path = [...path, container.count];
609
+ else if (container.expect === "value" && container.key !== undefined) path = [...path, container.key];
610
+ else return null;
611
+ }
612
+ const inner = flow.at(-1);
613
+ let start = position, end = position;
614
+ while (start > line.start && /[A-Za-z0-9_.$+:-]/.test(text[start - 1])) start--;
615
+ while (end < line.end && /[A-Za-z0-9_.$+:-]/.test(text[end])) end++;
616
+ const token = { quoted: false, closed: false, start, end, from: start, to: end };
617
+ if (token.from === position && token.to === position && !explicit) return null;
618
+ const common = { format: "yaml", token, document: wrapped(skeletonOf(frame, lines), prefix), itemStart: false, flow: true, needsSpace: false, column: 0, indent: 0 };
619
+ if (inner.kind === "seq") return { ...common, mode: "item", path: [...prefix, ...path, inner.count], key: undefined, existing: new Set() };
620
+ if (inner.expect === "key") return { ...common, mode: "key", path: [...prefix, ...path], key: undefined, existing: new Set([...inner.keys, ...exclude]) };
621
+ if (inner.expect === "value" && inner.key !== undefined) return { ...common, mode: "value", path: [...prefix, ...path, inner.key], key: inner.key, existing: new Set() };
622
+ return null;
623
+ }