@rohal12/spindle-lsp 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/dist/bin.js ADDED
@@ -0,0 +1,4663 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/core/workspace/document-store.ts
13
+ import { EventEmitter } from "node:events";
14
+ import { readFileSync } from "node:fs";
15
+ import { pathToFileURL } from "node:url";
16
+ var DocumentStore;
17
+ var init_document_store = __esm({
18
+ "src/core/workspace/document-store.ts"() {
19
+ "use strict";
20
+ DocumentStore = class extends EventEmitter {
21
+ docs = /* @__PURE__ */ new Map();
22
+ /** Open a document (or replace if already open). */
23
+ open(uri, text, version) {
24
+ this.docs.set(uri, { text, version: version ?? 0 });
25
+ this.emit("documentOpened", uri);
26
+ }
27
+ /** Update an already-open document's text. */
28
+ update(uri, text, version) {
29
+ const existing = this.docs.get(uri);
30
+ const newVersion = version ?? (existing ? existing.version + 1 : 0);
31
+ this.docs.set(uri, { text, version: newVersion });
32
+ this.emit("documentChanged", uri);
33
+ }
34
+ /** Close a document and remove it from the store. */
35
+ close(uri) {
36
+ this.docs.delete(uri);
37
+ this.emit("documentClosed", uri);
38
+ }
39
+ /** Get the text of a document, or undefined if not open. */
40
+ getText(uri) {
41
+ return this.docs.get(uri)?.text;
42
+ }
43
+ /** Get the version of a document (0 if unknown). */
44
+ getVersion(uri) {
45
+ return this.docs.get(uri)?.version ?? 0;
46
+ }
47
+ /** Get all open document URIs. */
48
+ getUris() {
49
+ return Array.from(this.docs.keys());
50
+ }
51
+ /** Check whether a document is currently open. */
52
+ has(uri) {
53
+ return this.docs.has(uri);
54
+ }
55
+ /** Load files from disk (CLI mode). Converts paths to file:// URIs. */
56
+ loadFromDisk(paths) {
57
+ for (const filePath of paths) {
58
+ try {
59
+ const text = readFileSync(filePath, "utf-8");
60
+ const uri = pathToFileURL(filePath).href;
61
+ this.open(uri, text);
62
+ } catch {
63
+ }
64
+ }
65
+ }
66
+ };
67
+ }
68
+ });
69
+
70
+ // src/core/parsing/passage-parser.ts
71
+ function parsePassageHeader(line, lineNumber) {
72
+ const escaped = line.replace(/\\./g, "ec");
73
+ const match = passageHeaderRegex.exec(escaped);
74
+ if (!match) return null;
75
+ const prefix = match[1] ?? "";
76
+ const rawName = match[2] ?? "";
77
+ const rawTags = match[3] ?? "";
78
+ const rawMeta = match[4] ?? "";
79
+ const nameStart = prefix.length;
80
+ const nameEnd = nameStart + rawName.length;
81
+ const name = line.substring(nameStart, nameEnd).trim();
82
+ if (!name) return null;
83
+ const escapedName = name.replace(/\\./g, "ec");
84
+ if (/[\[\]\{\}]/.test(escapedName)) return null;
85
+ const tagsStr = rawTags.trim();
86
+ let tags = [];
87
+ if (tagsStr) {
88
+ const inner = tagsStr.substring(1, tagsStr.length - 1).trim();
89
+ if (/[\[\]\{\}]/.test(inner.replace(/\\./g, "ec"))) return null;
90
+ tags = inner ? inner.split(/\s+/) : [];
91
+ }
92
+ let meta;
93
+ const metaStr = rawMeta.trim();
94
+ if (metaStr) {
95
+ try {
96
+ meta = JSON.parse(line.substring(nameStart + rawName.length + rawTags.length, nameStart + rawName.length + rawTags.length + rawMeta.length).trim());
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ const headerRange = {
102
+ start: { line: lineNumber, character: 0 },
103
+ end: { line: lineNumber, character: line.length }
104
+ };
105
+ const nameCharStart = prefix.length;
106
+ const nameCharEnd = nameCharStart + name.length;
107
+ const nameRange = {
108
+ start: { line: lineNumber, character: nameCharStart },
109
+ end: { line: lineNumber, character: nameCharEnd }
110
+ };
111
+ return {
112
+ name: name.replace(/\\(.)/g, "$1"),
113
+ // unescape
114
+ tags,
115
+ meta,
116
+ headerRange,
117
+ nameRange
118
+ };
119
+ }
120
+ var passageHeaderRegex;
121
+ var init_passage_parser = __esm({
122
+ "src/core/parsing/passage-parser.ts"() {
123
+ "use strict";
124
+ passageHeaderRegex = /^(::\s+)(.*?)(\[.*?\]\s*)?(\{.*?\}\s*)?\r?$/;
125
+ }
126
+ });
127
+
128
+ // src/core/workspace/passage-index.ts
129
+ var PassageIndex;
130
+ var init_passage_index = __esm({
131
+ "src/core/workspace/passage-index.ts"() {
132
+ "use strict";
133
+ init_passage_parser();
134
+ PassageIndex = class {
135
+ /** Per-URI list of passages. */
136
+ byUri = /* @__PURE__ */ new Map();
137
+ /**
138
+ * Parse all passage headers in the given document text and
139
+ * store them, replacing any previous entries for this URI.
140
+ */
141
+ rebuild(uri, text) {
142
+ this.byUri.delete(uri);
143
+ const lines = text.split("\n");
144
+ const passages = [];
145
+ for (let i = 0; i < lines.length; i++) {
146
+ const header = parsePassageHeader(lines[i], i);
147
+ if (!header) continue;
148
+ const passage = {
149
+ name: header.name,
150
+ range: {
151
+ start: { line: i, character: 0 },
152
+ end: { line: i, character: lines[i].length }
153
+ // will be extended below
154
+ },
155
+ headerEnd: header.headerRange,
156
+ uri,
157
+ tags: header.tags.length > 0 ? header.tags : void 0,
158
+ meta: header.meta
159
+ };
160
+ passages.push(passage);
161
+ }
162
+ for (let i = 0; i < passages.length; i++) {
163
+ const nextStart = i + 1 < passages.length ? passages[i + 1].range.start.line - 1 : lines.length - 1;
164
+ passages[i].range = {
165
+ start: passages[i].range.start,
166
+ end: { line: nextStart, character: lines[nextStart]?.length ?? 0 }
167
+ };
168
+ }
169
+ if (passages.length > 0) {
170
+ this.byUri.set(uri, passages);
171
+ }
172
+ }
173
+ /** Remove all passages for a URI. */
174
+ remove(uri) {
175
+ this.byUri.delete(uri);
176
+ }
177
+ /** Get a passage by name. Returns the first match found. */
178
+ getPassage(name) {
179
+ for (const passages of this.byUri.values()) {
180
+ for (const p of passages) {
181
+ if (p.name === name) return p;
182
+ }
183
+ }
184
+ return void 0;
185
+ }
186
+ /** Get the passage containing a specific line in a document. */
187
+ getPassageAt(uri, line) {
188
+ const passages = this.byUri.get(uri);
189
+ if (!passages) return void 0;
190
+ for (let i = passages.length - 1; i >= 0; i--) {
191
+ if (passages[i].range.start.line <= line) {
192
+ return passages[i];
193
+ }
194
+ }
195
+ return void 0;
196
+ }
197
+ /** Get all passages in a specific document. */
198
+ getPassagesInDocument(uri) {
199
+ return this.byUri.get(uri) ?? [];
200
+ }
201
+ /** Get all passages across all documents. */
202
+ getAllPassages() {
203
+ const result = [];
204
+ for (const passages of this.byUri.values()) {
205
+ result.push(...passages);
206
+ }
207
+ return result;
208
+ }
209
+ /** Get the StoryVariables passage, if any. */
210
+ getStoryVariables() {
211
+ return this.getPassage("StoryVariables");
212
+ }
213
+ /** Get the StoryInit passage, if any. */
214
+ getStoryInit() {
215
+ return this.getPassage("StoryInit");
216
+ }
217
+ /**
218
+ * Find passage names that appear more than once across all documents.
219
+ * Returns a map from name to the list of duplicate Passage objects.
220
+ */
221
+ getDuplicates() {
222
+ const byName = /* @__PURE__ */ new Map();
223
+ for (const passages of this.byUri.values()) {
224
+ for (const p of passages) {
225
+ const existing = byName.get(p.name);
226
+ if (existing) {
227
+ existing.push(p);
228
+ } else {
229
+ byName.set(p.name, [p]);
230
+ }
231
+ }
232
+ }
233
+ const duplicates = /* @__PURE__ */ new Map();
234
+ for (const [name, passages] of byName) {
235
+ if (passages.length > 1) {
236
+ duplicates.set(name, passages);
237
+ }
238
+ }
239
+ return duplicates;
240
+ }
241
+ };
242
+ }
243
+ });
244
+
245
+ // src/core/workspace/macro-registry.ts
246
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
247
+ import { dirname, join, parse as parsePath } from "node:path";
248
+ import { fileURLToPath } from "node:url";
249
+ var MacroRegistry;
250
+ var init_macro_registry = __esm({
251
+ "src/core/workspace/macro-registry.ts"() {
252
+ "use strict";
253
+ MacroRegistry = class {
254
+ macros = /* @__PURE__ */ new Map();
255
+ /**
256
+ * Load built-in macro metadata from @rohal12/spindle's macro-registry.json.
257
+ * This is the base layer that provides name, block, subMacros, flags, source.
258
+ */
259
+ /** Warnings collected during loadBuiltins for logging. */
260
+ warnings = [];
261
+ loadBuiltins() {
262
+ let builtinMacros = [];
263
+ try {
264
+ const thisDir = dirname(fileURLToPath(import.meta.url));
265
+ const registryPath = this.resolveRegistryPath(thisDir);
266
+ if (registryPath) {
267
+ const raw = readFileSync2(registryPath, "utf-8");
268
+ const parsed = JSON.parse(raw);
269
+ if (!Array.isArray(parsed)) {
270
+ this.warnings.push(
271
+ `macro-registry.json: expected array, got ${typeof parsed}. Continuing with supplements only.`
272
+ );
273
+ } else {
274
+ builtinMacros = parsed;
275
+ }
276
+ }
277
+ } catch (err) {
278
+ const message = err instanceof Error ? err.message : String(err);
279
+ this.warnings.push(
280
+ `Failed to load builtin macros: ${message}. Continuing with supplements only.`
281
+ );
282
+ }
283
+ for (const m of builtinMacros) {
284
+ try {
285
+ const key = m.name.toLowerCase();
286
+ this.macros.set(key, {
287
+ name: m.name,
288
+ block: m.block,
289
+ subMacros: m.subMacros ?? [],
290
+ storeVar: m.storeVar,
291
+ interpolate: m.interpolate,
292
+ merged: m.merged,
293
+ source: m.source === "builtin" ? "builtin" : "user"
294
+ });
295
+ } catch {
296
+ this.warnings.push(`Skipped malformed builtin macro entry: ${JSON.stringify(m)}`);
297
+ }
298
+ }
299
+ }
300
+ /**
301
+ * Overlay supplement data onto the registry.
302
+ * Supplements provide descriptions, parameters, children, parents, skipArgs.
303
+ * The `container` field maps to `block` on MacroInfo.
304
+ */
305
+ loadSupplements(supplements) {
306
+ this.mergeEntries(supplements);
307
+ }
308
+ /**
309
+ * Overlay user config onto the registry.
310
+ * Same format as supplements; applied last so it wins.
311
+ */
312
+ loadConfig(config) {
313
+ this.mergeEntries(config);
314
+ }
315
+ /** Add or replace a single macro entry. */
316
+ addMacro(info) {
317
+ const key = info.name.toLowerCase();
318
+ const existing = this.macros.get(key);
319
+ this.macros.set(key, {
320
+ name: info.name,
321
+ block: info.block ?? existing?.block ?? false,
322
+ subMacros: info.subMacros ?? existing?.subMacros ?? [],
323
+ source: info.source ?? existing?.source ?? "user",
324
+ storeVar: info.storeVar ?? existing?.storeVar,
325
+ interpolate: info.interpolate ?? existing?.interpolate,
326
+ merged: info.merged ?? existing?.merged,
327
+ description: info.description ?? existing?.description,
328
+ parameters: info.parameters ?? existing?.parameters,
329
+ children: info.children ?? existing?.children,
330
+ parents: info.parents ?? existing?.parents,
331
+ skipArgs: info.skipArgs ?? existing?.skipArgs
332
+ });
333
+ }
334
+ /** Get a macro by name (case-insensitive). */
335
+ getMacro(name) {
336
+ return this.macros.get(name.toLowerCase());
337
+ }
338
+ /** Check if a macro is a block (container) macro. */
339
+ isBlock(name) {
340
+ return this.macros.get(name.toLowerCase())?.block ?? false;
341
+ }
342
+ /** Check if a macro is a sub-macro (has parents). */
343
+ isSubMacro(name) {
344
+ const info = this.macros.get(name.toLowerCase());
345
+ return info?.parents != null && info.parents.length > 0;
346
+ }
347
+ /** Get all registered macros. */
348
+ getAllMacros() {
349
+ return Array.from(this.macros.values());
350
+ }
351
+ /**
352
+ * Walk up directory tree from `startDir` to find the
353
+ * @rohal12/spindle/dist/pkg/macro-registry.json file.
354
+ */
355
+ resolveRegistryPath(startDir) {
356
+ const target = join("node_modules", "@rohal12", "spindle", "dist", "pkg", "macro-registry.json");
357
+ let dir = startDir;
358
+ const { root } = parsePath(dir);
359
+ while (dir !== root) {
360
+ const candidate = join(dir, target);
361
+ if (existsSync(candidate)) {
362
+ return candidate;
363
+ }
364
+ dir = dirname(dir);
365
+ }
366
+ return null;
367
+ }
368
+ /** Merge a set of supplement/config entries into the registry. */
369
+ mergeEntries(entries) {
370
+ for (const [rawKey, entry] of Object.entries(entries)) {
371
+ const key = rawKey.toLowerCase();
372
+ const existing = this.macros.get(key);
373
+ if (existing) {
374
+ if (entry.description !== void 0) existing.description = entry.description;
375
+ if (entry.parameters !== void 0) existing.parameters = entry.parameters;
376
+ if (entry.children !== void 0) existing.children = entry.children;
377
+ if (entry.parents !== void 0) existing.parents = entry.parents;
378
+ if (entry.skipArgs !== void 0) existing.skipArgs = entry.skipArgs;
379
+ if (entry.container !== void 0) existing.block = entry.container;
380
+ } else {
381
+ this.macros.set(key, {
382
+ name: entry.name ?? rawKey,
383
+ block: entry.container ?? false,
384
+ subMacros: [],
385
+ source: "user",
386
+ description: entry.description,
387
+ parameters: entry.parameters,
388
+ children: entry.children,
389
+ parents: entry.parents,
390
+ skipArgs: entry.skipArgs
391
+ });
392
+ }
393
+ }
394
+ }
395
+ };
396
+ }
397
+ });
398
+
399
+ // src/core/workspace/variable-tracker.ts
400
+ var varRefRegex, EXCLUDED_PASSAGES, CLEAN_PATTERNS, VariableTracker;
401
+ var init_variable_tracker = __esm({
402
+ "src/core/workspace/variable-tracker.ts"() {
403
+ "use strict";
404
+ init_passage_parser();
405
+ varRefRegex = /\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
406
+ EXCLUDED_PASSAGES = /* @__PURE__ */ new Set([
407
+ "StoryVariables",
408
+ "StoryInit",
409
+ "StoryData",
410
+ "StoryScript",
411
+ "StoryInterface"
412
+ ]);
413
+ CLEAN_PATTERNS = [
414
+ /<!--[\s\S]*?-->/g,
415
+ // HTML comments
416
+ /<script(?:\s+[^>]*)?>[\s\S]*?<\/script>/gi,
417
+ // script tags
418
+ /<style>[\s\S]*?<\/style>/gi,
419
+ // style tags
420
+ /`(?:\\.|[^`\\])*`/g,
421
+ // backtick strings
422
+ /"(?:\\.|[^"\\])*"/g,
423
+ // double-quoted strings
424
+ /'(?:\\.|[^'\\])*'/g
425
+ // single-quoted strings
426
+ ];
427
+ VariableTracker = class {
428
+ declared = /* @__PURE__ */ new Map();
429
+ _hasStoryVariables = false;
430
+ /** Per-URI list of variable usages. */
431
+ usagesByUri = /* @__PURE__ */ new Map();
432
+ /**
433
+ * Parse the StoryVariables passage content for declarations.
434
+ * Each line like `$name = value` becomes a declaration.
435
+ */
436
+ parseStoryVariables(content) {
437
+ this.declared.clear();
438
+ this._hasStoryVariables = true;
439
+ const lines = content.split("\n");
440
+ for (const line of lines) {
441
+ const trimmed = line.trim();
442
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("<!--")) continue;
443
+ const match = /^\$([A-Za-z_$][\w$]*)\s*=\s*(.*)$/.exec(trimmed);
444
+ if (!match) continue;
445
+ const name = match[1];
446
+ const expr = match[2].trim();
447
+ const decl = { name, sigil: "$" };
448
+ if (expr.startsWith("{")) {
449
+ const fieldRegex = /(\w+)\s*:/g;
450
+ let fieldMatch;
451
+ const fields = [];
452
+ while ((fieldMatch = fieldRegex.exec(expr)) !== null) {
453
+ fields.push(fieldMatch[1]);
454
+ }
455
+ if (fields.length > 0) {
456
+ decl.fields = fields;
457
+ }
458
+ }
459
+ this.declared.set(name, decl);
460
+ }
461
+ }
462
+ /**
463
+ * Scan a document for variable usages.
464
+ * Identifies passages in the document and scans non-special ones.
465
+ */
466
+ scanDocument(uri, text, _macros) {
467
+ this.usagesByUri.delete(uri);
468
+ const lines = text.split("\n");
469
+ const usages = [];
470
+ const passageBoundaries = [];
471
+ for (let i = 0; i < lines.length; i++) {
472
+ const header = parsePassageHeader(lines[i], i);
473
+ if (header) {
474
+ passageBoundaries.push({ name: header.name, startLine: i });
475
+ }
476
+ }
477
+ for (let pi = 0; pi < passageBoundaries.length; pi++) {
478
+ const passage = passageBoundaries[pi];
479
+ if (EXCLUDED_PASSAGES.has(passage.name)) continue;
480
+ const contentStartLine = passage.startLine + 1;
481
+ const contentEndLine = pi + 1 < passageBoundaries.length ? passageBoundaries[pi + 1].startLine : lines.length;
482
+ const contentLines = lines.slice(contentStartLine, contentEndLine);
483
+ const content = contentLines.join("\n");
484
+ let cleaned = content;
485
+ for (const pattern of CLEAN_PATTERNS) {
486
+ cleaned = cleaned.replace(pattern, (m) => " ".repeat(m.length));
487
+ }
488
+ const lineOffsets = [0];
489
+ for (let i = 0; i < cleaned.length; i++) {
490
+ if (cleaned[i] === "\n") lineOffsets.push(i + 1);
491
+ }
492
+ const re = new RegExp(varRefRegex.source, "g");
493
+ let match;
494
+ while ((match = re.exec(cleaned)) !== null) {
495
+ const fullName = match[1];
496
+ const baseName = fullName.split(".")[0];
497
+ const charOffset = match.index;
498
+ let localLine = 0;
499
+ for (let i = 0; i < lineOffsets.length; i++) {
500
+ if (lineOffsets[i] > charOffset) break;
501
+ localLine = i;
502
+ }
503
+ const character = charOffset - lineOffsets[localLine];
504
+ const absoluteLine = contentStartLine + localLine;
505
+ const range = {
506
+ start: { line: absoluteLine, character },
507
+ end: { line: absoluteLine, character: character + match[0].length }
508
+ };
509
+ usages.push({ uri, baseName, fullName, range });
510
+ }
511
+ }
512
+ if (usages.length > 0) {
513
+ this.usagesByUri.set(uri, usages);
514
+ }
515
+ }
516
+ /** Get all declared variables. */
517
+ getDeclared() {
518
+ return this.declared;
519
+ }
520
+ /** Get all usages of a variable by base name. */
521
+ getUsages(name) {
522
+ const results = [];
523
+ for (const usages of this.usagesByUri.values()) {
524
+ for (const u of usages) {
525
+ if (u.baseName === name) {
526
+ results.push({ uri: u.uri, range: u.range });
527
+ }
528
+ }
529
+ }
530
+ return results;
531
+ }
532
+ /** Get undeclared variable usages in a specific document. */
533
+ getUndeclared(uri) {
534
+ const usages = this.usagesByUri.get(uri);
535
+ if (!usages) return [];
536
+ const results = [];
537
+ const seen = /* @__PURE__ */ new Set();
538
+ for (const u of usages) {
539
+ if (!this.declared.has(u.baseName) && !seen.has(u.baseName)) {
540
+ seen.add(u.baseName);
541
+ results.push({ name: u.baseName, range: u.range });
542
+ }
543
+ }
544
+ return results;
545
+ }
546
+ /** Whether a StoryVariables passage has been parsed. */
547
+ hasStoryVariables() {
548
+ return this._hasStoryVariables;
549
+ }
550
+ };
551
+ }
552
+ });
553
+
554
+ // src/core/workspace/widget-registry.ts
555
+ var widgetDefRegex, paramRegex, WidgetRegistry;
556
+ var init_widget_registry = __esm({
557
+ "src/core/workspace/widget-registry.ts"() {
558
+ "use strict";
559
+ widgetDefRegex = /\{widget\s+"([^"]+)"((?:\s+@[A-Za-z_$][\w$]*)*)\s*\}/gi;
560
+ paramRegex = /@([A-Za-z_$][\w$]*)/g;
561
+ WidgetRegistry = class {
562
+ widgets = /* @__PURE__ */ new Map();
563
+ /**
564
+ * Scan all passages for widget definitions.
565
+ * Only passages tagged `widget` are examined.
566
+ *
567
+ * @param passages - All known passages.
568
+ * @param getContent - Function to retrieve document text by URI.
569
+ */
570
+ scan(passages, getContent) {
571
+ this.widgets.clear();
572
+ for (const passage of passages) {
573
+ if (!passage.tags?.includes("widget")) continue;
574
+ const text = getContent(passage.uri);
575
+ if (!text) continue;
576
+ const lines = text.split("\n");
577
+ const contentStartLine = passage.range.start.line + 1;
578
+ let contentEndLine = lines.length;
579
+ for (let i = contentStartLine; i < lines.length; i++) {
580
+ if (/^::\s+/.test(lines[i])) {
581
+ contentEndLine = i;
582
+ break;
583
+ }
584
+ }
585
+ const contentLines = lines.slice(contentStartLine, contentEndLine);
586
+ for (let i = 0; i < contentLines.length; i++) {
587
+ const line = contentLines[i];
588
+ widgetDefRegex.lastIndex = 0;
589
+ let match;
590
+ while ((match = widgetDefRegex.exec(line)) !== null) {
591
+ const widgetName = match[1];
592
+ const paramString = match[2] || "";
593
+ const params = [];
594
+ let paramMatch;
595
+ paramRegex.lastIndex = 0;
596
+ while ((paramMatch = paramRegex.exec(paramString)) !== null) {
597
+ params.push(paramMatch[1]);
598
+ }
599
+ const lineNum = contentStartLine + i;
600
+ const charStart = match.index;
601
+ const charEnd = charStart + match[0].length;
602
+ const range = {
603
+ start: { line: lineNum, character: charStart },
604
+ end: { line: lineNum, character: charEnd }
605
+ };
606
+ this.widgets.set(widgetName, {
607
+ name: widgetName,
608
+ params,
609
+ uri: passage.uri,
610
+ range
611
+ });
612
+ }
613
+ }
614
+ }
615
+ }
616
+ /** Get a widget definition by name. */
617
+ getWidget(name) {
618
+ return this.widgets.get(name);
619
+ }
620
+ /** Get all registered widget definitions. */
621
+ getAllWidgets() {
622
+ return Array.from(this.widgets.values());
623
+ }
624
+ };
625
+ }
626
+ });
627
+
628
+ // src/core/parsing/macro-parser.ts
629
+ function buildLineStarts(text) {
630
+ const starts = [0];
631
+ for (let i = 0; i < text.length; i++) {
632
+ if (text[i] === "\n") {
633
+ starts.push(i + 1);
634
+ }
635
+ }
636
+ return starts;
637
+ }
638
+ function offsetToPosition(offset, lineStarts) {
639
+ let low = 0;
640
+ let high = lineStarts.length - 1;
641
+ while (low < high) {
642
+ const mid = low + high + 1 >> 1;
643
+ if (lineStarts[mid] <= offset) {
644
+ low = mid;
645
+ } else {
646
+ high = mid - 1;
647
+ }
648
+ }
649
+ return { line: low, character: offset - lineStarts[low] };
650
+ }
651
+ function parseMacros(text) {
652
+ const cleaned = text.replace(variableInterpolationRegex, (match2) => {
653
+ return " ".repeat(match2.length);
654
+ });
655
+ const lineStarts = buildLineStarts(text);
656
+ const macros = [];
657
+ let id = 0;
658
+ macroRegex.lastIndex = 0;
659
+ let match;
660
+ while ((match = macroRegex.exec(cleaned)) !== null) {
661
+ const closeSlash = match[1];
662
+ const cssPrefix = (match[2] || "").trim();
663
+ const macroName = match[3];
664
+ const rawArgs = match[4];
665
+ const open = closeSlash !== "/";
666
+ const matchStart = match.index;
667
+ const matchEnd = matchStart + match[0].length;
668
+ const startPos = offsetToPosition(matchStart, lineStarts);
669
+ const endPos = offsetToPosition(matchEnd, lineStarts);
670
+ const range = {
671
+ start: startPos,
672
+ end: endPos
673
+ };
674
+ macros.push({
675
+ id,
676
+ pair: -1,
677
+ name: macroName,
678
+ open,
679
+ range,
680
+ cssPrefix: cssPrefix || void 0,
681
+ rawArgs: rawArgs || void 0
682
+ });
683
+ id++;
684
+ }
685
+ return macros;
686
+ }
687
+ function pairMacros(macros, isBlock) {
688
+ const stacks = {};
689
+ for (const macro of macros) {
690
+ const name = macro.name.toLowerCase();
691
+ if (!isBlock(macro.name)) continue;
692
+ if (macro.open) {
693
+ if (!stacks[name]) stacks[name] = [];
694
+ stacks[name].push(macro.id);
695
+ } else {
696
+ const stack = stacks[name];
697
+ if (stack && stack.length > 0) {
698
+ const openId = stack.pop();
699
+ macros[openId].pair = macro.id;
700
+ macro.pair = openId;
701
+ }
702
+ }
703
+ }
704
+ }
705
+ var variableInterpolationRegex, macroRegex;
706
+ var init_macro_parser = __esm({
707
+ "src/core/parsing/macro-parser.ts"() {
708
+ "use strict";
709
+ variableInterpolationRegex = /(?<!\\)\{([$_@][A-Za-z_$][\w$.]*)\}/g;
710
+ macroRegex = /(?<!\\)\{(\/)?(?:((?:[#.][a-zA-Z][\w-]*\s*)*)([A-Za-z][\w-]*))(?:\s+((?:(?:`(?:\\.|[^`\\])*?`)|(?:"(?:\\.|[^"\\])*?")|(?:'(?:\\.|[^'\\])*?')|(?:\[(?:[<>]?[Ii][Mm][Gg])?\[[^\r\n]*?\]\]+)|[^}]|(?:}))*?))?\}/gmi;
711
+ }
712
+ });
713
+
714
+ // src/macro-supplements.json
715
+ var macro_supplements_default;
716
+ var init_macro_supplements = __esm({
717
+ "src/macro-supplements.json"() {
718
+ macro_supplements_default = {
719
+ if: {
720
+ name: "if",
721
+ children: [
722
+ { name: "else", max: 1 },
723
+ { name: "elseif" }
724
+ ],
725
+ container: true,
726
+ description: "Conditionally displays its contents based on the given expression.\n\nUsage:\n\n```\n{if condition}\n ...\n{elseif condition}\n ...\n{else}\n ...\n{/if}\n```",
727
+ parameters: ["...text"]
728
+ },
729
+ elseif: {
730
+ name: "elseif",
731
+ parents: ["if"],
732
+ description: "Used within `{if}` to provide an additional conditional branch.\n\nUsage: `{elseif condition}`",
733
+ parameters: ["...text"]
734
+ },
735
+ else: {
736
+ name: "else",
737
+ parents: ["if"],
738
+ description: "Used within `{if}` to provide a fallback branch.\n\nUsage: `{else}`",
739
+ parameters: []
740
+ },
741
+ for: {
742
+ name: "for",
743
+ children: [
744
+ { name: "next" },
745
+ { name: "stop" }
746
+ ],
747
+ container: true,
748
+ description: "Loop block. Usage: `{for @item of $list}...{/for}` or `{for @item, @index of $list}...{/for}`",
749
+ skipArgs: true
750
+ },
751
+ next: {
752
+ name: "next",
753
+ parents: ["for"],
754
+ description: "Continues to the next iteration of a `{for}` loop.\n\nUsage: `{next}`",
755
+ parameters: []
756
+ },
757
+ stop: {
758
+ name: "stop",
759
+ parents: ["for", "repeat"],
760
+ description: "Breaks out of a `{for}` or `{repeat}` loop.\n\nUsage: `{stop}`",
761
+ parameters: []
762
+ },
763
+ switch: {
764
+ name: "switch",
765
+ children: [
766
+ { name: "case", min: 1 },
767
+ { name: "default", max: 1 }
768
+ ],
769
+ container: true,
770
+ description: "Evaluates an expression and executes the matching `{case}` branch.\n\nUsage:\n\n```\n{switch expression}\n {case value1}\n ...\n {case value2}\n ...\n {default}\n ...\n{/switch}\n```",
771
+ parameters: ["...text"]
772
+ },
773
+ case: {
774
+ name: "case",
775
+ parents: ["switch"],
776
+ description: "Defines a branch within a `{switch}` macro.\n\nUsage: `{case value}`",
777
+ parameters: ["...text"]
778
+ },
779
+ default: {
780
+ name: "default",
781
+ parents: ["switch"],
782
+ description: "Defines the default branch within a `{switch}` macro.\n\nUsage: `{default}`",
783
+ parameters: []
784
+ },
785
+ set: {
786
+ name: "set",
787
+ description: "Sets variables based on the given expression.\n\nUsage: `{set $var to value}`\n\nSupports `$var` (story), `_var` (temporary), and `@var` (local) variables.",
788
+ parameters: ["...text"]
789
+ },
790
+ unset: {
791
+ name: "unset",
792
+ description: "Unsets one or more variables.\n\nUsage: `{unset $var}`",
793
+ parameters: ["...text"]
794
+ },
795
+ computed: {
796
+ name: "computed",
797
+ description: "Evaluates an expression and outputs the result.\n\nUsage: `{computed expression}`",
798
+ parameters: ["...text"]
799
+ },
800
+ print: {
801
+ name: "print",
802
+ description: "Outputs the result of the given expression.\n\nUsage: `{print expression}`",
803
+ parameters: ["...text"]
804
+ },
805
+ do: {
806
+ name: "do",
807
+ container: true,
808
+ description: "Displays its contents. Can be updated dynamically.\n\nUsage:\n\n```\n{do}\n ...\n{/do}\n```",
809
+ parameters: []
810
+ },
811
+ link: {
812
+ name: "link",
813
+ container: true,
814
+ description: 'Creates a link that executes its contents when clicked.\n\nUsage:\n\n```\n{link "text" "passage"}\n ...\n{/link}\n```',
815
+ parameters: ["text |+ text", ""]
816
+ },
817
+ button: {
818
+ name: "button",
819
+ container: true,
820
+ description: 'Creates a button that executes its contents when clicked.\n\nUsage:\n\n```\n{button "text" "passage"}\n ...\n{/button}\n```',
821
+ parameters: ["text |+ text", ""]
822
+ },
823
+ timed: {
824
+ name: "timed",
825
+ children: [
826
+ { name: "next" }
827
+ ],
828
+ container: true,
829
+ description: "Displays its contents after a specified delay.\n\nUsage:\n\n```\n{timed delay}\n ...\n {next delay}\n ...\n{/timed}\n```",
830
+ parameters: ["text"]
831
+ },
832
+ repeat: {
833
+ name: "repeat",
834
+ children: [
835
+ { name: "stop" }
836
+ ],
837
+ container: true,
838
+ description: "Repeatedly displays its contents at the specified interval.\n\nUsage:\n\n```\n{repeat delay}\n ...\n{/repeat}\n```",
839
+ parameters: ["text"]
840
+ },
841
+ type: {
842
+ name: "type",
843
+ container: true,
844
+ description: "Outputs its contents one character at a time, mimicking a typewriter effect.\n\nUsage:\n\n```\n{type speed}\n ...\n{/type}\n```",
845
+ parameters: ["text"]
846
+ },
847
+ listbox: {
848
+ name: "listbox",
849
+ children: [
850
+ { name: "option", min: 1 }
851
+ ],
852
+ container: true,
853
+ description: 'Creates a dropdown listbox.\n\nUsage:\n\n```\n{listbox "$receiver"}\n {option "label1"}\n {option "label2"}\n{/listbox}\n```',
854
+ parameters: ["receiver"]
855
+ },
856
+ cycle: {
857
+ name: "cycle",
858
+ children: [
859
+ { name: "option", min: 1 }
860
+ ],
861
+ container: true,
862
+ description: 'Creates a cycling link.\n\nUsage:\n\n```\n{cycle "$receiver"}\n {option "label1"}\n {option "label2"}\n{/cycle}\n```',
863
+ parameters: ["receiver"]
864
+ },
865
+ option: {
866
+ name: "option",
867
+ parents: ["listbox", "cycle"],
868
+ description: "Defines an option within a `{listbox}` or `{cycle}` macro.\n\nUsage: `{option label}`",
869
+ parameters: ["|+ text &+ text", "text"]
870
+ },
871
+ widget: {
872
+ name: "widget",
873
+ container: true,
874
+ description: 'Defines a custom widget (macro).\n\nUsage:\n\n```\n{widget "name"}\n ...\n{/widget}\n```',
875
+ parameters: ["...text"]
876
+ },
877
+ meter: {
878
+ name: "meter",
879
+ description: "Displays a progress meter.\n\nUsage: `{meter $var min max}`",
880
+ parameters: ["...text"]
881
+ },
882
+ goto: {
883
+ name: "goto",
884
+ description: 'Navigates to the specified passage.\n\nUsage: `{goto "passage"}`',
885
+ parameters: ["text"]
886
+ },
887
+ back: {
888
+ name: "back",
889
+ description: "Creates a link or navigates to the previous passage in history.\n\nUsage: `{back}`",
890
+ parameters: []
891
+ },
892
+ forward: {
893
+ name: "forward",
894
+ description: "Creates a link or navigates to the next passage in history.\n\nUsage: `{forward}`",
895
+ parameters: []
896
+ },
897
+ restart: {
898
+ name: "restart",
899
+ description: "Restarts the story.\n\nUsage: `{restart}`",
900
+ parameters: []
901
+ },
902
+ include: {
903
+ name: "include",
904
+ description: 'Outputs the contents of the specified passage.\n\nUsage: `{include "passage"}`',
905
+ parameters: ["text"]
906
+ },
907
+ passage: {
908
+ name: "passage",
909
+ description: "Outputs the name of the current passage.\n\nUsage: `{passage}`",
910
+ parameters: []
911
+ },
912
+ textbox: {
913
+ name: "textbox",
914
+ description: 'Creates a text input box.\n\nUsage: `{textbox "$receiver" defaultValue}`',
915
+ parameters: ["receiver &+ text"]
916
+ },
917
+ numberbox: {
918
+ name: "numberbox",
919
+ description: 'Creates a number input box.\n\nUsage: `{numberbox "$receiver" defaultValue}`',
920
+ parameters: ["receiver &+ text"]
921
+ },
922
+ textarea: {
923
+ name: "textarea",
924
+ description: 'Creates a multi-line text input area.\n\nUsage: `{textarea "$receiver" defaultValue}`',
925
+ parameters: ["receiver &+ text"]
926
+ },
927
+ checkbox: {
928
+ name: "checkbox",
929
+ description: 'Creates a checkbox input.\n\nUsage: `{checkbox "$receiver" uncheckedValue checkedValue}`',
930
+ parameters: ["receiver &+ text"]
931
+ },
932
+ radiobutton: {
933
+ name: "radiobutton",
934
+ description: 'Creates a radio button input.\n\nUsage: `{radiobutton "$receiver" value}`',
935
+ parameters: ["receiver &+ text &+ text"]
936
+ },
937
+ quicksave: {
938
+ name: "quicksave",
939
+ description: "Performs a quick save of the current story state.\n\nUsage: `{quicksave}`",
940
+ parameters: []
941
+ },
942
+ quickload: {
943
+ name: "quickload",
944
+ description: "Loads the most recent quick save.\n\nUsage: `{quickload}`",
945
+ parameters: []
946
+ },
947
+ saves: {
948
+ name: "saves",
949
+ description: "Opens the saves dialog.\n\nUsage: `{saves}`",
950
+ parameters: []
951
+ },
952
+ settings: {
953
+ name: "settings",
954
+ description: "Opens the settings dialog.\n\nUsage: `{settings}`",
955
+ parameters: []
956
+ },
957
+ "story-title": {
958
+ name: "story-title",
959
+ description: "Outputs the story title.\n\nUsage: `{story-title}`",
960
+ parameters: []
961
+ },
962
+ dialog: {
963
+ name: "dialog",
964
+ container: true,
965
+ description: 'Opens a dialog with its contents.\n\nUsage:\n\n```\n{dialog "title"}\n ...\n{/dialog}\n```',
966
+ parameters: ["text"]
967
+ },
968
+ nobr: {
969
+ name: "nobr",
970
+ container: true,
971
+ description: "Removes line breaks from its contents.\n\nUsage:\n\n```\n{nobr}\n ...\n{/nobr}\n```",
972
+ parameters: []
973
+ },
974
+ span: {
975
+ name: "span",
976
+ container: true,
977
+ description: "Wraps its contents in a `<span>` element.\n\nUsage:\n\n```\n{span}\n ...\n{/span}\n```",
978
+ parameters: []
979
+ },
980
+ "save-manager": {
981
+ name: "save-manager",
982
+ description: "Renders the save/load UI.\n\nUsage: `{save-manager}`",
983
+ parameters: []
984
+ },
985
+ "settings-controls": {
986
+ name: "settings-controls",
987
+ description: "Renders the settings controls UI.\n\nUsage: `{settings-controls}`",
988
+ parameters: []
989
+ },
990
+ watch: {
991
+ name: "watch",
992
+ description: "Watches a variable for changes and triggers a re-render.\n\nUsage: `{watch $var}`",
993
+ parameters: ["...text"]
994
+ },
995
+ unwatch: {
996
+ name: "unwatch",
997
+ description: "Stops watching a variable.\n\nUsage: `{unwatch $var}`",
998
+ parameters: ["...text"]
999
+ }
1000
+ };
1001
+ }
1002
+ });
1003
+
1004
+ // src/core/workspace/workspace-model.ts
1005
+ import { EventEmitter as EventEmitter2 } from "node:events";
1006
+ var WorkspaceModel;
1007
+ var init_workspace_model = __esm({
1008
+ "src/core/workspace/workspace-model.ts"() {
1009
+ "use strict";
1010
+ init_document_store();
1011
+ init_passage_index();
1012
+ init_macro_registry();
1013
+ init_variable_tracker();
1014
+ init_widget_registry();
1015
+ init_macro_parser();
1016
+ init_macro_supplements();
1017
+ WorkspaceModel = class extends EventEmitter2 {
1018
+ documents;
1019
+ passages;
1020
+ macros;
1021
+ variables;
1022
+ widgets;
1023
+ debounceTimer = null;
1024
+ DEBOUNCE_MS = 200;
1025
+ /** Bound listeners for cleanup in dispose(). */
1026
+ onDocumentChanged;
1027
+ onDocumentOpened;
1028
+ onDocumentClosed;
1029
+ constructor(config) {
1030
+ super();
1031
+ this.documents = new DocumentStore();
1032
+ this.passages = new PassageIndex();
1033
+ this.macros = new MacroRegistry();
1034
+ this.variables = new VariableTracker();
1035
+ this.widgets = new WidgetRegistry();
1036
+ this.onDocumentChanged = (uri) => this.handleDocumentChange(uri);
1037
+ this.onDocumentOpened = (uri) => this.handleDocumentChange(uri);
1038
+ this.onDocumentClosed = (uri) => this.handleDocumentClose(uri);
1039
+ this.documents.on("documentChanged", this.onDocumentChanged);
1040
+ this.documents.on("documentOpened", this.onDocumentOpened);
1041
+ this.documents.on("documentClosed", this.onDocumentClosed);
1042
+ }
1043
+ /**
1044
+ * Initialize the workspace in CLI/batch mode.
1045
+ * Loads all file contents, builds indices, and emits 'modelReady'.
1046
+ */
1047
+ initialize(fileContents) {
1048
+ this.macros.loadBuiltins();
1049
+ this.macros.loadSupplements(macro_supplements_default);
1050
+ this.documents.removeListener("documentOpened", this.onDocumentOpened);
1051
+ this.documents.removeListener("documentChanged", this.onDocumentChanged);
1052
+ for (const [uri, text] of fileContents) {
1053
+ this.documents.open(uri, text);
1054
+ }
1055
+ this.documents.on("documentOpened", this.onDocumentOpened);
1056
+ this.documents.on("documentChanged", this.onDocumentChanged);
1057
+ this.rebuildAll();
1058
+ this.scheduleModelReady();
1059
+ }
1060
+ /** Clean up listeners and timers. */
1061
+ dispose() {
1062
+ this.documents.removeListener("documentChanged", this.onDocumentChanged);
1063
+ this.documents.removeListener("documentOpened", this.onDocumentOpened);
1064
+ this.documents.removeListener("documentClosed", this.onDocumentClosed);
1065
+ if (this.debounceTimer) {
1066
+ clearTimeout(this.debounceTimer);
1067
+ this.debounceTimer = null;
1068
+ }
1069
+ this.removeAllListeners();
1070
+ }
1071
+ /** Handle a document change: rebuild passages for that document, then cascade. */
1072
+ handleDocumentChange(uri) {
1073
+ const text = this.documents.getText(uri);
1074
+ if (text !== void 0) {
1075
+ this.passages.rebuild(uri, text);
1076
+ }
1077
+ this.cascade();
1078
+ this.emit("documentChanged", uri);
1079
+ this.emit("passagesUpdated", uri);
1080
+ this.scheduleModelReady();
1081
+ }
1082
+ /** Handle a document close: remove its passages and cascade. */
1083
+ handleDocumentClose(uri) {
1084
+ this.passages.remove(uri);
1085
+ this.cascade();
1086
+ this.emit("documentClosed", uri);
1087
+ this.emit("passagesUpdated", uri);
1088
+ this.scheduleModelReady();
1089
+ }
1090
+ /** Rebuild all indices from scratch. */
1091
+ rebuildAll() {
1092
+ for (const uri of this.documents.getUris()) {
1093
+ const text = this.documents.getText(uri);
1094
+ if (text !== void 0) {
1095
+ this.passages.rebuild(uri, text);
1096
+ }
1097
+ }
1098
+ this.cascade();
1099
+ }
1100
+ /**
1101
+ * Cascade: rescan variables and widgets based on current passages.
1102
+ * Called after any passage index update.
1103
+ */
1104
+ cascade() {
1105
+ const storyVars = this.passages.getStoryVariables();
1106
+ if (storyVars) {
1107
+ const text = this.documents.getText(storyVars.uri);
1108
+ if (text) {
1109
+ const lines = text.split("\n");
1110
+ const contentStart = storyVars.headerEnd.end.line + 1;
1111
+ let contentEnd = lines.length;
1112
+ for (let i = contentStart; i < lines.length; i++) {
1113
+ if (/^::\s+/.test(lines[i])) {
1114
+ contentEnd = i;
1115
+ break;
1116
+ }
1117
+ }
1118
+ const content = lines.slice(contentStart, contentEnd).join("\n");
1119
+ this.variables.parseStoryVariables(content);
1120
+ }
1121
+ }
1122
+ for (const uri of this.documents.getUris()) {
1123
+ const text = this.documents.getText(uri);
1124
+ if (text) {
1125
+ const macros = parseMacros(text);
1126
+ this.variables.scanDocument(uri, text, macros);
1127
+ }
1128
+ }
1129
+ const allPassages = this.passages.getAllPassages();
1130
+ this.widgets.scan(allPassages, (uri) => this.documents.getText(uri));
1131
+ }
1132
+ /** Debounce modelReady emission. */
1133
+ scheduleModelReady() {
1134
+ if (this.debounceTimer) {
1135
+ clearTimeout(this.debounceTimer);
1136
+ }
1137
+ this.debounceTimer = setTimeout(() => {
1138
+ this.debounceTimer = null;
1139
+ this.emit("modelReady");
1140
+ }, this.DEBOUNCE_MS);
1141
+ }
1142
+ };
1143
+ }
1144
+ });
1145
+
1146
+ // src/core/diagnostic-codes.ts
1147
+ function getSeverity(code) {
1148
+ return severityMap[code];
1149
+ }
1150
+ var DiagnosticCode, severityMap;
1151
+ var init_diagnostic_codes = __esm({
1152
+ "src/core/diagnostic-codes.ts"() {
1153
+ "use strict";
1154
+ DiagnosticCode = {
1155
+ UndefinedMacro: "SP100",
1156
+ MalformedContainer: "SP101",
1157
+ IllegalClosingTag: "SP104",
1158
+ InvalidChildren: "SP107",
1159
+ ExpectedNoArguments: "SP108",
1160
+ ParameterTypeError: "SP109",
1161
+ ParameterWarning: "SP110",
1162
+ TooManyArguments: "SP111",
1163
+ ArgumentParsingWarning: "SP112",
1164
+ ChildMaxExceeded: "SP114",
1165
+ ChildMinNotMet: "SP115",
1166
+ UndeclaredVariable: "SP200",
1167
+ UndeclaredField: "SP201",
1168
+ NoStoryVariables: "SP202",
1169
+ BrokenPassageLink: "SP300",
1170
+ WidgetArgCountMismatch: "SP301",
1171
+ DeadEndPassage: "SP400",
1172
+ UnreachablePassage: "SP401"
1173
+ };
1174
+ severityMap = {
1175
+ SP100: "warning",
1176
+ SP101: "error",
1177
+ SP104: "error",
1178
+ SP107: "error",
1179
+ SP108: "error",
1180
+ SP109: "error",
1181
+ SP110: "warning",
1182
+ SP111: "error",
1183
+ SP112: "warning",
1184
+ SP114: "error",
1185
+ SP115: "error",
1186
+ SP200: "warning",
1187
+ SP201: "warning",
1188
+ SP202: "info",
1189
+ SP300: "warning",
1190
+ SP301: "warning",
1191
+ SP400: "hint",
1192
+ SP401: "hint"
1193
+ };
1194
+ }
1195
+ });
1196
+
1197
+ // src/core/parsing/argument-lexer.ts
1198
+ function lexArguments(source) {
1199
+ const lexer = new Lexer(source, lexSpace);
1200
+ const rawItems = lexer.run();
1201
+ const args2 = [];
1202
+ for (const item of rawItems) {
1203
+ if (item.type === 0 /* Error */) {
1204
+ break;
1205
+ }
1206
+ args2.push(classifyToken(item));
1207
+ }
1208
+ return args2;
1209
+ }
1210
+ function classifyToken(item) {
1211
+ const text = item.text;
1212
+ const base = { text, start: item.start, end: item.position };
1213
+ switch (item.type) {
1214
+ case 3 /* String */:
1215
+ return { ...base, type: 0 /* String */ };
1216
+ case 2 /* Expression */:
1217
+ return { ...base, type: 7 /* Expression */ };
1218
+ case 4 /* SquareBracket */: {
1219
+ const inner = text.slice(1);
1220
+ if (/^[<>]?[Ii][Mm][Gg]\[/.test(inner)) {
1221
+ return { ...base, type: 6 /* Image */ };
1222
+ }
1223
+ return { ...base, type: 5 /* Link */ };
1224
+ }
1225
+ case 1 /* Bareword */: {
1226
+ if (varTestRegexp.test(text)) {
1227
+ const sigil = text[0];
1228
+ const pathParts = text.slice(1).split(".");
1229
+ return {
1230
+ ...base,
1231
+ type: 4 /* Variable */,
1232
+ sigil,
1233
+ path: pathParts
1234
+ };
1235
+ }
1236
+ if (text === "null") return { ...base, type: 3 /* Null */ };
1237
+ if (text === "undefined") return { ...base, type: 10 /* Undefined */ };
1238
+ if (text === "true") return { ...base, type: 2 /* Boolean */ };
1239
+ if (text === "false") return { ...base, type: 2 /* Boolean */ };
1240
+ if (text === "NaN") return { ...base, type: 9 /* NaN */ };
1241
+ const asNum = Number(text);
1242
+ if (!Number.isNaN(asNum)) {
1243
+ return { ...base, type: 1 /* Number */ };
1244
+ }
1245
+ return { ...base, type: 8 /* Bareword */ };
1246
+ }
1247
+ default:
1248
+ return { ...base, type: 8 /* Bareword */ };
1249
+ }
1250
+ }
1251
+ function slurpQuote(lexer, endQuote) {
1252
+ for (; ; ) {
1253
+ const next = lexer.next();
1254
+ if (next === "\\") {
1255
+ const ch = lexer.next();
1256
+ if (ch !== EOF && ch !== "\n") continue;
1257
+ } else if (next === EOF) {
1258
+ return EOF;
1259
+ } else if (next === "\n" && endQuote !== "`") {
1260
+ return EOF;
1261
+ } else if (next === endQuote) {
1262
+ break;
1263
+ }
1264
+ }
1265
+ return lexer.pos;
1266
+ }
1267
+ function lexSpace(lexer) {
1268
+ const offset = lexer.source.slice(lexer.pos).search(notSpaceRegex);
1269
+ if (offset === -1) return null;
1270
+ if (offset !== 0) {
1271
+ lexer.pos += offset;
1272
+ lexer.ignore();
1273
+ }
1274
+ if (lexer.peek() === ",") {
1275
+ lexer.next();
1276
+ lexer.ignore();
1277
+ return lexSpace;
1278
+ }
1279
+ switch (lexer.next()) {
1280
+ case "`":
1281
+ return lexExpression;
1282
+ case '"':
1283
+ return lexDoubleQuote;
1284
+ case "'":
1285
+ return lexSingleQuote;
1286
+ case "[":
1287
+ return lexSquareBracket;
1288
+ default:
1289
+ return lexBareword;
1290
+ }
1291
+ }
1292
+ function lexExpression(lexer) {
1293
+ if (slurpQuote(lexer, "`") === EOF) {
1294
+ return lexer.error(0 /* Error */, "unterminated backquote expression");
1295
+ }
1296
+ lexer.emit(2 /* Expression */);
1297
+ return lexSpace;
1298
+ }
1299
+ function lexDoubleQuote(lexer) {
1300
+ if (slurpQuote(lexer, '"') === EOF) {
1301
+ return lexer.error(0 /* Error */, "unterminated double quoted string");
1302
+ }
1303
+ lexer.emit(3 /* String */);
1304
+ return lexSpace;
1305
+ }
1306
+ function lexSingleQuote(lexer) {
1307
+ if (slurpQuote(lexer, "'") === EOF) {
1308
+ return lexer.error(0 /* Error */, "unterminated single quoted string");
1309
+ }
1310
+ lexer.emit(3 /* String */);
1311
+ return lexSpace;
1312
+ }
1313
+ function lexSquareBracket(lexer) {
1314
+ const imgMeta = "<>IiMmGg";
1315
+ let what;
1316
+ if (lexer.accept(imgMeta)) {
1317
+ what = "image";
1318
+ lexer.acceptRun(imgMeta);
1319
+ } else {
1320
+ what = "link";
1321
+ }
1322
+ if (!lexer.accept("[")) {
1323
+ return lexer.error(0 /* Error */, `malformed ${what} markup`);
1324
+ }
1325
+ lexer.depth = 2;
1326
+ for (; ; ) {
1327
+ switch (lexer.next()) {
1328
+ case "\\": {
1329
+ const ch = lexer.next();
1330
+ if (ch !== EOF && ch !== "\n") break;
1331
+ }
1332
+ /* falls through */
1333
+ case EOF:
1334
+ case "\n":
1335
+ return lexer.error(0 /* Error */, `unterminated ${what} markup`);
1336
+ case "[":
1337
+ ++lexer.depth;
1338
+ break;
1339
+ case "]":
1340
+ --lexer.depth;
1341
+ if (lexer.depth < 0) {
1342
+ return lexer.error(0 /* Error */, "unexpected right square bracket ']'");
1343
+ }
1344
+ if (lexer.depth === 1) {
1345
+ if (lexer.next() === "]") {
1346
+ --lexer.depth;
1347
+ lexer.emit(4 /* SquareBracket */);
1348
+ return lexSpace;
1349
+ }
1350
+ lexer.backup();
1351
+ }
1352
+ break;
1353
+ }
1354
+ }
1355
+ }
1356
+ function lexBareword(lexer) {
1357
+ const offset = lexer.source.slice(lexer.pos).search(spaceRegex);
1358
+ if (offset === -1) {
1359
+ let end = lexer.source.length;
1360
+ while (end > lexer.pos && lexer.source[end - 1] === ",") {
1361
+ end--;
1362
+ }
1363
+ lexer.pos = end;
1364
+ lexer.emit(1 /* Bareword */);
1365
+ if (end < lexer.source.length) {
1366
+ lexer.pos = end + 1;
1367
+ lexer.ignore();
1368
+ return lexSpace;
1369
+ }
1370
+ return null;
1371
+ }
1372
+ const commaOffset = lexer.source.slice(lexer.pos).indexOf(",");
1373
+ if (commaOffset !== -1 && commaOffset < offset) {
1374
+ lexer.pos += commaOffset;
1375
+ lexer.emit(1 /* Bareword */);
1376
+ lexer.pos++;
1377
+ lexer.ignore();
1378
+ return lexSpace;
1379
+ }
1380
+ lexer.pos += offset;
1381
+ lexer.emit(1 /* Bareword */);
1382
+ return lexSpace;
1383
+ }
1384
+ var varTestRegexp, EOF, Lexer, notSpaceRegex, spaceRegex;
1385
+ var init_argument_lexer = __esm({
1386
+ "src/core/parsing/argument-lexer.ts"() {
1387
+ "use strict";
1388
+ varTestRegexp = /^[$_@][$A-Z_a-z][$0-9A-Z_.a-z]*/;
1389
+ EOF = -1;
1390
+ Lexer = class {
1391
+ source;
1392
+ state;
1393
+ start = 0;
1394
+ pos = 0;
1395
+ depth = 0;
1396
+ items = [];
1397
+ constructor(source, initial) {
1398
+ this.source = source;
1399
+ this.state = initial;
1400
+ }
1401
+ run() {
1402
+ while (this.state !== null) {
1403
+ this.state = this.state(this);
1404
+ }
1405
+ return this.items;
1406
+ }
1407
+ next() {
1408
+ const ch = this.peek();
1409
+ this.pos++;
1410
+ return ch;
1411
+ }
1412
+ peek() {
1413
+ if (this.pos >= this.source.length) return EOF;
1414
+ return this.source[this.pos];
1415
+ }
1416
+ backup(num = 1) {
1417
+ this.pos -= num;
1418
+ }
1419
+ forward(num = 1) {
1420
+ this.pos += num;
1421
+ }
1422
+ ignore() {
1423
+ this.start = this.pos;
1424
+ }
1425
+ accept(valid) {
1426
+ const ch = this.next();
1427
+ if (ch === EOF) return false;
1428
+ if (valid.includes(ch)) return true;
1429
+ this.backup();
1430
+ return false;
1431
+ }
1432
+ acceptRun(valid) {
1433
+ for (; ; ) {
1434
+ const ch = this.next();
1435
+ if (ch === EOF) return;
1436
+ if (!valid.includes(ch)) {
1437
+ break;
1438
+ }
1439
+ }
1440
+ this.backup();
1441
+ }
1442
+ emit(type) {
1443
+ this.items.push({
1444
+ type,
1445
+ text: this.source.slice(this.start, this.pos),
1446
+ start: this.start,
1447
+ position: this.pos
1448
+ });
1449
+ this.start = this.pos;
1450
+ }
1451
+ error(type, message) {
1452
+ this.items.push({
1453
+ type,
1454
+ message,
1455
+ text: this.source.slice(this.start, this.pos),
1456
+ start: this.start,
1457
+ position: this.pos
1458
+ });
1459
+ return null;
1460
+ }
1461
+ };
1462
+ notSpaceRegex = /\S/;
1463
+ spaceRegex = /\s/;
1464
+ }
1465
+ });
1466
+
1467
+ // src/core/parsing/parameter-validator.ts
1468
+ function isAlwaysArg(arg) {
1469
+ return arg.type === 4 /* Variable */ || arg.type === 7 /* Expression */;
1470
+ }
1471
+ function findParameterType(name) {
1472
+ for (const pt of parameterTypes) {
1473
+ if (pt.name.includes(name)) return pt;
1474
+ }
1475
+ return null;
1476
+ }
1477
+ function lexFormat(formatString) {
1478
+ let pos = 0;
1479
+ const lexed = [];
1480
+ let iter = 0;
1481
+ for (; ; ) {
1482
+ if (++iter > LEX_LIMIT) throw new Error("Format lex iteration limit exceeded");
1483
+ if (pos >= formatString.length) break;
1484
+ const ch = formatString[pos];
1485
+ if (wsRe.test(ch)) {
1486
+ pos++;
1487
+ } else if (ch === "&") {
1488
+ pos++;
1489
+ const next = formatString[pos];
1490
+ if (next === "+") {
1491
+ pos++;
1492
+ lexed.push({ kind: 3 /* AndNext */ });
1493
+ } else {
1494
+ throw new Error(`Lex: Expected '+' after '&', found '${next ?? "EOF"}'`);
1495
+ }
1496
+ } else if (ch === "|") {
1497
+ pos++;
1498
+ const next = formatString[pos];
1499
+ if (next === "+") {
1500
+ pos++;
1501
+ lexed.push({ kind: 4 /* MaybeNext */ });
1502
+ } else if (next === void 0) {
1503
+ throw new Error("Lex: Expected content after '|'");
1504
+ } else if (wsRe.test(next)) {
1505
+ pos++;
1506
+ lexed.push({ kind: 5 /* Or */ });
1507
+ } else if (identStartRe.test(next) || quoteRe.test(next)) {
1508
+ lexed.push({ kind: 5 /* Or */ });
1509
+ } else {
1510
+ throw new Error(`Lex: Invalid character after '|': '${next}'`);
1511
+ }
1512
+ } else if (quoteRe.test(ch)) {
1513
+ const openQuote = ch;
1514
+ const start = pos;
1515
+ pos++;
1516
+ while (pos < formatString.length && formatString[pos] !== openQuote) {
1517
+ if (++iter > LEX_LIMIT) throw new Error("Format lex string iteration limit exceeded");
1518
+ pos++;
1519
+ }
1520
+ if (pos >= formatString.length) throw new Error("Lex: Unterminated string literal");
1521
+ const value = formatString.slice(start + 1, pos);
1522
+ pos++;
1523
+ lexed.push({ kind: 1 /* Literal */, value });
1524
+ } else if (ch === "(" || ch === ")") {
1525
+ pos++;
1526
+ lexed.push({ kind: 2 /* Group */, open: ch === "(" });
1527
+ } else if (identStartRe.test(ch)) {
1528
+ const start = pos;
1529
+ pos++;
1530
+ while (pos < formatString.length) {
1531
+ if (++iter > LEX_LIMIT) throw new Error("Format lex identifier iteration limit exceeded");
1532
+ const c = formatString[pos];
1533
+ if (identStopRe.test(c) || c === void 0) break;
1534
+ if (identStartRe.test(c)) {
1535
+ pos++;
1536
+ } else if (digitRe.test(c)) {
1537
+ throw new Error(`Lex: Digit '${c}' not allowed in parameter type name`);
1538
+ } else {
1539
+ throw new Error(`Lex: Invalid character '${c}' in parameter type name`);
1540
+ }
1541
+ }
1542
+ const ident = formatString.slice(start, pos);
1543
+ const pt = findParameterType(ident);
1544
+ if (pt === null) throw new Error(`Lex: Unknown parameter type '${ident}'`);
1545
+ lexed.push({ kind: 0 /* Type */, type: pt });
1546
+ } else if (ch === "." && formatString[pos + 1] === "." && formatString[pos + 2] === ".") {
1547
+ pos += 3;
1548
+ lexed.push({ kind: 6 /* Repeat */ });
1549
+ } else {
1550
+ throw new Error(`Lex: Unexpected character '${ch}'`);
1551
+ }
1552
+ }
1553
+ return lexed;
1554
+ }
1555
+ function parseFormat(formatString) {
1556
+ const tokens = lexFormat(formatString);
1557
+ if (tokens.length === 0) return null;
1558
+ let index = 0;
1559
+ let iter = 0;
1560
+ function infixBp(kind) {
1561
+ switch (kind) {
1562
+ case 5 /* Or */:
1563
+ return [9, 10];
1564
+ case 3 /* AndNext */:
1565
+ return [4, 5];
1566
+ case 4 /* MaybeNext */:
1567
+ return [2, 3];
1568
+ default:
1569
+ return null;
1570
+ }
1571
+ }
1572
+ function prefixBp(kind) {
1573
+ switch (kind) {
1574
+ case 4 /* MaybeNext */:
1575
+ return 3;
1576
+ case 6 /* Repeat */:
1577
+ return 8;
1578
+ default:
1579
+ throw new Error(`Parse: Unexpected prefix operator kind ${kind}`);
1580
+ }
1581
+ }
1582
+ function isPrefixOp(t) {
1583
+ return t.kind === 4 /* MaybeNext */ || t.kind === 6 /* Repeat */;
1584
+ }
1585
+ function isValue(t) {
1586
+ return t.kind === 0 /* Type */ || t.kind === 1 /* Literal */;
1587
+ }
1588
+ function exprBp(minBp) {
1589
+ if (++iter > PARSE_LIMIT) throw new Error("Format parse iteration limit exceeded");
1590
+ const cur = tokens[index];
1591
+ if (!cur) throw new Error("Parse: Unexpected end of format string");
1592
+ index++;
1593
+ let lhs;
1594
+ if (isPrefixOp(cur)) {
1595
+ const rBp = prefixBp(cur.kind);
1596
+ const rhs = exprBp(rBp);
1597
+ lhs = { kind: cur.kind, right: rhs };
1598
+ } else if (cur.kind === 2 /* Group */) {
1599
+ const g = cur;
1600
+ if (!g.open) throw new Error("Parse: Unexpected closing parenthesis");
1601
+ lhs = exprBp(0);
1602
+ const closing = tokens[index];
1603
+ if (!closing || closing.kind !== 2 /* Group */ || closing.open) {
1604
+ throw new Error("Parse: Expected closing parenthesis");
1605
+ }
1606
+ index++;
1607
+ } else if (isValue(cur)) {
1608
+ lhs = cur;
1609
+ } else {
1610
+ throw new Error("Parse: Expected value or prefix operator");
1611
+ }
1612
+ for (; ; ) {
1613
+ if (++iter > PARSE_LIMIT) throw new Error("Format parse operator iteration limit exceeded");
1614
+ const op = tokens[index];
1615
+ if (!op) break;
1616
+ const bp = infixBp(op.kind);
1617
+ if (bp === null) break;
1618
+ const [lBp, rBp] = bp;
1619
+ if (lBp < minBp) break;
1620
+ index++;
1621
+ const rhs = exprBp(rBp);
1622
+ lhs = { kind: op.kind, left: lhs, right: rhs };
1623
+ }
1624
+ return lhs;
1625
+ }
1626
+ return exprBp(0);
1627
+ }
1628
+ function formatArgCountRange(format) {
1629
+ switch (format.kind) {
1630
+ case 0 /* Type */:
1631
+ case 1 /* Literal */:
1632
+ return { min: 1, max: 1 };
1633
+ case 3 /* AndNext */: {
1634
+ const l = formatArgCountRange(format.left);
1635
+ const r = formatArgCountRange(format.right);
1636
+ return { min: l.min + r.min, max: l.max + r.max };
1637
+ }
1638
+ case 4 /* MaybeNext */: {
1639
+ const l = format.left ? formatArgCountRange(format.left) : { min: 0, max: 0 };
1640
+ const r = formatArgCountRange(format.right);
1641
+ return { min: l.min, max: l.max + r.max };
1642
+ }
1643
+ case 5 /* Or */: {
1644
+ const l = formatArgCountRange(format.left);
1645
+ const r = formatArgCountRange(format.right);
1646
+ return { min: Math.min(l.min, r.min), max: Math.max(l.max, r.max) };
1647
+ }
1648
+ case 6 /* Repeat */:
1649
+ return { min: 0, max: Infinity };
1650
+ }
1651
+ }
1652
+ function makeError(status, message, argIndex, rank = 0) {
1653
+ return {
1654
+ status,
1655
+ rank,
1656
+ argIndex,
1657
+ errors: [{ message, argIndex }],
1658
+ warnings: []
1659
+ };
1660
+ }
1661
+ function makeSuccess(argIndex, rank, warnings = []) {
1662
+ return { status: 2 /* Success */, rank, argIndex, errors: [], warnings };
1663
+ }
1664
+ function isFail(s) {
1665
+ return s === 1 /* Failure */ || s === 0 /* NotFound */;
1666
+ }
1667
+ function crawlValidate(format, args2, startIndex, state) {
1668
+ let iter = 0;
1669
+ function crawl(fmt, argIdx) {
1670
+ if (++iter > VALIDATE_LIMIT) {
1671
+ throw new Error(`Validation iteration limit exceeded at argument ${argIdx}`);
1672
+ }
1673
+ switch (fmt.kind) {
1674
+ case 4 /* MaybeNext */: {
1675
+ let rank = 0;
1676
+ let warnings = [];
1677
+ let idx = argIdx;
1678
+ if (fmt.left) {
1679
+ const left = crawl(fmt.left, idx);
1680
+ if (isFail(left.status)) return left;
1681
+ rank += left.rank;
1682
+ idx = left.argIndex;
1683
+ warnings = left.warnings;
1684
+ }
1685
+ const right = crawl(fmt.right, idx);
1686
+ if (right.status === 0 /* NotFound */) {
1687
+ return makeSuccess(idx, rank, warnings);
1688
+ }
1689
+ if (isFail(right.status)) {
1690
+ right.status = 1 /* Failure */;
1691
+ return right;
1692
+ }
1693
+ return makeSuccess(right.argIndex, rank + right.rank, warnings.concat(right.warnings));
1694
+ }
1695
+ case 3 /* AndNext */: {
1696
+ const left = crawl(fmt.left, argIdx);
1697
+ if (isFail(left.status)) return left;
1698
+ const right = crawl(fmt.right, left.argIndex);
1699
+ if (isFail(right.status)) return right;
1700
+ return makeSuccess(
1701
+ right.argIndex,
1702
+ left.rank + right.rank,
1703
+ left.warnings.concat(right.warnings)
1704
+ );
1705
+ }
1706
+ case 5 /* Or */: {
1707
+ const left = crawl(fmt.left, argIdx);
1708
+ if (!isFail(left.status)) return left;
1709
+ const right = crawl(fmt.right, argIdx);
1710
+ if (!isFail(right.status)) return right;
1711
+ return left;
1712
+ }
1713
+ case 6 /* Repeat */: {
1714
+ let rank = 0;
1715
+ let warnings = [];
1716
+ let idx = argIdx;
1717
+ for (; ; ) {
1718
+ const r = crawl(fmt.right, idx);
1719
+ if (isFail(r.status)) break;
1720
+ idx = r.argIndex;
1721
+ rank += r.rank;
1722
+ warnings = warnings.concat(r.warnings);
1723
+ }
1724
+ return makeSuccess(idx, rank, warnings);
1725
+ }
1726
+ case 1 /* Literal */: {
1727
+ const arg = args2[argIdx];
1728
+ if (arg === void 0) {
1729
+ return makeError(0 /* NotFound */, `Expected literal '${fmt.value}' but no argument found`, argIdx);
1730
+ }
1731
+ if (isAlwaysArg(arg)) {
1732
+ return makeSuccess(argIdx + 1, CORRECT_RANK);
1733
+ }
1734
+ const argText = arg.type === 0 /* String */ ? arg.text.slice(1, -1) : arg.text;
1735
+ if (argText === fmt.value) {
1736
+ return makeSuccess(argIdx + 1, CORRECT_RANK);
1737
+ }
1738
+ if (fmt.value === "null" && arg.type === 3 /* Null */ || fmt.value === "undefined" && arg.type === 10 /* Undefined */ || fmt.value === "true" && arg.type === 2 /* Boolean */ && arg.text === "true" || fmt.value === "false" && arg.type === 2 /* Boolean */ && arg.text === "false" || fmt.value === "NaN" && arg.type === 9 /* NaN */) {
1739
+ return makeSuccess(argIdx + 1, CORRECT_RANK);
1740
+ }
1741
+ return makeError(1 /* Failure */, `Expected literal '${fmt.value}'`, argIdx, CORRECT_TYPE_RANK);
1742
+ }
1743
+ case 0 /* Type */: {
1744
+ const arg = args2[argIdx];
1745
+ const typeDef = fmt.type;
1746
+ if (arg === void 0) {
1747
+ return makeError(
1748
+ 0 /* NotFound */,
1749
+ `Expected type '${typeDef.name[0]}' but no argument found`,
1750
+ argIdx
1751
+ );
1752
+ }
1753
+ if (isAlwaysArg(arg)) {
1754
+ return makeSuccess(argIdx + 1, CORRECT_RANK);
1755
+ }
1756
+ const result = typeDef.validate(arg, args2, argIdx, state);
1757
+ if (result instanceof TypeError2) {
1758
+ return {
1759
+ status: 1 /* Failure */,
1760
+ rank: 0,
1761
+ argIndex: argIdx,
1762
+ errors: [{ message: result.message, argIndex: argIdx }],
1763
+ warnings: []
1764
+ };
1765
+ }
1766
+ if (result instanceof TypeWarning) {
1767
+ return makeSuccess(argIdx + 1, CORRECT_RANK, [
1768
+ { message: result.message, argIndex: argIdx }
1769
+ ]);
1770
+ }
1771
+ return makeSuccess(argIdx + 1, CORRECT_RANK);
1772
+ }
1773
+ }
1774
+ }
1775
+ return crawl(format, startIndex);
1776
+ }
1777
+ var TypeError2, TypeWarning, parameterTypes, LEX_LIMIT, wsRe, identStartRe, identStopRe, quoteRe, digitRe, PARSE_LIMIT, VALIDATE_LIMIT, CORRECT_TYPE_RANK, CORRECT_RANK, Parameters;
1778
+ var init_parameter_validator = __esm({
1779
+ "src/core/parsing/parameter-validator.ts"() {
1780
+ "use strict";
1781
+ init_argument_lexer();
1782
+ TypeError2 = class {
1783
+ constructor(message) {
1784
+ this.message = message;
1785
+ }
1786
+ };
1787
+ TypeWarning = class {
1788
+ constructor(message) {
1789
+ this.message = message;
1790
+ }
1791
+ };
1792
+ parameterTypes = [
1793
+ {
1794
+ name: ["bool", "boolean"],
1795
+ validate(arg) {
1796
+ return arg.type === 2 /* Boolean */ ? null : new TypeError2("Argument is not a boolean");
1797
+ }
1798
+ },
1799
+ {
1800
+ name: ["null"],
1801
+ validate(arg) {
1802
+ return arg.type === 3 /* Null */ ? null : new TypeError2("Argument is not 'null'");
1803
+ }
1804
+ },
1805
+ {
1806
+ name: ["undefined"],
1807
+ validate(arg) {
1808
+ return arg.type === 10 /* Undefined */ ? null : new TypeError2("Argument is not 'undefined'");
1809
+ }
1810
+ },
1811
+ {
1812
+ name: ["number"],
1813
+ validate(arg) {
1814
+ return arg.type === 1 /* Number */ ? null : new TypeError2("Argument is not a number");
1815
+ }
1816
+ },
1817
+ {
1818
+ name: ["NaN"],
1819
+ validate(arg) {
1820
+ return arg.type === 9 /* NaN */ ? null : new TypeError2("Argument is not 'NaN'");
1821
+ }
1822
+ },
1823
+ {
1824
+ name: ["link"],
1825
+ validate(arg) {
1826
+ return arg.type === 5 /* Link */ ? null : new TypeError2("Argument is not a link");
1827
+ }
1828
+ },
1829
+ {
1830
+ name: ["image"],
1831
+ validate(arg) {
1832
+ return arg.type === 6 /* Image */ ? null : new TypeError2("Argument is not an image");
1833
+ }
1834
+ },
1835
+ {
1836
+ name: ["bareword"],
1837
+ validate(arg) {
1838
+ return arg.type === 8 /* Bareword */ ? null : new TypeError2("Argument is not a bareword");
1839
+ }
1840
+ },
1841
+ {
1842
+ name: ["string"],
1843
+ validate(arg) {
1844
+ return arg.type === 0 /* String */ ? null : new TypeError2("Argument is not a quoted string");
1845
+ }
1846
+ },
1847
+ {
1848
+ name: ["text"],
1849
+ validate(arg) {
1850
+ const t = arg.type;
1851
+ if (t === 8 /* Bareword */ || t === 0 /* String */ || t === 2 /* Boolean */ || t === 3 /* Null */ || t === 9 /* NaN */ || t === 1 /* Number */) {
1852
+ return null;
1853
+ }
1854
+ return new TypeError2("Argument is not text");
1855
+ }
1856
+ },
1857
+ {
1858
+ name: ["var"],
1859
+ validate(arg) {
1860
+ return arg.type === 4 /* Variable */ ? null : new TypeError2("Argument is not a variable");
1861
+ }
1862
+ },
1863
+ {
1864
+ name: ["passage"],
1865
+ validate(arg, _args, _index, state) {
1866
+ let passageName;
1867
+ if (arg.type === 0 /* String */) {
1868
+ passageName = arg.text.slice(1, -1);
1869
+ } else if (arg.type === 8 /* Bareword */) {
1870
+ passageName = arg.text;
1871
+ } else if (arg.type === 1 /* Number */) {
1872
+ passageName = arg.text;
1873
+ } else if (arg.type === 9 /* NaN */) {
1874
+ passageName = "NaN";
1875
+ }
1876
+ if (passageName !== void 0) {
1877
+ passageName = passageName.replace(/\\/g, "");
1878
+ if (state && state.passages.length > 0) {
1879
+ if (!state.passages.includes(passageName)) {
1880
+ return new TypeWarning(`Nonexistent passage: "${passageName}"`);
1881
+ }
1882
+ }
1883
+ return null;
1884
+ }
1885
+ return new TypeError2("Argument is not an acceptable passage");
1886
+ }
1887
+ }
1888
+ ];
1889
+ LEX_LIMIT = 1e3;
1890
+ wsRe = /\s/;
1891
+ identStartRe = /[a-zA-Z]/;
1892
+ identStopRe = /[|&+\-\s)(]/;
1893
+ quoteRe = /['"]/;
1894
+ digitRe = /[0-9]/;
1895
+ PARSE_LIMIT = 2e3;
1896
+ VALIDATE_LIMIT = 2e3;
1897
+ CORRECT_TYPE_RANK = 1;
1898
+ CORRECT_RANK = 2;
1899
+ Parameters = class {
1900
+ variants;
1901
+ constructor(variants) {
1902
+ this.variants = variants.map((s) => ({
1903
+ formatString: s,
1904
+ format: parseFormat(s)
1905
+ }));
1906
+ }
1907
+ /**
1908
+ * Validate an array of Arg tokens against these parameter variants.
1909
+ * Selects the best-matching variant by rank.
1910
+ */
1911
+ validate(args2, stateInfo) {
1912
+ if (this.variants.length === 0) {
1913
+ if (args2.length === 0) {
1914
+ return { variantIndex: null, errors: [], warnings: [] };
1915
+ }
1916
+ return {
1917
+ variantIndex: null,
1918
+ errors: [{ message: "Expected no arguments", argIndex: 0 }],
1919
+ warnings: []
1920
+ };
1921
+ }
1922
+ let bestIndex = null;
1923
+ let bestRank = -Infinity;
1924
+ let bestErrors = [];
1925
+ let bestWarnings = [];
1926
+ let bestIsClean = false;
1927
+ for (let i = 0; i < this.variants.length; i++) {
1928
+ const variant = this.variants[i];
1929
+ if (variant.format === null) {
1930
+ if (args2.length === 0) {
1931
+ if (!bestIsClean || 0 > bestRank) {
1932
+ bestIndex = i;
1933
+ bestRank = 0;
1934
+ bestErrors = [];
1935
+ bestWarnings = [];
1936
+ bestIsClean = true;
1937
+ }
1938
+ } else {
1939
+ if (!bestIsClean && bestIndex === null) {
1940
+ bestIndex = i;
1941
+ bestRank = -1;
1942
+ bestErrors = [{ message: "Expected no arguments", argIndex: 0 }];
1943
+ bestWarnings = [];
1944
+ }
1945
+ }
1946
+ continue;
1947
+ }
1948
+ const result = crawlValidate(variant.format, args2, 0, stateInfo);
1949
+ let errors = result.errors;
1950
+ if (result.errors.length === 0 && result.argIndex < args2.length) {
1951
+ errors = [
1952
+ {
1953
+ message: `Too many arguments: expected ${result.argIndex}, got ${args2.length}`,
1954
+ argIndex: result.argIndex
1955
+ }
1956
+ ];
1957
+ }
1958
+ const rank = result.rank;
1959
+ const isClean = errors.length === 0;
1960
+ if (isClean && (!bestIsClean || rank > bestRank)) {
1961
+ bestIndex = i;
1962
+ bestRank = rank;
1963
+ bestErrors = [];
1964
+ bestWarnings = result.warnings;
1965
+ bestIsClean = true;
1966
+ } else if (!isClean && !bestIsClean && rank > bestRank) {
1967
+ bestIndex = i;
1968
+ bestRank = rank;
1969
+ bestErrors = errors;
1970
+ bestWarnings = result.warnings;
1971
+ } else if (bestIndex === null) {
1972
+ bestIndex = i;
1973
+ bestRank = rank;
1974
+ bestErrors = errors;
1975
+ bestWarnings = result.warnings;
1976
+ bestIsClean = isClean;
1977
+ }
1978
+ }
1979
+ return {
1980
+ variantIndex: bestIndex,
1981
+ errors: bestErrors,
1982
+ warnings: bestWarnings
1983
+ };
1984
+ }
1985
+ /**
1986
+ * Compute the min/max argument count across all variants.
1987
+ */
1988
+ argCountRange() {
1989
+ let min = Infinity;
1990
+ let max = 0;
1991
+ for (const variant of this.variants) {
1992
+ if (variant.format === null) {
1993
+ min = 0;
1994
+ } else {
1995
+ const range = formatArgCountRange(variant.format);
1996
+ min = Math.min(min, range.min);
1997
+ max = Math.max(max, range.max);
1998
+ }
1999
+ }
2000
+ if (min === Infinity) min = 0;
2001
+ return { min, max };
2002
+ }
2003
+ /**
2004
+ * Whether all variants are empty (accept no arguments).
2005
+ */
2006
+ isEmpty() {
2007
+ if (this.variants.length === 0) return true;
2008
+ return !this.variants.some((v) => v.format !== null);
2009
+ }
2010
+ };
2011
+ }
2012
+ });
2013
+
2014
+ // src/core/parsing/link-parser.ts
2015
+ function buildLineStarts2(text) {
2016
+ const starts = [0];
2017
+ for (let i = 0; i < text.length; i++) {
2018
+ if (text[i] === "\n") {
2019
+ starts.push(i + 1);
2020
+ }
2021
+ }
2022
+ return starts;
2023
+ }
2024
+ function offsetToPosition2(offset, lineStarts) {
2025
+ let low = 0;
2026
+ let high = lineStarts.length - 1;
2027
+ while (low < high) {
2028
+ const mid = low + high + 1 >> 1;
2029
+ if (lineStarts[mid] <= offset) {
2030
+ low = mid;
2031
+ } else {
2032
+ high = mid - 1;
2033
+ }
2034
+ }
2035
+ return { line: low, character: offset - lineStarts[low] };
2036
+ }
2037
+ function parseLinks(text, lineOffset = 0) {
2038
+ const lineStarts = buildLineStarts2(text);
2039
+ const refs = [];
2040
+ linkRegex.lastIndex = 0;
2041
+ let match;
2042
+ while ((match = linkRegex.exec(text)) !== null) {
2043
+ const passageName = match[2].trim();
2044
+ if (!passageName) continue;
2045
+ const nameStartInMatch = match[0].indexOf(passageName);
2046
+ const nameStart = match.index + nameStartInMatch;
2047
+ const nameEnd = nameStart + passageName.length;
2048
+ const startPos = offsetToPosition2(nameStart, lineStarts);
2049
+ const endPos = offsetToPosition2(nameEnd, lineStarts);
2050
+ const range = {
2051
+ start: { line: startPos.line + lineOffset, character: startPos.character },
2052
+ end: { line: endPos.line + lineOffset, character: endPos.character }
2053
+ };
2054
+ refs.push({
2055
+ name: passageName,
2056
+ range,
2057
+ source: "link"
2058
+ });
2059
+ }
2060
+ return refs;
2061
+ }
2062
+ var linkRegex;
2063
+ var init_link_parser = __esm({
2064
+ "src/core/parsing/link-parser.ts"() {
2065
+ "use strict";
2066
+ linkRegex = /\[\[([^\]]*?\|)?([^\]]+?)\]\]/g;
2067
+ }
2068
+ });
2069
+
2070
+ // src/plugins/diagnostics.ts
2071
+ function computeDiagnostics(uri, workspace) {
2072
+ try {
2073
+ const text = workspace.documents.getText(uri);
2074
+ if (text === void 0) return [];
2075
+ const passages = workspace.passages.getPassagesInDocument(uri);
2076
+ if (passages.length === 0) return [];
2077
+ const diagnostics = [];
2078
+ const macros = parseMacros(text);
2079
+ pairMacros(macros, (name) => workspace.macros.isBlock(name));
2080
+ const allPassages = workspace.passages.getAllPassages();
2081
+ const passageNames = new Set(allPassages.map((p) => p.name));
2082
+ try {
2083
+ validateMacros(macros, workspace, diagnostics);
2084
+ } catch {
2085
+ }
2086
+ try {
2087
+ validateArguments(macros, workspace, passageNames, diagnostics);
2088
+ } catch {
2089
+ }
2090
+ try {
2091
+ validateVariables(uri, workspace, diagnostics);
2092
+ } catch {
2093
+ }
2094
+ try {
2095
+ validateLinks(text, passages, passageNames, diagnostics);
2096
+ } catch {
2097
+ }
2098
+ try {
2099
+ validateWidgetInvocations(macros, workspace, diagnostics);
2100
+ } catch {
2101
+ }
2102
+ return diagnostics;
2103
+ } catch {
2104
+ return [];
2105
+ }
2106
+ }
2107
+ function validateMacros(macros, workspace, diagnostics) {
2108
+ for (let curIndex = 0; curIndex < macros.length; curIndex++) {
2109
+ const macro = macros[curIndex];
2110
+ const info = workspace.macros.getMacro(macro.name);
2111
+ if (info) {
2112
+ if (info.block) {
2113
+ if (macro.open && macro.pair === -1) {
2114
+ diagnostics.push(makeDiag(
2115
+ macro.range,
2116
+ DiagnosticCode.MalformedContainer,
2117
+ `Malformed container: no matching {/${macro.name}}`
2118
+ ));
2119
+ } else if (!macro.open && macro.pair === -1) {
2120
+ diagnostics.push(makeDiag(
2121
+ macro.range,
2122
+ DiagnosticCode.MalformedContainer,
2123
+ `Malformed container: no matching {${macro.name}}`
2124
+ ));
2125
+ }
2126
+ if (info.children && info.children.length > 0 && macro.open && macro.pair !== -1) {
2127
+ validateChildren(macros, curIndex, macro, info.children, workspace, diagnostics);
2128
+ }
2129
+ } else {
2130
+ if (!macro.open) {
2131
+ diagnostics.push(makeDiag(
2132
+ macro.range,
2133
+ DiagnosticCode.IllegalClosingTag,
2134
+ `Illegal closing tag: {${macro.name}} is not a container`
2135
+ ));
2136
+ }
2137
+ }
2138
+ if (info.parents && info.parents.length > 0 && macro.open) {
2139
+ if (!isInsideParent(macros, curIndex, info.parents, workspace)) {
2140
+ const parentList = info.parents.join(", ");
2141
+ diagnostics.push(makeDiag(
2142
+ macro.range,
2143
+ DiagnosticCode.InvalidChildren,
2144
+ `Invalid: {${macro.name}} can only be inside {${parentList}}`
2145
+ ));
2146
+ }
2147
+ }
2148
+ } else {
2149
+ const widget = workspace.widgets.getWidget(macro.name);
2150
+ if (!widget && macro.open) {
2151
+ diagnostics.push(makeDiag(
2152
+ macro.range,
2153
+ DiagnosticCode.UndefinedMacro,
2154
+ `Unrecognized macro: {${macro.name}}`
2155
+ ));
2156
+ }
2157
+ }
2158
+ }
2159
+ }
2160
+ function isInsideParent(macros, index, parents, workspace) {
2161
+ const parentSet = new Set(parents.map((p) => p.toLowerCase()));
2162
+ for (let i = index - 1; i >= 0; i--) {
2163
+ const candidate = macros[i];
2164
+ if (!candidate.open) continue;
2165
+ const candidateInfo = workspace.macros.getMacro(candidate.name);
2166
+ if (!candidateInfo?.block) continue;
2167
+ if (candidate.pair !== -1 && candidate.pair > index) {
2168
+ if (parentSet.has(candidate.name.toLowerCase())) {
2169
+ return true;
2170
+ }
2171
+ }
2172
+ }
2173
+ return false;
2174
+ }
2175
+ function validateChildren(macros, curIndex, parentMacro, childConstraints, workspace, diagnostics) {
2176
+ const children = /* @__PURE__ */ Object.create(null);
2177
+ const startIndex = curIndex + 1;
2178
+ const endIndex = parentMacro.pair;
2179
+ for (let i = startIndex; i < endIndex; i++) {
2180
+ const child = macros[i];
2181
+ const childInfo = workspace.macros.getMacro(child.name);
2182
+ if (!childInfo) continue;
2183
+ if (childInfo.block && child.open && child.pair !== -1) {
2184
+ i = child.pair;
2185
+ continue;
2186
+ }
2187
+ for (const constraint of childConstraints) {
2188
+ if (constraint.name === child.name) {
2189
+ children[child.name] = (children[child.name] ?? 0) + 1;
2190
+ }
2191
+ }
2192
+ }
2193
+ for (const constraint of childConstraints) {
2194
+ const count = children[constraint.name] ?? 0;
2195
+ if (constraint.max !== void 0 && count > constraint.max) {
2196
+ diagnostics.push(makeDiag(
2197
+ parentMacro.range,
2198
+ DiagnosticCode.ChildMaxExceeded,
2199
+ `Too many {${constraint.name}} in {${parentMacro.name}} (max ${constraint.max}, found ${count})`
2200
+ ));
2201
+ }
2202
+ if (constraint.min !== void 0 && count < constraint.min) {
2203
+ diagnostics.push(makeDiag(
2204
+ parentMacro.range,
2205
+ DiagnosticCode.ChildMinNotMet,
2206
+ `Too few {${constraint.name}} in {${parentMacro.name}} (min ${constraint.min}, found ${count})`
2207
+ ));
2208
+ }
2209
+ }
2210
+ }
2211
+ function validateArguments(macros, workspace, passageNames, diagnostics) {
2212
+ for (const macro of macros) {
2213
+ if (!macro.open) continue;
2214
+ const info = workspace.macros.getMacro(macro.name);
2215
+ if (!info) continue;
2216
+ if (info.skipArgs) continue;
2217
+ if (!info.parameters) continue;
2218
+ const rawArgs = macro.rawArgs ?? "";
2219
+ const args2 = lexArguments(rawArgs);
2220
+ const params = new Parameters(info.parameters);
2221
+ if (params.isEmpty()) {
2222
+ if (args2.length > 0) {
2223
+ diagnostics.push(makeDiag(
2224
+ macro.range,
2225
+ DiagnosticCode.ExpectedNoArguments,
2226
+ `Expected no arguments for {${macro.name}}, got ${args2.length}`
2227
+ ));
2228
+ }
2229
+ continue;
2230
+ }
2231
+ const stateInfo = { passages: Array.from(passageNames) };
2232
+ const result = params.validate(args2, stateInfo);
2233
+ if (result.variantIndex === null) {
2234
+ continue;
2235
+ }
2236
+ for (const error of result.errors) {
2237
+ if (error.message.startsWith("Too many arguments")) {
2238
+ diagnostics.push(makeDiag(
2239
+ macro.range,
2240
+ DiagnosticCode.TooManyArguments,
2241
+ `{${macro.name}}: ${error.message}`
2242
+ ));
2243
+ } else {
2244
+ diagnostics.push(makeDiag(
2245
+ macro.range,
2246
+ DiagnosticCode.ParameterTypeError,
2247
+ `{${macro.name}}: ${error.message}`
2248
+ ));
2249
+ }
2250
+ }
2251
+ for (const warning of result.warnings) {
2252
+ diagnostics.push(makeDiag(
2253
+ macro.range,
2254
+ DiagnosticCode.ParameterWarning,
2255
+ `{${macro.name}}: ${warning.message}`
2256
+ ));
2257
+ }
2258
+ }
2259
+ }
2260
+ function validateVariables(uri, workspace, diagnostics) {
2261
+ if (!workspace.variables.hasStoryVariables()) {
2262
+ const undeclared2 = workspace.variables.getUndeclared(uri);
2263
+ const text = workspace.documents.getText(uri);
2264
+ if (text && /\$[A-Za-z_$]/.test(text)) {
2265
+ const passages = workspace.passages.getPassagesInDocument(uri);
2266
+ const hasVarUsage = passages.some((p) => {
2267
+ const excluded = /* @__PURE__ */ new Set(["StoryVariables", "StoryInit", "StoryData", "StoryScript", "StoryInterface"]);
2268
+ return !excluded.has(p.name) && !p.tags?.includes("script") && !p.tags?.includes("stylesheet");
2269
+ });
2270
+ if (hasVarUsage) {
2271
+ diagnostics.push(makeDiag(
2272
+ passages[0].range,
2273
+ DiagnosticCode.NoStoryVariables,
2274
+ "No StoryVariables passage found. Declare all story variables with default values in a StoryVariables passage."
2275
+ ));
2276
+ }
2277
+ }
2278
+ return;
2279
+ }
2280
+ const undeclared = workspace.variables.getUndeclared(uri);
2281
+ for (const u of undeclared) {
2282
+ diagnostics.push(makeDiag(
2283
+ u.range,
2284
+ DiagnosticCode.UndeclaredVariable,
2285
+ `Variable '$${u.name}' is not declared in StoryVariables`
2286
+ ));
2287
+ }
2288
+ }
2289
+ function validateLinks(text, passages, passageNames, diagnostics) {
2290
+ const links = parseLinks(text);
2291
+ for (const link of links) {
2292
+ if (!passageNames.has(link.name)) {
2293
+ diagnostics.push(makeDiag(
2294
+ link.range,
2295
+ DiagnosticCode.BrokenPassageLink,
2296
+ `Passage "${link.name}" not found in workspace`
2297
+ ));
2298
+ }
2299
+ }
2300
+ }
2301
+ function validateWidgetInvocations(macros, workspace, diagnostics) {
2302
+ for (const macro of macros) {
2303
+ if (!macro.open) continue;
2304
+ const info = workspace.macros.getMacro(macro.name);
2305
+ if (info) continue;
2306
+ const widget = workspace.widgets.getWidget(macro.name);
2307
+ if (!widget) continue;
2308
+ const rawArgs = macro.rawArgs ?? "";
2309
+ const argCount = rawArgs.trim() === "" ? 0 : lexArguments(rawArgs).length;
2310
+ const expectedCount = widget.params.length;
2311
+ if (argCount !== expectedCount) {
2312
+ diagnostics.push(makeDiag(
2313
+ macro.range,
2314
+ DiagnosticCode.WidgetArgCountMismatch,
2315
+ `Widget {${macro.name}} expects ${expectedCount} argument(s), got ${argCount}`
2316
+ ));
2317
+ }
2318
+ }
2319
+ }
2320
+ function makeDiag(range, code, message) {
2321
+ return {
2322
+ range,
2323
+ message,
2324
+ severity: getSeverity(code),
2325
+ code,
2326
+ source: "spindle"
2327
+ };
2328
+ }
2329
+ function toLspDiagnostic(d) {
2330
+ const severityMap2 = {
2331
+ error: 1,
2332
+ warning: 2,
2333
+ info: 3,
2334
+ hint: 4
2335
+ };
2336
+ return {
2337
+ range: {
2338
+ start: { line: d.range.start.line, character: d.range.start.character },
2339
+ end: { line: d.range.end.line, character: d.range.end.character }
2340
+ },
2341
+ severity: severityMap2[d.severity],
2342
+ code: d.code,
2343
+ source: d.source,
2344
+ message: d.message
2345
+ };
2346
+ }
2347
+ var diagnosticsPlugin;
2348
+ var init_diagnostics = __esm({
2349
+ "src/plugins/diagnostics.ts"() {
2350
+ "use strict";
2351
+ init_diagnostic_codes();
2352
+ init_macro_parser();
2353
+ init_argument_lexer();
2354
+ init_parameter_validator();
2355
+ init_link_parser();
2356
+ diagnosticsPlugin = {
2357
+ id: "diagnostics",
2358
+ capabilities: {},
2359
+ initialize(ctx) {
2360
+ const publishDiagnostics = () => {
2361
+ for (const uri of ctx.workspace.documents.getUris()) {
2362
+ const diags = computeDiagnostics(uri, ctx.workspace);
2363
+ ctx.connection.sendDiagnostics({
2364
+ uri,
2365
+ diagnostics: diags.map((d) => toLspDiagnostic(d))
2366
+ });
2367
+ }
2368
+ };
2369
+ ctx.workspace.on("modelReady", publishDiagnostics);
2370
+ ctx.workspace.on("documentChanged", (uri) => {
2371
+ const diags = computeDiagnostics(uri, ctx.workspace);
2372
+ ctx.connection.sendDiagnostics({
2373
+ uri,
2374
+ diagnostics: diags.map((d) => toLspDiagnostic(d))
2375
+ });
2376
+ });
2377
+ }
2378
+ };
2379
+ }
2380
+ });
2381
+
2382
+ // src/core/workspace/config-loader.ts
2383
+ import { readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
2384
+ import { join as join2 } from "node:path";
2385
+ import { parse as parseYaml } from "yaml";
2386
+ function parseConfig(content, format) {
2387
+ if (!content.trim()) {
2388
+ return { macros: {} };
2389
+ }
2390
+ let raw;
2391
+ if (format === "json") {
2392
+ raw = JSON.parse(content);
2393
+ } else {
2394
+ raw = parseYaml(content);
2395
+ }
2396
+ if (typeof raw !== "object" || raw === null) {
2397
+ return { macros: {} };
2398
+ }
2399
+ const obj = raw;
2400
+ return {
2401
+ macros: typeof obj.macros === "object" && obj.macros !== null ? obj.macros : {}
2402
+ };
2403
+ }
2404
+ function findConfigFile(workspaceRoot) {
2405
+ for (const name of CONFIG_FILENAMES) {
2406
+ const fullPath = join2(workspaceRoot, name);
2407
+ if (existsSync2(fullPath)) {
2408
+ return fullPath;
2409
+ }
2410
+ }
2411
+ return null;
2412
+ }
2413
+ function loadConfigFromDisk(workspaceRoot) {
2414
+ const configPath = findConfigFile(workspaceRoot);
2415
+ if (!configPath) {
2416
+ return { ...EMPTY_CONFIG };
2417
+ }
2418
+ const content = readFileSync3(configPath, "utf-8");
2419
+ const format = configPath.endsWith(".json") ? "json" : "yaml";
2420
+ return parseConfig(content, format);
2421
+ }
2422
+ var CONFIG_FILENAMES, EMPTY_CONFIG;
2423
+ var init_config_loader = __esm({
2424
+ "src/core/workspace/config-loader.ts"() {
2425
+ "use strict";
2426
+ CONFIG_FILENAMES = [
2427
+ "spindle.config.yaml",
2428
+ "spindle.config.yml",
2429
+ "spindle.config.json"
2430
+ ];
2431
+ EMPTY_CONFIG = { macros: {} };
2432
+ }
2433
+ });
2434
+
2435
+ // src/cli/reporters/pretty.ts
2436
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2437
+ function formatPretty(results) {
2438
+ if (results.length === 0) {
2439
+ return `${colors.bold}No problems found${colors.reset}`;
2440
+ }
2441
+ const lines = [];
2442
+ let errorCount = 0;
2443
+ let warningCount = 0;
2444
+ let infoCount = 0;
2445
+ let hintCount = 0;
2446
+ for (const { uri, diagnostics } of results) {
2447
+ let filePath;
2448
+ try {
2449
+ filePath = fileURLToPath2(uri);
2450
+ } catch {
2451
+ filePath = uri;
2452
+ }
2453
+ for (const diag of diagnostics) {
2454
+ const line = diag.range.start.line + 1;
2455
+ const col = diag.range.start.character + 1;
2456
+ const color = severityColors[diag.severity] ?? colors.gray;
2457
+ lines.push(
2458
+ `${colors.bold}${filePath}:${line}:${col}${colors.reset} ${color}${diag.severity}${colors.reset} ${colors.gray}${diag.code}${colors.reset} ${diag.message}`
2459
+ );
2460
+ switch (diag.severity) {
2461
+ case "error":
2462
+ errorCount++;
2463
+ break;
2464
+ case "warning":
2465
+ warningCount++;
2466
+ break;
2467
+ case "info":
2468
+ infoCount++;
2469
+ break;
2470
+ case "hint":
2471
+ hintCount++;
2472
+ break;
2473
+ }
2474
+ }
2475
+ }
2476
+ const total = errorCount + warningCount + infoCount + hintCount;
2477
+ const parts = [];
2478
+ if (errorCount > 0) parts.push(`${errorCount} error${errorCount !== 1 ? "s" : ""}`);
2479
+ if (warningCount > 0) parts.push(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`);
2480
+ if (infoCount > 0) parts.push(`${infoCount} info`);
2481
+ if (hintCount > 0) parts.push(`${hintCount} hint${hintCount !== 1 ? "s" : ""}`);
2482
+ lines.push("");
2483
+ lines.push(
2484
+ `${colors.bold}Found ${total} problem${total !== 1 ? "s" : ""} (${parts.join(", ")})${colors.reset}`
2485
+ );
2486
+ return lines.join("\n");
2487
+ }
2488
+ var colors, severityColors;
2489
+ var init_pretty = __esm({
2490
+ "src/cli/reporters/pretty.ts"() {
2491
+ "use strict";
2492
+ colors = {
2493
+ red: "\x1B[31m",
2494
+ yellow: "\x1B[33m",
2495
+ blue: "\x1B[34m",
2496
+ cyan: "\x1B[36m",
2497
+ gray: "\x1B[90m",
2498
+ reset: "\x1B[0m",
2499
+ bold: "\x1B[1m"
2500
+ };
2501
+ severityColors = {
2502
+ error: colors.red,
2503
+ warning: colors.yellow,
2504
+ info: colors.blue,
2505
+ hint: colors.cyan
2506
+ };
2507
+ }
2508
+ });
2509
+
2510
+ // src/cli/reporters/json.ts
2511
+ function formatJson(results) {
2512
+ const output = {
2513
+ files: results.map(({ uri, diagnostics }) => ({
2514
+ uri,
2515
+ diagnostics: diagnostics.map((d) => ({
2516
+ range: d.range,
2517
+ message: d.message,
2518
+ severity: d.severity,
2519
+ code: d.code,
2520
+ source: d.source
2521
+ }))
2522
+ }))
2523
+ };
2524
+ return JSON.stringify(output, null, 2);
2525
+ }
2526
+ var init_json = __esm({
2527
+ "src/cli/reporters/json.ts"() {
2528
+ "use strict";
2529
+ }
2530
+ });
2531
+
2532
+ // src/cli/reporters/sarif.ts
2533
+ function formatSarif(results) {
2534
+ const ruleIds = /* @__PURE__ */ new Set();
2535
+ for (const { diagnostics } of results) {
2536
+ for (const d of diagnostics) {
2537
+ ruleIds.add(d.code);
2538
+ }
2539
+ }
2540
+ const sarifResults = [];
2541
+ for (const { uri, diagnostics } of results) {
2542
+ for (const d of diagnostics) {
2543
+ sarifResults.push({
2544
+ ruleId: d.code,
2545
+ level: sarifLevelMap[d.severity] ?? "note",
2546
+ message: { text: d.message },
2547
+ locations: [
2548
+ {
2549
+ physicalLocation: {
2550
+ artifactLocation: { uri },
2551
+ region: {
2552
+ startLine: d.range.start.line + 1,
2553
+ startColumn: d.range.start.character + 1,
2554
+ endLine: d.range.end.line + 1,
2555
+ endColumn: d.range.end.character + 1
2556
+ }
2557
+ }
2558
+ }
2559
+ ]
2560
+ });
2561
+ }
2562
+ }
2563
+ const sarif = {
2564
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
2565
+ version: "2.1.0",
2566
+ runs: [
2567
+ {
2568
+ tool: {
2569
+ driver: {
2570
+ name: "spindle-lsp",
2571
+ version: "0.1.0",
2572
+ rules: Array.from(ruleIds).map((id) => ({
2573
+ id,
2574
+ shortDescription: { text: `Spindle diagnostic ${id}` }
2575
+ }))
2576
+ }
2577
+ },
2578
+ results: sarifResults
2579
+ }
2580
+ ]
2581
+ };
2582
+ return JSON.stringify(sarif, null, 2);
2583
+ }
2584
+ var sarifLevelMap;
2585
+ var init_sarif = __esm({
2586
+ "src/cli/reporters/sarif.ts"() {
2587
+ "use strict";
2588
+ sarifLevelMap = {
2589
+ error: "error",
2590
+ warning: "warning",
2591
+ info: "note",
2592
+ hint: "note"
2593
+ };
2594
+ }
2595
+ });
2596
+
2597
+ // src/cli/check.ts
2598
+ var check_exports = {};
2599
+ __export(check_exports, {
2600
+ runCheck: () => runCheck
2601
+ });
2602
+ import { readFileSync as readFileSync4 } from "node:fs";
2603
+ import { resolve } from "node:path";
2604
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
2605
+ import { glob } from "glob";
2606
+ function parseArgs(args2) {
2607
+ const options = {
2608
+ format: "pretty",
2609
+ severity: null,
2610
+ configPath: null,
2611
+ patterns: []
2612
+ };
2613
+ let i = 0;
2614
+ while (i < args2.length) {
2615
+ const arg = args2[i];
2616
+ if (arg === "--format" && i + 1 < args2.length) {
2617
+ const fmt = args2[i + 1];
2618
+ if (fmt === "pretty" || fmt === "json" || fmt === "sarif") {
2619
+ options.format = fmt;
2620
+ }
2621
+ i += 2;
2622
+ } else if (arg.startsWith("--format=")) {
2623
+ const fmt = arg.slice("--format=".length);
2624
+ if (fmt === "pretty" || fmt === "json" || fmt === "sarif") {
2625
+ options.format = fmt;
2626
+ }
2627
+ i++;
2628
+ } else if (arg === "--severity" && i + 1 < args2.length) {
2629
+ const sev = args2[i + 1];
2630
+ if (sev === "error" || sev === "warning" || sev === "info" || sev === "hint") {
2631
+ options.severity = sev;
2632
+ }
2633
+ i += 2;
2634
+ } else if (arg.startsWith("--severity=")) {
2635
+ const sev = arg.slice("--severity=".length);
2636
+ if (sev === "error" || sev === "warning" || sev === "info" || sev === "hint") {
2637
+ options.severity = sev;
2638
+ }
2639
+ i++;
2640
+ } else if (arg === "--config" && i + 1 < args2.length) {
2641
+ options.configPath = args2[i + 1];
2642
+ i += 2;
2643
+ } else if (arg.startsWith("--config=")) {
2644
+ options.configPath = arg.slice("--config=".length);
2645
+ i++;
2646
+ } else if (!arg.startsWith("--")) {
2647
+ options.patterns.push(arg);
2648
+ i++;
2649
+ } else {
2650
+ i++;
2651
+ }
2652
+ }
2653
+ if (options.patterns.length === 0) {
2654
+ options.patterns = ["**/*.{tw,twee}"];
2655
+ }
2656
+ return options;
2657
+ }
2658
+ function filterBySeverity(diagnostics, minSeverity) {
2659
+ const minIdx = SEVERITY_ORDER.indexOf(minSeverity);
2660
+ return diagnostics.filter((d) => {
2661
+ const idx = SEVERITY_ORDER.indexOf(d.severity);
2662
+ return idx >= minIdx;
2663
+ });
2664
+ }
2665
+ async function runCheck(args2) {
2666
+ const options = parseArgs(args2);
2667
+ const cwd = process.cwd();
2668
+ const files = [];
2669
+ for (const pattern of options.patterns) {
2670
+ const matches = await glob(pattern, {
2671
+ cwd,
2672
+ absolute: true,
2673
+ nodir: true
2674
+ });
2675
+ files.push(...matches);
2676
+ }
2677
+ const uniqueFiles = [...new Set(files)];
2678
+ if (uniqueFiles.length === 0) {
2679
+ if (options.format === "pretty") {
2680
+ console.log("No files found");
2681
+ } else if (options.format === "json") {
2682
+ console.log(JSON.stringify({ files: [] }, null, 2));
2683
+ } else {
2684
+ console.log(formatSarif([]));
2685
+ }
2686
+ return 0;
2687
+ }
2688
+ const configRoot = options.configPath ? resolve(cwd, options.configPath, "..") : cwd;
2689
+ const projectConfig = loadConfigFromDisk(configRoot);
2690
+ const workspace = new WorkspaceModel();
2691
+ const fileContents = /* @__PURE__ */ new Map();
2692
+ for (const filePath of uniqueFiles) {
2693
+ try {
2694
+ const text = readFileSync4(filePath, "utf-8");
2695
+ const uri = pathToFileURL2(filePath).toString();
2696
+ fileContents.set(uri, text);
2697
+ } catch {
2698
+ }
2699
+ }
2700
+ workspace.initialize(fileContents);
2701
+ if (Object.keys(projectConfig.macros).length > 0) {
2702
+ workspace.macros.loadSupplements(projectConfig.macros);
2703
+ }
2704
+ const allResults = [];
2705
+ let hasErrors = false;
2706
+ for (const uri of fileContents.keys()) {
2707
+ let diags = computeDiagnostics(uri, workspace);
2708
+ if (options.severity) {
2709
+ diags = filterBySeverity(diags, options.severity);
2710
+ }
2711
+ if (diags.length > 0) {
2712
+ allResults.push({ uri, diagnostics: diags });
2713
+ if (diags.some((d) => d.severity === "error")) {
2714
+ hasErrors = true;
2715
+ }
2716
+ }
2717
+ }
2718
+ switch (options.format) {
2719
+ case "pretty":
2720
+ console.log(formatPretty(allResults));
2721
+ break;
2722
+ case "json":
2723
+ console.log(formatJson(allResults));
2724
+ break;
2725
+ case "sarif":
2726
+ console.log(formatSarif(allResults));
2727
+ break;
2728
+ }
2729
+ workspace.dispose();
2730
+ return hasErrors ? 1 : 0;
2731
+ }
2732
+ var SEVERITY_ORDER;
2733
+ var init_check = __esm({
2734
+ "src/cli/check.ts"() {
2735
+ "use strict";
2736
+ init_workspace_model();
2737
+ init_diagnostics();
2738
+ init_config_loader();
2739
+ init_pretty();
2740
+ init_json();
2741
+ init_sarif();
2742
+ SEVERITY_ORDER = ["hint", "info", "warning", "error"];
2743
+ }
2744
+ });
2745
+
2746
+ // src/plugins/format.ts
2747
+ function formatDocument(text) {
2748
+ const lines = text.split("\n");
2749
+ const result = [];
2750
+ let indentLevel = 0;
2751
+ for (let i = 0; i < lines.length; i++) {
2752
+ let line = lines[i];
2753
+ line = line.replace(/\s+$/, "");
2754
+ if (PASSAGE_HEADER_REGEX.test(line)) {
2755
+ line = normalizePassageHeader(line);
2756
+ indentLevel = 0;
2757
+ result.push(line);
2758
+ continue;
2759
+ }
2760
+ const trimmed = line.trim();
2761
+ if (trimmed === "") {
2762
+ result.push("");
2763
+ continue;
2764
+ }
2765
+ if (BLOCK_CLOSE_REGEX.test(trimmed)) {
2766
+ indentLevel = Math.max(0, indentLevel - 1);
2767
+ }
2768
+ const indented = indentLevel > 0 ? " ".repeat(indentLevel) + trimmed : trimmed;
2769
+ result.push(indented);
2770
+ if (BLOCK_OPEN_REGEX.test(trimmed)) {
2771
+ indentLevel++;
2772
+ }
2773
+ }
2774
+ let output = result.join("\n");
2775
+ output = output.replace(/\n*$/, "\n");
2776
+ return output;
2777
+ }
2778
+ function formatRange(text, range) {
2779
+ const lines = text.split("\n");
2780
+ const startLine = Math.max(0, range.start.line);
2781
+ const endLine = Math.min(lines.length - 1, range.end.line);
2782
+ const rangeLines = lines.slice(startLine, endLine + 1);
2783
+ const rangeText = rangeLines.join("\n");
2784
+ const formatted = formatDocument(rangeText);
2785
+ const formattedLines = formatted.replace(/\n$/, "").split("\n");
2786
+ const before = lines.slice(0, startLine);
2787
+ const after = lines.slice(endLine + 1);
2788
+ const result = [...before, ...formattedLines, ...after].join("\n");
2789
+ return result;
2790
+ }
2791
+ function normalizePassageHeader(line) {
2792
+ const headerMatch = line.match(/^::\s+(.*)/);
2793
+ if (!headerMatch) return line;
2794
+ const rest = headerMatch[1];
2795
+ const bracketIdx = rest.indexOf("[");
2796
+ const braceIdx = rest.indexOf("{");
2797
+ let name;
2798
+ let suffix = "";
2799
+ if (bracketIdx !== -1 && (braceIdx === -1 || bracketIdx < braceIdx)) {
2800
+ name = rest.substring(0, bracketIdx).trim();
2801
+ suffix = " " + rest.substring(bracketIdx).trim();
2802
+ } else if (braceIdx !== -1) {
2803
+ name = rest.substring(0, braceIdx).trim();
2804
+ suffix = " " + rest.substring(braceIdx).trim();
2805
+ } else {
2806
+ name = rest.trim();
2807
+ }
2808
+ return `:: ${name}${suffix}`;
2809
+ }
2810
+ var BLOCK_OPEN_REGEX, BLOCK_CLOSE_REGEX, PASSAGE_HEADER_REGEX, formatPlugin;
2811
+ var init_format = __esm({
2812
+ "src/plugins/format.ts"() {
2813
+ "use strict";
2814
+ BLOCK_OPEN_REGEX = /^\s*\{(if|for|switch|unless|widget)\b/i;
2815
+ BLOCK_CLOSE_REGEX = /^\s*\{\/(if|for|switch|unless|widget)\b/i;
2816
+ PASSAGE_HEADER_REGEX = /^(::)\s+/;
2817
+ formatPlugin = {
2818
+ id: "format",
2819
+ capabilities: {
2820
+ documentFormattingProvider: true,
2821
+ documentRangeFormattingProvider: true
2822
+ },
2823
+ initialize(ctx) {
2824
+ ctx.connection.onDocumentFormatting((params) => {
2825
+ const text = ctx.workspace.documents.getText(params.textDocument.uri);
2826
+ if (text === void 0) return [];
2827
+ const formatted = formatDocument(text);
2828
+ if (formatted === text) return [];
2829
+ const lines = text.split("\n");
2830
+ return [{
2831
+ range: {
2832
+ start: { line: 0, character: 0 },
2833
+ end: { line: lines.length - 1, character: lines[lines.length - 1].length }
2834
+ },
2835
+ newText: formatted
2836
+ }];
2837
+ });
2838
+ ctx.connection.onDocumentRangeFormatting((params) => {
2839
+ const text = ctx.workspace.documents.getText(params.textDocument.uri);
2840
+ if (text === void 0) return [];
2841
+ const range = {
2842
+ start: { line: params.range.start.line, character: params.range.start.character },
2843
+ end: { line: params.range.end.line, character: params.range.end.character }
2844
+ };
2845
+ const formatted = formatRange(text, range);
2846
+ if (formatted === text) return [];
2847
+ const lines = text.split("\n");
2848
+ return [{
2849
+ range: {
2850
+ start: { line: 0, character: 0 },
2851
+ end: { line: lines.length - 1, character: lines[lines.length - 1].length }
2852
+ },
2853
+ newText: formatted
2854
+ }];
2855
+ });
2856
+ }
2857
+ };
2858
+ }
2859
+ });
2860
+
2861
+ // src/cli/format.ts
2862
+ var format_exports = {};
2863
+ __export(format_exports, {
2864
+ runFormat: () => runFormat
2865
+ });
2866
+ import { readFileSync as readFileSync5, writeFileSync } from "node:fs";
2867
+ import { glob as glob2 } from "glob";
2868
+ function parseArgs2(args2) {
2869
+ const options = {
2870
+ check: false,
2871
+ patterns: []
2872
+ };
2873
+ for (const arg of args2) {
2874
+ if (arg === "--check") {
2875
+ options.check = true;
2876
+ } else if (!arg.startsWith("--")) {
2877
+ options.patterns.push(arg);
2878
+ }
2879
+ }
2880
+ if (options.patterns.length === 0) {
2881
+ options.patterns = ["**/*.{tw,twee}"];
2882
+ }
2883
+ return options;
2884
+ }
2885
+ async function runFormat(args2) {
2886
+ const options = parseArgs2(args2);
2887
+ const cwd = process.cwd();
2888
+ const files = [];
2889
+ for (const pattern of options.patterns) {
2890
+ const matches = await glob2(pattern, {
2891
+ cwd,
2892
+ absolute: true,
2893
+ nodir: true
2894
+ });
2895
+ files.push(...matches);
2896
+ }
2897
+ const uniqueFiles = [...new Set(files)];
2898
+ if (uniqueFiles.length === 0) {
2899
+ console.log("No files found");
2900
+ return 0;
2901
+ }
2902
+ const unformatted = [];
2903
+ for (const filePath of uniqueFiles) {
2904
+ try {
2905
+ const text = readFileSync5(filePath, "utf-8");
2906
+ const formatted = formatDocument(text);
2907
+ if (formatted !== text) {
2908
+ if (options.check) {
2909
+ const relative = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
2910
+ unformatted.push(relative);
2911
+ } else {
2912
+ writeFileSync(filePath, formatted, "utf-8");
2913
+ }
2914
+ }
2915
+ } catch {
2916
+ }
2917
+ }
2918
+ if (options.check) {
2919
+ if (unformatted.length > 0) {
2920
+ for (const file of unformatted) {
2921
+ console.log(file);
2922
+ }
2923
+ console.log(`
2924
+ ${unformatted.length} file${unformatted.length !== 1 ? "s" : ""} would be reformatted`);
2925
+ return 1;
2926
+ }
2927
+ return 0;
2928
+ }
2929
+ return 0;
2930
+ }
2931
+ var init_format2 = __esm({
2932
+ "src/cli/format.ts"() {
2933
+ "use strict";
2934
+ init_format();
2935
+ }
2936
+ });
2937
+
2938
+ // src/core/plugin/plugin-loader.ts
2939
+ function loadPlugins(allPlugins2, config) {
2940
+ const disabled = new Set(config.disabledPlugins ?? []);
2941
+ return allPlugins2.filter((p) => !disabled.has(p.id));
2942
+ }
2943
+ function mergeCapabilities(plugins) {
2944
+ const merged = {};
2945
+ for (const plugin of plugins) {
2946
+ for (const [key, value] of Object.entries(plugin.capabilities)) {
2947
+ const existing = merged[key];
2948
+ if (existing === void 0) {
2949
+ merged[key] = value;
2950
+ } else if (typeof existing === "object" && existing !== null && typeof value === "object" && value !== null && !Array.isArray(existing) && !Array.isArray(value)) {
2951
+ merged[key] = { ...existing, ...value };
2952
+ } else {
2953
+ merged[key] = value;
2954
+ }
2955
+ }
2956
+ }
2957
+ return merged;
2958
+ }
2959
+ var init_plugin_loader = __esm({
2960
+ "src/core/plugin/plugin-loader.ts"() {
2961
+ "use strict";
2962
+ }
2963
+ });
2964
+
2965
+ // src/server/capabilities.ts
2966
+ import { TextDocumentSyncKind } from "vscode-languageserver";
2967
+ function buildCapabilities(plugins) {
2968
+ const pluginCaps = mergeCapabilities(plugins);
2969
+ return {
2970
+ textDocumentSync: TextDocumentSyncKind.Incremental,
2971
+ ...pluginCaps
2972
+ };
2973
+ }
2974
+ var init_capabilities = __esm({
2975
+ "src/server/capabilities.ts"() {
2976
+ "use strict";
2977
+ init_plugin_loader();
2978
+ }
2979
+ });
2980
+
2981
+ // src/plugins/completions.ts
2982
+ function getCompletions(uri, position, triggerChar, workspace) {
2983
+ const text = workspace.documents.getText(uri);
2984
+ if (text === void 0) return [];
2985
+ const lines = text.split("\n");
2986
+ if (position.line >= lines.length) return [];
2987
+ const lineText = lines[position.line].substring(0, position.character);
2988
+ if (/\{\/[A-Za-z\w-]*$/.test(lineText)) {
2989
+ return getClosingMacroCompletions(text, position, workspace);
2990
+ }
2991
+ const dotPathMatch = /\$([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
2992
+ if (dotPathMatch) {
2993
+ return getDotPathCompletions(dotPathMatch[1], workspace);
2994
+ }
2995
+ if (/\$[A-Za-z_$]?[\w$]*$/.test(lineText) && !/\$[A-Za-z_$][\w$]*\./.test(lineText)) {
2996
+ return getStoryVariableCompletions(workspace);
2997
+ }
2998
+ if (/_[A-Za-z_$]?[\w$]*$/.test(lineText)) {
2999
+ return getTempVariableCompletions(text);
3000
+ }
3001
+ if (/@[A-Za-z_$]?[\w$]*$/.test(lineText)) {
3002
+ return getLocalVariableCompletions(text);
3003
+ }
3004
+ if (/\[\[[^\]]*$/.test(lineText)) {
3005
+ return getPassageNameCompletions(workspace);
3006
+ }
3007
+ if (/(?:^|[^\\])\{[A-Za-z\w-]*$/.test(lineText)) {
3008
+ return getMacroCompletions(workspace);
3009
+ }
3010
+ return [];
3011
+ }
3012
+ function getClosingMacroCompletions(text, position, workspace) {
3013
+ const macros = parseMacros(text);
3014
+ pairMacros(macros, (name) => workspace.macros.isBlock(name));
3015
+ const openStack = [];
3016
+ for (const macro of macros) {
3017
+ if (macro.range.start.line > position.line || macro.range.start.line === position.line && macro.range.start.character >= position.character) {
3018
+ break;
3019
+ }
3020
+ if (!macro.open) continue;
3021
+ if (!workspace.macros.isBlock(macro.name)) continue;
3022
+ if (macro.pair === -1) {
3023
+ openStack.push(macro.name);
3024
+ } else {
3025
+ const pairMacro = macros[macro.pair];
3026
+ if (pairMacro && (pairMacro.range.start.line > position.line || pairMacro.range.start.line === position.line && pairMacro.range.start.character > position.character)) {
3027
+ openStack.push(macro.name);
3028
+ }
3029
+ }
3030
+ }
3031
+ if (openStack.length === 0) return [];
3032
+ const seen = /* @__PURE__ */ new Set();
3033
+ const suggestions = [];
3034
+ for (let i = openStack.length - 1; i >= 0; i--) {
3035
+ const name = openStack[i];
3036
+ if (!seen.has(name)) {
3037
+ seen.add(name);
3038
+ suggestions.push(name);
3039
+ }
3040
+ }
3041
+ return suggestions.map((name, idx) => ({
3042
+ label: `{/${name}}`,
3043
+ kind: 14,
3044
+ // CompletionItemKind.Keyword
3045
+ detail: `Close {${name}}`,
3046
+ sortText: String(idx).padStart(3, "0"),
3047
+ insertText: `{/${name}}`
3048
+ }));
3049
+ }
3050
+ function getMacroCompletions(workspace) {
3051
+ const items = [];
3052
+ for (const macro of workspace.macros.getAllMacros()) {
3053
+ items.push({
3054
+ label: macro.name,
3055
+ kind: 3,
3056
+ // CompletionItemKind.Function
3057
+ detail: macro.block ? `(container macro) ${macro.name}` : `(macro) ${macro.name}`,
3058
+ documentation: macro.description ?? void 0
3059
+ });
3060
+ }
3061
+ for (const widget of workspace.widgets.getAllWidgets()) {
3062
+ items.push({
3063
+ label: widget.name,
3064
+ kind: 3,
3065
+ // CompletionItemKind.Function
3066
+ detail: `(widget) ${widget.name}`,
3067
+ documentation: widget.params.length > 0 ? `Parameters: ${widget.params.map((p) => `@${p}`).join(", ")}` : void 0
3068
+ });
3069
+ }
3070
+ return items;
3071
+ }
3072
+ function getStoryVariableCompletions(workspace) {
3073
+ const declared = workspace.variables.getDeclared();
3074
+ if (declared.size === 0) return [];
3075
+ const items = [];
3076
+ for (const [name, decl] of declared) {
3077
+ items.push({
3078
+ label: `$${name}`,
3079
+ kind: 6,
3080
+ // CompletionItemKind.Variable
3081
+ detail: "story variable",
3082
+ insertText: name,
3083
+ documentation: decl.fields && decl.fields.length > 0 ? `Fields: ${decl.fields.join(", ")}` : void 0
3084
+ });
3085
+ }
3086
+ return items;
3087
+ }
3088
+ function getTempVariableCompletions(text) {
3089
+ const tempVarRegex = /_([A-Za-z_$][\w$]*)/g;
3090
+ const names = /* @__PURE__ */ new Set();
3091
+ let m;
3092
+ while ((m = tempVarRegex.exec(text)) !== null) {
3093
+ names.add(m[1]);
3094
+ }
3095
+ if (names.size === 0) return [];
3096
+ return Array.from(names).map((name) => ({
3097
+ label: `_${name}`,
3098
+ kind: 6,
3099
+ // CompletionItemKind.Variable
3100
+ detail: "temporary variable",
3101
+ insertText: name
3102
+ }));
3103
+ }
3104
+ function getLocalVariableCompletions(text) {
3105
+ const atVarRegex = /@([A-Za-z_$][\w$]*)/g;
3106
+ const names = /* @__PURE__ */ new Set();
3107
+ let m;
3108
+ while ((m = atVarRegex.exec(text)) !== null) {
3109
+ names.add(m[1]);
3110
+ }
3111
+ if (names.size === 0) return [];
3112
+ return Array.from(names).map((name) => ({
3113
+ label: `@${name}`,
3114
+ kind: 6,
3115
+ // CompletionItemKind.Variable
3116
+ detail: "local/parameter variable",
3117
+ insertText: name
3118
+ }));
3119
+ }
3120
+ function getDotPathCompletions(varName, workspace) {
3121
+ const declared = workspace.variables.getDeclared();
3122
+ const decl = declared.get(varName);
3123
+ if (!decl || !decl.fields || decl.fields.length === 0) return [];
3124
+ return decl.fields.map((field) => ({
3125
+ label: field,
3126
+ kind: 5,
3127
+ // CompletionItemKind.Field
3128
+ detail: `field of $${varName}`
3129
+ }));
3130
+ }
3131
+ function getPassageNameCompletions(workspace) {
3132
+ const passages = workspace.passages.getAllPassages();
3133
+ if (passages.length === 0) return [];
3134
+ return passages.map((p) => ({
3135
+ label: p.name,
3136
+ kind: 18,
3137
+ // CompletionItemKind.Reference
3138
+ detail: p.uri
3139
+ }));
3140
+ }
3141
+ var completionsPlugin;
3142
+ var init_completions = __esm({
3143
+ "src/plugins/completions.ts"() {
3144
+ "use strict";
3145
+ init_macro_parser();
3146
+ completionsPlugin = {
3147
+ id: "completions",
3148
+ capabilities: {
3149
+ completionProvider: {
3150
+ triggerCharacters: ["{", "$", "_", "@", "[", "."]
3151
+ }
3152
+ },
3153
+ initialize(ctx) {
3154
+ ctx.connection.onCompletion((params) => {
3155
+ const uri = params.textDocument.uri;
3156
+ const position = {
3157
+ line: params.position.line,
3158
+ character: params.position.character
3159
+ };
3160
+ const triggerChar = params.context?.triggerCharacter;
3161
+ return getCompletions(uri, position, triggerChar, ctx.workspace);
3162
+ });
3163
+ }
3164
+ };
3165
+ }
3166
+ });
3167
+
3168
+ // src/plugins/hover.ts
3169
+ function getHoverInfo(uri, position, workspace) {
3170
+ const text = workspace.documents.getText(uri);
3171
+ if (text === void 0) return null;
3172
+ const lines = text.split("\n");
3173
+ if (position.line >= lines.length) return null;
3174
+ const line = lines[position.line];
3175
+ const macroResult = getMacroHover(line, position, workspace);
3176
+ if (macroResult) return macroResult;
3177
+ const varResult = getVariableHover(line, position, workspace);
3178
+ if (varResult) return varResult;
3179
+ const widgetResult = getWidgetHover(line, position, workspace);
3180
+ if (widgetResult) return widgetResult;
3181
+ return null;
3182
+ }
3183
+ function getMacroHover(line, position, workspace) {
3184
+ const macroRegex2 = /\{(\/)?(?:(?:[#.][a-zA-Z][\w-]*\s*)*)([A-Za-z][\w-]*)/g;
3185
+ let match;
3186
+ while ((match = macroRegex2.exec(line)) !== null) {
3187
+ const name = match[2];
3188
+ const nameStart = match.index + match[0].indexOf(name);
3189
+ const nameEnd = nameStart + name.length;
3190
+ if (position.character >= nameStart && position.character <= nameEnd) {
3191
+ const info = workspace.macros.getMacro(name);
3192
+ if (info) {
3193
+ const parts = [];
3194
+ parts.push(`**${info.name}** _(${info.block ? "container" : "inline"} macro)_`);
3195
+ if (info.description) {
3196
+ parts.push("", info.description);
3197
+ }
3198
+ if (info.parameters && info.parameters.length > 0) {
3199
+ parts.push("", `Parameters: \`${info.parameters.join(" ")}\``);
3200
+ }
3201
+ return {
3202
+ contents: parts.join("\n"),
3203
+ range: {
3204
+ start: { line: position.line, character: nameStart },
3205
+ end: { line: position.line, character: nameEnd }
3206
+ }
3207
+ };
3208
+ }
3209
+ const widget = workspace.widgets.getWidget(name);
3210
+ if (widget) {
3211
+ return buildWidgetHover(widget, position.line, nameStart, nameEnd);
3212
+ }
3213
+ }
3214
+ }
3215
+ return null;
3216
+ }
3217
+ function getVariableHover(line, position, workspace) {
3218
+ {
3219
+ const re = /\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
3220
+ let match;
3221
+ while ((match = re.exec(line)) !== null) {
3222
+ const start = match.index;
3223
+ const end = start + match[0].length;
3224
+ if (position.character >= start && position.character <= end) {
3225
+ const baseName = match[1].split(".")[0];
3226
+ const decl = workspace.variables.getDeclared().get(baseName);
3227
+ const typeInfo = decl?.fields && decl.fields.length > 0 ? `
3228
+
3229
+ Fields: ${decl.fields.map((f) => `\`${f}\``).join(", ")}` : "";
3230
+ return {
3231
+ contents: `**Story variable** \`$${match[1]}\`${typeInfo}`,
3232
+ range: {
3233
+ start: { line: position.line, character: start },
3234
+ end: { line: position.line, character: end }
3235
+ }
3236
+ };
3237
+ }
3238
+ }
3239
+ }
3240
+ {
3241
+ const re = /(?<!\w)_([A-Za-z_$][\w$]*)/g;
3242
+ let match;
3243
+ while ((match = re.exec(line)) !== null) {
3244
+ const start = match.index;
3245
+ const end = start + match[0].length;
3246
+ if (position.character >= start && position.character <= end) {
3247
+ return {
3248
+ contents: `**Temp variable** \`_${match[1]}\``,
3249
+ range: {
3250
+ start: { line: position.line, character: start },
3251
+ end: { line: position.line, character: end }
3252
+ }
3253
+ };
3254
+ }
3255
+ }
3256
+ }
3257
+ {
3258
+ const re = /(?<!\w)@([A-Za-z_$][\w$]*)/g;
3259
+ let match;
3260
+ while ((match = re.exec(line)) !== null) {
3261
+ const start = match.index;
3262
+ const end = start + match[0].length;
3263
+ if (position.character >= start && position.character <= end) {
3264
+ return {
3265
+ contents: `**Local variable** \`@${match[1]}\``,
3266
+ range: {
3267
+ start: { line: position.line, character: start },
3268
+ end: { line: position.line, character: end }
3269
+ }
3270
+ };
3271
+ }
3272
+ }
3273
+ }
3274
+ return null;
3275
+ }
3276
+ function getWidgetHover(line, position, workspace) {
3277
+ const re = /\{([A-Za-z_$][\w$]*)/g;
3278
+ let match;
3279
+ while ((match = re.exec(line)) !== null) {
3280
+ const name = match[1];
3281
+ const nameStart = match.index + 1;
3282
+ const nameEnd = nameStart + name.length;
3283
+ if (position.character >= nameStart && position.character <= nameEnd) {
3284
+ const widget = workspace.widgets.getWidget(name);
3285
+ if (widget) {
3286
+ return buildWidgetHover(widget, position.line, nameStart, nameEnd);
3287
+ }
3288
+ }
3289
+ }
3290
+ return null;
3291
+ }
3292
+ function buildWidgetHover(widget, line, nameStart, nameEnd) {
3293
+ const sig = widget.params.length > 0 ? widget.params.map((p) => `@${p}`).join(", ") : "no parameters";
3294
+ return {
3295
+ contents: `**Widget** \`${widget.name}\`
3296
+
3297
+ Parameters: ${sig}
3298
+
3299
+ Defined in: \`${widget.uri}\``,
3300
+ range: {
3301
+ start: { line, character: nameStart },
3302
+ end: { line, character: nameEnd }
3303
+ }
3304
+ };
3305
+ }
3306
+ function toLspRange(r) {
3307
+ return {
3308
+ start: { line: r.start.line, character: r.start.character },
3309
+ end: { line: r.end.line, character: r.end.character }
3310
+ };
3311
+ }
3312
+ var hoverPlugin;
3313
+ var init_hover = __esm({
3314
+ "src/plugins/hover.ts"() {
3315
+ "use strict";
3316
+ hoverPlugin = {
3317
+ id: "hover",
3318
+ capabilities: {
3319
+ hoverProvider: true
3320
+ },
3321
+ initialize(ctx) {
3322
+ ctx.connection.onHover((params) => {
3323
+ const result = getHoverInfo(
3324
+ params.textDocument.uri,
3325
+ { line: params.position.line, character: params.position.character },
3326
+ ctx.workspace
3327
+ );
3328
+ if (!result) return null;
3329
+ return {
3330
+ contents: { kind: "markdown", value: result.contents },
3331
+ range: toLspRange(result.range)
3332
+ };
3333
+ });
3334
+ }
3335
+ };
3336
+ }
3337
+ });
3338
+
3339
+ // src/plugins/signature.ts
3340
+ function getSignatureHelp(uri, position, workspace) {
3341
+ const text = workspace.documents.getText(uri);
3342
+ if (text === void 0) return null;
3343
+ const lines = text.split("\n");
3344
+ if (position.line >= lines.length) return null;
3345
+ const lineText = lines[position.line];
3346
+ const textBefore = lineText.substring(0, position.character);
3347
+ const macroMatch = textBefore.match(/\{(?:[#.][a-zA-Z][\w-]*\s*)*([A-Za-z][\w-]*)\s+([^}]*)$/);
3348
+ if (!macroMatch) return null;
3349
+ const macroName = macroMatch[1];
3350
+ const argsBefore = macroMatch[2];
3351
+ const activeParameter = argsBefore.trim() === "" ? 0 : lexArguments(argsBefore).length;
3352
+ const macroInfo = workspace.macros.getMacro(macroName);
3353
+ if (macroInfo && macroInfo.parameters && macroInfo.parameters.length > 0) {
3354
+ const paramLabels = macroInfo.parameters;
3355
+ return {
3356
+ signatures: [{
3357
+ label: `{${macroName} ${paramLabels.join(" ")}}`,
3358
+ documentation: macroInfo.description ?? void 0,
3359
+ parameters: paramLabels.map((p) => ({ label: p }))
3360
+ }],
3361
+ activeSignature: 0,
3362
+ activeParameter
3363
+ };
3364
+ }
3365
+ const widget = workspace.widgets.getWidget(macroName);
3366
+ if (widget && widget.params.length > 0) {
3367
+ const paramLabels = widget.params.map((p) => `@${p}`);
3368
+ return {
3369
+ signatures: [{
3370
+ label: `{${macroName} ${paramLabels.join(", ")}}`,
3371
+ documentation: `Widget defined in: ${widget.uri}`,
3372
+ parameters: paramLabels.map((p) => ({ label: p }))
3373
+ }],
3374
+ activeSignature: 0,
3375
+ activeParameter
3376
+ };
3377
+ }
3378
+ return null;
3379
+ }
3380
+ var signaturePlugin;
3381
+ var init_signature = __esm({
3382
+ "src/plugins/signature.ts"() {
3383
+ "use strict";
3384
+ init_argument_lexer();
3385
+ signaturePlugin = {
3386
+ id: "signature",
3387
+ capabilities: {
3388
+ signatureHelpProvider: {
3389
+ triggerCharacters: [" ", '"', "'"]
3390
+ }
3391
+ },
3392
+ initialize(ctx) {
3393
+ ctx.connection.onSignatureHelp((params) => {
3394
+ const result = getSignatureHelp(
3395
+ params.textDocument.uri,
3396
+ { line: params.position.line, character: params.position.character },
3397
+ ctx.workspace
3398
+ );
3399
+ if (!result) return null;
3400
+ const signatures = result.signatures.map((sig) => ({
3401
+ label: sig.label,
3402
+ documentation: sig.documentation,
3403
+ parameters: sig.parameters.map((p) => ({ label: p.label }))
3404
+ }));
3405
+ return {
3406
+ signatures,
3407
+ activeSignature: result.activeSignature,
3408
+ activeParameter: result.activeParameter
3409
+ };
3410
+ });
3411
+ }
3412
+ };
3413
+ }
3414
+ });
3415
+
3416
+ // src/plugins/definition.ts
3417
+ function getDefinition(uri, position, workspace) {
3418
+ const text = workspace.documents.getText(uri);
3419
+ if (text === void 0) return null;
3420
+ const lines = text.split("\n");
3421
+ if (position.line >= lines.length) return null;
3422
+ const line = lines[position.line];
3423
+ const linkResult = getPassageLinkDefinition(text, position, workspace);
3424
+ if (linkResult) return linkResult;
3425
+ const macroPassageResult = getMacroPassageDefinition(line, position, workspace);
3426
+ if (macroPassageResult) return macroPassageResult;
3427
+ const widgetResult = getWidgetDefinition(line, position, workspace);
3428
+ if (widgetResult) return widgetResult;
3429
+ return null;
3430
+ }
3431
+ function getPassageLinkDefinition(text, position, workspace) {
3432
+ const links = parseLinks(text);
3433
+ for (const link of links) {
3434
+ if (link.range.start.line === position.line && position.character >= link.range.start.character && position.character <= link.range.end.character) {
3435
+ const passage = workspace.passages.getPassage(link.name);
3436
+ if (passage) {
3437
+ return {
3438
+ uri: passage.uri,
3439
+ range: passage.headerEnd
3440
+ };
3441
+ }
3442
+ }
3443
+ }
3444
+ return null;
3445
+ }
3446
+ function getMacroPassageDefinition(line, position, workspace) {
3447
+ const patterns = [
3448
+ /\{(?:goto|include)\s+"([^"]+)"\s*\}/gi,
3449
+ /\{(?:link|button)\s+"[^"]*"\s+"([^"]+)"\s*\}/gi
3450
+ ];
3451
+ for (const pattern of patterns) {
3452
+ pattern.lastIndex = 0;
3453
+ let match;
3454
+ while ((match = pattern.exec(line)) !== null) {
3455
+ const passageName = match[1];
3456
+ const nameStart = match.index + match[0].indexOf(passageName);
3457
+ const nameEnd = nameStart + passageName.length;
3458
+ if (position.character >= nameStart && position.character <= nameEnd) {
3459
+ const passage = workspace.passages.getPassage(passageName);
3460
+ if (passage) {
3461
+ return {
3462
+ uri: passage.uri,
3463
+ range: passage.headerEnd
3464
+ };
3465
+ }
3466
+ }
3467
+ }
3468
+ }
3469
+ return null;
3470
+ }
3471
+ function getWidgetDefinition(line, position, workspace) {
3472
+ const re = /\{([A-Za-z_$][\w$]*)/g;
3473
+ let match;
3474
+ while ((match = re.exec(line)) !== null) {
3475
+ const name = match[1];
3476
+ const nameStart = match.index + 1;
3477
+ const nameEnd = nameStart + name.length;
3478
+ if (position.character >= nameStart && position.character <= nameEnd) {
3479
+ if (workspace.macros.getMacro(name)) continue;
3480
+ const widget = workspace.widgets.getWidget(name);
3481
+ if (widget) {
3482
+ return {
3483
+ uri: widget.uri,
3484
+ range: widget.range
3485
+ };
3486
+ }
3487
+ }
3488
+ }
3489
+ return null;
3490
+ }
3491
+ function toLspRange2(r) {
3492
+ return {
3493
+ start: { line: r.start.line, character: r.start.character },
3494
+ end: { line: r.end.line, character: r.end.character }
3495
+ };
3496
+ }
3497
+ var definitionPlugin;
3498
+ var init_definition = __esm({
3499
+ "src/plugins/definition.ts"() {
3500
+ "use strict";
3501
+ init_link_parser();
3502
+ definitionPlugin = {
3503
+ id: "definition",
3504
+ capabilities: {
3505
+ definitionProvider: true
3506
+ },
3507
+ initialize(ctx) {
3508
+ ctx.connection.onDefinition((params) => {
3509
+ const result = getDefinition(
3510
+ params.textDocument.uri,
3511
+ { line: params.position.line, character: params.position.character },
3512
+ ctx.workspace
3513
+ );
3514
+ if (!result) return null;
3515
+ return {
3516
+ uri: result.uri,
3517
+ range: toLspRange2(result.range)
3518
+ };
3519
+ });
3520
+ }
3521
+ };
3522
+ }
3523
+ });
3524
+
3525
+ // src/plugins/references.ts
3526
+ function findReferences(uri, position, workspace, includeDeclaration) {
3527
+ const text = workspace.documents.getText(uri);
3528
+ if (text === void 0) return [];
3529
+ const lines = text.split("\n");
3530
+ if (position.line >= lines.length) return [];
3531
+ const line = lines[position.line];
3532
+ const passageHeaderRegex2 = /^::\s*(\S.*?)(?:\s*\[|\s*\{|\s*$)/;
3533
+ if (line.trimStart().startsWith("::")) {
3534
+ const headerMatch = passageHeaderRegex2.exec(line);
3535
+ if (headerMatch) {
3536
+ const passageName = headerMatch[1].trim();
3537
+ const nameStart = line.indexOf(passageName);
3538
+ const nameEnd = nameStart + passageName.length;
3539
+ if (position.character >= nameStart && position.character <= nameEnd) {
3540
+ return findPassageReferences(passageName, workspace, includeDeclaration);
3541
+ }
3542
+ }
3543
+ }
3544
+ {
3545
+ const varRegex = /\$([A-Za-z_$][\w$]*)/g;
3546
+ let match;
3547
+ while ((match = varRegex.exec(line)) !== null) {
3548
+ const start = match.index;
3549
+ const end = start + match[0].length;
3550
+ if (position.character >= start && position.character <= end) {
3551
+ const varName = match[1];
3552
+ return findVariableReferences(varName, workspace, includeDeclaration);
3553
+ }
3554
+ }
3555
+ }
3556
+ {
3557
+ const widgetRegex = /\{([A-Za-z_$][\w$]*)/g;
3558
+ let match;
3559
+ while ((match = widgetRegex.exec(line)) !== null) {
3560
+ const name = match[1];
3561
+ const nameStart = match.index + 1;
3562
+ const nameEnd = nameStart + name.length;
3563
+ if (position.character >= nameStart && position.character <= nameEnd) {
3564
+ if (!workspace.macros.getMacro(name) && workspace.widgets.getWidget(name)) {
3565
+ return findWidgetReferences(name, workspace, includeDeclaration);
3566
+ }
3567
+ }
3568
+ }
3569
+ }
3570
+ {
3571
+ const allPassages = workspace.passages.getAllPassages();
3572
+ const passageNames = new Set(allPassages.map((p) => p.name));
3573
+ const wordRegex = /[A-Za-z_$][\w$\s]*/g;
3574
+ let match;
3575
+ while ((match = wordRegex.exec(line)) !== null) {
3576
+ const start = match.index;
3577
+ const end = start + match[0].length;
3578
+ if (position.character >= start && position.character <= end) {
3579
+ const word = match[0].trim();
3580
+ if (passageNames.has(word)) {
3581
+ return findPassageReferences(word, workspace, includeDeclaration);
3582
+ }
3583
+ }
3584
+ }
3585
+ }
3586
+ return [];
3587
+ }
3588
+ function findPassageReferences(passageName, workspace, includeDeclaration) {
3589
+ const locations = [];
3590
+ if (includeDeclaration) {
3591
+ const passage = workspace.passages.getPassage(passageName);
3592
+ if (passage) {
3593
+ locations.push({
3594
+ uri: passage.uri,
3595
+ range: passage.headerEnd
3596
+ });
3597
+ }
3598
+ }
3599
+ const gotoRegex = /\{(?:goto|include)\s+"([^"]+)"\s*\}/gi;
3600
+ const linkMacroRegex = /\{(?:link|button)\s+"[^"]*"\s+"([^"]+)"\s*\}/gi;
3601
+ for (const docUri of workspace.documents.getUris()) {
3602
+ const docText = workspace.documents.getText(docUri);
3603
+ if (!docText) continue;
3604
+ const links = parseLinks(docText);
3605
+ for (const link of links) {
3606
+ if (link.name === passageName) {
3607
+ locations.push({ uri: docUri, range: link.range });
3608
+ }
3609
+ }
3610
+ const lines = docText.split("\n");
3611
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
3612
+ const line = lines[lineNum];
3613
+ for (const regex of [gotoRegex, linkMacroRegex]) {
3614
+ regex.lastIndex = 0;
3615
+ let match;
3616
+ while ((match = regex.exec(line)) !== null) {
3617
+ if (match[1] === passageName) {
3618
+ const nameStart = match.index + match[0].indexOf(match[1]);
3619
+ locations.push({
3620
+ uri: docUri,
3621
+ range: {
3622
+ start: { line: lineNum, character: nameStart },
3623
+ end: { line: lineNum, character: nameStart + match[1].length }
3624
+ }
3625
+ });
3626
+ }
3627
+ }
3628
+ }
3629
+ }
3630
+ }
3631
+ return locations;
3632
+ }
3633
+ function findVariableReferences(varName, workspace, includeDeclaration) {
3634
+ const locations = [];
3635
+ if (includeDeclaration) {
3636
+ const decl = workspace.variables.getDeclared().get(varName);
3637
+ if (decl?.declarationRange && decl.declarationUri) {
3638
+ locations.push({
3639
+ uri: decl.declarationUri,
3640
+ range: decl.declarationRange
3641
+ });
3642
+ }
3643
+ }
3644
+ const usages = workspace.variables.getUsages(varName);
3645
+ for (const u of usages) {
3646
+ locations.push({ uri: u.uri, range: u.range });
3647
+ }
3648
+ return locations;
3649
+ }
3650
+ function findWidgetReferences(widgetName, workspace, includeDeclaration) {
3651
+ const locations = [];
3652
+ if (includeDeclaration) {
3653
+ const widget = workspace.widgets.getWidget(widgetName);
3654
+ if (widget) {
3655
+ locations.push({ uri: widget.uri, range: widget.range });
3656
+ }
3657
+ }
3658
+ const widgetInvocationRegex = /\{([A-Za-z_$][\w$]*)\b/g;
3659
+ for (const docUri of workspace.documents.getUris()) {
3660
+ const docText = workspace.documents.getText(docUri);
3661
+ if (!docText) continue;
3662
+ const lines = docText.split("\n");
3663
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
3664
+ const line = lines[lineNum];
3665
+ if (/\{widget\s+"/i.test(line)) continue;
3666
+ widgetInvocationRegex.lastIndex = 0;
3667
+ let match;
3668
+ while ((match = widgetInvocationRegex.exec(line)) !== null) {
3669
+ if (match[1] === widgetName) {
3670
+ const nameStart = match.index + 1;
3671
+ locations.push({
3672
+ uri: docUri,
3673
+ range: {
3674
+ start: { line: lineNum, character: nameStart },
3675
+ end: { line: lineNum, character: nameStart + widgetName.length }
3676
+ }
3677
+ });
3678
+ }
3679
+ }
3680
+ }
3681
+ }
3682
+ return locations;
3683
+ }
3684
+ function toLspRange3(r) {
3685
+ return {
3686
+ start: { line: r.start.line, character: r.start.character },
3687
+ end: { line: r.end.line, character: r.end.character }
3688
+ };
3689
+ }
3690
+ var referencesPlugin;
3691
+ var init_references = __esm({
3692
+ "src/plugins/references.ts"() {
3693
+ "use strict";
3694
+ init_link_parser();
3695
+ referencesPlugin = {
3696
+ id: "references",
3697
+ capabilities: {
3698
+ referencesProvider: true
3699
+ },
3700
+ initialize(ctx) {
3701
+ ctx.connection.onReferences((params) => {
3702
+ const refs = findReferences(
3703
+ params.textDocument.uri,
3704
+ { line: params.position.line, character: params.position.character },
3705
+ ctx.workspace,
3706
+ params.context.includeDeclaration
3707
+ );
3708
+ return refs.map((r) => ({
3709
+ uri: r.uri,
3710
+ range: toLspRange3(r.range)
3711
+ }));
3712
+ });
3713
+ }
3714
+ };
3715
+ }
3716
+ });
3717
+
3718
+ // src/plugins/rename.ts
3719
+ function prepareRename(uri, position, workspace) {
3720
+ const symbol = resolveSymbolAtCursor(uri, position, workspace);
3721
+ if (!symbol) return null;
3722
+ return { range: symbol.range, placeholder: symbol.name };
3723
+ }
3724
+ function computeRename(uri, position, newName, workspace) {
3725
+ const symbol = resolveSymbolAtCursor(uri, position, workspace);
3726
+ if (!symbol) return /* @__PURE__ */ new Map();
3727
+ const edits = /* @__PURE__ */ new Map();
3728
+ function addEdit(editUri, range, text) {
3729
+ const existing = edits.get(editUri) ?? [];
3730
+ existing.push({ range, newText: text });
3731
+ edits.set(editUri, existing);
3732
+ }
3733
+ switch (symbol.kind) {
3734
+ case "passage": {
3735
+ const refs = findPassageReferences(symbol.name, workspace, true);
3736
+ for (const ref of refs) {
3737
+ addEdit(ref.uri, ref.range, newName);
3738
+ }
3739
+ break;
3740
+ }
3741
+ case "variable": {
3742
+ const bareName = newName.startsWith("$") ? newName.slice(1) : newName;
3743
+ const refs = findVariableReferences(symbol.name, workspace, true);
3744
+ for (const ref of refs) {
3745
+ addEdit(ref.uri, ref.range, bareName);
3746
+ }
3747
+ break;
3748
+ }
3749
+ case "widget": {
3750
+ const invocationRefs = findWidgetReferences(symbol.name, workspace, false);
3751
+ for (const ref of invocationRefs) {
3752
+ addEdit(ref.uri, ref.range, newName);
3753
+ }
3754
+ const widget = workspace.widgets.getWidget(symbol.name);
3755
+ if (widget) {
3756
+ const docText = workspace.documents.getText(widget.uri);
3757
+ if (docText) {
3758
+ const lines = docText.split("\n");
3759
+ const defLine = lines[widget.range.start.line];
3760
+ if (defLine) {
3761
+ const defSlice = defLine.slice(widget.range.start.character);
3762
+ const nameMatch = /\{widget\s+"([^"]+)"/.exec(defSlice);
3763
+ if (nameMatch) {
3764
+ const nameOffsetInSlice = defSlice.indexOf(nameMatch[1]);
3765
+ const nameStart = widget.range.start.character + nameOffsetInSlice;
3766
+ addEdit(widget.uri, {
3767
+ start: { line: widget.range.start.line, character: nameStart },
3768
+ end: { line: widget.range.start.line, character: nameStart + symbol.name.length }
3769
+ }, newName);
3770
+ }
3771
+ }
3772
+ }
3773
+ }
3774
+ break;
3775
+ }
3776
+ }
3777
+ return edits;
3778
+ }
3779
+ function resolveSymbolAtCursor(uri, position, workspace) {
3780
+ const text = workspace.documents.getText(uri);
3781
+ if (text === void 0) return null;
3782
+ const lines = text.split("\n");
3783
+ if (position.line >= lines.length) return null;
3784
+ const line = lines[position.line];
3785
+ const passageHeaderRegex2 = /^::\s*(\S.*?)(?:\s*\[|\s*\{|\s*$)/;
3786
+ if (line.trimStart().startsWith("::")) {
3787
+ const headerMatch = passageHeaderRegex2.exec(line);
3788
+ if (headerMatch) {
3789
+ const passageName = headerMatch[1].trim();
3790
+ const nameStart = line.indexOf(passageName);
3791
+ const nameEnd = nameStart + passageName.length;
3792
+ if (position.character >= nameStart && position.character <= nameEnd) {
3793
+ return {
3794
+ kind: "passage",
3795
+ name: passageName,
3796
+ range: {
3797
+ start: { line: position.line, character: nameStart },
3798
+ end: { line: position.line, character: nameEnd }
3799
+ }
3800
+ };
3801
+ }
3802
+ }
3803
+ }
3804
+ {
3805
+ const varRegex = /\$([A-Za-z_$][\w$]*)/g;
3806
+ let match;
3807
+ while ((match = varRegex.exec(line)) !== null) {
3808
+ const start = match.index;
3809
+ const end = start + match[0].length;
3810
+ if (position.character >= start && position.character <= end) {
3811
+ return {
3812
+ kind: "variable",
3813
+ name: match[1],
3814
+ range: {
3815
+ start: { line: position.line, character: start },
3816
+ end: { line: position.line, character: end }
3817
+ }
3818
+ };
3819
+ }
3820
+ }
3821
+ }
3822
+ {
3823
+ const widgetDefRegex2 = /\{widget\s+"([^"]+)"/gi;
3824
+ let match;
3825
+ while ((match = widgetDefRegex2.exec(line)) !== null) {
3826
+ const name = match[1];
3827
+ const nameStart = match.index + match[0].indexOf(name);
3828
+ const nameEnd = nameStart + name.length;
3829
+ if (position.character >= nameStart && position.character <= nameEnd) {
3830
+ return {
3831
+ kind: "widget",
3832
+ name,
3833
+ range: {
3834
+ start: { line: position.line, character: nameStart },
3835
+ end: { line: position.line, character: nameEnd }
3836
+ }
3837
+ };
3838
+ }
3839
+ }
3840
+ }
3841
+ {
3842
+ const re = /\{([A-Za-z_$][\w$]*)/g;
3843
+ let match;
3844
+ while ((match = re.exec(line)) !== null) {
3845
+ const name = match[1];
3846
+ const nameStart = match.index + 1;
3847
+ const nameEnd = nameStart + name.length;
3848
+ if (position.character >= nameStart && position.character <= nameEnd) {
3849
+ if (!workspace.macros.getMacro(name) && workspace.widgets.getWidget(name)) {
3850
+ return {
3851
+ kind: "widget",
3852
+ name,
3853
+ range: {
3854
+ start: { line: position.line, character: nameStart },
3855
+ end: { line: position.line, character: nameEnd }
3856
+ }
3857
+ };
3858
+ }
3859
+ }
3860
+ }
3861
+ }
3862
+ return null;
3863
+ }
3864
+ function toLspRange4(r) {
3865
+ return {
3866
+ start: { line: r.start.line, character: r.start.character },
3867
+ end: { line: r.end.line, character: r.end.character }
3868
+ };
3869
+ }
3870
+ var renamePlugin;
3871
+ var init_rename = __esm({
3872
+ "src/plugins/rename.ts"() {
3873
+ "use strict";
3874
+ init_references();
3875
+ renamePlugin = {
3876
+ id: "rename",
3877
+ capabilities: {
3878
+ renameProvider: {
3879
+ prepareProvider: true
3880
+ }
3881
+ },
3882
+ initialize(ctx) {
3883
+ ctx.connection.onPrepareRename((params) => {
3884
+ const result = prepareRename(
3885
+ params.textDocument.uri,
3886
+ { line: params.position.line, character: params.position.character },
3887
+ ctx.workspace
3888
+ );
3889
+ if (!result) return null;
3890
+ return {
3891
+ range: toLspRange4(result.range),
3892
+ placeholder: result.placeholder
3893
+ };
3894
+ });
3895
+ ctx.connection.onRenameRequest((params) => {
3896
+ const editsMap = computeRename(
3897
+ params.textDocument.uri,
3898
+ { line: params.position.line, character: params.position.character },
3899
+ params.newName,
3900
+ ctx.workspace
3901
+ );
3902
+ const changes = {};
3903
+ for (const [editUri, edits] of editsMap) {
3904
+ changes[editUri] = edits.map((e) => ({
3905
+ range: toLspRange4(e.range),
3906
+ newText: e.newText
3907
+ }));
3908
+ }
3909
+ return { changes };
3910
+ });
3911
+ }
3912
+ };
3913
+ }
3914
+ });
3915
+
3916
+ // src/plugins/code-lens.ts
3917
+ function computeCodeLenses(uri, workspace) {
3918
+ const text = workspace.documents.getText(uri);
3919
+ if (text === void 0) return [];
3920
+ const lines = text.split("\n");
3921
+ const lenses = [];
3922
+ const storyVarsPassage = workspace.passages.getStoryVariables();
3923
+ const isStoryVarsFile = storyVarsPassage && storyVarsPassage.uri === uri;
3924
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
3925
+ const line = lines[lineNum];
3926
+ const passageMatch = line.match(/^::\s*(\S.*?)(?:\s*\[|\s*\{|\s*$)/);
3927
+ if (passageMatch) {
3928
+ const passageName = passageMatch[1].trim();
3929
+ if (passageName === "StoryData") continue;
3930
+ const refs = findPassageReferences(passageName, workspace, true);
3931
+ const refCount = Math.max(0, refs.length - 1);
3932
+ lenses.push({
3933
+ range: {
3934
+ start: { line: lineNum, character: 0 },
3935
+ end: { line: lineNum, character: line.length }
3936
+ },
3937
+ command: {
3938
+ title: `${refCount} reference${refCount !== 1 ? "s" : ""}`
3939
+ }
3940
+ });
3941
+ continue;
3942
+ }
3943
+ const widgetMatch = line.match(/\{widget\s+"([^"]+)"/);
3944
+ if (widgetMatch) {
3945
+ const widgetName = widgetMatch[1];
3946
+ const refs = findWidgetReferences(widgetName, workspace, true);
3947
+ const usageCount = Math.max(0, refs.length - 1);
3948
+ lenses.push({
3949
+ range: {
3950
+ start: { line: lineNum, character: 0 },
3951
+ end: { line: lineNum, character: line.length }
3952
+ },
3953
+ command: {
3954
+ title: `${usageCount} usage${usageCount !== 1 ? "s" : ""}`
3955
+ }
3956
+ });
3957
+ }
3958
+ if (isStoryVarsFile) {
3959
+ const varDeclMatch = line.match(/^\$([A-Za-z_$][\w$]*)\s*=/);
3960
+ if (varDeclMatch) {
3961
+ const varName = varDeclMatch[1];
3962
+ const refs = findVariableReferences(varName, workspace, true);
3963
+ const usageCount = Math.max(0, refs.length - 1);
3964
+ lenses.push({
3965
+ range: {
3966
+ start: { line: lineNum, character: 0 },
3967
+ end: { line: lineNum, character: line.length }
3968
+ },
3969
+ command: {
3970
+ title: `${usageCount} usage${usageCount !== 1 ? "s" : ""}`
3971
+ }
3972
+ });
3973
+ }
3974
+ }
3975
+ }
3976
+ return lenses;
3977
+ }
3978
+ function toLspRange5(r) {
3979
+ return {
3980
+ start: { line: r.start.line, character: r.start.character },
3981
+ end: { line: r.end.line, character: r.end.character }
3982
+ };
3983
+ }
3984
+ var codeLensPlugin;
3985
+ var init_code_lens = __esm({
3986
+ "src/plugins/code-lens.ts"() {
3987
+ "use strict";
3988
+ init_references();
3989
+ codeLensPlugin = {
3990
+ id: "code-lens",
3991
+ capabilities: {
3992
+ codeLensProvider: {
3993
+ resolveProvider: false
3994
+ }
3995
+ },
3996
+ initialize(ctx) {
3997
+ ctx.connection.onCodeLens((params) => {
3998
+ const lenses = computeCodeLenses(params.textDocument.uri, ctx.workspace);
3999
+ return lenses.map((l) => ({
4000
+ range: toLspRange5(l.range),
4001
+ command: {
4002
+ title: l.command.title,
4003
+ command: "spindle.findReferences"
4004
+ }
4005
+ }));
4006
+ });
4007
+ }
4008
+ };
4009
+ }
4010
+ });
4011
+
4012
+ // src/plugins/inlay-hints.ts
4013
+ function computeInlayHints(uri, range, workspace) {
4014
+ const text = workspace.documents.getText(uri);
4015
+ if (text === void 0) return [];
4016
+ const hints = [];
4017
+ addWidgetParamHints(text, range, workspace, hints);
4018
+ addVariableTypeHints(uri, text, range, workspace, hints);
4019
+ return hints;
4020
+ }
4021
+ function addWidgetParamHints(text, range, workspace, hints) {
4022
+ const allWidgets = workspace.widgets.getAllWidgets();
4023
+ if (allWidgets.length === 0) return;
4024
+ const widgetMap = new Map(allWidgets.map((w) => [w.name, w]));
4025
+ const macros = parseMacros(text);
4026
+ for (const macro of macros) {
4027
+ if (!macro.open) continue;
4028
+ if (macro.range.start.line < range.start.line || macro.range.start.line > range.end.line) continue;
4029
+ const widget = widgetMap.get(macro.name);
4030
+ if (!widget || widget.params.length === 0) continue;
4031
+ if (!macro.rawArgs || macro.rawArgs.trim() === "") continue;
4032
+ const args2 = lexArguments(macro.rawArgs);
4033
+ const lines = text.split("\n");
4034
+ const macroLine = lines[macro.range.start.line];
4035
+ if (!macroLine) continue;
4036
+ const macroText = macroLine.substring(macro.range.start.character, macro.range.end.character);
4037
+ const nameEnd = macroText.indexOf(macro.name) + macro.name.length;
4038
+ const argsText = macroText.substring(nameEnd).replace(/^\s+/, "").replace(/\}$/, "");
4039
+ if (!argsText) continue;
4040
+ for (let i = 0; i < Math.min(args2.length, widget.params.length); i++) {
4041
+ const argText = args2[i].text;
4042
+ const idx = macroLine.indexOf(argText, macro.range.start.character + nameEnd);
4043
+ if (idx >= 0) {
4044
+ hints.push({
4045
+ position: { line: macro.range.start.line, character: idx },
4046
+ label: `@${widget.params[i]}:`,
4047
+ kind: "parameter"
4048
+ });
4049
+ }
4050
+ }
4051
+ }
4052
+ }
4053
+ function addVariableTypeHints(uri, text, range, workspace, hints) {
4054
+ const storyVarsPassage = workspace.passages.getStoryVariables();
4055
+ if (!storyVarsPassage) return;
4056
+ if (storyVarsPassage.uri !== uri) return;
4057
+ const lines = text.split("\n");
4058
+ const startLine = Math.max(range.start.line, 0);
4059
+ const endLine = Math.min(range.end.line, lines.length - 1);
4060
+ for (let lineNum = startLine; lineNum <= endLine; lineNum++) {
4061
+ const line = lines[lineNum];
4062
+ const declMatch = line.match(/^\$([A-Za-z_$][\w$]*)\s*=\s*(.+)$/);
4063
+ if (!declMatch) continue;
4064
+ const valueStr = declMatch[2].trim();
4065
+ const inferredType = inferType(valueStr);
4066
+ if (!inferredType) continue;
4067
+ const eqIdx = line.indexOf("=");
4068
+ hints.push({
4069
+ position: { line: lineNum, character: eqIdx },
4070
+ label: `: ${inferredType}`,
4071
+ kind: "type"
4072
+ });
4073
+ }
4074
+ }
4075
+ function inferType(value) {
4076
+ if (/^\d+(\.\d+)?$/.test(value)) return "number";
4077
+ if (/^"[^"]*"$/.test(value) || /^'[^']*'$/.test(value)) return "string";
4078
+ if (/^\[/.test(value)) return "array";
4079
+ if (/^\{/.test(value)) return "object";
4080
+ if (value === "true" || value === "false") return "boolean";
4081
+ if (value === "null") return "null";
4082
+ if (value === "undefined") return "undefined";
4083
+ return null;
4084
+ }
4085
+ var inlayHintsPlugin;
4086
+ var init_inlay_hints = __esm({
4087
+ "src/plugins/inlay-hints.ts"() {
4088
+ "use strict";
4089
+ init_macro_parser();
4090
+ init_argument_lexer();
4091
+ inlayHintsPlugin = {
4092
+ id: "inlay-hints",
4093
+ capabilities: {
4094
+ inlayHintProvider: true
4095
+ },
4096
+ initialize(ctx) {
4097
+ ctx.connection.languages.inlayHint.on((params) => {
4098
+ const range = {
4099
+ start: { line: params.range.start.line, character: params.range.start.character },
4100
+ end: { line: params.range.end.line, character: params.range.end.character }
4101
+ };
4102
+ const hints = computeInlayHints(params.textDocument.uri, range, ctx.workspace);
4103
+ return hints.map((h) => ({
4104
+ position: { line: h.position.line, character: h.position.character },
4105
+ label: h.label,
4106
+ kind: h.kind === "type" ? 1 : 2,
4107
+ paddingRight: h.kind === "parameter",
4108
+ paddingLeft: h.kind === "type"
4109
+ }));
4110
+ });
4111
+ }
4112
+ };
4113
+ }
4114
+ });
4115
+
4116
+ // src/plugins/semantic-tokens.ts
4117
+ function encodeType(type) {
4118
+ return tokenTypeIndex.get(type) ?? 0;
4119
+ }
4120
+ function encodeModifiers(modifiers) {
4121
+ let bits = 0;
4122
+ for (const m of modifiers) {
4123
+ const idx = tokenModifierIndex.get(m);
4124
+ if (idx !== void 0) bits |= 1 << idx;
4125
+ }
4126
+ return bits;
4127
+ }
4128
+ function computeSemanticTokensAbsolute(uri, workspace) {
4129
+ const text = workspace.documents.getText(uri);
4130
+ if (text === void 0) return [];
4131
+ const lines = text.split("\n");
4132
+ const tokens = [];
4133
+ const headerLines = /* @__PURE__ */ new Set();
4134
+ const passages = workspace.passages.getPassagesInDocument(uri);
4135
+ for (const passage of passages) {
4136
+ const headerLine = passage.headerEnd.start.line;
4137
+ headerLines.add(headerLine);
4138
+ const rawLine = lines[headerLine] ?? "";
4139
+ tokens.push({
4140
+ line: headerLine,
4141
+ startChar: 0,
4142
+ length: 2,
4143
+ tokenType: encodeType("namespace"),
4144
+ tokenModifiers: 0
4145
+ });
4146
+ const nameMatch = rawLine.match(/^::\s*(\S.*?)(?:\s*\[|\s*\{|\s*$)/);
4147
+ if (nameMatch) {
4148
+ const name = nameMatch[1].trim();
4149
+ const nameStart = rawLine.indexOf(name);
4150
+ tokens.push({
4151
+ line: headerLine,
4152
+ startChar: nameStart,
4153
+ length: name.length,
4154
+ tokenType: encodeType("namespace"),
4155
+ tokenModifiers: encodeModifiers(["declaration"])
4156
+ });
4157
+ }
4158
+ }
4159
+ const macros = parseMacros(text);
4160
+ for (const macro of macros) {
4161
+ const macroLine = macro.range.start.line;
4162
+ const macroChar = macro.range.start.character;
4163
+ if (headerLines.has(macroLine)) continue;
4164
+ const isDefined = !!workspace.macros.getMacro(macro.name);
4165
+ let nameOffset = 1;
4166
+ if (!macro.open) nameOffset += 1;
4167
+ if (macro.cssPrefix) nameOffset += macro.cssPrefix.length + 1;
4168
+ tokens.push({
4169
+ line: macroLine,
4170
+ startChar: macroChar + nameOffset,
4171
+ length: macro.name.length,
4172
+ tokenType: encodeType("function"),
4173
+ tokenModifiers: encodeModifiers(isDefined ? ["defaultLibrary"] : [])
4174
+ });
4175
+ }
4176
+ const storyVarRegex = /(?<!\w)\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
4177
+ const tempVarRegex = /(?<!\w)_([A-Za-z_$][\w$]*)/g;
4178
+ const localVarRegex = /(?<!\w)@([A-Za-z_$][\w$]*)/g;
4179
+ const sugarKeywordRegex = /\b(to|is|isnot|eq|neq|gt|gte|lt|lte|and|or|not|def|ndef)\b/g;
4180
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
4181
+ if (headerLines.has(lineIndex)) continue;
4182
+ const line = lines[lineIndex];
4183
+ storyVarRegex.lastIndex = 0;
4184
+ let m;
4185
+ while ((m = storyVarRegex.exec(line)) !== null) {
4186
+ tokens.push({
4187
+ line: lineIndex,
4188
+ startChar: m.index,
4189
+ length: m[0].length,
4190
+ tokenType: encodeType("variable"),
4191
+ tokenModifiers: encodeModifiers(["global"])
4192
+ });
4193
+ }
4194
+ tempVarRegex.lastIndex = 0;
4195
+ while ((m = tempVarRegex.exec(line)) !== null) {
4196
+ tokens.push({
4197
+ line: lineIndex,
4198
+ startChar: m.index,
4199
+ length: m[0].length,
4200
+ tokenType: encodeType("variable"),
4201
+ tokenModifiers: encodeModifiers(["local"])
4202
+ });
4203
+ }
4204
+ localVarRegex.lastIndex = 0;
4205
+ while ((m = localVarRegex.exec(line)) !== null) {
4206
+ tokens.push({
4207
+ line: lineIndex,
4208
+ startChar: m.index,
4209
+ length: m[0].length,
4210
+ tokenType: encodeType("variable"),
4211
+ tokenModifiers: encodeModifiers(["readonly"])
4212
+ });
4213
+ }
4214
+ sugarKeywordRegex.lastIndex = 0;
4215
+ while ((m = sugarKeywordRegex.exec(line)) !== null) {
4216
+ tokens.push({
4217
+ line: lineIndex,
4218
+ startChar: m.index,
4219
+ length: m[0].length,
4220
+ tokenType: encodeType("keyword"),
4221
+ tokenModifiers: 0
4222
+ });
4223
+ }
4224
+ }
4225
+ tokens.sort((a, b) => a.line - b.line || a.startChar - b.startChar);
4226
+ return tokens;
4227
+ }
4228
+ function encodeTokens(absoluteTokens) {
4229
+ const data = [];
4230
+ let prevLine = 0;
4231
+ let prevChar = 0;
4232
+ for (const token of absoluteTokens) {
4233
+ const deltaLine = token.line - prevLine;
4234
+ const deltaStart = deltaLine === 0 ? token.startChar - prevChar : token.startChar;
4235
+ data.push(deltaLine, deltaStart, token.length, token.tokenType, token.tokenModifiers);
4236
+ prevLine = token.line;
4237
+ prevChar = token.startChar;
4238
+ }
4239
+ return data;
4240
+ }
4241
+ function computeSemanticTokens(uri, workspace) {
4242
+ const absoluteTokens = computeSemanticTokensAbsolute(uri, workspace);
4243
+ return encodeTokens(absoluteTokens);
4244
+ }
4245
+ var tokenTypesLegend, tokenModifiersLegend, tokenTypeIndex, tokenModifierIndex, semanticTokensPlugin;
4246
+ var init_semantic_tokens = __esm({
4247
+ "src/plugins/semantic-tokens.ts"() {
4248
+ "use strict";
4249
+ init_macro_parser();
4250
+ tokenTypesLegend = [
4251
+ "function",
4252
+ "variable",
4253
+ "parameter",
4254
+ "property",
4255
+ "keyword",
4256
+ "string",
4257
+ "number",
4258
+ "comment",
4259
+ "namespace",
4260
+ "type",
4261
+ "macro",
4262
+ "regexp"
4263
+ ];
4264
+ tokenModifiersLegend = [
4265
+ "declaration",
4266
+ "defaultLibrary",
4267
+ "global",
4268
+ "local",
4269
+ "readonly"
4270
+ ];
4271
+ tokenTypeIndex = new Map(
4272
+ tokenTypesLegend.map((t, i) => [t, i])
4273
+ );
4274
+ tokenModifierIndex = new Map(
4275
+ tokenModifiersLegend.map((m, i) => [m, i])
4276
+ );
4277
+ semanticTokensPlugin = {
4278
+ id: "semantic-tokens",
4279
+ capabilities: {
4280
+ semanticTokensProvider: {
4281
+ full: true,
4282
+ legend: {
4283
+ tokenTypes: tokenTypesLegend,
4284
+ tokenModifiers: tokenModifiersLegend
4285
+ }
4286
+ }
4287
+ },
4288
+ initialize(ctx) {
4289
+ ctx.connection.languages.semanticTokens.on((params) => {
4290
+ const data = computeSemanticTokens(params.textDocument.uri, ctx.workspace);
4291
+ return { data };
4292
+ });
4293
+ }
4294
+ };
4295
+ }
4296
+ });
4297
+
4298
+ // src/plugins/code-actions.ts
4299
+ function computeCodeActions(uri, diagnostics, workspace) {
4300
+ const actions = [];
4301
+ for (const diag of diagnostics) {
4302
+ switch (diag.code) {
4303
+ case DiagnosticCode.UndefinedMacro: {
4304
+ const action = fixUndefinedMacro(diag);
4305
+ if (action) actions.push(action);
4306
+ break;
4307
+ }
4308
+ case DiagnosticCode.UndeclaredVariable: {
4309
+ const action = fixUndeclaredVariable(diag, workspace);
4310
+ if (action) actions.push(action);
4311
+ break;
4312
+ }
4313
+ case DiagnosticCode.NoStoryVariables: {
4314
+ const action = fixNoStoryVariables(uri, workspace);
4315
+ if (action) actions.push(action);
4316
+ break;
4317
+ }
4318
+ }
4319
+ }
4320
+ return actions;
4321
+ }
4322
+ function fixUndefinedMacro(diag) {
4323
+ const match = diag.message.match(/\{(\w[\w-]*)\}/);
4324
+ if (!match) return null;
4325
+ const macroName = match[1];
4326
+ const configUri = "file:///spindle.config.yaml";
4327
+ const yamlSnippet = `
4328
+ ${macroName}:
4329
+ description: ""
4330
+ `;
4331
+ return {
4332
+ title: `Add '${macroName}' to spindle.config.yaml`,
4333
+ kind: "quickfix",
4334
+ diagnosticCodes: [DiagnosticCode.UndefinedMacro],
4335
+ edits: [{
4336
+ uri: configUri,
4337
+ range: {
4338
+ start: { line: Number.MAX_SAFE_INTEGER, character: 0 },
4339
+ end: { line: Number.MAX_SAFE_INTEGER, character: 0 }
4340
+ },
4341
+ newText: yamlSnippet
4342
+ }]
4343
+ };
4344
+ }
4345
+ function fixUndeclaredVariable(diag, workspace) {
4346
+ const match = diag.message.match(/'\$(\w+)'/);
4347
+ if (!match) return null;
4348
+ const varName = match[1];
4349
+ const storyVars = workspace.passages.getStoryVariables();
4350
+ if (!storyVars) return null;
4351
+ const storyVarsUri = storyVars.uri;
4352
+ const text = workspace.documents.getText(storyVarsUri);
4353
+ if (text === void 0) return null;
4354
+ const lines = text.split("\n");
4355
+ const contentStart = storyVars.headerEnd.end.line + 1;
4356
+ let contentEnd = lines.length;
4357
+ for (let i = contentStart; i < lines.length; i++) {
4358
+ if (/^::\s+/.test(lines[i])) {
4359
+ contentEnd = i;
4360
+ break;
4361
+ }
4362
+ }
4363
+ const insertLine = contentEnd;
4364
+ return {
4365
+ title: `Declare '$${varName}' in StoryVariables`,
4366
+ kind: "quickfix",
4367
+ diagnosticCodes: [DiagnosticCode.UndeclaredVariable],
4368
+ edits: [{
4369
+ uri: storyVarsUri,
4370
+ range: {
4371
+ start: { line: insertLine, character: 0 },
4372
+ end: { line: insertLine, character: 0 }
4373
+ },
4374
+ newText: `$${varName} = null
4375
+ `
4376
+ }]
4377
+ };
4378
+ }
4379
+ function fixNoStoryVariables(uri, workspace) {
4380
+ const uris = workspace.documents.getUris();
4381
+ const targetUri = uris.length > 0 ? uris[0] : uri;
4382
+ const text = workspace.documents.getText(targetUri);
4383
+ if (text === void 0) return null;
4384
+ const lines = text.split("\n");
4385
+ const lastLine = lines.length;
4386
+ return {
4387
+ title: "Create StoryVariables passage",
4388
+ kind: "quickfix",
4389
+ diagnosticCodes: [DiagnosticCode.NoStoryVariables],
4390
+ edits: [{
4391
+ uri: targetUri,
4392
+ range: {
4393
+ start: { line: lastLine, character: 0 },
4394
+ end: { line: lastLine, character: 0 }
4395
+ },
4396
+ newText: "\n:: StoryVariables\n"
4397
+ }]
4398
+ };
4399
+ }
4400
+ function toLspRange6(r) {
4401
+ return {
4402
+ start: { line: r.start.line, character: r.start.character },
4403
+ end: { line: r.end.line, character: r.end.character }
4404
+ };
4405
+ }
4406
+ var codeActionsPlugin;
4407
+ var init_code_actions = __esm({
4408
+ "src/plugins/code-actions.ts"() {
4409
+ "use strict";
4410
+ init_diagnostic_codes();
4411
+ codeActionsPlugin = {
4412
+ id: "code-actions",
4413
+ capabilities: {
4414
+ codeActionProvider: {
4415
+ codeActionKinds: ["quickfix"]
4416
+ }
4417
+ },
4418
+ initialize(ctx) {
4419
+ ctx.connection.onCodeAction((params) => {
4420
+ const diagnostics = params.context.diagnostics.filter((d) => d.source === "spindle").map((d) => ({
4421
+ range: {
4422
+ start: { line: d.range.start.line, character: d.range.start.character },
4423
+ end: { line: d.range.end.line, character: d.range.end.character }
4424
+ },
4425
+ message: d.message,
4426
+ severity: d.severity === 1 ? "error" : d.severity === 2 ? "warning" : d.severity === 3 ? "info" : "hint",
4427
+ code: String(d.code ?? ""),
4428
+ source: d.source ?? "spindle"
4429
+ }));
4430
+ const actions = computeCodeActions(
4431
+ params.textDocument.uri,
4432
+ diagnostics,
4433
+ ctx.workspace
4434
+ );
4435
+ return actions.map((a) => ({
4436
+ title: a.title,
4437
+ kind: a.kind,
4438
+ diagnostics: params.context.diagnostics.filter(
4439
+ (d) => a.diagnosticCodes.includes(String(d.code))
4440
+ ),
4441
+ edit: {
4442
+ changes: Object.fromEntries(
4443
+ a.edits.map((e) => [
4444
+ e.uri,
4445
+ [{ range: toLspRange6(e.range), newText: e.newText }]
4446
+ ])
4447
+ )
4448
+ }
4449
+ }));
4450
+ });
4451
+ }
4452
+ };
4453
+ }
4454
+ });
4455
+
4456
+ // src/plugins/index.ts
4457
+ var allPlugins;
4458
+ var init_plugins = __esm({
4459
+ "src/plugins/index.ts"() {
4460
+ "use strict";
4461
+ init_diagnostics();
4462
+ init_completions();
4463
+ init_hover();
4464
+ init_signature();
4465
+ init_definition();
4466
+ init_references();
4467
+ init_rename();
4468
+ init_code_lens();
4469
+ init_inlay_hints();
4470
+ init_semantic_tokens();
4471
+ init_code_actions();
4472
+ init_format();
4473
+ allPlugins = [
4474
+ diagnosticsPlugin,
4475
+ completionsPlugin,
4476
+ hoverPlugin,
4477
+ signaturePlugin,
4478
+ definitionPlugin,
4479
+ referencesPlugin,
4480
+ renamePlugin,
4481
+ codeLensPlugin,
4482
+ inlayHintsPlugin,
4483
+ semanticTokensPlugin,
4484
+ codeActionsPlugin,
4485
+ formatPlugin
4486
+ ];
4487
+ }
4488
+ });
4489
+
4490
+ // src/server/server.ts
4491
+ var server_exports = {};
4492
+ __export(server_exports, {
4493
+ startServer: () => startServer
4494
+ });
4495
+ import {
4496
+ createConnection,
4497
+ ProposedFeatures,
4498
+ DidChangeWatchedFilesNotification,
4499
+ FileChangeType
4500
+ } from "vscode-languageserver/node.js";
4501
+ import { TextDocument } from "vscode-languageserver-textdocument";
4502
+ import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL3 } from "node:url";
4503
+ import { readFileSync as readFileSync6 } from "node:fs";
4504
+ import { glob as glob3 } from "glob";
4505
+ function uriToFsPath(uri) {
4506
+ try {
4507
+ return fileURLToPath3(uri);
4508
+ } catch {
4509
+ return uri;
4510
+ }
4511
+ }
4512
+ function fsPathToUri(fsPath) {
4513
+ return pathToFileURL3(fsPath).toString();
4514
+ }
4515
+ function startServer(_args) {
4516
+ const connection = createConnection(ProposedFeatures.all);
4517
+ const documents = /* @__PURE__ */ new Map();
4518
+ let workspace;
4519
+ let activePlugins = [];
4520
+ let workspaceRoot;
4521
+ connection.onInitialize((params) => {
4522
+ const initOptions = params.initializationOptions ?? {};
4523
+ workspaceRoot = initOptions.workspaceRoot ?? (params.workspaceFolders?.[0]?.uri ? uriToFsPath(params.workspaceFolders[0].uri) : void 0);
4524
+ const projectConfig = workspaceRoot ? loadConfigFromDisk(workspaceRoot) : { macros: {} };
4525
+ const config = {
4526
+ disabledPlugins: initOptions.disabledPlugins,
4527
+ diagnostics: initOptions.diagnostics,
4528
+ workspaceRoot
4529
+ };
4530
+ workspace = new WorkspaceModel();
4531
+ activePlugins = loadPlugins(allPlugins, config);
4532
+ for (const plugin of activePlugins) {
4533
+ plugin.initialize({ connection, workspace, config });
4534
+ }
4535
+ if (Object.keys(projectConfig.macros).length > 0) {
4536
+ workspace.macros.loadSupplements(projectConfig.macros);
4537
+ }
4538
+ return {
4539
+ capabilities: buildCapabilities(activePlugins)
4540
+ };
4541
+ });
4542
+ connection.onInitialized(async () => {
4543
+ connection.client.register(DidChangeWatchedFilesNotification.type, {
4544
+ watchers: [
4545
+ { globPattern: "**/*.tw" },
4546
+ { globPattern: "**/*.twee" },
4547
+ { globPattern: "**/*.js" },
4548
+ { globPattern: "**/*.ts" },
4549
+ { globPattern: "**/spindle.config.*" }
4550
+ ]
4551
+ });
4552
+ if (workspace) {
4553
+ const fileContents = await scanWorkspaceFiles(workspaceRoot);
4554
+ workspace.initialize(fileContents);
4555
+ }
4556
+ });
4557
+ connection.onDidOpenTextDocument(({ textDocument }) => {
4558
+ const doc = TextDocument.create(
4559
+ textDocument.uri,
4560
+ textDocument.languageId,
4561
+ textDocument.version,
4562
+ textDocument.text
4563
+ );
4564
+ documents.set(textDocument.uri, doc);
4565
+ if (workspace) {
4566
+ workspace.documents.open(textDocument.uri, textDocument.text);
4567
+ }
4568
+ });
4569
+ connection.onDidChangeTextDocument(({ textDocument, contentChanges }) => {
4570
+ const existing = documents.get(textDocument.uri);
4571
+ if (existing) {
4572
+ const updated = TextDocument.update(
4573
+ existing,
4574
+ contentChanges,
4575
+ textDocument.version
4576
+ );
4577
+ documents.set(textDocument.uri, updated);
4578
+ if (workspace) {
4579
+ workspace.documents.update(textDocument.uri, updated.getText());
4580
+ }
4581
+ }
4582
+ });
4583
+ connection.onDidCloseTextDocument(({ textDocument }) => {
4584
+ documents.delete(textDocument.uri);
4585
+ if (workspace) {
4586
+ workspace.documents.close(textDocument.uri);
4587
+ }
4588
+ });
4589
+ connection.onDidChangeWatchedFiles(({ changes }) => {
4590
+ if (!workspace) return;
4591
+ for (const change of changes) {
4592
+ if (change.type === FileChangeType.Deleted) {
4593
+ workspace.documents.close(change.uri);
4594
+ documents.delete(change.uri);
4595
+ } else {
4596
+ try {
4597
+ const fsPath = uriToFsPath(change.uri);
4598
+ const text = readFileSync6(fsPath, "utf-8");
4599
+ if (workspace.documents.has(change.uri)) {
4600
+ workspace.documents.update(change.uri, text);
4601
+ } else {
4602
+ workspace.documents.open(change.uri, text);
4603
+ }
4604
+ } catch {
4605
+ }
4606
+ }
4607
+ }
4608
+ });
4609
+ connection.onShutdown(() => {
4610
+ for (const plugin of activePlugins) {
4611
+ plugin.dispose?.();
4612
+ }
4613
+ workspace?.dispose();
4614
+ });
4615
+ connection.listen();
4616
+ }
4617
+ async function scanWorkspaceFiles(root) {
4618
+ const contents = /* @__PURE__ */ new Map();
4619
+ if (!root) return contents;
4620
+ try {
4621
+ const files = await glob3("**/*.{tw,twee}", {
4622
+ cwd: root,
4623
+ absolute: true,
4624
+ nodir: true
4625
+ });
4626
+ for (const filePath of files) {
4627
+ try {
4628
+ const text = readFileSync6(filePath, "utf-8");
4629
+ const uri = fsPathToUri(filePath);
4630
+ contents.set(uri, text);
4631
+ } catch {
4632
+ }
4633
+ }
4634
+ } catch {
4635
+ }
4636
+ return contents;
4637
+ }
4638
+ var init_server = __esm({
4639
+ "src/server/server.ts"() {
4640
+ "use strict";
4641
+ init_workspace_model();
4642
+ init_plugin_loader();
4643
+ init_capabilities();
4644
+ init_plugins();
4645
+ init_config_loader();
4646
+ }
4647
+ });
4648
+
4649
+ // src/bin.ts
4650
+ var args = process.argv.slice(2);
4651
+ if (args[0] === "check") {
4652
+ const { runCheck: runCheck2 } = await Promise.resolve().then(() => (init_check(), check_exports));
4653
+ const exitCode = await runCheck2(args.slice(1));
4654
+ process.exit(exitCode);
4655
+ } else if (args[0] === "format") {
4656
+ const { runFormat: runFormat2 } = await Promise.resolve().then(() => (init_format2(), format_exports));
4657
+ const exitCode = await runFormat2(args.slice(1));
4658
+ process.exit(exitCode);
4659
+ } else {
4660
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4661
+ startServer2(args);
4662
+ }
4663
+ //# sourceMappingURL=bin.js.map