@rohal12/spindle-lsp 0.15.9 → 0.15.10

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.
Files changed (3) hide show
  1. package/dist/bin.js +1871 -1779
  2. package/dist/bin.js.map +4 -4
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -55,7 +55,7 @@ var VERSION;
55
55
  var init_version = __esm({
56
56
  "src/version.ts"() {
57
57
  "use strict";
58
- VERSION = false ? pkg.version : "0.15.9";
58
+ VERSION = false ? pkg.version : "0.15.10";
59
59
  }
60
60
  });
61
61
 
@@ -31245,6 +31245,7 @@ var init_text = __esm({
31245
31245
  });
31246
31246
 
31247
31247
  // src/core/parsing/passage-parser.ts
31248
+ import { createRequire as createCompilerRequire } from "node:module";
31248
31249
  function parsePassageHeader(rawLine, lineNumber) {
31249
31250
  const line = withoutCr(rawLine);
31250
31251
  const bomLength = lineNumber === 0 && line.charCodeAt(0) === 65279 ? 1 : 0;
@@ -31276,7 +31277,8 @@ function parsePassageHeader(rawLine, lineNumber) {
31276
31277
  try {
31277
31278
  meta3 = JSON.parse(raw);
31278
31279
  } catch {
31279
- return null;
31280
+ const { parseTwee } = createCompilerRequire(import.meta.url)("@rohal12/twee-ts");
31281
+ if (parseTwee(line.slice(bomLength)).passages.length !== 1) return null;
31280
31282
  }
31281
31283
  }
31282
31284
  const headerRange = {
@@ -31303,8 +31305,11 @@ function isReservedPassageName(name) {
31303
31305
  function isScriptOrStylesheetPassage(passage) {
31304
31306
  return passage.tags?.some((t) => CODE_PASSAGE_TAGS.has(t)) ?? false;
31305
31307
  }
31308
+ function isRuntimePassage(passage) {
31309
+ return passage.name !== "StoryTitle" && passage.name !== "StoryData" && !isScriptOrStylesheetPassage(passage) && !passage.tags?.includes("Twine.private");
31310
+ }
31306
31311
  function isMarkupPassage(passage) {
31307
- return !(passage.name !== void 0 && NON_MARKUP_PASSAGES.has(passage.name)) && !isScriptOrStylesheetPassage(passage);
31312
+ return isRuntimePassage(passage) && !(passage.name !== void 0 && NON_MARKUP_PASSAGES.has(passage.name));
31308
31313
  }
31309
31314
  var SPECIAL_PASSAGES, CODE_PASSAGE_TAGS, RESERVED_PASSAGE_NAMES, NON_MARKUP_PASSAGES;
31310
31315
  var init_passage_parser = __esm({
@@ -31501,834 +31506,1506 @@ var init_passage_index = __esm({
31501
31506
  }
31502
31507
  });
31503
31508
 
31504
- // src/core/workspace/macro-registry.ts
31505
- function applySupplement(info, entry) {
31506
- for (const field of ["description", "parameters", "parameterDocs", "children", "parents", "skipArgs"]) {
31507
- if (entry[field] !== void 0) Object.assign(info, { [field]: entry[field] });
31509
+ // src/core/parsing/argument-lexer.ts
31510
+ function lexArguments(source) {
31511
+ let from = 0;
31512
+ return splitArgs(source).map((text3) => {
31513
+ const start = source.indexOf(text3, from);
31514
+ from = start + text3.length;
31515
+ return classifyArgument(text3, start);
31516
+ });
31517
+ }
31518
+ function classifyArgument(text3, start = 0) {
31519
+ const base = { text: text3, start, end: start + text3.length };
31520
+ if (readQuoted(text3, 0)?.end === text3.length) return { ...base, type: 0 /* String */ };
31521
+ if (text3 === "null") return { ...base, type: 3 /* Null */ };
31522
+ if (text3 === "undefined") return { ...base, type: 10 /* Undefined */ };
31523
+ if (text3 === "true" || text3 === "false") return { ...base, type: 2 /* Boolean */ };
31524
+ if (text3 === "NaN") return { ...base, type: 9 /* NaN */ };
31525
+ if (text3 !== "" && !Number.isNaN(Number(text3))) return { ...base, type: 1 /* Number */ };
31526
+ if (/^[$_@%]\w+(?:\.\w+)*$/.test(text3)) {
31527
+ return { ...base, type: 4 /* Variable */, sigil: text3[0], path: text3.slice(1).split(".") };
31508
31528
  }
31509
- if (entry.container !== void 0) info.block = entry.container;
31529
+ if (text3.startsWith("[[") && text3.endsWith("]]")) return { ...base, type: 5 /* Link */ };
31530
+ if (/^\[[<>]?img\[/i.test(text3) && text3.endsWith("]]")) return { ...base, type: 6 /* Image */ };
31531
+ return { ...base, type: /^[-\w]+$/.test(text3) ? 8 /* Bareword */ : 7 /* Expression */ };
31510
31532
  }
31511
- var MacroRegistry;
31512
- var init_macro_registry2 = __esm({
31513
- "src/core/workspace/macro-registry.ts"() {
31533
+ var init_argument_lexer = __esm({
31534
+ "src/core/parsing/argument-lexer.ts"() {
31514
31535
  "use strict";
31515
31536
  init_tooling();
31516
- init_config_loader();
31517
- MacroRegistry = class {
31518
- macros = /* @__PURE__ */ new Map();
31519
- /** Entries as they were before the current user config was applied (undefined = absent). */
31520
- configSnapshot = /* @__PURE__ */ new Map();
31521
- /** Tier 3: macros discovered from Story.defineMacro() calls, keyed by lowercase name. */
31522
- discovered = /* @__PURE__ */ new Map();
31523
- /** Called after the set of macros or their metadata changed. */
31524
- onChange;
31525
- /** Tier 4: user config entries, kept so they can be re-applied over discovered macros. */
31526
- configEntries = /* @__PURE__ */ new Map();
31527
- /**
31528
- * Load the built-in macros: name, block, sub-macros, flags and typed
31529
- * parameters, from Spindle's tooling API (the release the LSP is built on).
31530
- * This is the base layer the other tiers overlay.
31531
- */
31532
- loadBuiltins() {
31533
- for (const m of builtinMacros) {
31534
- this.macros.set(m.name.toLowerCase(), {
31535
- name: m.name,
31536
- block: m.block,
31537
- subMacros: [...m.subMacros],
31538
- storeVar: m.storeVar,
31539
- interpolate: m.interpolate,
31540
- merged: m.merged,
31541
- source: m.source === "builtin" ? "builtin" : "user",
31542
- parameterDefs: m.parameters
31543
- });
31544
- }
31545
- }
31546
- /**
31547
- * Overlay supplement data onto the registry.
31548
- * Supplements provide descriptions, parameters, children, parents, skipArgs.
31549
- * The `container` field maps to `block` on MacroInfo.
31550
- */
31551
- loadSupplements(supplements) {
31552
- this.mergeEntries(supplements);
31553
- this.onChange?.();
31554
- }
31555
- /**
31556
- * Overlay user config onto the registry.
31557
- * Same format as supplements; applied last so it wins.
31558
- *
31559
- * Replaces any previously loaded config: entries from the last call are
31560
- * reverted first, so macros removed from the config disappear (or fall
31561
- * back to their builtin/supplement definition).
31562
- */
31563
- loadConfig(config2) {
31564
- config2 = normalizeMacroEntries(config2);
31565
- for (const [key, previous2] of this.configSnapshot) {
31566
- if (previous2) {
31567
- this.macros.set(key, previous2);
31568
- } else {
31569
- this.macros.delete(key);
31570
- }
31571
- }
31572
- this.configSnapshot.clear();
31573
- this.configEntries.clear();
31574
- for (const rawKey of Object.keys(config2)) {
31575
- const key = rawKey.toLowerCase();
31576
- if (!this.configSnapshot.has(key)) {
31577
- const existing = this.macros.get(key);
31578
- this.configSnapshot.set(key, existing ? { ...existing } : void 0);
31579
- }
31580
- this.configEntries.set(key, { ...this.configEntries.get(key), ...config2[rawKey] });
31581
- }
31582
- this.mergeEntries(config2);
31583
- this.onChange?.();
31584
- }
31585
- /**
31586
- * Replace the set of macros discovered from Story.defineMacro() calls.
31587
- * Previously discovered definitions that are no longer present are dropped,
31588
- * restoring whatever the other tiers define for that name.
31589
- *
31590
- * Each sub-macro becomes a known macro that may only appear inside its
31591
- * parent. Whether a macro is a block is the tooling API's reading of the
31592
- * definition (`discoverMacros`).
31593
- */
31594
- setDiscoveredMacros(macros) {
31595
- this.discovered.clear();
31596
- for (const m of macros) {
31597
- this.discovered.set(m.name.toLowerCase(), {
31598
- name: m.name,
31599
- block: m.block,
31600
- subMacros: m.subMacros,
31601
- storeVar: m.storeVar,
31602
- interpolate: m.interpolate,
31603
- merged: m.merged,
31604
- description: m.description,
31605
- source: "user",
31606
- parameterDefs: m.parameters
31607
- });
31608
- }
31609
- for (const m of [...this.discovered.values()]) {
31610
- for (const sub of m.subMacros ?? []) {
31611
- const key = sub.toLowerCase();
31612
- const existing = this.discovered.get(key);
31613
- if (existing) {
31614
- existing.parents = [...existing.parents ?? [], m.name];
31615
- } else {
31616
- this.discovered.set(key, { name: sub, block: false, subMacros: [], source: "user", parents: [m.name] });
31617
- }
31618
- }
31619
- }
31620
- this.onChange?.();
31621
- }
31622
- /** Add or replace a single macro entry. */
31623
- addMacro(info) {
31624
- const key = info.name.toLowerCase();
31625
- const existing = this.macros.get(key);
31626
- this.macros.set(key, {
31627
- name: info.name,
31628
- block: info.block ?? existing?.block ?? false,
31629
- subMacros: info.subMacros ?? existing?.subMacros ?? [],
31630
- source: info.source ?? existing?.source ?? "user",
31631
- storeVar: info.storeVar ?? existing?.storeVar,
31632
- interpolate: info.interpolate ?? existing?.interpolate,
31633
- merged: info.merged ?? existing?.merged,
31634
- description: info.description ?? existing?.description,
31635
- parameters: info.parameters ?? existing?.parameters,
31636
- parameterDefs: info.parameterDefs ?? existing?.parameterDefs,
31637
- parameterDocs: info.parameterDocs ?? existing?.parameterDocs,
31638
- children: info.children ?? existing?.children,
31639
- parents: info.parents ?? existing?.parents,
31640
- skipArgs: info.skipArgs ?? existing?.skipArgs
31641
- });
31642
- this.onChange?.();
31643
- }
31644
- /** Get a macro by name (case-insensitive). */
31645
- getMacro(name) {
31646
- return this.resolve(name.toLowerCase());
31647
- }
31648
- /** A project's explicit schema takes precedence over runtime signature metadata. */
31649
- hasConfiguredParameters(name) {
31650
- return this.configEntries.get(name.toLowerCase())?.parameters !== void 0;
31651
- }
31652
- /** Check if a macro is a block (container) macro. */
31653
- isBlock(name) {
31654
- return this.getMacro(name)?.block ?? false;
31655
- }
31656
- /** Check if a macro is a sub-macro (has parents). */
31657
- isSubMacro(name) {
31658
- const info = this.getMacro(name);
31659
- return info?.parents != null && info.parents.length > 0;
31660
- }
31661
- /** The macros as the tooling API reads them (`passagePieces`, `validateStoryMarkup`). */
31662
- toolingMacros() {
31663
- return this.getAllMacros().map((m) => ({
31664
- name: m.name,
31665
- block: m.block,
31666
- subMacros: m.subMacros,
31667
- storeVar: m.storeVar,
31668
- interpolate: m.interpolate,
31669
- parameters: m.parameterDefs
31670
- }));
31671
- }
31672
- /** Get all registered macros. */
31673
- getAllMacros() {
31674
- const keys = /* @__PURE__ */ new Set([...this.macros.keys(), ...this.discovered.keys()]);
31675
- return Array.from(keys, (key) => this.resolve(key));
31676
- }
31677
- /**
31678
- * Compose the effective entry for a lowercase key: builtins/supplements/
31679
- * config (stored in `macros`), overlaid by discovered metadata, with user
31680
- * config re-applied on top so it keeps precedence.
31681
- */
31682
- resolve(key) {
31683
- const base = this.macros.get(key);
31684
- const found = this.discovered.get(key);
31685
- if (!found) return base;
31686
- const info = {
31687
- name: found.name,
31688
- block: found.block ?? false,
31689
- subMacros: found.subMacros ?? [],
31690
- source: "user",
31691
- storeVar: found.storeVar,
31692
- interpolate: found.interpolate,
31693
- merged: found.merged,
31694
- description: found.description,
31695
- // A definition replaces the macro it names, parameters included
31696
- parameterDefs: found.parameterDefs,
31697
- parents: found.parents
31698
- };
31699
- const config2 = this.configEntries.get(key);
31700
- if (config2) applySupplement(info, config2);
31701
- return info;
31702
- }
31703
- /** Merge a set of supplement/config entries into the registry. */
31704
- mergeEntries(entries) {
31705
- for (const [rawKey, entry] of Object.entries(entries)) {
31706
- const key = rawKey.toLowerCase();
31707
- const existing = this.macros.get(key);
31708
- if (existing) {
31709
- applySupplement(existing, entry);
31710
- } else {
31711
- this.macros.set(key, {
31712
- name: entry.name ?? rawKey,
31713
- block: entry.container ?? false,
31714
- subMacros: [],
31715
- source: "user",
31716
- description: entry.description,
31717
- parameters: entry.parameters,
31718
- parameterDocs: entry.parameterDocs,
31719
- children: entry.children,
31720
- parents: entry.parents,
31721
- skipArgs: entry.skipArgs
31722
- });
31723
- }
31724
- }
31725
- }
31726
- };
31727
31537
  }
31728
31538
  });
31729
31539
 
31730
- // src/core/parsing/executable-refs.ts
31731
- function collectVariableReferences2(markup) {
31732
- if (!markup.isMarkup) return [];
31733
- return variableReferences(markup.content, markup.doc.context.macros, { all: true }).map((ref2) => ({
31734
- sigil: ref2.sigil,
31735
- name: ref2.name,
31736
- path: [ref2.name, ...ref2.path].join("."),
31737
- start: ref2.start,
31738
- end: ref2.end
31739
- }));
31540
+ // src/core/parsing/parameter-validator.ts
31541
+ function isAlwaysArg(arg) {
31542
+ return arg.type === 4 /* Variable */ || arg.type === 7 /* Expression */;
31740
31543
  }
31741
- var init_executable_refs = __esm({
31742
- "src/core/parsing/executable-refs.ts"() {
31743
- "use strict";
31744
- init_tooling();
31544
+ function findParameterType(name) {
31545
+ for (const pt2 of parameterTypes) {
31546
+ if (pt2.name.includes(name)) return pt2;
31745
31547
  }
31746
- });
31747
-
31748
- // src/core/markup/passage-markup.ts
31749
- var PassageMarkup, DocumentMarkup;
31750
- var init_passage_markup = __esm({
31751
- "src/core/markup/passage-markup.ts"() {
31752
- "use strict";
31753
- init_tooling();
31754
- init_passage_parser();
31755
- init_text();
31756
- PassageMarkup = class {
31757
- constructor(doc, passage, bodyStart, bodyEnd) {
31758
- this.doc = doc;
31759
- this.passage = passage;
31760
- this.bodyStart = bodyStart;
31761
- this.bodyEnd = bodyEnd;
31762
- const raw = doc.text.slice(bodyStart, bodyEnd);
31763
- this.isMarkup = isMarkupPassage(passage);
31764
- if (raw.includes("\r\n")) {
31765
- const map2 = [];
31766
- let content2 = "";
31767
- for (let i2 = 0; i2 < raw.length; i2++) {
31768
- if (raw[i2] === "\r" && raw[i2 + 1] === "\n") continue;
31769
- map2.push(bodyStart + i2);
31770
- content2 += raw[i2];
31771
- }
31772
- map2.push(bodyEnd);
31773
- this.content = content2;
31774
- this.toDoc = Int32Array.from(map2);
31775
- } else {
31776
- this.content = raw;
31777
- this.toDoc = null;
31778
- }
31779
- }
31780
- doc;
31781
- passage;
31782
- bodyStart;
31783
- bodyEnd;
31784
- /** The body, LF-normalized: the text the tooling API analyzes. */
31785
- content;
31786
- /** Whether Spindle reads the body as story markup (see isMarkupPassage). */
31787
- isMarkup;
31788
- /** For each `content` offset, its offset in the document; null when the body has no CRLF. */
31789
- toDoc;
31790
- cachedTokens;
31791
- cachedPairing;
31792
- cachedPieces;
31793
- cachedDeclarations;
31794
- /** The document offset of `contentOffset`. */
31795
- docOffset(contentOffset) {
31796
- if (!this.toDoc) return this.bodyStart + contentOffset;
31797
- return this.toDoc[Math.min(Math.max(contentOffset, 0), this.toDoc.length - 1)];
31798
- }
31799
- /**
31800
- * The document offset just past the character before `contentEnd`: where an
31801
- * exclusive end maps to. (`docOffset()` of an end that sits before a line
31802
- * break would land behind the `\r` of a CRLF.)
31803
- */
31804
- docEnd(contentEnd) {
31805
- return contentEnd <= 0 ? this.docOffset(0) : this.docOffset(contentEnd - 1) + 1;
31806
- }
31807
- /** The `content` offset of a document offset (the nearest one at or after it). */
31808
- contentOffset(docOffset) {
31809
- if (!this.toDoc) return Math.min(Math.max(docOffset - this.bodyStart, 0), this.content.length);
31810
- let low = 0;
31811
- let high = this.toDoc.length - 1;
31812
- while (low < high) {
31813
- const mid = low + high >> 1;
31814
- if (this.toDoc[mid] < docOffset) low = mid + 1;
31815
- else high = mid;
31816
- }
31817
- return low;
31818
- }
31819
- /** The document position of `contentOffset`. */
31820
- position(contentOffset) {
31821
- return offsetToPosition(this.docOffset(contentOffset), this.doc.lineStarts);
31822
- }
31823
- /** The document range of `[start, end)` in `content` offsets. */
31824
- range(start, end) {
31825
- return offsetsToRange(this.docOffset(start), end > start ? this.docEnd(end) : this.docOffset(start), this.doc.lineStarts);
31826
- }
31827
- /** The flat tokens and the malformed tags; none for a passage that is not markup. */
31828
- get tokenization() {
31829
- return this.cachedTokens ??= this.isMarkup ? tokenizeMarkupTolerant(this.content) : { tokens: [], errors: [] };
31830
- }
31831
- get tokens() {
31832
- return this.tokenization.tokens;
31833
- }
31834
- /** The tree of paired tokens, and what does not pair. */
31835
- get pairing() {
31836
- return this.cachedPairing ??= pairMarkup(this.tokens, {
31837
- isBlock: (name) => this.doc.context.isBlock(name),
31838
- source: this.content
31839
- });
31840
- }
31841
- /** What the markup runs and names, and where (see `passagePieces`). */
31842
- get pieces() {
31843
- return this.cachedPieces ??= this.isMarkup ? passagePieces(this.content, this.doc.context.macros) : [];
31844
- }
31845
- /** The declarations of a StoryVariables / StoryTransients passage. */
31846
- get declarations() {
31847
- return this.cachedDeclarations ??= parseDeclarations(this.content, this.passage.name === "StoryTransients" ? "%" : "$");
31548
+ return null;
31549
+ }
31550
+ function lexFormat(formatString) {
31551
+ let pos = 0;
31552
+ const lexed = [];
31553
+ let iter = 0;
31554
+ for (; ; ) {
31555
+ if (++iter > LEX_LIMIT) throw new Error("Format lex iteration limit exceeded");
31556
+ if (pos >= formatString.length) break;
31557
+ const ch = formatString[pos];
31558
+ if (wsRe.test(ch)) {
31559
+ pos++;
31560
+ } else if (ch === "&") {
31561
+ pos++;
31562
+ const next = formatString[pos];
31563
+ if (next === "+") {
31564
+ pos++;
31565
+ lexed.push({ kind: 3 /* AndNext */ });
31566
+ } else {
31567
+ throw new Error(`Lex: Expected '+' after '&', found '${next ?? "EOF"}'`);
31848
31568
  }
31849
- };
31850
- DocumentMarkup = class {
31851
- constructor(uri, text3, passages, context) {
31852
- this.uri = uri;
31853
- this.text = text3;
31854
- this.context = context;
31855
- this.lineStarts = buildLineStarts(text3);
31856
- this.passages = passages.map((passage, i2) => {
31857
- const bodyStart = this.lineStarts[passage.headerEnd.end.line + 1] ?? text3.length;
31858
- const next = passages[i2 + 1];
31859
- const bodyEnd = next ? this.lineStarts[next.range.start.line] : text3.length;
31860
- return new PassageMarkup(this, passage, Math.min(bodyStart, bodyEnd), bodyEnd);
31861
- });
31569
+ } else if (ch === "|") {
31570
+ pos++;
31571
+ const next = formatString[pos];
31572
+ if (next === "+") {
31573
+ pos++;
31574
+ lexed.push({ kind: 4 /* MaybeNext */ });
31575
+ } else if (next === void 0) {
31576
+ throw new Error("Lex: Expected content after '|'");
31577
+ } else if (wsRe.test(next)) {
31578
+ pos++;
31579
+ lexed.push({ kind: 5 /* Or */ });
31580
+ } else if (identStartRe.test(next) || quoteRe.test(next)) {
31581
+ lexed.push({ kind: 5 /* Or */ });
31582
+ } else {
31583
+ throw new Error(`Lex: Invalid character after '|': '${next}'`);
31862
31584
  }
31863
- uri;
31864
- text;
31865
- context;
31866
- lineStarts;
31867
- passages;
31868
- /** The passage that contains `position`. */
31869
- passageAt(position) {
31870
- for (let i2 = this.passages.length - 1; i2 >= 0; i2--) {
31871
- if (this.passages[i2].passage.range.start.line <= position.line) return this.passages[i2];
31585
+ } else if (quoteRe.test(ch)) {
31586
+ const quoted = readQuoted(formatString, pos);
31587
+ if (!quoted) throw new Error("Lex: Unterminated string literal");
31588
+ pos = quoted.end;
31589
+ lexed.push({ kind: 1 /* Literal */, value: quoted.value });
31590
+ } else if (ch === "(" || ch === ")") {
31591
+ pos++;
31592
+ lexed.push({ kind: 2 /* Group */, open: ch === "(" });
31593
+ } else if (identStartRe.test(ch)) {
31594
+ const start = pos;
31595
+ pos++;
31596
+ while (pos < formatString.length) {
31597
+ if (++iter > LEX_LIMIT) throw new Error("Format lex identifier iteration limit exceeded");
31598
+ const c = formatString[pos];
31599
+ if (identStopRe.test(c) || c === void 0) break;
31600
+ if (identStartRe.test(c)) {
31601
+ pos++;
31602
+ } else if (digitRe.test(c)) {
31603
+ throw new Error(`Lex: Digit '${c}' not allowed in parameter type name`);
31604
+ } else {
31605
+ throw new Error(`Lex: Invalid character '${c}' in parameter type name`);
31872
31606
  }
31873
- return void 0;
31874
31607
  }
31875
- };
31876
- }
31877
- });
31878
-
31879
- // src/core/workspace/declaration-check.ts
31880
- function readDeclarations(content2, sigil) {
31881
- const passage = PASSAGE[sigil];
31882
- const lines = content2.replace(/\r\n/g, "\n").split("\n");
31883
- const header = lines.findIndex((line) => /^?::/.test(line));
31884
- const text3 = (header === -1 ? lines : lines.slice(0, header)).join("\n");
31885
- const { declarations, errors } = parseDeclarations(text3.replace(/\r/g, " "), sigil);
31886
- const problems = [];
31887
- for (const error48 of errors) {
31888
- if (error48.code === "duplicate-declaration") continue;
31889
- const [start, end] = lineSpan(text3, error48.offset);
31890
- if (isUnjudged(text3.slice(start, end))) continue;
31891
- let message = `${passage}: ${error48.message}`;
31892
- if (error48.code === "invalid-declaration" && COMMENT_START.test(text3.slice(error48.offset, error48.end))) {
31893
- message += `. ${passage} has no comment syntax.`;
31894
- } else if (!message.endsWith(".")) {
31895
- message += ".";
31608
+ const ident = formatString.slice(start, pos);
31609
+ const pt2 = findParameterType(ident);
31610
+ if (pt2 === null) throw new Error(`Lex: Unknown parameter type '${ident}'`);
31611
+ lexed.push({ kind: 0 /* Type */, type: pt2 });
31612
+ } else if (ch === "." && formatString[pos + 1] === "." && formatString[pos + 2] === ".") {
31613
+ pos += 3;
31614
+ lexed.push({ kind: 6 /* Repeat */ });
31615
+ } else {
31616
+ throw new Error(`Lex: Unexpected character '${ch}'`);
31896
31617
  }
31897
- problems.push({ message, start, end });
31898
31618
  }
31899
- return { text: text3, declarations, problems: problems.sort((a, b) => a.start - b.start) };
31900
- }
31901
- function isUnjudged(line) {
31902
- return line.includes("\r") || line.includes("\xA0");
31903
- }
31904
- function lineSpan(text3, offset2) {
31905
- const lineStart = text3.lastIndexOf("\n", offset2 - 1) + 1;
31906
- const lineEnd = text3.indexOf("\n", offset2);
31907
- const line = text3.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
31908
- const start = lineStart + line.length - line.trimStart().length;
31909
- return [start, lineStart + line.trimEnd().length];
31619
+ return lexed;
31910
31620
  }
31911
- var PASSAGE, COMMENT_START;
31912
- var init_declaration_check = __esm({
31913
- "src/core/workspace/declaration-check.ts"() {
31914
- "use strict";
31915
- init_tooling();
31916
- PASSAGE = { "$": "StoryVariables", "%": "StoryTransients" };
31917
- COMMENT_START = /^(?:\/\/|\/\*|<!--|#)/;
31621
+ function parseFormat(formatString) {
31622
+ const tokens2 = lexFormat(formatString);
31623
+ if (tokens2.length === 0) return null;
31624
+ let index = 0;
31625
+ let iter = 0;
31626
+ function infixBp(kind) {
31627
+ switch (kind) {
31628
+ case 5 /* Or */:
31629
+ return [9, 10];
31630
+ case 3 /* AndNext */:
31631
+ return [4, 5];
31632
+ case 4 /* MaybeNext */:
31633
+ return [2, 3];
31634
+ default:
31635
+ return null;
31636
+ }
31918
31637
  }
31919
- });
31920
-
31921
- // src/core/workspace/variable-tracker.ts
31922
- function standaloneMarkup(uri, text3) {
31923
- const index = new PassageIndex();
31924
- index.rebuild(uri, text3);
31925
- return new DocumentMarkup(uri, text3, index.getPassagesInDocument(uri), BUILTIN_CONTEXT);
31638
+ function prefixBp(kind) {
31639
+ switch (kind) {
31640
+ case 4 /* MaybeNext */:
31641
+ return 3;
31642
+ case 6 /* Repeat */:
31643
+ return 8;
31644
+ default:
31645
+ throw new Error(`Parse: Unexpected prefix operator kind ${kind}`);
31646
+ }
31647
+ }
31648
+ function isPrefixOp(t) {
31649
+ return t.kind === 4 /* MaybeNext */ || t.kind === 6 /* Repeat */;
31650
+ }
31651
+ function isValue(t) {
31652
+ return t.kind === 0 /* Type */ || t.kind === 1 /* Literal */;
31653
+ }
31654
+ function exprBp(minBp) {
31655
+ if (++iter > PARSE_LIMIT) throw new Error("Format parse iteration limit exceeded");
31656
+ const cur = tokens2[index];
31657
+ if (!cur) throw new Error("Parse: Unexpected end of format string");
31658
+ index++;
31659
+ let lhs;
31660
+ if (isPrefixOp(cur)) {
31661
+ const rBp = prefixBp(cur.kind);
31662
+ const rhs = exprBp(rBp);
31663
+ lhs = { kind: cur.kind, right: rhs };
31664
+ } else if (cur.kind === 2 /* Group */) {
31665
+ const g = cur;
31666
+ if (!g.open) throw new Error("Parse: Unexpected closing parenthesis");
31667
+ lhs = exprBp(0);
31668
+ const closing = tokens2[index];
31669
+ if (!closing || closing.kind !== 2 /* Group */ || closing.open) {
31670
+ throw new Error("Parse: Expected closing parenthesis");
31671
+ }
31672
+ index++;
31673
+ } else if (isValue(cur)) {
31674
+ lhs = cur;
31675
+ } else {
31676
+ throw new Error("Parse: Expected value or prefix operator");
31677
+ }
31678
+ for (; ; ) {
31679
+ if (++iter > PARSE_LIMIT) throw new Error("Format parse operator iteration limit exceeded");
31680
+ const op = tokens2[index];
31681
+ if (!op) break;
31682
+ const bp = infixBp(op.kind);
31683
+ if (bp === null) break;
31684
+ const [lBp, rBp] = bp;
31685
+ if (lBp < minBp) break;
31686
+ index++;
31687
+ const rhs = exprBp(rBp);
31688
+ lhs = { kind: op.kind, left: lhs, right: rhs };
31689
+ }
31690
+ return lhs;
31691
+ }
31692
+ return exprBp(0);
31926
31693
  }
31927
- function usagesOf(byUri, name) {
31928
- const results = [];
31929
- for (const usages of byUri.values()) {
31930
- for (const u of usages) {
31931
- if (u.baseName === name) results.push({ uri: u.uri, range: u.range });
31694
+ function slotAccepts(leaf) {
31695
+ return (arg) => crawlValidate(leaf, [arg], 0).status === 2 /* Success */;
31696
+ }
31697
+ function describeFormat(format) {
31698
+ const cap = (list3) => list3.slice(0, DESCRIBE_ALTERNATIVE_LIMIT);
31699
+ const join5 = (lefts, rights) => cap(lefts.flatMap((l) => rights.map((r) => [...l, ...r])));
31700
+ switch (format.kind) {
31701
+ case 0 /* Type */:
31702
+ return [[{ label: format.type.name[0], optional: false, repeat: false, accepts: slotAccepts(format) }]];
31703
+ case 1 /* Literal */:
31704
+ return [[{ label: `'${format.value}'`, optional: false, repeat: false, accepts: slotAccepts(format) }]];
31705
+ case 3 /* AndNext */:
31706
+ return join5(describeFormat(format.left), describeFormat(format.right));
31707
+ case 4 /* MaybeNext */: {
31708
+ const rights = describeFormat(format.right).map(
31709
+ (seq) => seq.map((slot) => ({ ...slot, optional: true }))
31710
+ );
31711
+ return join5(format.left ? describeFormat(format.left) : [[]], rights);
31932
31712
  }
31713
+ case 5 /* Or */:
31714
+ return cap([...describeFormat(format.left), ...describeFormat(format.right)]);
31715
+ case 6 /* Repeat */:
31716
+ return describeFormat(format.right).map(
31717
+ (seq) => seq.map((slot) => ({ ...slot, optional: true, repeat: true }))
31718
+ );
31933
31719
  }
31934
- return results;
31935
31720
  }
31936
- var BUILTIN_CONTEXT, ARRAY_MEMBERS, DeclarationSet, VariableTracker;
31937
- var init_variable_tracker = __esm({
31938
- "src/core/workspace/variable-tracker.ts"() {
31721
+ function passageSlots(format) {
31722
+ const single = (passage) => ({ slots: [passage], variable: false, unsafe: false });
31723
+ switch (format.kind) {
31724
+ case 0 /* Type */:
31725
+ return single(format.type.name.includes("passage"));
31726
+ case 1 /* Literal */:
31727
+ return single(false);
31728
+ case 6 /* Repeat */: {
31729
+ const child = passageSlots(format.right);
31730
+ return { slots: [], variable: true, unsafe: child.unsafe || child.slots.includes(true), restFrom: 0 };
31731
+ }
31732
+ case 5 /* Or */:
31733
+ return mergePassageSlots(passageSlots(format.left), passageSlots(format.right));
31734
+ case 3 /* AndNext */:
31735
+ case 4 /* MaybeNext */: {
31736
+ const left = format.left ? passageSlots(format.left) : { slots: [], variable: false, unsafe: false };
31737
+ const right = passageSlots(format.right);
31738
+ return {
31739
+ slots: [...left.slots, ...right.slots],
31740
+ variable: left.variable || right.variable || format.kind === 4 /* MaybeNext */,
31741
+ restFrom: left.restFrom ?? (right.restFrom === void 0 ? void 0 : left.slots.length + right.restFrom),
31742
+ // A variable-width prefix can move every position that follows it.
31743
+ // Refuse rather than attributing an ordinary string to a passage.
31744
+ unsafe: left.unsafe || right.unsafe || left.variable && right.slots.length > 0 && (left.slots.includes(true) || right.slots.includes(true))
31745
+ };
31746
+ }
31747
+ }
31748
+ }
31749
+ function mergePassageSlots(left, right) {
31750
+ const repeatedConflict = (repeated, other) => repeated.restFrom !== void 0 && other.slots.some((passage, i2) => passage && i2 >= repeated.restFrom);
31751
+ return {
31752
+ slots: Array.from({ length: Math.max(left.slots.length, right.slots.length) }, (_2, i2) => left.slots[i2] === true || right.slots[i2] === true),
31753
+ variable: left.variable || right.variable || left.slots.length !== right.slots.length,
31754
+ restFrom: left.restFrom === void 0 ? right.restFrom : right.restFrom === void 0 ? left.restFrom : Math.min(left.restFrom, right.restFrom),
31755
+ unsafe: left.unsafe || right.unsafe || left.slots.some((passage, i2) => right.slots[i2] !== void 0 && passage !== right.slots[i2]) || repeatedConflict(left, right) || repeatedConflict(right, left)
31756
+ };
31757
+ }
31758
+ function makeError(status, message, argIndex, rank = 0) {
31759
+ return {
31760
+ status,
31761
+ rank,
31762
+ argIndex,
31763
+ errors: [{ message, argIndex }],
31764
+ warnings: []
31765
+ };
31766
+ }
31767
+ function makeSuccess(argIndex, rank, warnings = []) {
31768
+ return { status: 2 /* Success */, rank, argIndex, errors: [], warnings };
31769
+ }
31770
+ function isFail(s) {
31771
+ return s === 1 /* Failure */ || s === 0 /* NotFound */;
31772
+ }
31773
+ function crawlValidate(format, args2, startIndex, state) {
31774
+ const memo2 = /* @__PURE__ */ new Map();
31775
+ function append(left, right) {
31776
+ return { ...right, rank: left.rank + right.rank, warnings: left.warnings.concat(right.warnings) };
31777
+ }
31778
+ function follow(left, right) {
31779
+ return left.flatMap((matched) => isFail(matched.status) ? [matched] : crawl(right, matched.argIndex).map((result2) => append(matched, result2)));
31780
+ }
31781
+ function crawl(fmt, argIdx) {
31782
+ let positions = memo2.get(fmt);
31783
+ if (!positions) memo2.set(fmt, positions = /* @__PURE__ */ new Map());
31784
+ const cached2 = positions.get(argIdx);
31785
+ if (cached2) return cached2;
31786
+ if (positions.size >= VALIDATE_LIMIT) {
31787
+ throw new Error(`Validation iteration limit exceeded at argument ${argIdx}`);
31788
+ }
31789
+ const unique = /* @__PURE__ */ new Map();
31790
+ for (const result2 of match2(fmt, argIdx)) {
31791
+ const key = isFail(result2.status) ? "failure" : result2.argIndex;
31792
+ const previous2 = unique.get(key);
31793
+ if (!previous2 || key === "failure" && result2.rank > previous2.rank) unique.set(key, result2);
31794
+ }
31795
+ const results2 = [...unique.values()];
31796
+ positions.set(argIdx, results2);
31797
+ return results2;
31798
+ }
31799
+ function match2(fmt, argIdx) {
31800
+ switch (fmt.kind) {
31801
+ case 4 /* MaybeNext */: {
31802
+ const left = fmt.left ? crawl(fmt.left, argIdx) : [makeSuccess(argIdx, 0)];
31803
+ return [...follow(left, fmt.right), ...left];
31804
+ }
31805
+ case 3 /* AndNext */: {
31806
+ return follow(crawl(fmt.left, argIdx), fmt.right);
31807
+ }
31808
+ case 5 /* Or */: {
31809
+ return [...crawl(fmt.left, argIdx), ...crawl(fmt.right, argIdx)];
31810
+ }
31811
+ case 6 /* Repeat */: {
31812
+ const matches = [];
31813
+ const pending = [makeSuccess(argIdx, 0)];
31814
+ const reached = /* @__PURE__ */ new Set();
31815
+ while (pending.length > 0) {
31816
+ const matched = pending.pop();
31817
+ if (reached.has(matched.argIndex)) continue;
31818
+ reached.add(matched.argIndex);
31819
+ matches.push(matched);
31820
+ const items = crawl(fmt.right, matched.argIndex);
31821
+ for (let i2 = items.length - 1; i2 >= 0; i2--) {
31822
+ const item = items[i2];
31823
+ if (!isFail(item.status) && !reached.has(item.argIndex)) pending.push(append(matched, item));
31824
+ }
31825
+ }
31826
+ return matches.sort((left, right) => right.argIndex - left.argIndex);
31827
+ }
31828
+ case 1 /* Literal */: {
31829
+ const arg = args2[argIdx];
31830
+ if (arg === void 0) {
31831
+ return [makeError(0 /* NotFound */, `Expected literal '${fmt.value}' but no argument found`, argIdx)];
31832
+ }
31833
+ if (isAlwaysArg(arg)) {
31834
+ return [makeSuccess(argIdx + 1, CORRECT_RANK)];
31835
+ }
31836
+ const argText = arg.type === 0 /* String */ ? readQuoted(arg.text, 0)?.value : arg.text;
31837
+ if (argText === fmt.value) {
31838
+ return [makeSuccess(argIdx + 1, CORRECT_RANK)];
31839
+ }
31840
+ 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 */) {
31841
+ return [makeSuccess(argIdx + 1, CORRECT_RANK)];
31842
+ }
31843
+ return [makeError(1 /* Failure */, `Expected literal '${fmt.value}'`, argIdx, CORRECT_TYPE_RANK)];
31844
+ }
31845
+ case 0 /* Type */: {
31846
+ const arg = args2[argIdx];
31847
+ const typeDef = fmt.type;
31848
+ if (arg === void 0) {
31849
+ return [makeError(
31850
+ 0 /* NotFound */,
31851
+ `Expected type '${typeDef.name[0]}' but no argument found`,
31852
+ argIdx
31853
+ )];
31854
+ }
31855
+ if (isAlwaysArg(arg)) {
31856
+ return [makeSuccess(argIdx + 1, CORRECT_RANK)];
31857
+ }
31858
+ const result2 = typeDef.validate(arg, args2, argIdx, state);
31859
+ if (result2 instanceof TypeError2) {
31860
+ return [{
31861
+ status: 1 /* Failure */,
31862
+ rank: 0,
31863
+ argIndex: argIdx,
31864
+ errors: [{ message: result2.message, argIndex: argIdx }],
31865
+ warnings: []
31866
+ }];
31867
+ }
31868
+ if (result2 instanceof TypeWarning) {
31869
+ return [makeSuccess(argIdx + 1, CORRECT_RANK, [
31870
+ { message: result2.message, argIndex: argIdx }
31871
+ ])];
31872
+ }
31873
+ return [makeSuccess(argIdx + 1, CORRECT_RANK)];
31874
+ }
31875
+ }
31876
+ }
31877
+ const results = crawl(format, startIndex);
31878
+ return results.find((result2) => !isFail(result2.status) && result2.argIndex === args2.length) ?? results.reduce((best, result2) => result2.rank > best.rank ? result2 : best);
31879
+ }
31880
+ var TypeError2, TypeWarning, parameterTypes, LEX_LIMIT, wsRe, identStartRe, identStopRe, quoteRe, digitRe, PARSE_LIMIT, DESCRIBE_ALTERNATIVE_LIMIT, VALIDATE_LIMIT, CORRECT_TYPE_RANK, CORRECT_RANK, Parameters;
31881
+ var init_parameter_validator = __esm({
31882
+ "src/core/parsing/parameter-validator.ts"() {
31939
31883
  "use strict";
31940
31884
  init_tooling();
31941
- init_executable_refs();
31942
- init_passage_markup();
31943
- init_text();
31944
- init_passage_index();
31945
- init_declaration_check();
31946
- BUILTIN_CONTEXT = { macros: builtinMacros, isBlock: isBlockMacro };
31947
- ARRAY_MEMBERS = /* @__PURE__ */ new Set([
31948
- ...Object.getOwnPropertyNames(Array.prototype),
31949
- ...Object.getOwnPropertyNames(Object.prototype),
31950
- "at",
31951
- "flat",
31952
- "flatMap",
31953
- "includes",
31954
- "findLast",
31955
- "findLastIndex",
31956
- "toReversed",
31957
- "toSorted",
31958
- "toSpliced",
31959
- "with"
31960
- ]);
31961
- DeclarationSet = class {
31962
- constructor(sigil) {
31963
- this.sigil = sigil;
31885
+ init_argument_lexer();
31886
+ TypeError2 = class {
31887
+ constructor(message) {
31888
+ this.message = message;
31964
31889
  }
31965
- sigil;
31966
- present = false;
31967
- declared = /* @__PURE__ */ new Map();
31968
- /** Every declaration, including earlier defaults for the same binding. */
31969
- occurrences = [];
31970
- /** Lines Spindle rejects. */
31971
- problems = [];
31972
- /** The names declared, with the range of sigil and name of the first. */
31973
- names = /* @__PURE__ */ new Map();
31974
- clear() {
31975
- this.present = false;
31976
- this.declared.clear();
31977
- this.occurrences.length = 0;
31978
- this.problems = [];
31979
- this.names.clear();
31890
+ message;
31891
+ };
31892
+ TypeWarning = class {
31893
+ constructor(message) {
31894
+ this.message = message;
31980
31895
  }
31981
- /** Read the passage `content`, which starts at line `contentStartLine` of the document `uri`. */
31982
- read(content2, contentStartLine, uri) {
31983
- this.clear();
31984
- this.present = true;
31985
- const { text: text3, declarations, problems } = readDeclarations(content2, this.sigil);
31986
- const lineStarts = buildLineStarts(text3);
31987
- const position = (offset2) => {
31988
- const { line, character } = offsetToPosition(offset2, lineStarts);
31989
- return { line: contentStartLine + line, character };
31990
- };
31991
- for (const problem of problems) {
31992
- this.problems.push({ message: problem.message, range: { start: position(problem.start), end: position(problem.end) } });
31896
+ message;
31897
+ };
31898
+ parameterTypes = [
31899
+ {
31900
+ name: ["bool", "boolean"],
31901
+ validate(arg) {
31902
+ return arg.type === 2 /* Boolean */ ? null : new TypeError2("Argument is not a boolean");
31993
31903
  }
31994
- for (const declaration of declarations) {
31995
- const { name, schema } = declaration;
31996
- const declarationRange = { start: position(declaration.nameStart - 1), end: position(declaration.nameEnd) };
31997
- const declared = { name, sigil: this.sigil, declarationRange };
31998
- if (uri !== void 0) declared.declarationUri = uri;
31999
- if (schema) {
32000
- if (schema.type !== "null") declared.type = schema.type;
32001
- if (schema.fields && schema.fields.size > 0) declared.fields = [...schema.fields.keys()];
31904
+ },
31905
+ {
31906
+ name: ["null"],
31907
+ validate(arg) {
31908
+ return arg.type === 3 /* Null */ ? null : new TypeError2("Argument is not 'null'");
31909
+ }
31910
+ },
31911
+ {
31912
+ name: ["undefined"],
31913
+ validate(arg) {
31914
+ return arg.type === 10 /* Undefined */ ? null : new TypeError2("Argument is not 'undefined'");
31915
+ }
31916
+ },
31917
+ {
31918
+ name: ["number"],
31919
+ validate(arg) {
31920
+ return arg.type === 1 /* Number */ ? null : new TypeError2("Argument is not a number");
31921
+ }
31922
+ },
31923
+ {
31924
+ name: ["NaN"],
31925
+ validate(arg) {
31926
+ return arg.type === 9 /* NaN */ ? null : new TypeError2("Argument is not 'NaN'");
31927
+ }
31928
+ },
31929
+ {
31930
+ name: ["link"],
31931
+ validate(arg) {
31932
+ return arg.type === 5 /* Link */ ? null : new TypeError2("Argument is not a link");
31933
+ }
31934
+ },
31935
+ {
31936
+ name: ["image"],
31937
+ validate(arg) {
31938
+ return arg.type === 6 /* Image */ ? null : new TypeError2("Argument is not an image");
31939
+ }
31940
+ },
31941
+ {
31942
+ name: ["bareword"],
31943
+ validate(arg) {
31944
+ return arg.type === 8 /* Bareword */ ? null : new TypeError2("Argument is not a bareword");
31945
+ }
31946
+ },
31947
+ {
31948
+ name: ["string"],
31949
+ validate(arg) {
31950
+ return arg.type === 0 /* String */ ? null : new TypeError2("Argument is not a quoted string");
31951
+ }
31952
+ },
31953
+ {
31954
+ name: ["text"],
31955
+ validate(arg) {
31956
+ const t = arg.type;
31957
+ if (t === 8 /* Bareword */ || t === 0 /* String */ || t === 2 /* Boolean */ || t === 3 /* Null */ || t === 9 /* NaN */ || t === 1 /* Number */) {
31958
+ return null;
32002
31959
  }
32003
- this.declared.set(name, declared);
32004
- this.occurrences.push(declared);
32005
- if (!this.names.has(name)) this.names.set(name, declarationRange);
31960
+ return new TypeError2("Argument is not text");
31961
+ }
31962
+ },
31963
+ {
31964
+ name: ["var"],
31965
+ validate(arg) {
31966
+ return arg.type === 4 /* Variable */ ? null : new TypeError2("Argument is not a variable");
31967
+ }
31968
+ },
31969
+ {
31970
+ // The story variable a form control binds to: `$name` or `"$name"`.
31971
+ // Spindle strips the quotes and the `$` sigil from the first argument
31972
+ // and reads the rest as a dot path into the story variables.
31973
+ name: ["receiver"],
31974
+ validate(arg) {
31975
+ let name;
31976
+ if (arg.type === 0 /* String */) {
31977
+ name = arg.text.slice(1, -1);
31978
+ } else if (arg.type === 8 /* Bareword */) {
31979
+ name = arg.text;
31980
+ }
31981
+ if (name === void 0) {
31982
+ return new TypeError2("Argument is not a variable receiver");
31983
+ }
31984
+ if (/^[_@%]/.test(name)) {
31985
+ return new TypeError2(`Variable receiver must be a story variable ($name), got '${name}'`);
31986
+ }
31987
+ if (!/^(?:\$\w|[A-Za-z_])[\w$]*(?:\.[\w$]+)*$/.test(name)) {
31988
+ return new TypeError2(`Argument is not a valid variable receiver: '${name}'`);
31989
+ }
31990
+ return null;
31991
+ }
31992
+ },
31993
+ {
31994
+ name: ["passage"],
31995
+ validate(arg, _args, _index, state) {
31996
+ let passageName;
31997
+ if (arg.type === 0 /* String */) {
31998
+ const target = passageTarget(arg.text);
31999
+ if (target.kind === "name") passageName = target.name;
32000
+ } else if (arg.type === 8 /* Bareword */) {
32001
+ passageName = arg.text;
32002
+ } else if (arg.type === 1 /* Number */) {
32003
+ passageName = arg.text;
32004
+ } else if (arg.type === 9 /* NaN */) {
32005
+ passageName = "NaN";
32006
+ }
32007
+ if (passageName !== void 0) {
32008
+ if (state && state.passages.length > 0) {
32009
+ if (!state.passages.includes(passageName)) {
32010
+ return new TypeWarning(`Nonexistent passage: "${passageName}"`);
32011
+ }
32012
+ }
32013
+ return null;
32014
+ }
32015
+ return new TypeError2("Argument is not an acceptable passage");
32006
32016
  }
32007
32017
  }
32008
- };
32009
- VariableTracker = class {
32010
- variables = new DeclarationSet("$");
32011
- transients = new DeclarationSet("%");
32012
- /** Per-URI list of variable usages. */
32013
- usagesByUri = /* @__PURE__ */ new Map();
32014
- /** Per-URI list of transient variable usages. */
32015
- transientUsagesByUri = /* @__PURE__ */ new Map();
32018
+ ];
32019
+ LEX_LIMIT = 1e3;
32020
+ wsRe = /\s/;
32021
+ identStartRe = /[a-zA-Z]/;
32022
+ identStopRe = /[|&+\-\s)(]/;
32023
+ quoteRe = /['"]/;
32024
+ digitRe = /[0-9]/;
32025
+ PARSE_LIMIT = 2e3;
32026
+ DESCRIBE_ALTERNATIVE_LIMIT = 16;
32027
+ VALIDATE_LIMIT = 2e3;
32028
+ CORRECT_TYPE_RANK = 1;
32029
+ CORRECT_RANK = 2;
32030
+ Parameters = class {
32031
+ variants;
32032
+ constructor(variants) {
32033
+ this.variants = variants.map((s) => ({
32034
+ formatString: s,
32035
+ format: parseFormat(s)
32036
+ }));
32037
+ }
32016
32038
  /**
32017
- * Parse the StoryVariables passage content for declarations.
32018
- * Each line like `$name = value` becomes a declaration.
32039
+ * Describe every argument position of every schema variant. Alternatives
32040
+ * (`|`) and optional positions (`|+`) become separate sequences; an empty
32041
+ * schema is the empty sequence. Variants whose format cannot be parsed
32042
+ * contribute nothing.
32019
32043
  */
32020
- parseStoryVariables(content2, contentStartLine = 0, uri) {
32021
- this.variables.read(content2, contentStartLine, uri);
32022
- }
32023
- /** Forget StoryVariables declarations, e.g. after the passage was removed. */
32024
- clearStoryVariables() {
32025
- this.variables.clear();
32026
- }
32027
- /** Forget StoryTransients declarations, e.g. after the passage was removed. */
32028
- clearStoryTransients() {
32029
- this.transients.clear();
32044
+ describe() {
32045
+ const sequences = [];
32046
+ for (const variant of this.variants) {
32047
+ if (variant.format === null) sequences.push([]);
32048
+ else sequences.push(...describeFormat(variant.format));
32049
+ }
32050
+ return sequences;
32030
32051
  }
32031
32052
  /**
32032
- * Parse the StoryTransients passage content for declarations.
32033
- * Each line like `%name = value` becomes a declaration.
32053
+ * Passage positions that have the same role in every schema alternative.
32054
+ * Unlike describe(), this walks the whole tree without truncating branches.
32055
+ * Repeated or conflicting passage roles return null so callers can refuse
32056
+ * an unsafe rename; actual argument spans and values remain Spindle's job.
32034
32057
  */
32035
- parseStoryTransients(content2, contentStartLine = 0, uri) {
32036
- this.transients.read(content2, contentStartLine, uri);
32058
+ passagePositions() {
32059
+ const summary = this.variants.map((variant) => variant.format ? passageSlots(variant.format) : { slots: [], variable: false, unsafe: false }).reduce(mergePassageSlots, { slots: [], variable: false, unsafe: false });
32060
+ return summary.unsafe ? null : summary.slots.flatMap((passage, i2) => passage ? [i2] : []);
32037
32061
  }
32038
32062
  /**
32039
- * Scan a document for variable usages: the `$` and `%` references in the
32040
- * markup of its passages (see `collectVariableReferences`). The workspace
32041
- * passes the `markup` it reads the document with, which knows the project's
32042
- * macros and widgets; without it the built-in macros decide.
32063
+ * Validate an array of Arg tokens against these parameter variants.
32064
+ * Selects the best-matching variant by rank.
32043
32065
  */
32044
- scanDocument(uri, text3, markup = standaloneMarkup(uri, text3)) {
32045
- this.removeDocument(uri);
32046
- const usages = [];
32047
- const transientUsages = [];
32048
- for (const passage of markup.passages) {
32049
- for (const ref2 of collectVariableReferences2(passage)) {
32050
- const usage = {
32051
- uri,
32052
- baseName: ref2.name,
32053
- fullName: ref2.path,
32054
- range: passage.range(ref2.start, ref2.end)
32055
- };
32056
- (ref2.sigil === "$" ? usages : transientUsages).push(usage);
32066
+ validate(args2, stateInfo) {
32067
+ if (this.variants.length === 0) {
32068
+ if (args2.length === 0) {
32069
+ return { variantIndex: null, errors: [], warnings: [] };
32057
32070
  }
32071
+ return {
32072
+ variantIndex: null,
32073
+ errors: [{ message: "Expected no arguments", argIndex: 0 }],
32074
+ warnings: []
32075
+ };
32058
32076
  }
32059
- if (usages.length > 0) this.usagesByUri.set(uri, usages);
32060
- if (transientUsages.length > 0) this.transientUsagesByUri.set(uri, transientUsages);
32061
- }
32062
- /** Forget the usages recorded for a document, e.g. after it was deleted. */
32063
- removeDocument(uri) {
32064
- this.usagesByUri.delete(uri);
32065
- this.transientUsagesByUri.delete(uri);
32066
- }
32067
- /** Get all declared variables. */
32068
- getDeclared() {
32069
- return this.variables.declared;
32070
- }
32071
- /** Get all usages of a variable by base name. */
32072
- getUsages(name) {
32073
- return usagesOf(this.usagesByUri, name);
32074
- }
32075
- /** Every declaration location of a story or transient binding, in source order. */
32076
- getDeclarations(name, sigil) {
32077
- const declarations = sigil === "$" ? this.variables : this.transients;
32078
- return declarations.occurrences.flatMap((d) => d.name === name && d.declarationUri !== void 0 && d.declarationRange ? [{ uri: d.declarationUri, range: d.declarationRange }] : []);
32079
- }
32080
- /** The executable usage or declaration containing the cursor, in source coordinates. */
32081
- referenceAt(uri, position) {
32082
- const contains = (range2) => (position.line > range2.start.line || position.line === range2.start.line && position.character >= range2.start.character) && (position.line < range2.end.line || position.line === range2.end.line && position.character <= range2.end.character);
32083
- let atEnd = null;
32084
- for (const [sigil, usages, declarations] of [
32085
- ["$", this.usagesByUri.get(uri), this.variables.occurrences],
32086
- ["%", this.transientUsagesByUri.get(uri), this.transients.occurrences]
32087
- ]) {
32088
- const candidates = (usages ?? []).map((u) => ({ name: u.baseName, range: u.range }));
32089
- for (const declaration of declarations) {
32090
- if (declaration.declarationUri === uri && declaration.declarationRange) candidates.push({ name: declaration.name, range: declaration.declarationRange });
32077
+ let bestIndex = null;
32078
+ let bestRank = -Infinity;
32079
+ let bestErrors = [];
32080
+ let bestWarnings = [];
32081
+ let bestIsClean = false;
32082
+ for (let i2 = 0; i2 < this.variants.length; i2++) {
32083
+ const variant = this.variants[i2];
32084
+ if (variant.format === null) {
32085
+ if (args2.length === 0) {
32086
+ if (!bestIsClean || 0 > bestRank) {
32087
+ bestIndex = i2;
32088
+ bestRank = 0;
32089
+ bestErrors = [];
32090
+ bestWarnings = [];
32091
+ bestIsClean = true;
32092
+ }
32093
+ } else {
32094
+ if (!bestIsClean && bestIndex === null) {
32095
+ bestIndex = i2;
32096
+ bestRank = -1;
32097
+ bestErrors = [{ message: "Expected no arguments", argIndex: 0 }];
32098
+ bestWarnings = [];
32099
+ }
32100
+ }
32101
+ continue;
32091
32102
  }
32092
- for (const candidate of candidates) {
32093
- if (!contains(candidate.range)) continue;
32094
- const symbol2 = { sigil, ...candidate };
32095
- if (position.line !== candidate.range.end.line || position.character < candidate.range.end.character) return symbol2;
32096
- atEnd ??= symbol2;
32103
+ const result2 = crawlValidate(variant.format, args2, 0, stateInfo);
32104
+ let errors = result2.errors;
32105
+ if (result2.errors.length === 0 && result2.argIndex < args2.length) {
32106
+ errors = [
32107
+ {
32108
+ message: `Too many arguments: expected ${result2.argIndex}, got ${args2.length}`,
32109
+ argIndex: result2.argIndex
32110
+ }
32111
+ ];
32097
32112
  }
32098
- }
32099
- return atEnd;
32100
- }
32101
- /** Whether a StoryVariables passage has been parsed. */
32102
- hasStoryVariables() {
32103
- return this.variables.present;
32104
- }
32105
- /** Get all declared transient variables. */
32106
- getDeclaredTransient() {
32107
- return this.transients.declared;
32108
- }
32109
- /** Get all usages of a transient variable by base name. */
32110
- getTransientUsages(name) {
32111
- return usagesOf(this.transientUsagesByUri, name);
32112
- }
32113
- /** Whether a StoryTransients passage has been parsed. */
32114
- hasStoryTransients() {
32115
- return this.transients.present;
32116
- }
32117
- /**
32118
- * Get `$var.member` / `%var.member` accesses in a document where `var` is
32119
- * declared with an array literal default and `member` is not a property
32120
- * that JavaScript arrays have (so the access always yields undefined).
32121
- */
32122
- getArrayMemberAccesses(uri) {
32123
- const results = [];
32124
- const sources = [
32125
- ["$", this.usagesByUri.get(uri), this.variables.declared],
32126
- ["%", this.transientUsagesByUri.get(uri), this.transients.declared]
32127
- ];
32128
- for (const [sigil, usages, declared] of sources) {
32129
- for (const u of usages ?? []) {
32130
- const member = u.fullName.split(".")[1];
32131
- if (member === void 0) continue;
32132
- if (declared.get(u.baseName)?.type !== "array") continue;
32133
- if (ARRAY_MEMBERS.has(member) || /^(0|[1-9]\d*)$/.test(member) && Number(member) < 4294967295) continue;
32134
- results.push({ sigil, name: u.baseName, member, range: u.range });
32113
+ const rank = result2.rank;
32114
+ const isClean = errors.length === 0;
32115
+ if (isClean && (!bestIsClean || rank > bestRank)) {
32116
+ bestIndex = i2;
32117
+ bestRank = rank;
32118
+ bestErrors = [];
32119
+ bestWarnings = result2.warnings;
32120
+ bestIsClean = true;
32121
+ } else if (!isClean && !bestIsClean && rank > bestRank) {
32122
+ bestIndex = i2;
32123
+ bestRank = rank;
32124
+ bestErrors = errors;
32125
+ bestWarnings = result2.warnings;
32126
+ } else if (bestIndex === null) {
32127
+ bestIndex = i2;
32128
+ bestRank = rank;
32129
+ bestErrors = errors;
32130
+ bestWarnings = result2.warnings;
32131
+ bestIsClean = isClean;
32135
32132
  }
32136
32133
  }
32137
- return results;
32138
- }
32139
- /** StoryVariables lines that stop Spindle from starting. */
32140
- getInvalidDeclarations() {
32141
- return this.variables.problems;
32134
+ return {
32135
+ variantIndex: bestIndex,
32136
+ errors: bestErrors,
32137
+ warnings: bestWarnings
32138
+ };
32142
32139
  }
32143
32140
  /**
32144
- * StoryTransients lines that stop Spindle from starting, and the names it
32145
- * declares that StoryVariables declares too.
32141
+ * Whether all variants are empty (accept no arguments).
32146
32142
  */
32147
- getInvalidTransientDeclarations() {
32148
- const collisions = [];
32149
- if (this.variables.present) {
32150
- for (const [name, range2] of this.transients.names) {
32151
- if (!this.variables.names.has(name)) continue;
32152
- collisions.push({
32153
- message: `StoryTransients: Variable "${name}" is already declared in StoryVariables. Names must be unique across scopes.`,
32154
- range: range2
32155
- });
32156
- }
32157
- }
32158
- return [...this.transients.problems, ...collisions].sort((a, b) => a.range.start.line - b.range.start.line);
32143
+ isEmpty() {
32144
+ if (this.variants.length === 0) return true;
32145
+ return !this.variants.some((v2) => v2.format !== null);
32159
32146
  }
32160
32147
  };
32161
32148
  }
32162
32149
  });
32163
32150
 
32164
- // src/core/markup/tokens.ts
32165
- function* markupTokens(passage) {
32166
- yield* passage.tokens;
32167
- for (const piece of passage.pieces) {
32168
- if (piece.kind === "text") yield* piece.tokens;
32169
- }
32170
- }
32171
- function* macroTokens(passage) {
32172
- for (const token of markupTokens(passage)) {
32173
- if (token.type === "macro") yield token;
32174
- }
32175
- }
32176
- var init_tokens = __esm({
32177
- "src/core/markup/tokens.ts"() {
32178
- "use strict";
32179
- }
32180
- });
32181
-
32182
- // src/core/markup/widgets.ts
32183
- function widgetSources(passages, macros) {
32184
- const sources = /* @__PURE__ */ new Map();
32185
- for (const passage of passages) {
32186
- if (!passage.isMarkup) continue;
32187
- const { name, tags } = passage.passage;
32188
- for (const definition2 of widgetDefinitions([{ name, tags, content: passage.content }], macros)) {
32189
- sources.set(definition2, passage);
32190
- }
32151
+ // src/core/workspace/macro-registry.ts
32152
+ function applySupplement(info, entry) {
32153
+ for (const field of ["description", "parameters", "parameterDocs", "children", "parents", "skipArgs"]) {
32154
+ if (entry[field] !== void 0) Object.assign(info, { [field]: entry[field] });
32191
32155
  }
32192
- return sources;
32156
+ if (entry.container !== void 0) info.block = entry.container;
32193
32157
  }
32194
- var init_widgets = __esm({
32195
- "src/core/markup/widgets.ts"() {
32196
- "use strict";
32197
- init_tooling();
32198
- }
32199
- });
32200
-
32201
- // src/core/workspace/widget-registry.ts
32202
- var WidgetRegistry;
32203
- var init_widget_registry = __esm({
32204
- "src/core/workspace/widget-registry.ts"() {
32158
+ var MacroRegistry;
32159
+ var init_macro_registry2 = __esm({
32160
+ "src/core/workspace/macro-registry.ts"() {
32205
32161
  "use strict";
32206
32162
  init_tooling();
32207
- init_tokens();
32208
- init_widgets();
32209
- WidgetRegistry = class {
32210
- /** Widgets keyed by lower-cased name; Spindle resolves widgets case-insensitively. */
32211
- widgets = /* @__PURE__ */ new Map();
32212
- /** Half-typed declarations remain discoverable in the editor, without replacing effective widgets. */
32213
- incomplete = /* @__PURE__ */ new Map();
32214
- /** Per-URI set of lower-cased macro names opened in that document. */
32215
- invokedByUri = /* @__PURE__ */ new Map();
32163
+ init_parameter_validator();
32164
+ init_config_loader();
32165
+ MacroRegistry = class {
32166
+ macros = /* @__PURE__ */ new Map();
32167
+ /** Entries as they were before the current user config was applied (undefined = absent). */
32168
+ configSnapshot = /* @__PURE__ */ new Map();
32169
+ /** Tier 3: macros discovered from Story.defineMacro() calls, keyed by lowercase name. */
32170
+ discovered = /* @__PURE__ */ new Map();
32171
+ /** Called after the set of macros or their metadata changed. */
32172
+ onChange;
32173
+ /** Tier 4: user config entries, kept so they can be re-applied over discovered macros. */
32174
+ configEntries = /* @__PURE__ */ new Map();
32216
32175
  /**
32217
- * Read the widget definitions of `passages` (all of them, in workspace
32218
- * order). Public tooling selects the closed definitions in runtime phase
32219
- * order, preserving their identities so the original source is retained.
32176
+ * Load the built-in macros: name, block, sub-macros, flags and typed
32177
+ * parameters, from Spindle's tooling API (the release the LSP is built on).
32178
+ * This is the base layer the other tiers overlay.
32220
32179
  */
32221
- scan(passages, macros) {
32222
- this.widgets.clear();
32223
- this.incomplete.clear();
32224
- const sources = widgetSources(passages, [...macros]);
32225
- const effective = new Set(effectiveWidgetDefinitions([...sources.keys()]));
32226
- for (const [definition2, markup] of sources) {
32227
- const widget = {
32228
- tooling: definition2,
32229
- name: definition2.name,
32230
- params: definition2.params,
32231
- uri: markup.passage.uri,
32232
- range: markup.range(definition2.start, definition2.end),
32233
- nameRange: markup.range(definition2.nameStart, definition2.nameEnd),
32234
- block: definition2.block
32235
- };
32236
- const key = definition2.name.toLowerCase();
32237
- if (effective.has(definition2)) this.widgets.set(key, widget);
32238
- else if (definition2.closeStart === void 0) this.incomplete.set(key, widget);
32180
+ loadBuiltins() {
32181
+ for (const m of builtinMacros) {
32182
+ this.macros.set(m.name.toLowerCase(), {
32183
+ name: m.name,
32184
+ block: m.block,
32185
+ subMacros: [...m.subMacros],
32186
+ storeVar: m.storeVar,
32187
+ interpolate: m.interpolate,
32188
+ merged: m.merged,
32189
+ source: m.source === "builtin" ? "builtin" : "user",
32190
+ parameterDefs: m.parameters
32191
+ });
32239
32192
  }
32240
32193
  }
32241
- /** The effective definition, or an incomplete declaration while its first definition is being typed. */
32242
- getWidget(name) {
32243
- const key = name.toLowerCase();
32244
- return this.widgets.get(key) ?? this.incomplete.get(key);
32245
- }
32246
- /** Get the effective registered definitions, excluding incomplete editor declarations. */
32247
- getAllWidgets() {
32248
- return Array.from(this.widgets.values());
32194
+ /**
32195
+ * Overlay supplement data onto the registry.
32196
+ * Supplements provide descriptions, parameters, children, parents, skipArgs.
32197
+ * The `container` field maps to `block` on MacroInfo.
32198
+ */
32199
+ loadSupplements(supplements) {
32200
+ this.mergeEntries(supplements);
32201
+ this.onChange?.();
32249
32202
  }
32250
- /** The effective widget whose declared name contains the cursor. */
32251
- definitionAt(uri, position) {
32252
- return this.getAllWidgets().find((widget) => {
32253
- const { start, end } = widget.nameRange;
32254
- return widget.uri === uri && position.line === start.line && position.character >= start.character && position.character <= end.character;
32255
- });
32203
+ /**
32204
+ * Overlay user config onto the registry.
32205
+ * Same format as supplements; applied last so it wins.
32206
+ *
32207
+ * Replaces any previously loaded config: entries from the last call are
32208
+ * reverted first, so macros removed from the config disappear (or fall
32209
+ * back to their builtin/supplement definition).
32210
+ */
32211
+ loadConfig(config2) {
32212
+ config2 = normalizeMacroEntries(config2);
32213
+ for (const [key, previous2] of this.configSnapshot) {
32214
+ if (previous2) {
32215
+ this.macros.set(key, previous2);
32216
+ } else {
32217
+ this.macros.delete(key);
32218
+ }
32219
+ }
32220
+ this.configSnapshot.clear();
32221
+ this.configEntries.clear();
32222
+ for (const rawKey of Object.keys(config2)) {
32223
+ const key = rawKey.toLowerCase();
32224
+ if (!this.configSnapshot.has(key)) {
32225
+ const existing = this.macros.get(key);
32226
+ this.configSnapshot.set(key, existing ? { ...existing } : void 0);
32227
+ }
32228
+ this.configEntries.set(key, { ...this.configEntries.get(key), ...config2[rawKey] });
32229
+ }
32230
+ this.mergeEntries(config2);
32231
+ this.onChange?.();
32256
32232
  }
32257
32233
  /**
32258
- * Record the macros opened in a document (its tags and those in the labels
32259
- * and attribute values that hold markup), replacing any previous record for
32260
- * that URI. Spindle resolves widget names case-insensitively, so names are
32261
- * stored lower-cased.
32234
+ * Replace the set of macros discovered from Story.defineMacro() calls.
32235
+ * Previously discovered definitions that are no longer present are dropped,
32236
+ * restoring whatever the other tiers define for that name.
32237
+ *
32238
+ * Each sub-macro becomes a known macro that may only appear inside its
32239
+ * parent. Whether a macro is a block is the tooling API's reading of the
32240
+ * definition (`discoverMacros`).
32262
32241
  */
32263
- recordInvocations(uri, passages) {
32264
- const names = /* @__PURE__ */ new Set();
32265
- for (const passage of passages) {
32266
- for (const macro of macroTokens(passage)) {
32267
- if (!macro.isClose) names.add(macro.name.toLowerCase());
32242
+ setDiscoveredMacros(macros) {
32243
+ this.discovered.clear();
32244
+ for (const m of macros) {
32245
+ this.discovered.set(m.name.toLowerCase(), {
32246
+ name: m.name,
32247
+ block: m.block,
32248
+ subMacros: m.subMacros,
32249
+ storeVar: m.storeVar,
32250
+ interpolate: m.interpolate,
32251
+ merged: m.merged,
32252
+ description: m.description,
32253
+ source: "user",
32254
+ parameterDefs: m.parameters
32255
+ });
32256
+ }
32257
+ for (const m of [...this.discovered.values()]) {
32258
+ for (const sub of m.subMacros ?? []) {
32259
+ const key = sub.toLowerCase();
32260
+ const existing = this.discovered.get(key);
32261
+ if (existing) {
32262
+ existing.parents = [...existing.parents ?? [], m.name];
32263
+ } else {
32264
+ this.discovered.set(key, { name: sub, block: false, subMacros: [], source: "user", parents: [m.name] });
32265
+ }
32268
32266
  }
32269
32267
  }
32270
- this.invokedByUri.set(uri, names);
32268
+ this.onChange?.();
32271
32269
  }
32272
- /** Forget all recorded invocations. */
32273
- clearInvocations() {
32274
- this.invokedByUri.clear();
32270
+ /** Add or replace a single macro entry. */
32271
+ addMacro(info) {
32272
+ const key = info.name.toLowerCase();
32273
+ const existing = this.macros.get(key);
32274
+ this.macros.set(key, {
32275
+ name: info.name,
32276
+ block: info.block ?? existing?.block ?? false,
32277
+ subMacros: info.subMacros ?? existing?.subMacros ?? [],
32278
+ source: info.source ?? existing?.source ?? "user",
32279
+ storeVar: info.storeVar ?? existing?.storeVar,
32280
+ interpolate: info.interpolate ?? existing?.interpolate,
32281
+ merged: info.merged ?? existing?.merged,
32282
+ description: info.description ?? existing?.description,
32283
+ parameters: info.parameters ?? existing?.parameters,
32284
+ parameterDefs: info.parameterDefs ?? existing?.parameterDefs,
32285
+ parameterDocs: info.parameterDocs ?? existing?.parameterDocs,
32286
+ children: info.children ?? existing?.children,
32287
+ parents: info.parents ?? existing?.parents,
32288
+ skipArgs: info.skipArgs ?? existing?.skipArgs
32289
+ });
32290
+ this.onChange?.();
32275
32291
  }
32276
- /** Whether any recorded document invokes the widget `{name}`. */
32277
- isInvoked(name) {
32278
- const key = name.toLowerCase();
32279
- for (const names of this.invokedByUri.values()) {
32280
- if (names.has(key)) return true;
32292
+ /** Get a macro by name (case-insensitive). */
32293
+ getMacro(name) {
32294
+ return this.resolve(name.toLowerCase());
32295
+ }
32296
+ /** A project's explicit schema takes precedence over runtime signature metadata. */
32297
+ hasConfiguredParameters(name) {
32298
+ return this.configEntries.get(name.toLowerCase())?.parameters !== void 0;
32299
+ }
32300
+ /** Check if a macro is a block (container) macro. */
32301
+ isBlock(name) {
32302
+ return this.getMacro(name)?.block ?? false;
32303
+ }
32304
+ /** Check if a macro is a sub-macro (has parents). */
32305
+ isSubMacro(name) {
32306
+ const info = this.getMacro(name);
32307
+ return info?.parents != null && info.parents.length > 0;
32308
+ }
32309
+ /** The macros as the tooling API reads them (`passagePieces`, `validateStoryMarkup`). */
32310
+ toolingMacros() {
32311
+ return this.getAllMacros().map((m) => ({
32312
+ name: m.name,
32313
+ block: m.block,
32314
+ subMacros: m.subMacros,
32315
+ storeVar: m.storeVar,
32316
+ interpolate: m.interpolate,
32317
+ parameters: m.parameterDefs,
32318
+ widget: this.configuredToolingParameters(m.name)
32319
+ }));
32320
+ }
32321
+ /** Whether an invoked configured schema prevents a safe passage rename. */
32322
+ hasUnsafeConfiguredPassages(name) {
32323
+ const positions = this.configuredPassagePositions(name);
32324
+ if (positions === void 0) return false;
32325
+ if (positions === null) return true;
32326
+ const definitions = this.getMacro(name)?.parameterDefs;
32327
+ if (!definitions?.length) return false;
32328
+ const runtimePositions = definitions.flatMap((parameter, i2) => parameter.type === "passage" || parameter.holds === "passage" ? [i2] : []);
32329
+ if (definitions.some((parameter) => parameter.type === "options" || parameter.type === "names" || parameter.type === "text" || parameter.type === "separator")) {
32330
+ return positions.length > 0 || runtimePositions.length > 0;
32331
+ }
32332
+ return positions.length !== runtimePositions.length || positions.some((position, i2) => position !== runtimePositions[i2]);
32333
+ }
32334
+ configuredPassagePositions(name) {
32335
+ const parameters = this.configEntries.get(name.toLowerCase())?.parameters;
32336
+ if (parameters === void 0) return void 0;
32337
+ try {
32338
+ return new Parameters(parameters).passagePositions();
32339
+ } catch {
32340
+ return null;
32341
+ }
32342
+ }
32343
+ configuredToolingParameters(name) {
32344
+ const positions = this.configuredPassagePositions(name);
32345
+ if (!positions?.length || this.getMacro(name)?.parameterDefs?.length) return void 0;
32346
+ const params = Array.from({ length: positions.at(-1) + 1 }, (_2, i2) => `arg${i2}`);
32347
+ return { params, holds: Object.fromEntries(positions.map((i2) => [params[i2], "passage"])) };
32348
+ }
32349
+ /** Get all registered macros. */
32350
+ getAllMacros() {
32351
+ const keys = /* @__PURE__ */ new Set([...this.macros.keys(), ...this.discovered.keys()]);
32352
+ return Array.from(keys, (key) => this.resolve(key));
32353
+ }
32354
+ /**
32355
+ * Compose the effective entry for a lowercase key: builtins/supplements/
32356
+ * config (stored in `macros`), overlaid by discovered metadata, with user
32357
+ * config re-applied on top so it keeps precedence.
32358
+ */
32359
+ resolve(key) {
32360
+ const base = this.macros.get(key);
32361
+ const found = this.discovered.get(key);
32362
+ if (!found) return base;
32363
+ const info = {
32364
+ name: found.name,
32365
+ block: found.block ?? false,
32366
+ subMacros: found.subMacros ?? [],
32367
+ source: "user",
32368
+ storeVar: found.storeVar,
32369
+ interpolate: found.interpolate,
32370
+ merged: found.merged,
32371
+ description: found.description,
32372
+ // A definition replaces the macro it names, parameters included
32373
+ parameterDefs: found.parameterDefs,
32374
+ parents: found.parents
32375
+ };
32376
+ const config2 = this.configEntries.get(key);
32377
+ if (config2) applySupplement(info, config2);
32378
+ return info;
32379
+ }
32380
+ /** Merge a set of supplement/config entries into the registry. */
32381
+ mergeEntries(entries) {
32382
+ for (const [rawKey, entry] of Object.entries(entries)) {
32383
+ const key = rawKey.toLowerCase();
32384
+ const existing = this.macros.get(key);
32385
+ if (existing) {
32386
+ applySupplement(existing, entry);
32387
+ } else {
32388
+ this.macros.set(key, {
32389
+ name: entry.name ?? rawKey,
32390
+ block: entry.container ?? false,
32391
+ subMacros: [],
32392
+ source: "user",
32393
+ description: entry.description,
32394
+ parameters: entry.parameters,
32395
+ parameterDocs: entry.parameterDocs,
32396
+ children: entry.children,
32397
+ parents: entry.parents,
32398
+ skipArgs: entry.skipArgs
32399
+ });
32400
+ }
32281
32401
  }
32282
- return false;
32283
32402
  }
32284
32403
  };
32285
32404
  }
32286
32405
  });
32287
32406
 
32288
- // src/core/markup/markup-index.ts
32289
- function sameHeaders(a, b) {
32290
- return a.length === b.length && a.every((p, i2) => p === b[i2]);
32407
+ // src/core/parsing/executable-refs.ts
32408
+ function collectVariableReferences2(markup) {
32409
+ if (!markup.isMarkup) return [];
32410
+ return variableReferences(markup.content, markup.doc.context.macros, { all: true }).map((ref2) => ({
32411
+ sigil: ref2.sigil,
32412
+ name: ref2.name,
32413
+ path: [ref2.name, ...ref2.path].join("."),
32414
+ start: ref2.start,
32415
+ end: ref2.end
32416
+ }));
32291
32417
  }
32292
- var MarkupIndex;
32293
- var init_markup_index = __esm({
32294
- "src/core/markup/markup-index.ts"() {
32418
+ var init_executable_refs = __esm({
32419
+ "src/core/parsing/executable-refs.ts"() {
32295
32420
  "use strict";
32296
- init_passage_markup();
32297
- MarkupIndex = class {
32298
- constructor(sources) {
32299
- this.sources = sources;
32300
- }
32301
- sources;
32302
- cache = /* @__PURE__ */ new Map();
32303
- context;
32304
- /** The markup of document `uri`, or undefined if it is not open. */
32305
- get(uri) {
32306
- const text3 = this.sources.text(uri);
32307
- if (text3 === void 0) return void 0;
32308
- const kept3 = this.cache.get(uri);
32309
- const passages = this.sources.passages(uri);
32310
- if (kept3 && kept3.text === text3 && sameHeaders(kept3.passages.map((p) => p.passage), passages)) return kept3;
32311
- this.context ??= this.sources.context();
32312
- const fresh = new DocumentMarkup(uri, text3, passages, this.context);
32313
- this.cache.set(uri, fresh);
32314
- return fresh;
32315
- }
32316
- /** The macros or widgets changed: every document's markup is read again on demand. */
32317
- invalidate() {
32318
- this.cache.clear();
32319
- this.context = void 0;
32320
- }
32321
- };
32421
+ init_tooling();
32322
32422
  }
32323
32423
  });
32324
32424
 
32325
- // src/core/workspace/spindle-version.ts
32326
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
32327
- import { dirname as dirname2, join as join3, resolve as resolve4 } from "node:path";
32328
- import { fileURLToPath } from "node:url";
32329
- function parseSpindleVersion(text3) {
32330
- const m = /^\s*v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?\s*$/.exec(text3);
32331
- return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : void 0;
32425
+ // src/core/markup/passage-markup.ts
32426
+ var PassageMarkup, DocumentMarkup;
32427
+ var init_passage_markup = __esm({
32428
+ "src/core/markup/passage-markup.ts"() {
32429
+ "use strict";
32430
+ init_tooling();
32431
+ init_passage_parser();
32432
+ init_text();
32433
+ PassageMarkup = class {
32434
+ constructor(doc, passage, bodyStart, bodyEnd) {
32435
+ this.doc = doc;
32436
+ this.passage = passage;
32437
+ this.bodyStart = bodyStart;
32438
+ this.bodyEnd = bodyEnd;
32439
+ const raw = doc.text.slice(bodyStart, bodyEnd);
32440
+ this.isMarkup = isMarkupPassage(passage);
32441
+ if (raw.includes("\r\n")) {
32442
+ const map2 = [];
32443
+ let content2 = "";
32444
+ for (let i2 = 0; i2 < raw.length; i2++) {
32445
+ if (raw[i2] === "\r" && raw[i2 + 1] === "\n") continue;
32446
+ map2.push(bodyStart + i2);
32447
+ content2 += raw[i2];
32448
+ }
32449
+ map2.push(bodyEnd);
32450
+ this.content = content2;
32451
+ this.toDoc = Int32Array.from(map2);
32452
+ } else {
32453
+ this.content = raw;
32454
+ this.toDoc = null;
32455
+ }
32456
+ }
32457
+ doc;
32458
+ passage;
32459
+ bodyStart;
32460
+ bodyEnd;
32461
+ /** The body, LF-normalized: the text the tooling API analyzes. */
32462
+ content;
32463
+ /** Whether Spindle reads the body as story markup (see isMarkupPassage). */
32464
+ isMarkup;
32465
+ /** For each `content` offset, its offset in the document; null when the body has no CRLF. */
32466
+ toDoc;
32467
+ cachedTokens;
32468
+ cachedPairing;
32469
+ cachedPieces;
32470
+ cachedDeclarations;
32471
+ /** The document offset of `contentOffset`. */
32472
+ docOffset(contentOffset) {
32473
+ if (!this.toDoc) return this.bodyStart + contentOffset;
32474
+ return this.toDoc[Math.min(Math.max(contentOffset, 0), this.toDoc.length - 1)];
32475
+ }
32476
+ /**
32477
+ * The document offset just past the character before `contentEnd`: where an
32478
+ * exclusive end maps to. (`docOffset()` of an end that sits before a line
32479
+ * break would land behind the `\r` of a CRLF.)
32480
+ */
32481
+ docEnd(contentEnd) {
32482
+ return contentEnd <= 0 ? this.docOffset(0) : this.docOffset(contentEnd - 1) + 1;
32483
+ }
32484
+ /** The `content` offset of a document offset (the nearest one at or after it). */
32485
+ contentOffset(docOffset) {
32486
+ if (!this.toDoc) return Math.min(Math.max(docOffset - this.bodyStart, 0), this.content.length);
32487
+ let low = 0;
32488
+ let high = this.toDoc.length - 1;
32489
+ while (low < high) {
32490
+ const mid = low + high >> 1;
32491
+ if (this.toDoc[mid] < docOffset) low = mid + 1;
32492
+ else high = mid;
32493
+ }
32494
+ return low;
32495
+ }
32496
+ /** The document position of `contentOffset`. */
32497
+ position(contentOffset) {
32498
+ return offsetToPosition(this.docOffset(contentOffset), this.doc.lineStarts);
32499
+ }
32500
+ /** The document range of `[start, end)` in `content` offsets. */
32501
+ range(start, end) {
32502
+ return offsetsToRange(this.docOffset(start), end > start ? this.docEnd(end) : this.docOffset(start), this.doc.lineStarts);
32503
+ }
32504
+ /** The flat tokens and the malformed tags; none for a passage that is not markup. */
32505
+ get tokenization() {
32506
+ return this.cachedTokens ??= this.isMarkup ? tokenizeMarkupTolerant(this.content) : { tokens: [], errors: [] };
32507
+ }
32508
+ get tokens() {
32509
+ return this.tokenization.tokens;
32510
+ }
32511
+ /** The tree of paired tokens, and what does not pair. */
32512
+ get pairing() {
32513
+ return this.cachedPairing ??= pairMarkup(this.tokens, {
32514
+ isBlock: (name) => this.doc.context.isBlock(name),
32515
+ source: this.content
32516
+ });
32517
+ }
32518
+ /** What the markup runs and names, and where (see `passagePieces`). */
32519
+ get pieces() {
32520
+ return this.cachedPieces ??= this.isMarkup ? passagePieces(this.content, this.doc.context.macros) : [];
32521
+ }
32522
+ /** The declarations of a StoryVariables / StoryTransients passage. */
32523
+ get declarations() {
32524
+ return this.cachedDeclarations ??= parseDeclarations(this.content, this.passage.name === "StoryTransients" ? "%" : "$");
32525
+ }
32526
+ };
32527
+ DocumentMarkup = class {
32528
+ constructor(uri, text3, passages, context) {
32529
+ this.uri = uri;
32530
+ this.text = text3;
32531
+ this.context = context;
32532
+ this.lineStarts = buildLineStarts(text3);
32533
+ this.passages = passages.map((passage, i2) => {
32534
+ const bodyStart = this.lineStarts[passage.headerEnd.end.line + 1] ?? text3.length;
32535
+ const next = passages[i2 + 1];
32536
+ const bodyEnd = next ? this.lineStarts[next.range.start.line] : text3.length;
32537
+ return new PassageMarkup(this, passage, Math.min(bodyStart, bodyEnd), bodyEnd);
32538
+ });
32539
+ }
32540
+ uri;
32541
+ text;
32542
+ context;
32543
+ lineStarts;
32544
+ passages;
32545
+ /** The passage that contains `position`. */
32546
+ passageAt(position) {
32547
+ for (let i2 = this.passages.length - 1; i2 >= 0; i2--) {
32548
+ if (this.passages[i2].passage.range.start.line <= position.line) return this.passages[i2];
32549
+ }
32550
+ return void 0;
32551
+ }
32552
+ };
32553
+ }
32554
+ });
32555
+
32556
+ // src/core/workspace/declaration-check.ts
32557
+ function readDeclarations(content2, sigil) {
32558
+ const passage = PASSAGE[sigil];
32559
+ const lines = content2.replace(/\r\n/g, "\n").split("\n");
32560
+ const header = lines.findIndex((line) => /^?::/.test(line));
32561
+ const text3 = (header === -1 ? lines : lines.slice(0, header)).join("\n");
32562
+ const { declarations, errors } = parseDeclarations(text3.replace(/\r/g, " "), sigil);
32563
+ const problems = [];
32564
+ for (const error48 of errors) {
32565
+ if (error48.code === "duplicate-declaration") continue;
32566
+ const [start, end] = lineSpan(text3, error48.offset);
32567
+ if (isUnjudged(text3.slice(start, end))) continue;
32568
+ let message = `${passage}: ${error48.message}`;
32569
+ if (error48.code === "invalid-declaration" && COMMENT_START.test(text3.slice(error48.offset, error48.end))) {
32570
+ message += `. ${passage} has no comment syntax.`;
32571
+ } else if (!message.endsWith(".")) {
32572
+ message += ".";
32573
+ }
32574
+ problems.push({ message, start, end });
32575
+ }
32576
+ return { text: text3, declarations, problems: problems.sort((a, b) => a.start - b.start) };
32577
+ }
32578
+ function isUnjudged(line) {
32579
+ return line.includes("\r") || line.includes("\xA0");
32580
+ }
32581
+ function lineSpan(text3, offset2) {
32582
+ const lineStart = text3.lastIndexOf("\n", offset2 - 1) + 1;
32583
+ const lineEnd = text3.indexOf("\n", offset2);
32584
+ const line = text3.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
32585
+ const start = lineStart + line.length - line.trimStart().length;
32586
+ return [start, lineStart + line.trimEnd().length];
32587
+ }
32588
+ var PASSAGE, COMMENT_START;
32589
+ var init_declaration_check = __esm({
32590
+ "src/core/workspace/declaration-check.ts"() {
32591
+ "use strict";
32592
+ init_tooling();
32593
+ PASSAGE = { "$": "StoryVariables", "%": "StoryTransients" };
32594
+ COMMENT_START = /^(?:\/\/|\/\*|<!--|#)/;
32595
+ }
32596
+ });
32597
+
32598
+ // src/core/workspace/variable-tracker.ts
32599
+ function standaloneMarkup(uri, text3) {
32600
+ const index = new PassageIndex();
32601
+ index.rebuild(uri, text3);
32602
+ return new DocumentMarkup(uri, text3, index.getPassagesInDocument(uri), BUILTIN_CONTEXT);
32603
+ }
32604
+ function usagesOf(byUri, name) {
32605
+ const results = [];
32606
+ for (const usages of byUri.values()) {
32607
+ for (const u of usages) {
32608
+ if (u.baseName === name) results.push({ uri: u.uri, range: u.range });
32609
+ }
32610
+ }
32611
+ return results;
32612
+ }
32613
+ var BUILTIN_CONTEXT, ARRAY_MEMBERS, DeclarationSet, VariableTracker;
32614
+ var init_variable_tracker = __esm({
32615
+ "src/core/workspace/variable-tracker.ts"() {
32616
+ "use strict";
32617
+ init_tooling();
32618
+ init_executable_refs();
32619
+ init_passage_markup();
32620
+ init_text();
32621
+ init_passage_index();
32622
+ init_declaration_check();
32623
+ BUILTIN_CONTEXT = { macros: builtinMacros, isBlock: isBlockMacro };
32624
+ ARRAY_MEMBERS = /* @__PURE__ */ new Set([
32625
+ ...Object.getOwnPropertyNames(Array.prototype),
32626
+ ...Object.getOwnPropertyNames(Object.prototype),
32627
+ "at",
32628
+ "flat",
32629
+ "flatMap",
32630
+ "includes",
32631
+ "findLast",
32632
+ "findLastIndex",
32633
+ "toReversed",
32634
+ "toSorted",
32635
+ "toSpliced",
32636
+ "with"
32637
+ ]);
32638
+ DeclarationSet = class {
32639
+ constructor(sigil) {
32640
+ this.sigil = sigil;
32641
+ }
32642
+ sigil;
32643
+ present = false;
32644
+ declared = /* @__PURE__ */ new Map();
32645
+ /** Every declaration, including earlier defaults for the same binding. */
32646
+ occurrences = [];
32647
+ /** Lines Spindle rejects. */
32648
+ problems = [];
32649
+ /** The names declared, with the range of sigil and name of the first. */
32650
+ names = /* @__PURE__ */ new Map();
32651
+ clear() {
32652
+ this.present = false;
32653
+ this.declared.clear();
32654
+ this.occurrences.length = 0;
32655
+ this.problems = [];
32656
+ this.names.clear();
32657
+ }
32658
+ /** Read the passage `content`, which starts at line `contentStartLine` of the document `uri`. */
32659
+ read(content2, contentStartLine, uri) {
32660
+ this.clear();
32661
+ this.present = true;
32662
+ const { text: text3, declarations, problems } = readDeclarations(content2, this.sigil);
32663
+ const lineStarts = buildLineStarts(text3);
32664
+ const position = (offset2) => {
32665
+ const { line, character } = offsetToPosition(offset2, lineStarts);
32666
+ return { line: contentStartLine + line, character };
32667
+ };
32668
+ for (const problem of problems) {
32669
+ this.problems.push({ message: problem.message, range: { start: position(problem.start), end: position(problem.end) } });
32670
+ }
32671
+ for (const declaration of declarations) {
32672
+ const { name, schema } = declaration;
32673
+ const declarationRange = { start: position(declaration.nameStart - 1), end: position(declaration.nameEnd) };
32674
+ const declared = { name, sigil: this.sigil, declarationRange };
32675
+ if (uri !== void 0) declared.declarationUri = uri;
32676
+ if (schema) {
32677
+ if (schema.type !== "null") declared.type = schema.type;
32678
+ if (schema.fields && schema.fields.size > 0) declared.fields = [...schema.fields.keys()];
32679
+ }
32680
+ this.declared.set(name, declared);
32681
+ this.occurrences.push(declared);
32682
+ if (!this.names.has(name)) this.names.set(name, declarationRange);
32683
+ }
32684
+ }
32685
+ };
32686
+ VariableTracker = class {
32687
+ variables = new DeclarationSet("$");
32688
+ transients = new DeclarationSet("%");
32689
+ /** Per-URI list of variable usages. */
32690
+ usagesByUri = /* @__PURE__ */ new Map();
32691
+ /** Per-URI list of transient variable usages. */
32692
+ transientUsagesByUri = /* @__PURE__ */ new Map();
32693
+ /**
32694
+ * Parse the StoryVariables passage content for declarations.
32695
+ * Each line like `$name = value` becomes a declaration.
32696
+ */
32697
+ parseStoryVariables(content2, contentStartLine = 0, uri) {
32698
+ this.variables.read(content2, contentStartLine, uri);
32699
+ }
32700
+ /** Forget StoryVariables declarations, e.g. after the passage was removed. */
32701
+ clearStoryVariables() {
32702
+ this.variables.clear();
32703
+ }
32704
+ /** Forget StoryTransients declarations, e.g. after the passage was removed. */
32705
+ clearStoryTransients() {
32706
+ this.transients.clear();
32707
+ }
32708
+ /**
32709
+ * Parse the StoryTransients passage content for declarations.
32710
+ * Each line like `%name = value` becomes a declaration.
32711
+ */
32712
+ parseStoryTransients(content2, contentStartLine = 0, uri) {
32713
+ this.transients.read(content2, contentStartLine, uri);
32714
+ }
32715
+ /**
32716
+ * Scan a document for variable usages: the `$` and `%` references in the
32717
+ * markup of its passages (see `collectVariableReferences`). The workspace
32718
+ * passes the `markup` it reads the document with, which knows the project's
32719
+ * macros and widgets; without it the built-in macros decide.
32720
+ */
32721
+ scanDocument(uri, text3, markup = standaloneMarkup(uri, text3)) {
32722
+ this.removeDocument(uri);
32723
+ const usages = [];
32724
+ const transientUsages = [];
32725
+ for (const passage of markup.passages) {
32726
+ for (const ref2 of collectVariableReferences2(passage)) {
32727
+ const usage = {
32728
+ uri,
32729
+ baseName: ref2.name,
32730
+ fullName: ref2.path,
32731
+ range: passage.range(ref2.start, ref2.end)
32732
+ };
32733
+ (ref2.sigil === "$" ? usages : transientUsages).push(usage);
32734
+ }
32735
+ }
32736
+ if (usages.length > 0) this.usagesByUri.set(uri, usages);
32737
+ if (transientUsages.length > 0) this.transientUsagesByUri.set(uri, transientUsages);
32738
+ }
32739
+ /** Forget the usages recorded for a document, e.g. after it was deleted. */
32740
+ removeDocument(uri) {
32741
+ this.usagesByUri.delete(uri);
32742
+ this.transientUsagesByUri.delete(uri);
32743
+ }
32744
+ /** Get all declared variables. */
32745
+ getDeclared() {
32746
+ return this.variables.declared;
32747
+ }
32748
+ /** Get all usages of a variable by base name. */
32749
+ getUsages(name) {
32750
+ return usagesOf(this.usagesByUri, name);
32751
+ }
32752
+ /** Every declaration location of a story or transient binding, in source order. */
32753
+ getDeclarations(name, sigil) {
32754
+ const declarations = sigil === "$" ? this.variables : this.transients;
32755
+ return declarations.occurrences.flatMap((d) => d.name === name && d.declarationUri !== void 0 && d.declarationRange ? [{ uri: d.declarationUri, range: d.declarationRange }] : []);
32756
+ }
32757
+ /** The executable usage or declaration containing the cursor, in source coordinates. */
32758
+ referenceAt(uri, position) {
32759
+ const contains = (range2) => (position.line > range2.start.line || position.line === range2.start.line && position.character >= range2.start.character) && (position.line < range2.end.line || position.line === range2.end.line && position.character <= range2.end.character);
32760
+ let atEnd = null;
32761
+ for (const [sigil, usages, declarations] of [
32762
+ ["$", this.usagesByUri.get(uri), this.variables.occurrences],
32763
+ ["%", this.transientUsagesByUri.get(uri), this.transients.occurrences]
32764
+ ]) {
32765
+ const candidates = (usages ?? []).map((u) => ({ name: u.baseName, range: u.range }));
32766
+ for (const declaration of declarations) {
32767
+ if (declaration.declarationUri === uri && declaration.declarationRange) candidates.push({ name: declaration.name, range: declaration.declarationRange });
32768
+ }
32769
+ for (const candidate of candidates) {
32770
+ if (!contains(candidate.range)) continue;
32771
+ const symbol2 = { sigil, ...candidate };
32772
+ if (position.line !== candidate.range.end.line || position.character < candidate.range.end.character) return symbol2;
32773
+ atEnd ??= symbol2;
32774
+ }
32775
+ }
32776
+ return atEnd;
32777
+ }
32778
+ /** Whether a StoryVariables passage has been parsed. */
32779
+ hasStoryVariables() {
32780
+ return this.variables.present;
32781
+ }
32782
+ /** Get all declared transient variables. */
32783
+ getDeclaredTransient() {
32784
+ return this.transients.declared;
32785
+ }
32786
+ /** Get all usages of a transient variable by base name. */
32787
+ getTransientUsages(name) {
32788
+ return usagesOf(this.transientUsagesByUri, name);
32789
+ }
32790
+ /** Whether a StoryTransients passage has been parsed. */
32791
+ hasStoryTransients() {
32792
+ return this.transients.present;
32793
+ }
32794
+ /**
32795
+ * Get `$var.member` / `%var.member` accesses in a document where `var` is
32796
+ * declared with an array literal default and `member` is not a property
32797
+ * that JavaScript arrays have (so the access always yields undefined).
32798
+ */
32799
+ getArrayMemberAccesses(uri) {
32800
+ const results = [];
32801
+ const sources = [
32802
+ ["$", this.usagesByUri.get(uri), this.variables.declared],
32803
+ ["%", this.transientUsagesByUri.get(uri), this.transients.declared]
32804
+ ];
32805
+ for (const [sigil, usages, declared] of sources) {
32806
+ for (const u of usages ?? []) {
32807
+ const member = u.fullName.split(".")[1];
32808
+ if (member === void 0) continue;
32809
+ if (declared.get(u.baseName)?.type !== "array") continue;
32810
+ if (ARRAY_MEMBERS.has(member) || /^(0|[1-9]\d*)$/.test(member) && Number(member) < 4294967295) continue;
32811
+ results.push({ sigil, name: u.baseName, member, range: u.range });
32812
+ }
32813
+ }
32814
+ return results;
32815
+ }
32816
+ /** StoryVariables lines that stop Spindle from starting. */
32817
+ getInvalidDeclarations() {
32818
+ return this.variables.problems;
32819
+ }
32820
+ /**
32821
+ * StoryTransients lines that stop Spindle from starting, and the names it
32822
+ * declares that StoryVariables declares too.
32823
+ */
32824
+ getInvalidTransientDeclarations() {
32825
+ const collisions = [];
32826
+ if (this.variables.present) {
32827
+ for (const [name, range2] of this.transients.names) {
32828
+ if (!this.variables.names.has(name)) continue;
32829
+ collisions.push({
32830
+ message: `StoryTransients: Variable "${name}" is already declared in StoryVariables. Names must be unique across scopes.`,
32831
+ range: range2
32832
+ });
32833
+ }
32834
+ }
32835
+ return [...this.transients.problems, ...collisions].sort((a, b) => a.range.start.line - b.range.start.line);
32836
+ }
32837
+ };
32838
+ }
32839
+ });
32840
+
32841
+ // src/core/markup/tokens.ts
32842
+ function* markupTokens(passage) {
32843
+ yield* passage.tokens;
32844
+ for (const piece of passage.pieces) {
32845
+ if (piece.kind === "text") yield* piece.tokens;
32846
+ }
32847
+ }
32848
+ function* macroTokens(passage) {
32849
+ for (const token of markupTokens(passage)) {
32850
+ if (token.type === "macro") yield token;
32851
+ }
32852
+ }
32853
+ var init_tokens = __esm({
32854
+ "src/core/markup/tokens.ts"() {
32855
+ "use strict";
32856
+ }
32857
+ });
32858
+
32859
+ // src/core/markup/widgets.ts
32860
+ function widgetSources(passages, macros) {
32861
+ const sources = /* @__PURE__ */ new Map();
32862
+ for (const passage of passages) {
32863
+ if (!passage.isMarkup) continue;
32864
+ const { name, tags } = passage.passage;
32865
+ for (const definition2 of widgetDefinitions([{ name, tags, content: passage.content }], macros)) {
32866
+ sources.set(definition2, passage);
32867
+ }
32868
+ }
32869
+ return sources;
32870
+ }
32871
+ var init_widgets = __esm({
32872
+ "src/core/markup/widgets.ts"() {
32873
+ "use strict";
32874
+ init_tooling();
32875
+ }
32876
+ });
32877
+
32878
+ // src/core/workspace/widget-registry.ts
32879
+ var WidgetRegistry;
32880
+ var init_widget_registry = __esm({
32881
+ "src/core/workspace/widget-registry.ts"() {
32882
+ "use strict";
32883
+ init_tooling();
32884
+ init_tokens();
32885
+ init_widgets();
32886
+ WidgetRegistry = class {
32887
+ /** Widgets keyed by lower-cased name; Spindle resolves widgets case-insensitively. */
32888
+ widgets = /* @__PURE__ */ new Map();
32889
+ /** Half-typed declarations remain discoverable in the editor, without replacing effective widgets. */
32890
+ incomplete = /* @__PURE__ */ new Map();
32891
+ /** Per-URI set of lower-cased macro names opened in that document. */
32892
+ invokedByUri = /* @__PURE__ */ new Map();
32893
+ /**
32894
+ * Read the widget definitions of `passages` (all of them, in workspace
32895
+ * order). Public tooling selects the closed definitions in runtime phase
32896
+ * order, preserving their identities so the original source is retained.
32897
+ */
32898
+ scan(passages, macros) {
32899
+ this.widgets.clear();
32900
+ this.incomplete.clear();
32901
+ const sources = widgetSources(passages, [...macros]);
32902
+ const effective = new Set(effectiveWidgetDefinitions([...sources.keys()]));
32903
+ for (const [definition2, markup] of sources) {
32904
+ const widget = {
32905
+ tooling: definition2,
32906
+ name: definition2.name,
32907
+ params: definition2.params,
32908
+ uri: markup.passage.uri,
32909
+ range: markup.range(definition2.start, definition2.end),
32910
+ nameRange: markup.range(definition2.nameStart, definition2.nameEnd),
32911
+ block: definition2.block
32912
+ };
32913
+ const key = definition2.name.toLowerCase();
32914
+ if (effective.has(definition2)) this.widgets.set(key, widget);
32915
+ else if (definition2.closeStart === void 0) this.incomplete.set(key, widget);
32916
+ }
32917
+ }
32918
+ /** The effective definition, or an incomplete declaration while its first definition is being typed. */
32919
+ getWidget(name) {
32920
+ const key = name.toLowerCase();
32921
+ return this.widgets.get(key) ?? this.incomplete.get(key);
32922
+ }
32923
+ /** Get the effective registered definitions, excluding incomplete editor declarations. */
32924
+ getAllWidgets() {
32925
+ return Array.from(this.widgets.values());
32926
+ }
32927
+ /** The effective widget whose declared name contains the cursor. */
32928
+ definitionAt(uri, position) {
32929
+ return this.getAllWidgets().find((widget) => {
32930
+ const { start, end } = widget.nameRange;
32931
+ return widget.uri === uri && position.line === start.line && position.character >= start.character && position.character <= end.character;
32932
+ });
32933
+ }
32934
+ /**
32935
+ * Record the macros opened in a document (its tags and those in the labels
32936
+ * and attribute values that hold markup), replacing any previous record for
32937
+ * that URI. Spindle resolves widget names case-insensitively, so names are
32938
+ * stored lower-cased.
32939
+ */
32940
+ recordInvocations(uri, passages) {
32941
+ const names = /* @__PURE__ */ new Set();
32942
+ for (const passage of passages) {
32943
+ for (const macro of macroTokens(passage)) {
32944
+ if (!macro.isClose) names.add(macro.name.toLowerCase());
32945
+ }
32946
+ }
32947
+ this.invokedByUri.set(uri, names);
32948
+ }
32949
+ /** Forget all recorded invocations. */
32950
+ clearInvocations() {
32951
+ this.invokedByUri.clear();
32952
+ }
32953
+ /** Whether any recorded document invokes the widget `{name}`. */
32954
+ isInvoked(name) {
32955
+ const key = name.toLowerCase();
32956
+ for (const names of this.invokedByUri.values()) {
32957
+ if (names.has(key)) return true;
32958
+ }
32959
+ return false;
32960
+ }
32961
+ };
32962
+ }
32963
+ });
32964
+
32965
+ // src/core/markup/markup-index.ts
32966
+ function sameHeaders(a, b) {
32967
+ return a.length === b.length && a.every((p, i2) => p === b[i2]);
32968
+ }
32969
+ var MarkupIndex;
32970
+ var init_markup_index = __esm({
32971
+ "src/core/markup/markup-index.ts"() {
32972
+ "use strict";
32973
+ init_passage_markup();
32974
+ MarkupIndex = class {
32975
+ constructor(sources) {
32976
+ this.sources = sources;
32977
+ }
32978
+ sources;
32979
+ cache = /* @__PURE__ */ new Map();
32980
+ context;
32981
+ /** The markup of document `uri`, or undefined if it is not open. */
32982
+ get(uri) {
32983
+ const text3 = this.sources.text(uri);
32984
+ if (text3 === void 0) return void 0;
32985
+ const kept3 = this.cache.get(uri);
32986
+ const passages = this.sources.passages(uri);
32987
+ if (kept3 && kept3.text === text3 && sameHeaders(kept3.passages.map((p) => p.passage), passages)) return kept3;
32988
+ this.context ??= this.sources.context();
32989
+ const fresh = new DocumentMarkup(uri, text3, passages, this.context);
32990
+ this.cache.set(uri, fresh);
32991
+ return fresh;
32992
+ }
32993
+ /** The macros or widgets changed: every document's markup is read again on demand. */
32994
+ invalidate() {
32995
+ this.cache.clear();
32996
+ this.context = void 0;
32997
+ }
32998
+ };
32999
+ }
33000
+ });
33001
+
33002
+ // src/core/workspace/spindle-version.ts
33003
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
33004
+ import { dirname as dirname2, join as join3, resolve as resolve4 } from "node:path";
33005
+ import { fileURLToPath } from "node:url";
33006
+ function parseSpindleVersion(text3) {
33007
+ const m = /^\s*v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?\s*$/.exec(text3);
33008
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : void 0;
32332
33009
  }
32333
33010
  function atLeast(version2, minimum) {
32334
33011
  const min = parseSpindleVersion(minimum);
@@ -32924,1065 +33601,470 @@ var init_macro_supplements = __esm({
32924
33601
  { name: "variable", documentation: "Variable to watch for changes." }
32925
33602
  ]
32926
33603
  },
32927
- unwatch: {
32928
- name: "unwatch",
32929
- description: "Stops watching a variable.\n\nUsage: `{unwatch $var}`",
32930
- parameters: ["...text"],
32931
- parameterDocs: [
32932
- { name: "variable", documentation: "Variable to stop watching." }
32933
- ]
32934
- },
32935
- unless: {
32936
- name: "unless",
32937
- container: true,
32938
- description: "Conditionally displays its contents when the expression is falsy. The inverse of `{if}`.\n\nUsage:\n\n```\n{unless condition}\n ...\n{/unless}\n```",
32939
- parameters: ["...text"],
32940
- parameterDocs: [
32941
- { name: "condition", documentation: "Expression; the contents show when it is falsy." }
32942
- ]
32943
- }
32944
- };
32945
- }
32946
- });
32947
-
32948
- // src/core/workspace/workspace-model.ts
32949
- import { EventEmitter as EventEmitter2 } from "node:events";
32950
- function storyInitMacros(content2) {
32951
- return passagePieces(content2, builtinMacros).flatMap((piece) => piece.kind === "code" && piece.goal === "statements" ? discoverMacros(piece.code) : []);
32952
- }
32953
- var WorkspaceModel;
32954
- var init_workspace_model = __esm({
32955
- "src/core/workspace/workspace-model.ts"() {
32956
- "use strict";
32957
- init_tooling();
32958
- init_document_store();
32959
- init_passage_index();
32960
- init_macro_registry2();
32961
- init_variable_tracker();
32962
- init_widget_registry();
32963
- init_markup_index();
32964
- init_macro_sources();
32965
- init_story_format();
32966
- init_spindle_version();
32967
- init_macro_supplements();
32968
- WorkspaceModel = class extends EventEmitter2 {
32969
- documents;
32970
- passages;
32971
- _macros;
32972
- get macros() {
32973
- this.ensureCurrent();
32974
- return this._macros;
32975
- }
32976
- _variables;
32977
- get variables() {
32978
- this.ensureCurrent();
32979
- return this._variables;
32980
- }
32981
- _widgets;
32982
- get widgets() {
32983
- this.ensureCurrent();
32984
- return this._widgets;
32985
- }
32986
- /** Every document's markup, read through Spindle's tooling API. */
32987
- _markup;
32988
- get markup() {
32989
- this.ensureCurrent();
32990
- return this._markup;
32991
- }
32992
- /** True after initialize() has completed (full workspace scan done). */
32993
- initialized = false;
32994
- /** The story format declared by StoryData, updated with the passages. */
32995
- format = UNDECLARED_STORY_FORMAT;
32996
- /**
32997
- * The Spindle the project targets: the version installed under the
32998
- * workspace root, else StoryData's `format-version`. Only a version below
32999
- * {@link MINIMUM_SPINDLE_VERSION} matters (it is reported, not emulated).
33000
- */
33001
- _target;
33002
- get target() {
33003
- this.ensureCurrent();
33004
- return this._target;
33005
- }
33006
- workspaceRoot;
33007
- /** The version installed under the workspace root, read at startup and on refresh(). */
33008
- installedVersion;
33009
- /** The `format-version` StoryData declares. */
33010
- declaredVersion;
33011
- analysisPending = false;
33012
- debounceTimer = null;
33013
- DEBOUNCE_MS = 200;
33014
- /** Bound listeners for cleanup in dispose(). */
33015
- onDocumentChanged;
33016
- onDocumentOpened;
33017
- onDocumentClosed;
33018
- constructor(config2) {
33019
- super();
33020
- this.documents = new DocumentStore();
33021
- this.passages = new PassageIndex();
33022
- this._macros = new MacroRegistry();
33023
- this.macros.onChange = () => this.markup?.invalidate();
33024
- this._variables = new VariableTracker();
33025
- this._widgets = new WidgetRegistry();
33026
- this._markup = new MarkupIndex({
33027
- text: (uri) => this.documents.getText(uri),
33028
- passages: (uri) => this.passages.getPassagesInDocument(uri),
33029
- context: () => ({
33030
- macros: this.toolingMacros(),
33031
- isBlock: (name) => this.isContainer(name)
33032
- })
33033
- });
33034
- this.workspaceRoot = config2?.workspaceRoot;
33035
- this.installedVersion = this.workspaceRoot ? readInstalledSpindleVersion(this.workspaceRoot) : void 0;
33036
- this._target = resolveSpindleTarget(this.installedVersion);
33037
- this.macros.loadBuiltins();
33038
- this.macros.loadSupplements(macro_supplements_default);
33039
- this.onDocumentChanged = (uri) => this.handleDocumentChange(uri);
33040
- this.onDocumentOpened = (uri) => this.handleDocumentChange(uri);
33041
- this.onDocumentClosed = (uri) => this.handleDocumentClose(uri);
33042
- this.documents.on("documentChanged", this.onDocumentChanged);
33043
- this.documents.on("documentOpened", this.onDocumentOpened);
33044
- this.documents.on("documentClosed", this.onDocumentClosed);
33045
- }
33046
- /**
33047
- * Initialize the workspace in CLI/batch mode.
33048
- * Loads all file contents, builds indices, and emits 'modelReady'.
33049
- */
33050
- initialize(fileContents) {
33051
- this.documents.removeListener("documentOpened", this.onDocumentOpened);
33052
- this.documents.removeListener("documentChanged", this.onDocumentChanged);
33053
- for (const [uri, text3] of fileContents) {
33054
- this.documents.open(uri, text3);
33055
- }
33056
- this.documents.on("documentOpened", this.onDocumentOpened);
33057
- this.documents.on("documentChanged", this.onDocumentChanged);
33058
- this.rebuildAll();
33059
- this.initialized = true;
33060
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
33061
- this.debounceTimer = null;
33062
- this.emit("modelReady");
33063
- }
33064
- /**
33065
- * Re-run cross-document analysis and schedule 'modelReady', e.g. after
33066
- * the macro configuration changed without any document changing.
33067
- */
33068
- refresh() {
33069
- if (this.workspaceRoot) this.installedVersion = readInstalledSpindleVersion(this.workspaceRoot);
33070
- this.analysisPending = true;
33071
- this.ensureCurrent();
33072
- this.scheduleModelReady();
33073
- }
33074
- /**
33075
- * Whether `{name}` opens a container that needs a `{/name}` closing tag:
33076
- * a block macro, or a block widget (one whose body renders `{@children}`).
33077
- * Like Spindle's set of block macros, either source makes a name a block.
33078
- */
33079
- isContainer(name) {
33080
- return this.macros.isBlock(name) || (this.widgets.getWidget(name)?.block ?? false);
33081
- }
33082
- /**
33083
- * Whether document `uri` declares any passage. A Twee document with no
33084
- * `::` header holds no passage (the compiler ignores text outside one), so
33085
- * there is no markup in it for any consumer to read.
33086
- */
33087
- hasPassages(uri) {
33088
- return this.passages.getPassagesInDocument(uri).length > 0;
33089
- }
33090
- /**
33091
- * The story format named by the project's StoryData passage (trimmed), or
33092
- * undefined if no StoryData passage names one.
33093
- */
33094
- get storyFormat() {
33095
- this.ensureCurrent();
33096
- return this.format.name;
33097
- }
33098
- /**
33099
- * Whether Spindle semantics apply to this project. False only if a
33100
- * StoryData passage names another story format (SugarCube, Harlowe, ...)
33101
- * and none names Spindle; a missing or unreadable StoryData counts as
33102
- * Spindle.
33103
- */
33104
- isSpindleProject() {
33105
- this.ensureCurrent();
33106
- return this.format.isSpindle;
33107
- }
33108
- /** Clean up listeners and timers. */
33109
- dispose() {
33110
- this.documents.removeListener("documentChanged", this.onDocumentChanged);
33111
- this.documents.removeListener("documentOpened", this.onDocumentOpened);
33112
- this.documents.removeListener("documentClosed", this.onDocumentClosed);
33113
- if (this.debounceTimer) {
33114
- clearTimeout(this.debounceTimer);
33115
- this.debounceTimer = null;
33116
- }
33117
- this.removeAllListeners();
33118
- }
33119
- /** Index local headers immediately; defer expensive cross-document analysis. */
33120
- handleDocumentChange(uri) {
33121
- const text3 = this.documents.getText(uri);
33122
- if (text3 !== void 0 && !isMacroSource(uri)) {
33123
- this.passages.rebuild(uri, text3);
33124
- }
33125
- this.analysisPending = true;
33126
- this.updateStoryFormat();
33127
- this.emit("documentChanged", uri);
33128
- this.emit("passagesUpdated", uri);
33129
- this.scheduleModelReady();
33130
- }
33131
- /** Remove local indices immediately and schedule cross-document analysis. */
33132
- handleDocumentClose(uri) {
33133
- this.passages.remove(uri);
33134
- this._variables.removeDocument(uri);
33135
- this.analysisPending = true;
33136
- this.updateStoryFormat();
33137
- this.emit("documentClosed", uri);
33138
- this.emit("passagesUpdated", uri);
33139
- this.scheduleModelReady();
33140
- }
33141
- /** Rebuild all indices from scratch. */
33142
- rebuildAll() {
33143
- for (const uri of this.documents.getUris()) {
33144
- const text3 = this.documents.getText(uri);
33145
- if (text3 !== void 0 && !isMacroSource(uri)) {
33146
- this.passages.rebuild(uri, text3);
33147
- }
33148
- }
33149
- this.analysisPending = true;
33150
- this.ensureCurrent();
33151
- }
33152
- /** Finish pending cross-document work once, before a feature reads the indices. */
33153
- ensureCurrent() {
33154
- if (!this.analysisPending) return;
33155
- this.analysisPending = false;
33156
- this.refreshDiscoveredMacros();
33157
- this.cascade();
33158
- this.updateStoryFormat();
33159
- }
33160
- /**
33161
- * Re-read the story format from the StoryData passages after the passage
33162
- * index changed, emitting 'storyFormatChanged' if it differs. Listeners
33163
- * that read the analysis indices finish pending analysis through getters.
33164
- */
33165
- updateStoryFormat() {
33166
- const format = resolveStoryFormat(storyDataFormats(
33167
- this.passages.getAllPassages(),
33168
- (uri) => this.documents.getText(uri)
33169
- ));
33170
- if (format.name === this.format.name && format.isSpindle === this.format.isSpindle) return;
33171
- this.format = format;
33172
- this.emit("storyFormatChanged", format);
33173
- }
33174
- /**
33175
- * Re-run static macro discovery over the workspace and replace the
33176
- * registry's discovered tier (dropping definitions whose source is gone).
33177
- *
33178
- * Sources scanned for Story.defineMacro({...}) calls:
33179
- * - `{do}` blocks in StoryInit passages
33180
- * - passages tagged `script` (Story JavaScript)
33181
- * - JavaScript/TypeScript documents in the workspace
33182
- */
33183
- refreshDiscoveredMacros() {
33184
- const found = [];
33185
- const init = [];
33186
- for (const uri of this.documents.getUris().sort()) {
33187
- const text3 = this.documents.getText(uri);
33188
- if (!text3) continue;
33189
- if (isMacroSource(uri)) {
33190
- found.push(...discoverMacros(text3));
33191
- continue;
33192
- }
33193
- let lines;
33194
- for (const passage of this.passages.getPassagesInDocument(uri)) {
33195
- const isStoryInit = passage.name === "StoryInit";
33196
- const isScript = passage.tags?.includes("script") ?? false;
33197
- if (!isStoryInit && !isScript) continue;
33198
- lines ??= text3.split("\n");
33199
- const content2 = lines.slice(passage.headerEnd.end.line + 1, passage.range.end.line + 1).join("\n");
33200
- if (isScript) found.push(...discoverMacros(content2));
33201
- else init.push(...storyInitMacros(content2));
33202
- }
33203
- }
33204
- this.macros.setDiscoveredMacros([...found, ...init]);
33205
- }
33206
- /**
33207
- * Cascade: rescan variables and widgets based on current passages.
33208
- * Called after any passage index update.
33209
- *
33210
- * The markup of every document is read again (the macros may have changed),
33211
- * the widgets are defined from it, and then the variable usages and widget
33212
- * invocations are read from the same markup, which pairs its tags with the
33213
- * widgets just defined.
33214
- */
33215
- cascade() {
33216
- this.markup.invalidate();
33217
- this.declaredVersion = storyDataFormatVersion(this.passages.getAllPassages(), (uri) => this.documents.getText(uri));
33218
- this._target = resolveSpindleTarget(this.installedVersion, this.declaredVersion);
33219
- const declarations = (name) => {
33220
- const passage = this.passages.getPassage(name);
33221
- return passage ? this.markup.get(passage.uri)?.passages.find((p) => p.passage === passage) : void 0;
33222
- };
33223
- const variables = declarations("StoryVariables");
33224
- if (variables) this.variables.parseStoryVariables(variables.content, variables.passage.headerEnd.end.line + 1, variables.doc.uri);
33225
- else this.variables.clearStoryVariables();
33226
- const transients = declarations("StoryTransients");
33227
- if (transients) this.variables.parseStoryTransients(transients.content, transients.passage.headerEnd.end.line + 1, transients.doc.uri);
33228
- else this.variables.clearStoryTransients();
33229
- const readDocuments = () => this.documents.getUris().flatMap((uri) => {
33230
- const markup = isMacroSource(uri) ? void 0 : this.markup.get(uri);
33231
- return markup ? [markup] : [];
33232
- });
33233
- const markups = /* @__PURE__ */ new Map();
33234
- for (const doc of readDocuments()) for (const passage of doc.passages) markups.set(passage.passage, passage);
33235
- this.widgets.scan(
33236
- this.passages.getAllPassages().flatMap((passage) => markups.get(passage) ?? []),
33237
- this.macros.toolingMacros()
33238
- );
33239
- this.markup.invalidate();
33240
- this.widgets.clearInvocations();
33241
- for (const doc of readDocuments()) {
33242
- this.variables.scanDocument(doc.uri, doc.text, doc);
33243
- this.widgets.recordInvocations(doc.uri, doc.passages);
33244
- }
33245
- }
33246
- /** All runtime metadata, with real macros taking precedence over widgets. */
33247
- toolingMacros() {
33248
- const macros = this.macros.toolingMacros();
33249
- return [...macros, ...this.widgets.getAllWidgets().filter((widget) => !this.macros.getMacro(widget.name)).map((widget) => ({ name: widget.name, block: widget.block, subMacros: [], widget: widget.tooling }))];
33250
- }
33251
- /** Coalesce analysis and diagnostics across a burst of document changes. */
33252
- scheduleModelReady() {
33253
- if (this.debounceTimer) {
33254
- clearTimeout(this.debounceTimer);
33255
- }
33256
- this.debounceTimer = setTimeout(() => {
33257
- this.debounceTimer = null;
33258
- this.ensureCurrent();
33259
- this.emit("modelReady");
33260
- }, this.DEBOUNCE_MS);
33261
- }
33262
- };
33263
- }
33264
- });
33265
-
33266
- // src/core/diagnostic-codes.ts
33267
- function getSeverity(code) {
33268
- return severityMap[code];
33269
- }
33270
- var DiagnosticCode, severityMap;
33271
- var init_diagnostic_codes = __esm({
33272
- "src/core/diagnostic-codes.ts"() {
33273
- "use strict";
33274
- DiagnosticCode = {
33275
- /** The Spindle the project targets is older than the oldest one supported. */
33276
- UnsupportedSpindleVersion: "SP001",
33277
- /** `unknown-macro`: a macro that is none of the built-in, configured or defined ones. */
33278
- UndefinedMacro: "SP100",
33279
- /** `unclosed-block`, `mismatched-closer`, `stray-closer` of a macro. */
33280
- MalformedContainer: "SP101",
33281
- /** Tags of an HTML element Spindle cannot read or pair. */
33282
- MalformedElement: "SP102",
33283
- /** A closing tag that closes no macro (`{/set}`), or is malformed (`{/}`, `{/if x}`). */
33284
- IllegalClosingTag: "SP104",
33285
- /** `unclosed-link`, `unclosed-expression`, `unclosed-macro`: a `[[`, `{$` or `{name` that is never ended. */
33286
- UnclosedMarkup: "SP105",
33287
- /** `code-syntax`: JavaScript that does not parse. */
33288
- CodeSyntaxError: "SP106",
33289
- /** `misplaced-branch` (`{else}` outside `{if}`), and a macro outside the parents it needs. */
33290
- InvalidChildren: "SP107",
33291
- /** A configured parameter schema rejects the arguments (macros Spindle declares nothing for). */
33292
- ExpectedNoArguments: "SP108",
33293
- /** `argument-error`: arguments that do not have their parameters' forms. */
33294
- ParameterTypeError: "SP109",
33295
- ParameterWarning: "SP110",
33296
- TooManyArguments: "SP111",
33297
- /** `unquoted-passage-name`: `{goto Kitchen}`, which throws a ReferenceError. */
33298
- UnquotedPassageName: "SP113",
33299
- ChildMaxExceeded: "SP114",
33300
- ChildMinNotMet: "SP115",
33301
- /** `undeclared-variable`. */
33302
- UndeclaredVariable: "SP200",
33303
- /** `primitive-field`: a field of a number, string or boolean. */
33304
- PrimitiveFieldAccess: "SP201",
33305
- NoStoryVariables: "SP202",
33306
- /** `undeclared-transient`. */
33307
- UndeclaredTransient: "SP203",
33308
- TemporaryAssignedInLoop: "SP205",
33309
- ArrayMemberAccess: "SP206",
33310
- InvalidDeclaration: "SP207",
33311
- /** `reserved-name`: a variable named `__proto__`. */
33312
- ReservedVariableName: "SP208",
33313
- /** `unknown-passage`. */
33314
- BrokenPassageLink: "SP300",
33315
- WidgetArgCountMismatch: "SP301",
33316
- IncludeWidgetPassage: "SP302",
33317
- UnusedWidget: "SP303",
33318
- LinkRuntimeMismatch: "SP304",
33319
- LiteralLinkInterpolation: "SP305",
33320
- DeadEndPassage: "SP400",
33321
- UnreachablePassage: "SP401",
33322
- LineTooLong: "SP500"
33323
- };
33324
- severityMap = {
33325
- SP001: "warning",
33326
- SP100: "warning",
33327
- SP101: "error",
33328
- SP102: "error",
33329
- SP104: "error",
33330
- SP105: "error",
33331
- SP106: "error",
33332
- SP107: "error",
33333
- SP108: "error",
33334
- SP109: "error",
33335
- SP110: "warning",
33336
- SP111: "error",
33337
- SP113: "error",
33338
- SP114: "error",
33339
- SP115: "error",
33340
- SP200: "error",
33341
- SP201: "error",
33342
- SP202: "info",
33343
- SP203: "warning",
33344
- SP205: "warning",
33345
- SP206: "warning",
33346
- SP207: "error",
33347
- SP208: "error",
33348
- SP300: "warning",
33349
- SP301: "warning",
33350
- SP302: "warning",
33351
- SP303: "hint",
33352
- SP304: "warning",
33353
- SP305: "warning",
33354
- SP400: "hint",
33355
- SP401: "hint",
33356
- SP500: "warning"
33604
+ unwatch: {
33605
+ name: "unwatch",
33606
+ description: "Stops watching a variable.\n\nUsage: `{unwatch $var}`",
33607
+ parameters: ["...text"],
33608
+ parameterDocs: [
33609
+ { name: "variable", documentation: "Variable to stop watching." }
33610
+ ]
33611
+ },
33612
+ unless: {
33613
+ name: "unless",
33614
+ container: true,
33615
+ description: "Conditionally displays its contents when the expression is falsy. The inverse of `{if}`.\n\nUsage:\n\n```\n{unless condition}\n ...\n{/unless}\n```",
33616
+ parameters: ["...text"],
33617
+ parameterDocs: [
33618
+ { name: "condition", documentation: "Expression; the contents show when it is falsy." }
33619
+ ]
33620
+ }
33357
33621
  };
33358
33622
  }
33359
33623
  });
33360
33624
 
33361
- // src/core/markup/source-view.ts
33362
- function viewIndex(view, offset2) {
33363
- if (!view.sourceOffsets) return offset2 - view.offset;
33364
- let low = 0;
33365
- let high = view.sourceOffsets.length - 1;
33366
- while (low < high) {
33367
- const mid = Math.ceil((low + high) / 2);
33368
- if (view.sourceOffsets[mid] <= offset2) low = mid;
33369
- else high = mid - 1;
33370
- }
33371
- return low;
33372
- }
33373
- function textAt(passage, start, end) {
33374
- return passage.pieces.filter((piece) => piece.kind === "text" && piece.offset <= start && pieceOffset(piece, piece.text.length) >= end).at(-1);
33375
- }
33376
- function sourceView(passage, start, end) {
33377
- const owner = passage.pieces.filter((piece) => {
33378
- const value = piece.kind === "text" ? piece.text : piece.kind === "code" ? piece.code : void 0;
33379
- return value !== void 0 && piece.offset <= start && pieceOffset(piece, value.length) >= end;
33380
- }).at(-1);
33381
- if (owner?.kind === "code") return { ...owner, text: owner.code };
33382
- return owner?.kind === "text" ? owner : { offset: 0, text: passage.content };
33383
- }
33384
- var init_source_view = __esm({
33385
- "src/core/markup/source-view.ts"() {
33386
- "use strict";
33387
- init_tooling();
33388
- }
33389
- });
33390
-
33391
- // src/core/parsing/argument-lexer.ts
33392
- function lexArguments(source) {
33393
- let from = 0;
33394
- return splitArgs(source).map((text3) => {
33395
- const start = source.indexOf(text3, from);
33396
- from = start + text3.length;
33397
- return classifyArgument(text3, start);
33398
- });
33399
- }
33400
- function classifyArgument(text3, start = 0) {
33401
- const base = { text: text3, start, end: start + text3.length };
33402
- if (readQuoted(text3, 0)?.end === text3.length) return { ...base, type: 0 /* String */ };
33403
- if (text3 === "null") return { ...base, type: 3 /* Null */ };
33404
- if (text3 === "undefined") return { ...base, type: 10 /* Undefined */ };
33405
- if (text3 === "true" || text3 === "false") return { ...base, type: 2 /* Boolean */ };
33406
- if (text3 === "NaN") return { ...base, type: 9 /* NaN */ };
33407
- if (text3 !== "" && !Number.isNaN(Number(text3))) return { ...base, type: 1 /* Number */ };
33408
- if (/^[$_@%]\w+(?:\.\w+)*$/.test(text3)) {
33409
- return { ...base, type: 4 /* Variable */, sigil: text3[0], path: text3.slice(1).split(".") };
33410
- }
33411
- if (text3.startsWith("[[") && text3.endsWith("]]")) return { ...base, type: 5 /* Link */ };
33412
- if (/^\[[<>]?img\[/i.test(text3) && text3.endsWith("]]")) return { ...base, type: 6 /* Image */ };
33413
- return { ...base, type: /^[-\w]+$/.test(text3) ? 8 /* Bareword */ : 7 /* Expression */ };
33625
+ // src/core/workspace/workspace-model.ts
33626
+ import { EventEmitter as EventEmitter2 } from "node:events";
33627
+ function storyInitMacros(content2) {
33628
+ return passagePieces(content2, builtinMacros).flatMap((piece) => piece.kind === "code" && piece.goal === "statements" ? discoverMacros(piece.code) : []);
33414
33629
  }
33415
- var init_argument_lexer = __esm({
33416
- "src/core/parsing/argument-lexer.ts"() {
33630
+ var WorkspaceModel;
33631
+ var init_workspace_model = __esm({
33632
+ "src/core/workspace/workspace-model.ts"() {
33417
33633
  "use strict";
33418
33634
  init_tooling();
33419
- }
33420
- });
33421
-
33422
- // src/core/parsing/parameter-validator.ts
33423
- function isAlwaysArg(arg) {
33424
- return arg.type === 4 /* Variable */ || arg.type === 7 /* Expression */;
33425
- }
33426
- function findParameterType(name) {
33427
- for (const pt2 of parameterTypes) {
33428
- if (pt2.name.includes(name)) return pt2;
33429
- }
33430
- return null;
33431
- }
33432
- function lexFormat(formatString) {
33433
- let pos = 0;
33434
- const lexed = [];
33435
- let iter = 0;
33436
- for (; ; ) {
33437
- if (++iter > LEX_LIMIT) throw new Error("Format lex iteration limit exceeded");
33438
- if (pos >= formatString.length) break;
33439
- const ch = formatString[pos];
33440
- if (wsRe.test(ch)) {
33441
- pos++;
33442
- } else if (ch === "&") {
33443
- pos++;
33444
- const next = formatString[pos];
33445
- if (next === "+") {
33446
- pos++;
33447
- lexed.push({ kind: 3 /* AndNext */ });
33448
- } else {
33449
- throw new Error(`Lex: Expected '+' after '&', found '${next ?? "EOF"}'`);
33450
- }
33451
- } else if (ch === "|") {
33452
- pos++;
33453
- const next = formatString[pos];
33454
- if (next === "+") {
33455
- pos++;
33456
- lexed.push({ kind: 4 /* MaybeNext */ });
33457
- } else if (next === void 0) {
33458
- throw new Error("Lex: Expected content after '|'");
33459
- } else if (wsRe.test(next)) {
33460
- pos++;
33461
- lexed.push({ kind: 5 /* Or */ });
33462
- } else if (identStartRe.test(next) || quoteRe.test(next)) {
33463
- lexed.push({ kind: 5 /* Or */ });
33464
- } else {
33465
- throw new Error(`Lex: Invalid character after '|': '${next}'`);
33635
+ init_document_store();
33636
+ init_passage_index();
33637
+ init_macro_registry2();
33638
+ init_variable_tracker();
33639
+ init_widget_registry();
33640
+ init_markup_index();
33641
+ init_macro_sources();
33642
+ init_story_format();
33643
+ init_spindle_version();
33644
+ init_macro_supplements();
33645
+ WorkspaceModel = class extends EventEmitter2 {
33646
+ documents;
33647
+ passages;
33648
+ _macros;
33649
+ get macros() {
33650
+ this.ensureCurrent();
33651
+ return this._macros;
33466
33652
  }
33467
- } else if (quoteRe.test(ch)) {
33468
- const quoted = readQuoted(formatString, pos);
33469
- if (!quoted) throw new Error("Lex: Unterminated string literal");
33470
- pos = quoted.end;
33471
- lexed.push({ kind: 1 /* Literal */, value: quoted.value });
33472
- } else if (ch === "(" || ch === ")") {
33473
- pos++;
33474
- lexed.push({ kind: 2 /* Group */, open: ch === "(" });
33475
- } else if (identStartRe.test(ch)) {
33476
- const start = pos;
33477
- pos++;
33478
- while (pos < formatString.length) {
33479
- if (++iter > LEX_LIMIT) throw new Error("Format lex identifier iteration limit exceeded");
33480
- const c = formatString[pos];
33481
- if (identStopRe.test(c) || c === void 0) break;
33482
- if (identStartRe.test(c)) {
33483
- pos++;
33484
- } else if (digitRe.test(c)) {
33485
- throw new Error(`Lex: Digit '${c}' not allowed in parameter type name`);
33486
- } else {
33487
- throw new Error(`Lex: Invalid character '${c}' in parameter type name`);
33488
- }
33653
+ _variables;
33654
+ get variables() {
33655
+ this.ensureCurrent();
33656
+ return this._variables;
33489
33657
  }
33490
- const ident = formatString.slice(start, pos);
33491
- const pt2 = findParameterType(ident);
33492
- if (pt2 === null) throw new Error(`Lex: Unknown parameter type '${ident}'`);
33493
- lexed.push({ kind: 0 /* Type */, type: pt2 });
33494
- } else if (ch === "." && formatString[pos + 1] === "." && formatString[pos + 2] === ".") {
33495
- pos += 3;
33496
- lexed.push({ kind: 6 /* Repeat */ });
33497
- } else {
33498
- throw new Error(`Lex: Unexpected character '${ch}'`);
33499
- }
33500
- }
33501
- return lexed;
33502
- }
33503
- function parseFormat(formatString) {
33504
- const tokens2 = lexFormat(formatString);
33505
- if (tokens2.length === 0) return null;
33506
- let index = 0;
33507
- let iter = 0;
33508
- function infixBp(kind) {
33509
- switch (kind) {
33510
- case 5 /* Or */:
33511
- return [9, 10];
33512
- case 3 /* AndNext */:
33513
- return [4, 5];
33514
- case 4 /* MaybeNext */:
33515
- return [2, 3];
33516
- default:
33517
- return null;
33518
- }
33519
- }
33520
- function prefixBp(kind) {
33521
- switch (kind) {
33522
- case 4 /* MaybeNext */:
33523
- return 3;
33524
- case 6 /* Repeat */:
33525
- return 8;
33526
- default:
33527
- throw new Error(`Parse: Unexpected prefix operator kind ${kind}`);
33528
- }
33529
- }
33530
- function isPrefixOp(t) {
33531
- return t.kind === 4 /* MaybeNext */ || t.kind === 6 /* Repeat */;
33532
- }
33533
- function isValue(t) {
33534
- return t.kind === 0 /* Type */ || t.kind === 1 /* Literal */;
33535
- }
33536
- function exprBp(minBp) {
33537
- if (++iter > PARSE_LIMIT) throw new Error("Format parse iteration limit exceeded");
33538
- const cur = tokens2[index];
33539
- if (!cur) throw new Error("Parse: Unexpected end of format string");
33540
- index++;
33541
- let lhs;
33542
- if (isPrefixOp(cur)) {
33543
- const rBp = prefixBp(cur.kind);
33544
- const rhs = exprBp(rBp);
33545
- lhs = { kind: cur.kind, right: rhs };
33546
- } else if (cur.kind === 2 /* Group */) {
33547
- const g = cur;
33548
- if (!g.open) throw new Error("Parse: Unexpected closing parenthesis");
33549
- lhs = exprBp(0);
33550
- const closing = tokens2[index];
33551
- if (!closing || closing.kind !== 2 /* Group */ || closing.open) {
33552
- throw new Error("Parse: Expected closing parenthesis");
33658
+ _widgets;
33659
+ get widgets() {
33660
+ this.ensureCurrent();
33661
+ return this._widgets;
33553
33662
  }
33554
- index++;
33555
- } else if (isValue(cur)) {
33556
- lhs = cur;
33557
- } else {
33558
- throw new Error("Parse: Expected value or prefix operator");
33559
- }
33560
- for (; ; ) {
33561
- if (++iter > PARSE_LIMIT) throw new Error("Format parse operator iteration limit exceeded");
33562
- const op = tokens2[index];
33563
- if (!op) break;
33564
- const bp = infixBp(op.kind);
33565
- if (bp === null) break;
33566
- const [lBp, rBp] = bp;
33567
- if (lBp < minBp) break;
33568
- index++;
33569
- const rhs = exprBp(rBp);
33570
- lhs = { kind: op.kind, left: lhs, right: rhs };
33571
- }
33572
- return lhs;
33573
- }
33574
- return exprBp(0);
33575
- }
33576
- function slotAccepts(leaf) {
33577
- return (arg) => crawlValidate(leaf, [arg], 0).status === 2 /* Success */;
33578
- }
33579
- function describeFormat(format) {
33580
- const cap = (list3) => list3.slice(0, DESCRIBE_ALTERNATIVE_LIMIT);
33581
- const join5 = (lefts, rights) => cap(lefts.flatMap((l) => rights.map((r) => [...l, ...r])));
33582
- switch (format.kind) {
33583
- case 0 /* Type */:
33584
- return [[{ label: format.type.name[0], optional: false, repeat: false, accepts: slotAccepts(format) }]];
33585
- case 1 /* Literal */:
33586
- return [[{ label: `'${format.value}'`, optional: false, repeat: false, accepts: slotAccepts(format) }]];
33587
- case 3 /* AndNext */:
33588
- return join5(describeFormat(format.left), describeFormat(format.right));
33589
- case 4 /* MaybeNext */: {
33590
- const rights = describeFormat(format.right).map(
33591
- (seq) => seq.map((slot) => ({ ...slot, optional: true }))
33592
- );
33593
- return join5(format.left ? describeFormat(format.left) : [[]], rights);
33594
- }
33595
- case 5 /* Or */:
33596
- return cap([...describeFormat(format.left), ...describeFormat(format.right)]);
33597
- case 6 /* Repeat */:
33598
- return describeFormat(format.right).map(
33599
- (seq) => seq.map((slot) => ({ ...slot, optional: true, repeat: true }))
33600
- );
33601
- }
33602
- }
33603
- function makeError(status, message, argIndex, rank = 0) {
33604
- return {
33605
- status,
33606
- rank,
33607
- argIndex,
33608
- errors: [{ message, argIndex }],
33609
- warnings: []
33610
- };
33611
- }
33612
- function makeSuccess(argIndex, rank, warnings = []) {
33613
- return { status: 2 /* Success */, rank, argIndex, errors: [], warnings };
33614
- }
33615
- function isFail(s) {
33616
- return s === 1 /* Failure */ || s === 0 /* NotFound */;
33617
- }
33618
- function crawlValidate(format, args2, startIndex, state) {
33619
- const memo2 = /* @__PURE__ */ new Map();
33620
- function append(left, right) {
33621
- return { ...right, rank: left.rank + right.rank, warnings: left.warnings.concat(right.warnings) };
33622
- }
33623
- function follow(left, right) {
33624
- return left.flatMap((matched) => isFail(matched.status) ? [matched] : crawl(right, matched.argIndex).map((result2) => append(matched, result2)));
33625
- }
33626
- function crawl(fmt, argIdx) {
33627
- let positions = memo2.get(fmt);
33628
- if (!positions) memo2.set(fmt, positions = /* @__PURE__ */ new Map());
33629
- const cached2 = positions.get(argIdx);
33630
- if (cached2) return cached2;
33631
- if (positions.size >= VALIDATE_LIMIT) {
33632
- throw new Error(`Validation iteration limit exceeded at argument ${argIdx}`);
33633
- }
33634
- const unique = /* @__PURE__ */ new Map();
33635
- for (const result2 of match2(fmt, argIdx)) {
33636
- const key = isFail(result2.status) ? "failure" : result2.argIndex;
33637
- const previous2 = unique.get(key);
33638
- if (!previous2 || key === "failure" && result2.rank > previous2.rank) unique.set(key, result2);
33639
- }
33640
- const results2 = [...unique.values()];
33641
- positions.set(argIdx, results2);
33642
- return results2;
33643
- }
33644
- function match2(fmt, argIdx) {
33645
- switch (fmt.kind) {
33646
- case 4 /* MaybeNext */: {
33647
- const left = fmt.left ? crawl(fmt.left, argIdx) : [makeSuccess(argIdx, 0)];
33648
- return [...follow(left, fmt.right), ...left];
33663
+ /** Every document's markup, read through Spindle's tooling API. */
33664
+ _markup;
33665
+ get markup() {
33666
+ this.ensureCurrent();
33667
+ return this._markup;
33649
33668
  }
33650
- case 3 /* AndNext */: {
33651
- return follow(crawl(fmt.left, argIdx), fmt.right);
33669
+ /** True after initialize() has completed (full workspace scan done). */
33670
+ initialized = false;
33671
+ /** The story format declared by StoryData, updated with the passages. */
33672
+ format = UNDECLARED_STORY_FORMAT;
33673
+ /**
33674
+ * The Spindle the project targets: the version installed under the
33675
+ * workspace root, else StoryData's `format-version`. Only a version below
33676
+ * {@link MINIMUM_SPINDLE_VERSION} matters (it is reported, not emulated).
33677
+ */
33678
+ _target;
33679
+ get target() {
33680
+ this.ensureCurrent();
33681
+ return this._target;
33652
33682
  }
33653
- case 5 /* Or */: {
33654
- return [...crawl(fmt.left, argIdx), ...crawl(fmt.right, argIdx)];
33683
+ workspaceRoot;
33684
+ /** The version installed under the workspace root, read at startup and on refresh(). */
33685
+ installedVersion;
33686
+ /** The `format-version` StoryData declares. */
33687
+ declaredVersion;
33688
+ analysisPending = false;
33689
+ debounceTimer = null;
33690
+ DEBOUNCE_MS = 200;
33691
+ /** Bound listeners for cleanup in dispose(). */
33692
+ onDocumentChanged;
33693
+ onDocumentOpened;
33694
+ onDocumentClosed;
33695
+ constructor(config2) {
33696
+ super();
33697
+ this.documents = new DocumentStore();
33698
+ this.passages = new PassageIndex();
33699
+ this._macros = new MacroRegistry();
33700
+ this.macros.onChange = () => this.markup?.invalidate();
33701
+ this._variables = new VariableTracker();
33702
+ this._widgets = new WidgetRegistry();
33703
+ this._markup = new MarkupIndex({
33704
+ text: (uri) => this.documents.getText(uri),
33705
+ passages: (uri) => this.passages.getPassagesInDocument(uri),
33706
+ context: () => ({
33707
+ macros: this.toolingMacros(),
33708
+ isBlock: (name) => this.isContainer(name)
33709
+ })
33710
+ });
33711
+ this.workspaceRoot = config2?.workspaceRoot;
33712
+ this.installedVersion = this.workspaceRoot ? readInstalledSpindleVersion(this.workspaceRoot) : void 0;
33713
+ this._target = resolveSpindleTarget(this.installedVersion);
33714
+ this.macros.loadBuiltins();
33715
+ this.macros.loadSupplements(macro_supplements_default);
33716
+ this.onDocumentChanged = (uri) => this.handleDocumentChange(uri);
33717
+ this.onDocumentOpened = (uri) => this.handleDocumentChange(uri);
33718
+ this.onDocumentClosed = (uri) => this.handleDocumentClose(uri);
33719
+ this.documents.on("documentChanged", this.onDocumentChanged);
33720
+ this.documents.on("documentOpened", this.onDocumentOpened);
33721
+ this.documents.on("documentClosed", this.onDocumentClosed);
33655
33722
  }
33656
- case 6 /* Repeat */: {
33657
- const matches = [];
33658
- const pending = [makeSuccess(argIdx, 0)];
33659
- const reached = /* @__PURE__ */ new Set();
33660
- while (pending.length > 0) {
33661
- const matched = pending.pop();
33662
- if (reached.has(matched.argIndex)) continue;
33663
- reached.add(matched.argIndex);
33664
- matches.push(matched);
33665
- const items = crawl(fmt.right, matched.argIndex);
33666
- for (let i2 = items.length - 1; i2 >= 0; i2--) {
33667
- const item = items[i2];
33668
- if (!isFail(item.status) && !reached.has(item.argIndex)) pending.push(append(matched, item));
33669
- }
33723
+ /**
33724
+ * Initialize the workspace in CLI/batch mode.
33725
+ * Loads all file contents, builds indices, and emits 'modelReady'.
33726
+ */
33727
+ initialize(fileContents) {
33728
+ this.documents.removeListener("documentOpened", this.onDocumentOpened);
33729
+ this.documents.removeListener("documentChanged", this.onDocumentChanged);
33730
+ for (const [uri, text3] of fileContents) {
33731
+ this.documents.open(uri, text3);
33670
33732
  }
33671
- return matches.sort((left, right) => right.argIndex - left.argIndex);
33733
+ this.documents.on("documentOpened", this.onDocumentOpened);
33734
+ this.documents.on("documentChanged", this.onDocumentChanged);
33735
+ this.rebuildAll();
33736
+ this.initialized = true;
33737
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
33738
+ this.debounceTimer = null;
33739
+ this.emit("modelReady");
33672
33740
  }
33673
- case 1 /* Literal */: {
33674
- const arg = args2[argIdx];
33675
- if (arg === void 0) {
33676
- return [makeError(0 /* NotFound */, `Expected literal '${fmt.value}' but no argument found`, argIdx)];
33677
- }
33678
- if (isAlwaysArg(arg)) {
33679
- return [makeSuccess(argIdx + 1, CORRECT_RANK)];
33680
- }
33681
- const argText = arg.type === 0 /* String */ ? readQuoted(arg.text, 0)?.value : arg.text;
33682
- if (argText === fmt.value) {
33683
- return [makeSuccess(argIdx + 1, CORRECT_RANK)];
33684
- }
33685
- 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 */) {
33686
- return [makeSuccess(argIdx + 1, CORRECT_RANK)];
33687
- }
33688
- return [makeError(1 /* Failure */, `Expected literal '${fmt.value}'`, argIdx, CORRECT_TYPE_RANK)];
33741
+ /**
33742
+ * Re-run cross-document analysis and schedule 'modelReady', e.g. after
33743
+ * the macro configuration changed without any document changing.
33744
+ */
33745
+ refresh() {
33746
+ if (this.workspaceRoot) this.installedVersion = readInstalledSpindleVersion(this.workspaceRoot);
33747
+ this.analysisPending = true;
33748
+ this.ensureCurrent();
33749
+ this.scheduleModelReady();
33689
33750
  }
33690
- case 0 /* Type */: {
33691
- const arg = args2[argIdx];
33692
- const typeDef = fmt.type;
33693
- if (arg === void 0) {
33694
- return [makeError(
33695
- 0 /* NotFound */,
33696
- `Expected type '${typeDef.name[0]}' but no argument found`,
33697
- argIdx
33698
- )];
33699
- }
33700
- if (isAlwaysArg(arg)) {
33701
- return [makeSuccess(argIdx + 1, CORRECT_RANK)];
33702
- }
33703
- const result2 = typeDef.validate(arg, args2, argIdx, state);
33704
- if (result2 instanceof TypeError2) {
33705
- return [{
33706
- status: 1 /* Failure */,
33707
- rank: 0,
33708
- argIndex: argIdx,
33709
- errors: [{ message: result2.message, argIndex: argIdx }],
33710
- warnings: []
33711
- }];
33712
- }
33713
- if (result2 instanceof TypeWarning) {
33714
- return [makeSuccess(argIdx + 1, CORRECT_RANK, [
33715
- { message: result2.message, argIndex: argIdx }
33716
- ])];
33717
- }
33718
- return [makeSuccess(argIdx + 1, CORRECT_RANK)];
33751
+ /**
33752
+ * Whether `{name}` opens a container that needs a `{/name}` closing tag:
33753
+ * a block macro, or a block widget (one whose body renders `{@children}`).
33754
+ * Like Spindle's set of block macros, either source makes a name a block.
33755
+ */
33756
+ isContainer(name) {
33757
+ return this.macros.isBlock(name) || (this.widgets.getWidget(name)?.block ?? false);
33719
33758
  }
33720
- }
33721
- }
33722
- const results = crawl(format, startIndex);
33723
- return results.find((result2) => !isFail(result2.status) && result2.argIndex === args2.length) ?? results.reduce((best, result2) => result2.rank > best.rank ? result2 : best);
33724
- }
33725
- var TypeError2, TypeWarning, parameterTypes, LEX_LIMIT, wsRe, identStartRe, identStopRe, quoteRe, digitRe, PARSE_LIMIT, DESCRIBE_ALTERNATIVE_LIMIT, VALIDATE_LIMIT, CORRECT_TYPE_RANK, CORRECT_RANK, Parameters;
33726
- var init_parameter_validator = __esm({
33727
- "src/core/parsing/parameter-validator.ts"() {
33728
- "use strict";
33729
- init_tooling();
33730
- init_argument_lexer();
33731
- TypeError2 = class {
33732
- constructor(message) {
33733
- this.message = message;
33759
+ /**
33760
+ * Whether document `uri` declares any passage. A Twee document with no
33761
+ * `::` header holds no passage (the compiler ignores text outside one), so
33762
+ * there is no markup in it for any consumer to read.
33763
+ */
33764
+ hasPassages(uri) {
33765
+ return this.passages.getPassagesInDocument(uri).length > 0;
33766
+ }
33767
+ /**
33768
+ * The story format named by the project's StoryData passage (trimmed), or
33769
+ * undefined if no StoryData passage names one.
33770
+ */
33771
+ get storyFormat() {
33772
+ this.ensureCurrent();
33773
+ return this.format.name;
33734
33774
  }
33735
- message;
33736
- };
33737
- TypeWarning = class {
33738
- constructor(message) {
33739
- this.message = message;
33775
+ /**
33776
+ * Whether Spindle semantics apply to this project. False only if a
33777
+ * StoryData passage names another story format (SugarCube, Harlowe, ...)
33778
+ * and none names Spindle; a missing or unreadable StoryData counts as
33779
+ * Spindle.
33780
+ */
33781
+ isSpindleProject() {
33782
+ this.ensureCurrent();
33783
+ return this.format.isSpindle;
33740
33784
  }
33741
- message;
33742
- };
33743
- parameterTypes = [
33744
- {
33745
- name: ["bool", "boolean"],
33746
- validate(arg) {
33747
- return arg.type === 2 /* Boolean */ ? null : new TypeError2("Argument is not a boolean");
33748
- }
33749
- },
33750
- {
33751
- name: ["null"],
33752
- validate(arg) {
33753
- return arg.type === 3 /* Null */ ? null : new TypeError2("Argument is not 'null'");
33754
- }
33755
- },
33756
- {
33757
- name: ["undefined"],
33758
- validate(arg) {
33759
- return arg.type === 10 /* Undefined */ ? null : new TypeError2("Argument is not 'undefined'");
33760
- }
33761
- },
33762
- {
33763
- name: ["number"],
33764
- validate(arg) {
33765
- return arg.type === 1 /* Number */ ? null : new TypeError2("Argument is not a number");
33766
- }
33767
- },
33768
- {
33769
- name: ["NaN"],
33770
- validate(arg) {
33771
- return arg.type === 9 /* NaN */ ? null : new TypeError2("Argument is not 'NaN'");
33772
- }
33773
- },
33774
- {
33775
- name: ["link"],
33776
- validate(arg) {
33777
- return arg.type === 5 /* Link */ ? null : new TypeError2("Argument is not a link");
33778
- }
33779
- },
33780
- {
33781
- name: ["image"],
33782
- validate(arg) {
33783
- return arg.type === 6 /* Image */ ? null : new TypeError2("Argument is not an image");
33784
- }
33785
- },
33786
- {
33787
- name: ["bareword"],
33788
- validate(arg) {
33789
- return arg.type === 8 /* Bareword */ ? null : new TypeError2("Argument is not a bareword");
33790
- }
33791
- },
33792
- {
33793
- name: ["string"],
33794
- validate(arg) {
33795
- return arg.type === 0 /* String */ ? null : new TypeError2("Argument is not a quoted string");
33796
- }
33797
- },
33798
- {
33799
- name: ["text"],
33800
- validate(arg) {
33801
- const t = arg.type;
33802
- if (t === 8 /* Bareword */ || t === 0 /* String */ || t === 2 /* Boolean */ || t === 3 /* Null */ || t === 9 /* NaN */ || t === 1 /* Number */) {
33803
- return null;
33804
- }
33805
- return new TypeError2("Argument is not text");
33806
- }
33807
- },
33808
- {
33809
- name: ["var"],
33810
- validate(arg) {
33811
- return arg.type === 4 /* Variable */ ? null : new TypeError2("Argument is not a variable");
33785
+ /** Clean up listeners and timers. */
33786
+ dispose() {
33787
+ this.documents.removeListener("documentChanged", this.onDocumentChanged);
33788
+ this.documents.removeListener("documentOpened", this.onDocumentOpened);
33789
+ this.documents.removeListener("documentClosed", this.onDocumentClosed);
33790
+ if (this.debounceTimer) {
33791
+ clearTimeout(this.debounceTimer);
33792
+ this.debounceTimer = null;
33812
33793
  }
33813
- },
33814
- {
33815
- // The story variable a form control binds to: `$name` or `"$name"`.
33816
- // Spindle strips the quotes and the `$` sigil from the first argument
33817
- // and reads the rest as a dot path into the story variables.
33818
- name: ["receiver"],
33819
- validate(arg) {
33820
- let name;
33821
- if (arg.type === 0 /* String */) {
33822
- name = arg.text.slice(1, -1);
33823
- } else if (arg.type === 8 /* Bareword */) {
33824
- name = arg.text;
33825
- }
33826
- if (name === void 0) {
33827
- return new TypeError2("Argument is not a variable receiver");
33828
- }
33829
- if (/^[_@%]/.test(name)) {
33830
- return new TypeError2(`Variable receiver must be a story variable ($name), got '${name}'`);
33831
- }
33832
- if (!/^(?:\$\w|[A-Za-z_])[\w$]*(?:\.[\w$]+)*$/.test(name)) {
33833
- return new TypeError2(`Argument is not a valid variable receiver: '${name}'`);
33834
- }
33835
- return null;
33794
+ this.removeAllListeners();
33795
+ }
33796
+ /** Index local headers immediately; defer expensive cross-document analysis. */
33797
+ handleDocumentChange(uri) {
33798
+ const text3 = this.documents.getText(uri);
33799
+ if (text3 !== void 0 && !isMacroSource(uri)) {
33800
+ this.passages.rebuild(uri, text3);
33836
33801
  }
33837
- },
33838
- {
33839
- name: ["passage"],
33840
- validate(arg, _args, _index, state) {
33841
- let passageName;
33842
- if (arg.type === 0 /* String */) {
33843
- const target = passageTarget(arg.text);
33844
- if (target.kind === "name") passageName = target.name;
33845
- } else if (arg.type === 8 /* Bareword */) {
33846
- passageName = arg.text;
33847
- } else if (arg.type === 1 /* Number */) {
33848
- passageName = arg.text;
33849
- } else if (arg.type === 9 /* NaN */) {
33850
- passageName = "NaN";
33851
- }
33852
- if (passageName !== void 0) {
33853
- if (state && state.passages.length > 0) {
33854
- if (!state.passages.includes(passageName)) {
33855
- return new TypeWarning(`Nonexistent passage: "${passageName}"`);
33856
- }
33857
- }
33858
- return null;
33802
+ this.analysisPending = true;
33803
+ this.updateStoryFormat();
33804
+ this.emit("documentChanged", uri);
33805
+ this.emit("passagesUpdated", uri);
33806
+ this.scheduleModelReady();
33807
+ }
33808
+ /** Remove local indices immediately and schedule cross-document analysis. */
33809
+ handleDocumentClose(uri) {
33810
+ this.passages.remove(uri);
33811
+ this._variables.removeDocument(uri);
33812
+ this.analysisPending = true;
33813
+ this.updateStoryFormat();
33814
+ this.emit("documentClosed", uri);
33815
+ this.emit("passagesUpdated", uri);
33816
+ this.scheduleModelReady();
33817
+ }
33818
+ /** Rebuild all indices from scratch. */
33819
+ rebuildAll() {
33820
+ for (const uri of this.documents.getUris()) {
33821
+ const text3 = this.documents.getText(uri);
33822
+ if (text3 !== void 0 && !isMacroSource(uri)) {
33823
+ this.passages.rebuild(uri, text3);
33859
33824
  }
33860
- return new TypeError2("Argument is not an acceptable passage");
33861
33825
  }
33826
+ this.analysisPending = true;
33827
+ this.ensureCurrent();
33862
33828
  }
33863
- ];
33864
- LEX_LIMIT = 1e3;
33865
- wsRe = /\s/;
33866
- identStartRe = /[a-zA-Z]/;
33867
- identStopRe = /[|&+\-\s)(]/;
33868
- quoteRe = /['"]/;
33869
- digitRe = /[0-9]/;
33870
- PARSE_LIMIT = 2e3;
33871
- DESCRIBE_ALTERNATIVE_LIMIT = 16;
33872
- VALIDATE_LIMIT = 2e3;
33873
- CORRECT_TYPE_RANK = 1;
33874
- CORRECT_RANK = 2;
33875
- Parameters = class {
33876
- variants;
33877
- constructor(variants) {
33878
- this.variants = variants.map((s) => ({
33879
- formatString: s,
33880
- format: parseFormat(s)
33881
- }));
33829
+ /** Finish pending cross-document work once, before a feature reads the indices. */
33830
+ ensureCurrent() {
33831
+ if (!this.analysisPending) return;
33832
+ this.analysisPending = false;
33833
+ this.refreshDiscoveredMacros();
33834
+ this.cascade();
33835
+ this.updateStoryFormat();
33882
33836
  }
33883
33837
  /**
33884
- * Describe every argument position of every schema variant. Alternatives
33885
- * (`|`) and optional positions (`|+`) become separate sequences; an empty
33886
- * schema is the empty sequence. Variants whose format cannot be parsed
33887
- * contribute nothing.
33838
+ * Re-read the story format from the StoryData passages after the passage
33839
+ * index changed, emitting 'storyFormatChanged' if it differs. Listeners
33840
+ * that read the analysis indices finish pending analysis through getters.
33888
33841
  */
33889
- describe() {
33890
- const sequences = [];
33891
- for (const variant of this.variants) {
33892
- if (variant.format === null) sequences.push([]);
33893
- else sequences.push(...describeFormat(variant.format));
33894
- }
33895
- return sequences;
33842
+ updateStoryFormat() {
33843
+ const format = resolveStoryFormat(storyDataFormats(
33844
+ this.passages.getAllPassages(),
33845
+ (uri) => this.documents.getText(uri)
33846
+ ));
33847
+ if (format.name === this.format.name && format.isSpindle === this.format.isSpindle) return;
33848
+ this.format = format;
33849
+ this.emit("storyFormatChanged", format);
33896
33850
  }
33897
33851
  /**
33898
- * Validate an array of Arg tokens against these parameter variants.
33899
- * Selects the best-matching variant by rank.
33852
+ * Re-run static macro discovery over the workspace and replace the
33853
+ * registry's discovered tier (dropping definitions whose source is gone).
33854
+ *
33855
+ * Sources scanned for Story.defineMacro({...}) calls:
33856
+ * - `{do}` blocks in StoryInit passages
33857
+ * - passages tagged `script` (Story JavaScript)
33858
+ * - JavaScript/TypeScript documents in the workspace
33900
33859
  */
33901
- validate(args2, stateInfo) {
33902
- if (this.variants.length === 0) {
33903
- if (args2.length === 0) {
33904
- return { variantIndex: null, errors: [], warnings: [] };
33905
- }
33906
- return {
33907
- variantIndex: null,
33908
- errors: [{ message: "Expected no arguments", argIndex: 0 }],
33909
- warnings: []
33910
- };
33911
- }
33912
- let bestIndex = null;
33913
- let bestRank = -Infinity;
33914
- let bestErrors = [];
33915
- let bestWarnings = [];
33916
- let bestIsClean = false;
33917
- for (let i2 = 0; i2 < this.variants.length; i2++) {
33918
- const variant = this.variants[i2];
33919
- if (variant.format === null) {
33920
- if (args2.length === 0) {
33921
- if (!bestIsClean || 0 > bestRank) {
33922
- bestIndex = i2;
33923
- bestRank = 0;
33924
- bestErrors = [];
33925
- bestWarnings = [];
33926
- bestIsClean = true;
33927
- }
33928
- } else {
33929
- if (!bestIsClean && bestIndex === null) {
33930
- bestIndex = i2;
33931
- bestRank = -1;
33932
- bestErrors = [{ message: "Expected no arguments", argIndex: 0 }];
33933
- bestWarnings = [];
33934
- }
33935
- }
33860
+ refreshDiscoveredMacros() {
33861
+ const found = [];
33862
+ const init = [];
33863
+ for (const uri of this.documents.getUris().sort()) {
33864
+ const text3 = this.documents.getText(uri);
33865
+ if (!text3) continue;
33866
+ if (isMacroSource(uri)) {
33867
+ found.push(...discoverMacros(text3));
33936
33868
  continue;
33937
33869
  }
33938
- const result2 = crawlValidate(variant.format, args2, 0, stateInfo);
33939
- let errors = result2.errors;
33940
- if (result2.errors.length === 0 && result2.argIndex < args2.length) {
33941
- errors = [
33942
- {
33943
- message: `Too many arguments: expected ${result2.argIndex}, got ${args2.length}`,
33944
- argIndex: result2.argIndex
33945
- }
33946
- ];
33947
- }
33948
- const rank = result2.rank;
33949
- const isClean = errors.length === 0;
33950
- if (isClean && (!bestIsClean || rank > bestRank)) {
33951
- bestIndex = i2;
33952
- bestRank = rank;
33953
- bestErrors = [];
33954
- bestWarnings = result2.warnings;
33955
- bestIsClean = true;
33956
- } else if (!isClean && !bestIsClean && rank > bestRank) {
33957
- bestIndex = i2;
33958
- bestRank = rank;
33959
- bestErrors = errors;
33960
- bestWarnings = result2.warnings;
33961
- } else if (bestIndex === null) {
33962
- bestIndex = i2;
33963
- bestRank = rank;
33964
- bestErrors = errors;
33965
- bestWarnings = result2.warnings;
33966
- bestIsClean = isClean;
33870
+ let lines;
33871
+ for (const passage of this.passages.getPassagesInDocument(uri)) {
33872
+ const isStoryInit = passage.name === "StoryInit";
33873
+ const isScript = passage.tags?.includes("script") ?? false;
33874
+ if (!isStoryInit && !isScript) continue;
33875
+ lines ??= text3.split("\n");
33876
+ const content2 = lines.slice(passage.headerEnd.end.line + 1, passage.range.end.line + 1).join("\n");
33877
+ if (isScript) found.push(...discoverMacros(content2));
33878
+ else init.push(...storyInitMacros(content2));
33967
33879
  }
33968
33880
  }
33969
- return {
33970
- variantIndex: bestIndex,
33971
- errors: bestErrors,
33972
- warnings: bestWarnings
33973
- };
33881
+ this.macros.setDiscoveredMacros([...found, ...init]);
33974
33882
  }
33975
33883
  /**
33976
- * Whether all variants are empty (accept no arguments).
33884
+ * Cascade: rescan variables and widgets based on current passages.
33885
+ * Called after any passage index update.
33886
+ *
33887
+ * The markup of every document is read again (the macros may have changed),
33888
+ * the widgets are defined from it, and then the variable usages and widget
33889
+ * invocations are read from the same markup, which pairs its tags with the
33890
+ * widgets just defined.
33977
33891
  */
33978
- isEmpty() {
33979
- if (this.variants.length === 0) return true;
33980
- return !this.variants.some((v2) => v2.format !== null);
33892
+ cascade() {
33893
+ this.markup.invalidate();
33894
+ this.declaredVersion = storyDataFormatVersion(this.passages.getAllPassages(), (uri) => this.documents.getText(uri));
33895
+ this._target = resolveSpindleTarget(this.installedVersion, this.declaredVersion);
33896
+ const declarations = (name) => {
33897
+ const passage = this.passages.getPassage(name);
33898
+ return passage ? this.markup.get(passage.uri)?.passages.find((p) => p.passage === passage) : void 0;
33899
+ };
33900
+ const variables = declarations("StoryVariables");
33901
+ if (variables) this.variables.parseStoryVariables(variables.content, variables.passage.headerEnd.end.line + 1, variables.doc.uri);
33902
+ else this.variables.clearStoryVariables();
33903
+ const transients = declarations("StoryTransients");
33904
+ if (transients) this.variables.parseStoryTransients(transients.content, transients.passage.headerEnd.end.line + 1, transients.doc.uri);
33905
+ else this.variables.clearStoryTransients();
33906
+ const readDocuments = () => this.documents.getUris().flatMap((uri) => {
33907
+ const markup = isMacroSource(uri) ? void 0 : this.markup.get(uri);
33908
+ return markup ? [markup] : [];
33909
+ });
33910
+ const markups = /* @__PURE__ */ new Map();
33911
+ for (const doc of readDocuments()) for (const passage of doc.passages) markups.set(passage.passage, passage);
33912
+ this.widgets.scan(
33913
+ this.passages.getAllPassages().flatMap((passage) => markups.get(passage) ?? []),
33914
+ this.macros.toolingMacros()
33915
+ );
33916
+ this.markup.invalidate();
33917
+ this.widgets.clearInvocations();
33918
+ for (const doc of readDocuments()) {
33919
+ this.variables.scanDocument(doc.uri, doc.text, doc);
33920
+ this.widgets.recordInvocations(doc.uri, doc.passages);
33921
+ }
33922
+ }
33923
+ /** All runtime metadata, with real macros taking precedence over widgets. */
33924
+ toolingMacros() {
33925
+ const macros = this.macros.toolingMacros();
33926
+ return [...macros, ...this.widgets.getAllWidgets().filter((widget) => !this.macros.getMacro(widget.name)).map((widget) => ({ name: widget.name, block: widget.block, subMacros: [], widget: widget.tooling }))];
33927
+ }
33928
+ /** Coalesce analysis and diagnostics across a burst of document changes. */
33929
+ scheduleModelReady() {
33930
+ if (this.debounceTimer) {
33931
+ clearTimeout(this.debounceTimer);
33932
+ }
33933
+ this.debounceTimer = setTimeout(() => {
33934
+ this.debounceTimer = null;
33935
+ this.ensureCurrent();
33936
+ this.emit("modelReady");
33937
+ }, this.DEBOUNCE_MS);
33981
33938
  }
33982
33939
  };
33983
33940
  }
33984
33941
  });
33985
33942
 
33943
+ // src/core/diagnostic-codes.ts
33944
+ function getSeverity(code) {
33945
+ return severityMap[code];
33946
+ }
33947
+ var DiagnosticCode, severityMap;
33948
+ var init_diagnostic_codes = __esm({
33949
+ "src/core/diagnostic-codes.ts"() {
33950
+ "use strict";
33951
+ DiagnosticCode = {
33952
+ /** The Spindle the project targets is older than the oldest one supported. */
33953
+ UnsupportedSpindleVersion: "SP001",
33954
+ /** `unknown-macro`: a macro that is none of the built-in, configured or defined ones. */
33955
+ UndefinedMacro: "SP100",
33956
+ /** `unclosed-block`, `mismatched-closer`, `stray-closer` of a macro. */
33957
+ MalformedContainer: "SP101",
33958
+ /** Tags of an HTML element Spindle cannot read or pair. */
33959
+ MalformedElement: "SP102",
33960
+ /** A closing tag that closes no macro (`{/set}`), or is malformed (`{/}`, `{/if x}`). */
33961
+ IllegalClosingTag: "SP104",
33962
+ /** `unclosed-link`, `unclosed-expression`, `unclosed-macro`: a `[[`, `{$` or `{name` that is never ended. */
33963
+ UnclosedMarkup: "SP105",
33964
+ /** `code-syntax`: JavaScript that does not parse. */
33965
+ CodeSyntaxError: "SP106",
33966
+ /** `misplaced-branch` (`{else}` outside `{if}`), and a macro outside the parents it needs. */
33967
+ InvalidChildren: "SP107",
33968
+ /** A configured parameter schema rejects the arguments (macros Spindle declares nothing for). */
33969
+ ExpectedNoArguments: "SP108",
33970
+ /** `argument-error`: arguments that do not have their parameters' forms. */
33971
+ ParameterTypeError: "SP109",
33972
+ ParameterWarning: "SP110",
33973
+ TooManyArguments: "SP111",
33974
+ /** `unquoted-passage-name`: `{goto Kitchen}`, which throws a ReferenceError. */
33975
+ UnquotedPassageName: "SP113",
33976
+ ChildMaxExceeded: "SP114",
33977
+ ChildMinNotMet: "SP115",
33978
+ /** `undeclared-variable`. */
33979
+ UndeclaredVariable: "SP200",
33980
+ /** `primitive-field`: a field of a number, string or boolean. */
33981
+ PrimitiveFieldAccess: "SP201",
33982
+ NoStoryVariables: "SP202",
33983
+ /** `undeclared-transient`. */
33984
+ UndeclaredTransient: "SP203",
33985
+ TemporaryAssignedInLoop: "SP205",
33986
+ ArrayMemberAccess: "SP206",
33987
+ InvalidDeclaration: "SP207",
33988
+ /** `reserved-name`: a variable named `__proto__`. */
33989
+ ReservedVariableName: "SP208",
33990
+ /** `unknown-passage`. */
33991
+ BrokenPassageLink: "SP300",
33992
+ WidgetArgCountMismatch: "SP301",
33993
+ IncludeWidgetPassage: "SP302",
33994
+ UnusedWidget: "SP303",
33995
+ LinkRuntimeMismatch: "SP304",
33996
+ LiteralLinkInterpolation: "SP305",
33997
+ DeadEndPassage: "SP400",
33998
+ UnreachablePassage: "SP401",
33999
+ LineTooLong: "SP500"
34000
+ };
34001
+ severityMap = {
34002
+ SP001: "warning",
34003
+ SP100: "warning",
34004
+ SP101: "error",
34005
+ SP102: "error",
34006
+ SP104: "error",
34007
+ SP105: "error",
34008
+ SP106: "error",
34009
+ SP107: "error",
34010
+ SP108: "error",
34011
+ SP109: "error",
34012
+ SP110: "warning",
34013
+ SP111: "error",
34014
+ SP113: "error",
34015
+ SP114: "error",
34016
+ SP115: "error",
34017
+ SP200: "error",
34018
+ SP201: "error",
34019
+ SP202: "info",
34020
+ SP203: "warning",
34021
+ SP205: "warning",
34022
+ SP206: "warning",
34023
+ SP207: "error",
34024
+ SP208: "error",
34025
+ SP300: "warning",
34026
+ SP301: "warning",
34027
+ SP302: "warning",
34028
+ SP303: "hint",
34029
+ SP304: "warning",
34030
+ SP305: "warning",
34031
+ SP400: "hint",
34032
+ SP401: "hint",
34033
+ SP500: "warning"
34034
+ };
34035
+ }
34036
+ });
34037
+
34038
+ // src/core/markup/source-view.ts
34039
+ function viewIndex(view, offset2) {
34040
+ if (!view.sourceOffsets) return offset2 - view.offset;
34041
+ let low = 0;
34042
+ let high = view.sourceOffsets.length - 1;
34043
+ while (low < high) {
34044
+ const mid = Math.ceil((low + high) / 2);
34045
+ if (view.sourceOffsets[mid] <= offset2) low = mid;
34046
+ else high = mid - 1;
34047
+ }
34048
+ return low;
34049
+ }
34050
+ function textAt(passage, start, end) {
34051
+ return passage.pieces.filter((piece) => piece.kind === "text" && piece.offset <= start && pieceOffset(piece, piece.text.length) >= end).at(-1);
34052
+ }
34053
+ function sourceView(passage, start, end) {
34054
+ const owner = passage.pieces.filter((piece) => {
34055
+ const value = piece.kind === "text" ? piece.text : piece.kind === "code" ? piece.code : void 0;
34056
+ return value !== void 0 && piece.offset <= start && pieceOffset(piece, value.length) >= end;
34057
+ }).at(-1);
34058
+ if (owner?.kind === "code") return { ...owner, text: owner.code };
34059
+ return owner?.kind === "text" ? owner : { offset: 0, text: passage.content };
34060
+ }
34061
+ var init_source_view = __esm({
34062
+ "src/core/markup/source-view.ts"() {
34063
+ "use strict";
34064
+ init_tooling();
34065
+ }
34066
+ });
34067
+
33986
34068
  // src/core/parsing/link-runtime.ts
33987
34069
  function quoteArg2(value) {
33988
34070
  return `"${value.replace(/[\\"]/g, "\\$&")}"`;
@@ -34252,10 +34334,11 @@ function buildReport(documents, workspace) {
34252
34334
  const passages = [];
34253
34335
  for (const doc of documents) {
34254
34336
  for (const passage of doc.passages) {
34255
- if (!COMPILER_PASSAGES.has(passage.passage.name)) passages.push(passage);
34337
+ if (isRuntimePassage(passage.passage)) passages.push(passage);
34256
34338
  }
34257
34339
  }
34258
34340
  const markup = /* @__PURE__ */ new Map();
34341
+ const validationMacros = toolingMacros.map((macro) => workspace.macros.hasConfiguredParameters(macro.name) ? { ...macro, widget: void 0 } : macro);
34259
34342
  const found = validateStoryMarkup(
34260
34343
  passages.map((passage, index) => ({
34261
34344
  name: passage.passage.name,
@@ -34263,7 +34346,7 @@ function buildReport(documents, workspace) {
34263
34346
  tags: passage.passage.tags,
34264
34347
  metadata: { "data-source-file": String(index) }
34265
34348
  })),
34266
- toolingMacros,
34349
+ validationMacros,
34267
34350
  // Every malformed tag and pairing error, and the checks of the markup around them
34268
34351
  { tolerant: true }
34269
34352
  );
@@ -34607,7 +34690,7 @@ function isReadInBody(passage, bodyStart, bodyEnd, name, macroStart, macroEnd) {
34607
34690
  }
34608
34691
  function argumentDiagnostics(passage, workspace) {
34609
34692
  const diagnostics = [];
34610
- const passageNames = Array.from(workspace.passages.getAllPassages(), (candidate) => candidate.name);
34693
+ const passageNames = workspace.passages.getAllPassages().filter(isRuntimePassage).map((candidate) => candidate.name);
34611
34694
  for (const token of passage.isMarkup ? macroTokens(passage) : []) {
34612
34695
  if (token.type !== "macro" || token.isClose) continue;
34613
34696
  const info = workspace.macros.getMacro(token.name);
@@ -35135,12 +35218,12 @@ async function runCheck(args2) {
35135
35218
  return 0;
35136
35219
  }
35137
35220
  const workspace = new WorkspaceModel({ workspaceRoot: commonDir });
35138
- const workspaceContents = new Map(fileContents);
35139
- await addProjectMacroSources(workspaceContents, commonDir);
35140
- workspace.initialize(workspaceContents);
35141
35221
  if (Object.keys(projectConfig.macros).length > 0) {
35142
35222
  workspace.macros.loadConfig(projectConfig.macros);
35143
35223
  }
35224
+ const workspaceContents = new Map(fileContents);
35225
+ await addProjectMacroSources(workspaceContents, commonDir);
35226
+ workspace.initialize(workspaceContents);
35144
35227
  const diagOptions = {};
35145
35228
  if (options.maxLineLength !== null) {
35146
35229
  diagOptions.maxLineLength = options.maxLineLength;
@@ -69102,12 +69185,12 @@ async function createWorkspace(files, fileContents) {
69102
69185
  const configRoot = findConfigRoot(commonDir);
69103
69186
  const projectConfig = loadConfigFromDisk(configRoot);
69104
69187
  const workspace = new WorkspaceModel({ workspaceRoot: configRoot });
69105
- fileContents = new Map(fileContents);
69106
- await addProjectMacroSources(fileContents, commonDir);
69107
- workspace.initialize(fileContents);
69108
69188
  if (Object.keys(projectConfig.macros).length > 0) {
69109
69189
  workspace.macros.loadConfig(projectConfig.macros);
69110
69190
  }
69191
+ fileContents = new Map(fileContents);
69192
+ await addProjectMacroSources(fileContents, commonDir);
69193
+ workspace.initialize(fileContents);
69111
69194
  return workspace;
69112
69195
  }
69113
69196
  async function checkFiles(pattern, severity, cwd = process.cwd()) {
@@ -80456,6 +80539,15 @@ function computeRename(uri, position, newName, workspace) {
80456
80539
  if (workspace.passages.getPassage(newName)) {
80457
80540
  throw new RenameError(`Cannot rename to ${JSON.stringify(newName)}: a passage with that name already exists.`).at(uri, symbol2.range);
80458
80541
  }
80542
+ for (const documentUri of workspace.documents.getUris()) {
80543
+ const doc = workspace.markup.get(documentUri);
80544
+ if (!doc) continue;
80545
+ for (const head of documentMacroHeads(doc)) {
80546
+ if (!head.closing && workspace.macros.hasUnsafeConfiguredPassages(head.name)) {
80547
+ throw new RenameError(`Cannot rename: {${head.name}} has configured passage parameters whose positions are ambiguous, repeated or conflict with its runtime signature.`).at(documentUri, head.range);
80548
+ }
80549
+ }
80550
+ }
80459
80551
  const refEdits = [];
80460
80552
  for (const { uri: refUri, ref: ref2 } of findPassageRefs(symbol2.name, workspace)) {
80461
80553
  try {