@rohal12/spindle-lsp 0.15.14 → 0.15.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js 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.14";
58
+ VERSION = false ? pkg.version : "0.15.16";
59
59
  }
60
60
  });
61
61
 
@@ -10488,10 +10488,30 @@ var init_config_loader = __esm({
10488
10488
  }
10489
10489
  });
10490
10490
 
10491
+ // src/core/workspace/source-order.ts
10492
+ import { pathToFileURL } from "node:url";
10493
+ function compareSourceUris(left, right) {
10494
+ const a = left.split("/").map(decodeURIComponent);
10495
+ const b = right.split("/").map(decodeURIComponent);
10496
+ for (let i2 = 0; i2 < Math.min(a.length, b.length); i2++) {
10497
+ const order = Buffer.compare(Buffer.from(a[i2], "utf8"), Buffer.from(b[i2], "utf8"));
10498
+ if (order !== 0) return order;
10499
+ }
10500
+ return a.length - b.length;
10501
+ }
10502
+ function compareSourcePaths(left, right) {
10503
+ return compareSourceUris(pathToFileURL(left).href, pathToFileURL(right).href);
10504
+ }
10505
+ var init_source_order = __esm({
10506
+ "src/core/workspace/source-order.ts"() {
10507
+ "use strict";
10508
+ }
10509
+ });
10510
+
10491
10511
  // src/core/workspace/macro-sources.ts
10492
10512
  import { existsSync as existsSync2, lstatSync, readdir, readFileSync as readFileSync2 } from "node:fs";
10493
10513
  import { dirname, isAbsolute, join as join2, relative, resolve as resolve2, sep } from "node:path";
10494
- import { pathToFileURL } from "node:url";
10514
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
10495
10515
  function isExcludedSegment(segment) {
10496
10516
  return EXCLUDED_DIRS.includes(segment) || segment.startsWith(".") && segment !== "." && segment !== "..";
10497
10517
  }
@@ -10535,7 +10555,7 @@ async function findProjectFiles(root, pattern) {
10535
10555
  }
10536
10556
  }
10537
10557
  });
10538
- return files.filter((file2) => canDiscoverProjectPath(file2, root));
10558
+ return files.filter((file2) => canDiscoverProjectPath(file2, root)).sort(compareSourcePaths);
10539
10559
  }
10540
10560
  function isWithin(path2, dir) {
10541
10561
  const rel = relative(dir, path2);
@@ -10557,7 +10577,7 @@ function findProjectRoot(dir) {
10557
10577
  }
10558
10578
  async function addProjectMacroSources(contents, dir) {
10559
10579
  for (const filePath of await findMacroSourceFiles(findProjectRoot(dir))) {
10560
- const uri = pathToFileURL(filePath).toString();
10580
+ const uri = pathToFileURL2(filePath).toString();
10561
10581
  if (contents.has(uri)) continue;
10562
10582
  try {
10563
10583
  contents.set(uri, readFileSync2(filePath, "utf-8"));
@@ -10571,6 +10591,7 @@ var init_macro_sources = __esm({
10571
10591
  "use strict";
10572
10592
  init_index_min();
10573
10593
  init_config_loader();
10594
+ init_source_order();
10574
10595
  MACRO_SOURCE_PATTERN = /\.[cm]?[jt]s$/i;
10575
10596
  MACRO_SOURCE_GLOB = "**/*.{js,cjs,mjs,ts,cts,mts}";
10576
10597
  EXCLUDED_DIRS = ["node_modules", "dist", "build"];
@@ -10600,7 +10621,7 @@ async function resolveFiles(pattern, cwd) {
10600
10621
  return [...new Set(await findProjectFiles(directory, input))];
10601
10622
  }
10602
10623
  function readFileContents(files) {
10603
- return new Map(files.map((file2) => [file2, readFileSync3(file2, "utf8")]));
10624
+ return new Map([...files].sort(compareSourcePaths).map((file2) => [file2, readFileSync3(file2, "utf8")]));
10604
10625
  }
10605
10626
  async function resolveInputs(patterns, cwd) {
10606
10627
  const files = await Promise.all(patterns.map((pattern) => resolveFiles(pattern, cwd)));
@@ -10614,6 +10635,7 @@ var init_file_inputs = __esm({
10614
10635
  "src/core/workspace/file-inputs.ts"() {
10615
10636
  "use strict";
10616
10637
  init_macro_sources();
10638
+ init_source_order();
10617
10639
  }
10618
10640
  });
10619
10641
 
@@ -14645,6 +14667,31 @@ function parseWithAst(src, goal) {
14645
14667
  }
14646
14668
  }
14647
14669
  }
14670
+ function parseReferencePath(src) {
14671
+ const { ast } = parseWithAst(src, "expression");
14672
+ let node = ast.type === "ChainExpression" ? ast.expression : ast;
14673
+ let nested = false;
14674
+ while (node.type === "MemberExpression") {
14675
+ node = node.object;
14676
+ nested = true;
14677
+ }
14678
+ if (node.type !== "Identifier") return null;
14679
+ const sigil = sigilOf(node, src);
14680
+ if (!sigil || node.start !== 0) return null;
14681
+ return {
14682
+ sigil: sigil[0],
14683
+ name: sigil[1],
14684
+ nested
14685
+ };
14686
+ }
14687
+ function parseMemberIdentifier(src) {
14688
+ const { ast } = parseWithAst(`x.${src}`, "expression");
14689
+ if (ast.type !== "MemberExpression" || ast.computed) return null;
14690
+ const object3 = ast.object;
14691
+ const property = ast.property;
14692
+ if (object3.type !== "Identifier" || object3.start !== 0 || object3.end !== 1 || property.type !== "Identifier" || property.start !== 2 || property.end !== src.length + 2) return null;
14693
+ return property.name;
14694
+ }
14648
14695
  function parseAst(src, goal, percentAt, transientsRead) {
14649
14696
  try {
14650
14697
  const p = tokenizerAt(src, 0, goal);
@@ -15194,9 +15241,13 @@ function indexCodeEnds(input) {
15194
15241
  if (open !== void 0) possible.add(open);
15195
15242
  } else if (/['"`/\\]/.test(c)) uncertain();
15196
15243
  }
15197
- return (start) => {
15198
- if (start > lastBrace) return -1;
15199
- return possible.has(start - 1) ? defaultCodeEnd.closeBrace(input, start, start) : parsedCodeEnd(input, start, "expression");
15244
+ return (start, codeStart = start, limit = input.length) => {
15245
+ if (start > lastBrace || codeStart >= limit) return -1;
15246
+ const source = limit === input.length ? input : input.slice(0, limit);
15247
+ if (input[start - 1] !== "{" || possible.has(start - 1) || input.slice(start, codeStart).includes("{")) return defaultCodeEnd.closeBrace(source, codeStart, start);
15248
+ const base = Math.max(0, codeStart - 1);
15249
+ const end = parsedCodeEnd(source.slice(base), codeStart - base, "expression");
15250
+ return end < 0 ? -1 : base + end;
15200
15251
  };
15201
15252
  }
15202
15253
  function isCodeAttribute(name) {
@@ -15232,10 +15283,14 @@ function splitSigilTemplate(template, incomplete = false) {
15232
15283
  return parts;
15233
15284
  }
15234
15285
  function run$1(source, startRule, options, completion = false) {
15235
- const hooks = options.hooks ? {
15286
+ const hooks = {
15236
15287
  ...defaultHooks,
15237
15288
  ...options.hooks
15238
- } : defaultHooks;
15289
+ };
15290
+ if (!options.hooks?.closeBrace) {
15291
+ const codeEnd = indexCodeEnds(source);
15292
+ hooks.closeBrace = (_input, codeStart, lenientStart) => codeEnd(lenientStart, codeStart);
15293
+ }
15239
15294
  try {
15240
15295
  return peg$parse(source, {
15241
15296
  startRule,
@@ -15290,7 +15345,7 @@ function pairingError(position, error48) {
15290
15345
  const { line, column } = position(error48.start);
15291
15346
  return new MarkupError(error48.message, error48.start, line, column, error48.code, error48.end, error48.data);
15292
15347
  }
15293
- function tokenizeMarkup(source, options = {}) {
15348
+ function tokenizeMarkup$1(source, options = {}) {
15294
15349
  return run$1(source, "Tokens", options);
15295
15350
  }
15296
15351
  function parseSelectors(source, at3 = 0) {
@@ -15300,7 +15355,7 @@ function parseSelectors(source, at3 = 0) {
15300
15355
  end: at3 + found.end
15301
15356
  };
15302
15357
  }
15303
- function tokenizeMarkupTolerant(source, options = {}) {
15358
+ function tokenizeMarkupTolerant$1(source, options = {}) {
15304
15359
  const { tokens: tokens2, errors } = scanMarkupTolerant(source, options);
15305
15360
  return {
15306
15361
  tokens: tokens2,
@@ -15312,15 +15367,18 @@ function scanMarkupTolerant(source, options = {}, completion = false) {
15312
15367
  const errors = [];
15313
15368
  const damage = [];
15314
15369
  let position;
15370
+ const codeEnd = indexCodeEnds(source);
15371
+ const customCloseBrace = options.hooks?.closeBrace;
15315
15372
  const lastBrace = source.lastIndexOf("}");
15316
- const { closeBrace } = {
15317
- ...defaultHooks,
15318
- ...options.hooks
15319
- };
15320
15373
  const attempt = (text3, base2) => {
15321
15374
  const hooks = {
15322
15375
  ...options.hooks,
15323
- closeBrace: (input, codeStart, lenientStart) => base2 + Math.min(codeStart, lenientStart) > lastBrace ? -1 : closeBrace(input, codeStart, lenientStart)
15376
+ closeBrace: (input, codeStart, lenientStart) => {
15377
+ if (base2 + Math.min(codeStart, lenientStart) > lastBrace) return -1;
15378
+ if (customCloseBrace) return customCloseBrace(input, codeStart, lenientStart);
15379
+ const end = codeEnd(base2 + lenientStart, base2 + codeStart, base2 + text3.length);
15380
+ return end < 0 ? -1 : end - base2;
15381
+ }
15324
15382
  };
15325
15383
  try {
15326
15384
  return run$1(text3, "Tokens", {
@@ -15401,6 +15459,20 @@ function createListeners() {
15401
15459
  }
15402
15460
  };
15403
15461
  }
15462
+ function variableNameError(name, label2 = name) {
15463
+ return name === "__proto__" ? `"${label2}" cannot be used as a variable name (${RESERVED_NAME} is reserved)` : void 0;
15464
+ }
15465
+ function ownEntries(sources, keep = () => true) {
15466
+ const record2 = /* @__PURE__ */ Object.create(null);
15467
+ for (const source of sources) {
15468
+ if (!source) continue;
15469
+ for (const key of Object.keys(source)) {
15470
+ const value = source[key];
15471
+ if (keep(value)) record2[key] = value;
15472
+ }
15473
+ }
15474
+ return record2;
15475
+ }
15404
15476
  function isWhitespace(ch) {
15405
15477
  return /\s/.test(ch);
15406
15478
  }
@@ -15721,6 +15793,51 @@ function passageTarget(arg) {
15721
15793
  name
15722
15794
  };
15723
15795
  }
15796
+ function readBoundVariable(rawArgs) {
15797
+ const src = rawArgs.trim();
15798
+ const first = terms(src)[0];
15799
+ return first ? slice(src, first) : "";
15800
+ }
15801
+ function parseBoundVariable(rawArgs) {
15802
+ const term = readBoundVariable(rawArgs);
15803
+ if (!term) return null;
15804
+ const lead = rawArgs.length - rawArgs.trimStart().length;
15805
+ const offsets = [];
15806
+ for (let i2 = 0; i2 < term.length; i2++) if (term[i2] !== '"' && term[i2] !== "'") offsets.push(lead + i2);
15807
+ const firstToken = term.replace(/["']/g, "");
15808
+ const kind = {
15809
+ "%": "transient variables",
15810
+ _: "temporary variables",
15811
+ "@": "locals"
15812
+ }[firstToken.charAt(0)];
15813
+ if (kind) throw new Error(`${kind} (${firstToken}) cannot be bound to input macros`);
15814
+ const explicitSigil = firstToken.startsWith("$");
15815
+ if (!explicitSigil && firstToken.split(".")[0].includes("\\")) throw new Error("Escaped binding identifiers require a literal $ story-variable sigil");
15816
+ const segments = firstToken.replace(/^\$/, "").split(".").map((segment, index) => {
15817
+ if (!segment.includes("\\")) return segment;
15818
+ if (index > 0) {
15819
+ const name = parseMemberIdentifier(segment);
15820
+ if (name === null) throw new Error("Invalid bound identifier");
15821
+ return name;
15822
+ }
15823
+ const reference = parseReferencePath(`$${segment}`);
15824
+ if (!reference || reference.nested) throw new Error("Invalid bound identifier");
15825
+ return reference.name;
15826
+ });
15827
+ if (segments.includes("__proto__")) throw new Error(`"$${segments.join(".")}" cannot be bound: ${RESERVED_NAME} is reserved`);
15828
+ const rootStart = explicitSigil ? 1 : 0;
15829
+ const rootEnd = firstToken.indexOf(".", rootStart);
15830
+ const nameEnd = rootEnd < 0 ? firstToken.length : rootEnd;
15831
+ return {
15832
+ name: segments[0],
15833
+ path: segments.slice(1),
15834
+ explicitSigil,
15835
+ start: offsets[0] ?? lead,
15836
+ end: offsets.length ? offsets[offsets.length - 1] + 1 : lead,
15837
+ nameStart: offsets[rootStart] ?? lead + rootStart,
15838
+ nameEnd: offsets[nameEnd - 1] === void 0 ? lead : offsets[nameEnd - 1] + 1
15839
+ };
15840
+ }
15724
15841
  function macroArgumentSpans(rawArgs, parameters) {
15725
15842
  const spans = /* @__PURE__ */ new Map();
15726
15843
  parseMacroArgs(rawArgs, parameters, void 0, spans);
@@ -15745,20 +15862,6 @@ function macroArgumentSpans(rawArgs, parameters) {
15745
15862
  function subMacroParameters(name) {
15746
15863
  return SUB_MACRO_PARAMETERS.get(name.toLowerCase());
15747
15864
  }
15748
- function variableNameError(name, label2 = name) {
15749
- return name === "__proto__" ? `"${label2}" cannot be used as a variable name (${RESERVED_NAME} is reserved)` : void 0;
15750
- }
15751
- function ownEntries(sources, keep = () => true) {
15752
- const record2 = /* @__PURE__ */ Object.create(null);
15753
- for (const source of sources) {
15754
- if (!source) continue;
15755
- for (const key of Object.keys(source)) {
15756
- const value = source[key];
15757
- if (keep(value)) record2[key] = value;
15758
- }
15759
- }
15760
- return record2;
15761
- }
15762
15865
  function withParseCache(fn) {
15763
15866
  const outer = parses;
15764
15867
  parses = /* @__PURE__ */ new Map();
@@ -15794,8 +15897,11 @@ function widgetParameters(params, holds = {}) {
15794
15897
  ...hasOwn(holds, name) ? { holds: holds[name] } : {}
15795
15898
  }));
15796
15899
  }
15900
+ function usesBuiltinSemantics(name, parametersOf2) {
15901
+ return parametersOf2.widget?.(name) === void 0 && parametersOf2.source?.(name) !== "user";
15902
+ }
15797
15903
  function executesRawBody(name, parametersOf2) {
15798
- return isRawMacro(name, parametersOf2.widget?.(name) !== void 0) && parametersOf2.source?.(name) !== "user";
15904
+ return isRawMacro(name, false) && usesBuiltinSemantics(name, parametersOf2);
15799
15905
  }
15800
15906
  function withHolds(params, holds) {
15801
15907
  return params.map((param) => {
@@ -15816,12 +15922,13 @@ function parameterLookup(macros) {
15816
15922
  for (const { name, parameters: params, interpolate, widget, source } of macros) {
15817
15923
  sources.set(name, source ?? "user");
15818
15924
  if (params) parameters.set(name, withHolds(params, holdsByDefault(interpolate)));
15925
+ else parameters.delete(name);
15819
15926
  if (widget) widgets.set(name, widgetParameters(widget.params, widget.holds) ?? []);
15820
15927
  }
15821
15928
  const lookup = (name) => {
15822
15929
  if (widgets.has(name)) return void 0;
15823
15930
  let params = parameters.get(name);
15824
- if (!params) {
15931
+ if (!params && sources.get(name) !== "user") {
15825
15932
  const sub = subMacroParameters(name);
15826
15933
  if (sub) parameters.set(name, params = withHolds(sub, "text"));
15827
15934
  }
@@ -15897,7 +16004,7 @@ function* codeAndText(src, tokens2, parametersOf2) {
15897
16004
  continue;
15898
16005
  }
15899
16006
  const args2 = token.rawArgs;
15900
- if (PASSAGE_BODIES.has(name) && !widget) {
16007
+ if (PASSAGE_BODIES.has(name) && usesBuiltinSemantics(name, parametersOf2)) {
15901
16008
  const close = closeOfTextBody(t, name);
15902
16009
  if (close > t + 1) {
15903
16010
  const text3 = src.slice(token.end, tokens2[close].start);
@@ -15915,7 +16022,7 @@ function* codeAndText(src, tokens2, parametersOf2) {
15915
16022
  if (!args2) continue;
15916
16023
  const label2 = `{${token.name} ${args2}}`;
15917
16024
  const argsAt = token.argsStart;
15918
- if (CONDITION_MACROS.has(name) && !widget) {
16025
+ if (CONDITION_MACROS.has(name) && usesBuiltinSemantics(name, parametersOf2)) {
15919
16026
  yield {
15920
16027
  kind: "code",
15921
16028
  code: args2,
@@ -16179,7 +16286,7 @@ function locate(text3, part, from) {
16179
16286
  function completionContext(source, cursor, macros = []) {
16180
16287
  if (!Number.isInteger(cursor) || cursor < 0 || cursor > source.length) throw new RangeError("cursor must be a UTF-16 offset within source");
16181
16288
  const parametersOf2 = parameterLookup(macros);
16182
- const codeContext = (code, goal, map2) => {
16289
+ const codeContext = (code, goal, map2, containers) => {
16183
16290
  const probe = code.length + 1;
16184
16291
  let executable = false;
16185
16292
  lexJs(code + " __spindle_cursor__", {
@@ -16199,15 +16306,16 @@ function completionContext(source, cursor, macros = []) {
16199
16306
  goal,
16200
16307
  offset: offset2,
16201
16308
  cursor: code.length,
16309
+ ...containers.length ? { containers } : {},
16202
16310
  ...offsets.some((at3, i2) => at3 !== offset2 + i2) ? { sourceOffsets: offsets } : {}
16203
16311
  };
16204
16312
  };
16205
- const read = (src, map2, text3 = false, depth = 0, sigilsOnly = false) => {
16313
+ const read = (src, map2, text3 = false, depth = 0, sigilsOnly = false, containers = []) => {
16206
16314
  if (depth >= 64) return LITERAL;
16207
16315
  if (sigilsOnly) {
16208
16316
  let at3 = 0;
16209
16317
  for (const part of splitSigilTemplate(src, true)) if ("expr" in part) {
16210
- if (at3 + 1 + part.expr.length === src.length) return codeContext(part.expr, "expression", (i2) => map2(at3 + 1 + i2));
16318
+ if (at3 + 1 + part.expr.length === src.length) return codeContext(part.expr, "expression", (i2) => map2(at3 + 1 + i2), containers);
16211
16319
  at3 += part.expr.length + 2;
16212
16320
  } else at3 += ("text" in part ? part.text : part.verbatim).length;
16213
16321
  return LITERAL;
@@ -16218,13 +16326,21 @@ function completionContext(source, cursor, macros = []) {
16218
16326
  });
16219
16327
  const token = tokens2[tokens2.length - 1];
16220
16328
  if (!token || token.end !== src.length) return LITERAL;
16221
- const codeAt = (code, at3, goal = "expression") => codeContext(code, goal, (i2) => map2(at3 + i2));
16222
- const nested = (value, at3, offsets, onlySigils = false) => read(value, (i2) => map2(offsets?.[i2] ?? at3 + i2), true, depth + 1, onlySigils);
16329
+ const codeAt = (code, at3, goal = "expression") => codeContext(code, goal, (i2) => map2(at3 + i2), containers);
16330
+ const nested = (value, at3, offsets, onlySigils = false, container) => read(value, (i2) => map2(offsets?.[i2] ?? at3 + i2), true, depth + 1, onlySigils, container ? [...containers, container] : containers);
16223
16331
  if (token.type === "variable" && token.nameEnd === src.length) return codeAt(src.slice(token.nameStart - 1), token.nameStart - 1);
16224
16332
  if (token.type === "expression" && token.expressionEnd === src.length) return codeAt(token.expression, token.expressionStart);
16225
16333
  if (token.type === "html") {
16226
- for (const { name, value, at: at3 } of attributeValues(src, token)) if (at3 + value.length === src.length) return nested(value, at3, void 0, isCodeAttribute(name));
16227
- } else if (token.type === "link" && token.displayEnd === src.length) return nested(token.display, token.displayStart);
16334
+ for (const { name, value, at: at3 } of attributeValues(src, token)) if (at3 + value.length === src.length) {
16335
+ const span = token.attributeSpans.find((span2) => span2.valueStart === at3);
16336
+ return nested(value, at3, void 0, isCodeAttribute(name), {
16337
+ kind: "attribute",
16338
+ name,
16339
+ quote: span?.quote ?? null,
16340
+ code: isCodeAttribute(name)
16341
+ });
16342
+ }
16343
+ } else if (token.type === "link" && token.displayEnd === src.length) return nested(token.display, token.displayStart, void 0, false, { kind: "link-label" });
16228
16344
  else if (token.type === "text" && !token.comment) {
16229
16345
  const opener = tokens2[tokens2.length - 2];
16230
16346
  if (opener?.type === "macro" && !opener.isClose && executesRawBody(opener.name, parametersOf2)) return codeAt(token.value, token.start, "statements");
@@ -16232,7 +16348,7 @@ function completionContext(source, cursor, macros = []) {
16232
16348
  const name = token.name.toLowerCase();
16233
16349
  const args2 = token.rawArgs;
16234
16350
  const widget = parametersOf2.widget?.(name);
16235
- if (!widget && [
16351
+ if (usesBuiltinSemantics(name, parametersOf2) && [
16236
16352
  "if",
16237
16353
  "elseif",
16238
16354
  "case"
@@ -16246,9 +16362,14 @@ function completionContext(source, cursor, macros = []) {
16246
16362
  if (scanStringLiteral(raw2, 0).closed) return LITERAL;
16247
16363
  const value = escapes === "javascript" ? stringLiteralValue(raw2 + raw2[0]) : quoted.value;
16248
16364
  if (value == null) return LITERAL;
16365
+ const container = {
16366
+ kind: "string",
16367
+ quote: raw2[0],
16368
+ escapes
16369
+ };
16249
16370
  const offsets = escapeOffsets(raw2.slice(1), at4 + 1, value, escapes);
16250
- if (holds === "expression" || holds === "statements") return codeContext(value, holds, (i2) => map2(offsets?.[i2] ?? at4 + 1 + i2));
16251
- return holds === "markup" ? nested(value, at4 + 1, offsets) : LITERAL;
16371
+ if (holds === "expression" || holds === "statements") return codeContext(value, holds, (i2) => map2(offsets?.[i2] ?? at4 + 1 + i2), [...containers, container]);
16372
+ return holds === "markup" ? nested(value, at4 + 1, offsets, false, container) : LITERAL;
16252
16373
  };
16253
16374
  if (widget) {
16254
16375
  const spans2 = splitArgSpans(args2);
@@ -16368,7 +16489,7 @@ function widgetDefinitions$1(passages, parametersOf2 = registeredParameters) {
16368
16489
  if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
16369
16490
  const open = [];
16370
16491
  const done = [];
16371
- for (const token of tokenizeMarkupTolerant(passage.content, { hooks }).tokens) if (token.type === "macro" && token.name.toLowerCase() === "widget") {
16492
+ for (const token of tokenizeMarkupTolerant$1(passage.content, { hooks }).tokens) if (token.type === "macro" && token.name.toLowerCase() === "widget") {
16372
16493
  if (!token.isClose) open.push({
16373
16494
  token,
16374
16495
  isBlock: false
@@ -16507,7 +16628,7 @@ function validateMarkup(passages, options) {
16507
16628
  continue;
16508
16629
  }
16509
16630
  try {
16510
- const tokens2 = tokenizeMarkup(passage.content, { hooks });
16631
+ const tokens2 = tokenizeMarkup$1(passage.content, { hooks });
16511
16632
  addWidgets(tokens2);
16512
16633
  if (!reported) continue;
16513
16634
  parseMarkup(passage.content, { hooks });
@@ -16614,9 +16735,9 @@ function tokensOf(text3, textMode, { tolerant, parametersOf: parametersOf2 } = {
16614
16735
  text: textMode,
16615
16736
  hooks: parametersOf2 && { isRaw: (name) => isRawMacro(name, parametersOf2.widget?.(name) !== void 0) }
16616
16737
  };
16617
- if (tolerant) return tokenizeMarkupTolerant(text3, options).tokens;
16738
+ if (tolerant) return tokenizeMarkupTolerant$1(text3, options).tokens;
16618
16739
  try {
16619
- return tokenizeMarkup(text3, options);
16740
+ return tokenizeMarkup$1(text3, options);
16620
16741
  } catch (err) {
16621
16742
  if (err instanceof MarkupError) return [];
16622
16743
  throw err;
@@ -16846,6 +16967,29 @@ function scanSelectors(content2, token, at3, emit, options) {
16846
16967
  storeVarMacros: NO_STORE_VAR_MACROS
16847
16968
  });
16848
16969
  }
16970
+ function boundReference(token, at3, all) {
16971
+ try {
16972
+ const binding = parseBoundVariable(token.rawArgs);
16973
+ if (!binding || !all && !binding.explicitSigil) return void 0;
16974
+ const mapped = {
16975
+ ...binding,
16976
+ start: at3(token.argsStart + binding.start),
16977
+ end: at3(token.argsStart + binding.end),
16978
+ nameStart: at3(token.argsStart + binding.nameStart),
16979
+ nameEnd: at3(token.argsStart + binding.nameEnd)
16980
+ };
16981
+ return {
16982
+ sigil: "$",
16983
+ name: binding.name,
16984
+ path: binding.path,
16985
+ start: mapped.start,
16986
+ end: mapped.end,
16987
+ binding: mapped
16988
+ };
16989
+ } catch {
16990
+ return;
16991
+ }
16992
+ }
16849
16993
  function collectTokenRefs(content2, tokens2, at3, emit, options) {
16850
16994
  const { storeVarMacros, parametersOf: parametersOf2 } = options;
16851
16995
  for (let t = 0; t < tokens2.length; t++) {
@@ -16873,24 +17017,12 @@ function collectTokenRefs(content2, tokens2, at3, emit, options) {
16873
17017
  else scanInterpolations(value, valueIn, emit, options);
16874
17018
  }
16875
17019
  else if (token.type === "macro" && !token.isClose) {
16876
- scanArgs(token, parametersOf2, at3, emit, options);
17020
+ const binding = storeVarMacros.has(token.name.toLowerCase()) && !parametersOf2.widget?.(token.name) ? boundReference(token, at3, options.all) : void 0;
17021
+ scanArgs(token, parametersOf2, at3, (ref2) => {
17022
+ if (!binding || ref2.start < binding.start || ref2.start >= binding.end) emit(ref2);
17023
+ }, options);
16877
17024
  scanSelectors(content2, token, at3, emit, options);
16878
- if (storeVarMacros.has(token.name.toLowerCase()) && !parametersOf2.widget?.(token.name)) {
16879
- const lead = token.rawArgs.length - token.rawArgs.trimStart().length;
16880
- const first = token.rawArgs.trim().split(/\s+/)[0] ?? "";
16881
- const quoted = QUOTED_VAR_ARG_RE.exec(first);
16882
- if (quoted) {
16883
- const [name, ...path2] = quoted[1].split(".");
16884
- const start = token.argsStart + lead + 1;
16885
- emit({
16886
- sigil: "$",
16887
- name,
16888
- path: path2,
16889
- start: at3(start),
16890
- end: at3(start + 1 + quoted[1].length)
16891
- });
16892
- } else if (!/^["'`]/.test(first)) scanCode(first, (i2) => at3(token.argsStart + lead + i2), emit);
16893
- }
17025
+ if (binding) emit(binding);
16894
17026
  if (executesRawBody(token.name, parametersOf2)) {
16895
17027
  let close = t + 1;
16896
17028
  while (close < tokens2.length) {
@@ -20983,7 +21115,7 @@ function scanSpindleMarkup(text3) {
20983
21115
  if (inAttribute) match2.inAttribute = true;
20984
21116
  tokens2.push(match2);
20985
21117
  };
20986
- for (const t of tokenizeMarkupTolerant(text3).tokens) if (t.type === "html") {
21118
+ for (const t of tokenizeMarkupTolerant$1(text3).tokens) if (t.type === "html") {
20987
21119
  tags.push({
20988
21120
  start: t.start,
20989
21121
  end: t.end
@@ -20992,7 +21124,7 @@ function scanSpindleMarkup(text3) {
20992
21124
  if (attribute.valueStart === void 0 || attribute.valueEnd === void 0) continue;
20993
21125
  const value = text3.slice(attribute.valueStart, attribute.valueEnd);
20994
21126
  if (!value.includes("{")) continue;
20995
- for (const inner of tokenizeMarkupTolerant(value, { text: true }).tokens) if (inner.type === "macro" || inner.type === "variable" || inner.type === "expression") push2({
21127
+ for (const inner of tokenizeMarkupTolerant$1(value, { text: true }).tokens) if (inner.type === "macro" || inner.type === "variable" || inner.type === "expression") push2({
20996
21128
  start: attribute.valueStart + inner.start,
20997
21129
  end: attribute.valueStart + inner.end
20998
21130
  }, true);
@@ -21145,7 +21277,7 @@ function replaceSpindleTokens(html) {
21145
21277
  }
21146
21278
  function closedMacroNames(text3) {
21147
21279
  const found = /* @__PURE__ */ new Set();
21148
- for (const t of tokenizeMarkupTolerant(text3).tokens) if (t.type === "macro" && t.isClose) found.add(t.name.toLowerCase());
21280
+ for (const t of tokenizeMarkupTolerant$1(text3).tokens) if (t.type === "macro" && t.isClose) found.add(t.name.toLowerCase());
21149
21281
  return found;
21150
21282
  }
21151
21283
  function defaultIsBlock(text3, extra = []) {
@@ -21398,7 +21530,7 @@ function macroTokensByLine(lines) {
21398
21530
  offset2 += line2.length + 1;
21399
21531
  }
21400
21532
  let line = 0;
21401
- for (const token of tokenizeMarkupTolerant(lines.join("\n")).tokens) {
21533
+ for (const token of tokenizeMarkupTolerant$1(lines.join("\n")).tokens) {
21402
21534
  if (token.type !== "macro") continue;
21403
21535
  while (line + 1 < lines.length && starts[line + 1] <= token.start) line++;
21404
21536
  const lineStart = starts[line];
@@ -21462,7 +21594,7 @@ function verbatimLines(lines) {
21462
21594
  function wordWrap(text3, maxLen, indent) {
21463
21595
  const lines = [];
21464
21596
  const inToken = new Array(text3.length).fill(false);
21465
- for (const token of tokenizeMarkupTolerant(text3).tokens) {
21597
+ for (const token of tokenizeMarkupTolerant$1(text3).tokens) {
21466
21598
  if (token.type === "text") continue;
21467
21599
  inToken.fill(true, token.start, token.end);
21468
21600
  }
@@ -21491,6 +21623,23 @@ function wordWrap(text3, maxLen, indent) {
21491
21623
  }
21492
21624
  return lines;
21493
21625
  }
21626
+ function tokenizationOptions(options) {
21627
+ if (!options.macros) return options;
21628
+ const macros = [...options.macros];
21629
+ const parametersOf2 = parameterLookup(macros);
21630
+ return {
21631
+ ...options,
21632
+ hooks: {
21633
+ isRaw: (name) => isRawMacro(name, parametersOf2.widget?.(name) !== void 0),
21634
+ isBlock: blockLookup(macros),
21635
+ branchParents: (name) => branchParents(name, macros),
21636
+ ...options.hooks
21637
+ }
21638
+ };
21639
+ }
21640
+ function tokenizeMarkupTolerant(source, options = {}) {
21641
+ return tokenizeMarkupTolerant$1(source, tokenizationOptions(options));
21642
+ }
21494
21643
  function validateStoryMarkup(passages, macros, options = {}) {
21495
21644
  const list3 = [...passages];
21496
21645
  const all = [...macros];
@@ -21538,10 +21687,12 @@ function widgetDefinitions(passages, macros = []) {
21538
21687
  }
21539
21688
  function referenceOptions(macros, all = false, passages = []) {
21540
21689
  const list3 = [...macros];
21690
+ const effective = new NameMap();
21691
+ for (const macro of list3) effective.set(macro.name, macro);
21541
21692
  const widgets = effectiveWidgetDefinitions(widgetDefinitions$1(passages, parameterLookup(list3)));
21542
21693
  return {
21543
21694
  all,
21544
- storeVarMacros: new Set(list3.filter((m) => m.storeVar).map((m) => m.name.toLowerCase())),
21695
+ storeVarMacros: new Set([...effective.values()].filter((m) => m.storeVar).map((m) => m.name.toLowerCase())),
21545
21696
  parametersOf: parameterLookup([...list3, ...definedWidgetMacros(widgets)]),
21546
21697
  tolerant: true
21547
21698
  };
@@ -21553,7 +21704,7 @@ function validateVariableReferences(passages, declarations, macros = []) {
21553
21704
  const list3 = [...passages];
21554
21705
  return checkVariableReferences(list3.filter((p) => !p.tags?.some((t) => NOT_MARKUP_TAGS.includes(t))), declarations, referenceOptions(macros, false, list3));
21555
21706
  }
21556
- var __defProp2, __exportAll, SIGIL_SCOPES, SIGIL_CHARS, grammar_shared_default, LB, RB, SIGIL_SCOPES$1, withSelectors, VOID_TAGS, BRANCH_PARENT$1, LINK_SEPARATORS, NO_SELECTORS, EMAIL_AUTOLINK, peg$SyntaxError, NameMap, NameSet, BLOCK_MACROS, BLOCK_WIDGETS, WIDGETS, DEFINED_BLOCK_MACROS, RAW_BODY_MACROS, BRANCHING, quoteArg, PARAMETER_TYPES, STRING_HOLDS, metadataRegistry, companionParents, last$1, BRANCH_PARENT, lowerName, nameOf, closerOf, closes, astralIdentifierCodes, astralIdentifierStartCodes, nonASCIIidentifierChars, nonASCIIidentifierStartChars, reservedWords, ecma5AndLessKeywords, keywords$1, keywordRelationalOperator, nonASCIIidentifierStart, nonASCIIidentifier, TokenType, beforeExpr, startsExpr, keywords, types$1, lineBreak, lineBreakG, nonASCIIwhitespace, skipWhiteSpace, ref, hasOwnProperty$1, toString, hasOwn$1, isArray, regexpCache, loneSurrogate, Position, SourceLocation, defaultOptions, warnedAboutEcmaVersion, SCOPE_TOP, SCOPE_FUNCTION, SCOPE_ASYNC, SCOPE_GENERATOR, SCOPE_ARROW, SCOPE_SIMPLE_CATCH, SCOPE_SUPER, SCOPE_DIRECT_SUPER, SCOPE_CLASS_STATIC_BLOCK, SCOPE_CLASS_FIELD_INIT, SCOPE_SWITCH, SCOPE_VAR, BIND_NONE, BIND_VAR, BIND_LEXICAL, BIND_FUNCTION, BIND_SIMPLE_CATCH, BIND_OUTSIDE, Parser, prototypeAccessors, pp$9, literal, DestructuringErrors, pp$8, loopLabel, switchLabel, empty$1, FUNC_STATEMENT, FUNC_HANGING_STATEMENT, FUNC_NULLABLE_ID, pp$7, TokContext, types, pp$6, pp$5, empty, pp$4, pp$3, Scope, Node, pp$2, scriptValuesAddedInUnicode, ecma9BinaryProperties, ecma10BinaryProperties, ecma11BinaryProperties, ecma12BinaryProperties, ecma13BinaryProperties, unicodeBinaryProperties, unicodeBinaryPropertiesOfStrings, unicodeGeneralCategoryValues, ecma9ScriptValues, ecma10ScriptValues, ecma11ScriptValues, ecma12ScriptValues, ecma13ScriptValues, unicodeScriptValues, data, ecmaVersion, i, list$2, pp$1, BranchID, RegExpValidationState, CharSetNone, CharSetOk, CharSetString, Token, pp, INVALID_TEMPLATE_ESCAPE_ERROR, OPTIONS, AT_NAME_RE, TRANS_NAME_RE, SIGIL_IDENT_RE, LINE_BREAK_RE, NOT_A_BODY_RE, OPERAND_ENDS, SigilParser, OPENERS, CLOSERS$1, CodeSyntaxError, last, CLOSER, isNode, NAMESPACE_NAME, WORD_RE$2, FLAGS_RE, WORDS_BEFORE_STRING, NON_STRING_QUOTE_PREFIX, memoInput, memo, defaultCodeEnd, defaultHooks, MarkupError, OFFSET_FIELD, listeners, TRAILING_OPERATOR_RE, slice, MacroArgumentError, WORD_RE$1, NUMBER_RE$1, WORD_OR_PUNCT_RE, LEADING_OPERATOR_RE, takesRest, SUB_MACRO_PARAMETERS, RESERVED_NAME, isArrayIndex, hasOwn, createNamespace, parses, CONDITION_MACROS, PASSAGE_BODIES, holdsByDefault, JS_ESCAPE_RE, textPiece, LITERAL, registeredParameters, WIDGET_PARAMETERS, NOT_MARKUP, NOT_MARKUP_TAGS$1, BRANCH_MACROS, didYouMean, PASSAGE_ARGUMENT_HINT, BARE_WORD_RE, NUMBER_RE, IDENT_RE, KEY_RE, ARROW_RE, LINE_END_RE, ARRAY_INDEX_RE, UNSUPPORTED_UNDEFINED, Reader, VAR_PATH_RE, QUOTED_VAR_ARG_RE, VALID_VAR_TYPES, PRIMITIVE_SAMPLES, UNSUPPORTED_TYPE, NO_STORE_VAR_MACROS, FIELD_PATH_RE, CLOSERS, BLOCK_WORDS, NOT_PARAMETERS, DECLARATIONS, WRITE_RE, MUTATORS, MUTATING_CALL_RE, BEFORE_BINDING, isWordChar, Bindings, CALL_RE, IDENTIFIER_ARGUMENT_RE, WORD_RE, MODIFIER_RE, KEY_START_RE, CONFIG_KEYS, ConfigReader, stringOf, booleanOf, textOf, stringsOf, VOID_ELEMENTS, characterEntities, own, hasOwnProperty, asciiAlpha, asciiAlphanumeric, asciiAtext, asciiDigit, asciiHexDigit, asciiPunctuation, unicodePunctuation, unicodeWhitespace, content$1, EditMap$1, document$1, containerConstruct, attention, autolink, blankLine, blockQuote, characterEscape, characterReference, nonLazyContinuation, codeFenced, codeIndented, furtherStart, codeText, SpliceBuffer, content, continuationConstruct, definition, titleBefore, hardBreakEscape, headingAtx, htmlBlockNames, htmlRawNames, htmlFlow, blankLineBefore, nonLazyContinuationStart, htmlText, labelEnd, resourceConstruct, referenceFullConstruct, referenceCollapsedConstruct, labelStartImage, labelStartLink, lineEnding, thematicBreak, list, listItemPrefixWhitespaceConstruct, indentConstruct, setextUnderline, flow$1, resolver, string$1, text$1, constructs_exports, document, contentInitial, flowInitial, flow, string, text, insideSpan, attentionMarkers, disable, search, alignment, EditMap, DEFAULT_PRETTIER_OPTIONS, SVG_PLACEHOLDERS, SPINDLE_PLACEHOLDERS, PLACEHOLDER_PATTERN, DEFAULT_DEDENTING, WHITESPACE_AS_WRITTEN, LIST_ITEM_REGEX, CODE_FENCE_REGEX, NO_WRAP, NOT_MARKUP_TAGS;
21707
+ var __defProp2, __exportAll, NameMap, NameSet, SIGIL_SCOPES, SIGIL_CHARS, grammar_shared_default, LB, RB, SIGIL_SCOPES$1, withSelectors, VOID_TAGS, BRANCH_PARENT$1, LINK_SEPARATORS, NO_SELECTORS, EMAIL_AUTOLINK, peg$SyntaxError, BLOCK_MACROS, BLOCK_WIDGETS, WIDGETS, DEFINED_BLOCK_MACROS, RAW_BODY_MACROS, BRANCHING, quoteArg, PARAMETER_TYPES, STRING_HOLDS, metadataRegistry, companionParents, last$1, BRANCH_PARENT, lowerName, nameOf, closerOf, closes, astralIdentifierCodes, astralIdentifierStartCodes, nonASCIIidentifierChars, nonASCIIidentifierStartChars, reservedWords, ecma5AndLessKeywords, keywords$1, keywordRelationalOperator, nonASCIIidentifierStart, nonASCIIidentifier, TokenType, beforeExpr, startsExpr, keywords, types$1, lineBreak, lineBreakG, nonASCIIwhitespace, skipWhiteSpace, ref, hasOwnProperty$1, toString, hasOwn$1, isArray, regexpCache, loneSurrogate, Position, SourceLocation, defaultOptions, warnedAboutEcmaVersion, SCOPE_TOP, SCOPE_FUNCTION, SCOPE_ASYNC, SCOPE_GENERATOR, SCOPE_ARROW, SCOPE_SIMPLE_CATCH, SCOPE_SUPER, SCOPE_DIRECT_SUPER, SCOPE_CLASS_STATIC_BLOCK, SCOPE_CLASS_FIELD_INIT, SCOPE_SWITCH, SCOPE_VAR, BIND_NONE, BIND_VAR, BIND_LEXICAL, BIND_FUNCTION, BIND_SIMPLE_CATCH, BIND_OUTSIDE, Parser, prototypeAccessors, pp$9, literal, DestructuringErrors, pp$8, loopLabel, switchLabel, empty$1, FUNC_STATEMENT, FUNC_HANGING_STATEMENT, FUNC_NULLABLE_ID, pp$7, TokContext, types, pp$6, pp$5, empty, pp$4, pp$3, Scope, Node, pp$2, scriptValuesAddedInUnicode, ecma9BinaryProperties, ecma10BinaryProperties, ecma11BinaryProperties, ecma12BinaryProperties, ecma13BinaryProperties, unicodeBinaryProperties, unicodeBinaryPropertiesOfStrings, unicodeGeneralCategoryValues, ecma9ScriptValues, ecma10ScriptValues, ecma11ScriptValues, ecma12ScriptValues, ecma13ScriptValues, unicodeScriptValues, data, ecmaVersion, i, list$2, pp$1, BranchID, RegExpValidationState, CharSetNone, CharSetOk, CharSetString, Token, pp, INVALID_TEMPLATE_ESCAPE_ERROR, OPTIONS, AT_NAME_RE, TRANS_NAME_RE, SIGIL_IDENT_RE, LINE_BREAK_RE, NOT_A_BODY_RE, OPERAND_ENDS, SigilParser, OPENERS, CLOSERS$1, CodeSyntaxError, last, CLOSER, isNode, NAMESPACE_NAME, WORD_RE$2, FLAGS_RE, WORDS_BEFORE_STRING, NON_STRING_QUOTE_PREFIX, memoInput, memo, defaultCodeEnd, defaultHooks, MarkupError, OFFSET_FIELD, listeners, RESERVED_NAME, isArrayIndex, hasOwn, createNamespace, TRAILING_OPERATOR_RE, slice, MacroArgumentError, WORD_RE$1, NUMBER_RE$1, WORD_OR_PUNCT_RE, LEADING_OPERATOR_RE, takesRest, SUB_MACRO_PARAMETERS, parses, CONDITION_MACROS, PASSAGE_BODIES, holdsByDefault, JS_ESCAPE_RE, textPiece, LITERAL, registeredParameters, WIDGET_PARAMETERS, NOT_MARKUP, NOT_MARKUP_TAGS$1, BRANCH_MACROS, didYouMean, PASSAGE_ARGUMENT_HINT, BARE_WORD_RE, NUMBER_RE, IDENT_RE, KEY_RE, ARROW_RE, LINE_END_RE, ARRAY_INDEX_RE, UNSUPPORTED_UNDEFINED, Reader, VAR_PATH_RE, VALID_VAR_TYPES, PRIMITIVE_SAMPLES, UNSUPPORTED_TYPE, NO_STORE_VAR_MACROS, FIELD_PATH_RE, CLOSERS, BLOCK_WORDS, NOT_PARAMETERS, DECLARATIONS, WRITE_RE, MUTATORS, MUTATING_CALL_RE, BEFORE_BINDING, isWordChar, Bindings, CALL_RE, IDENTIFIER_ARGUMENT_RE, WORD_RE, MODIFIER_RE, KEY_START_RE, CONFIG_KEYS, ConfigReader, stringOf, booleanOf, textOf, stringsOf, VOID_ELEMENTS, characterEntities, own, hasOwnProperty, asciiAlpha, asciiAlphanumeric, asciiAtext, asciiDigit, asciiHexDigit, asciiPunctuation, unicodePunctuation, unicodeWhitespace, content$1, EditMap$1, document$1, containerConstruct, attention, autolink, blankLine, blockQuote, characterEscape, characterReference, nonLazyContinuation, codeFenced, codeIndented, furtherStart, codeText, SpliceBuffer, content, continuationConstruct, definition, titleBefore, hardBreakEscape, headingAtx, htmlBlockNames, htmlRawNames, htmlFlow, blankLineBefore, nonLazyContinuationStart, htmlText, labelEnd, resourceConstruct, referenceFullConstruct, referenceCollapsedConstruct, labelStartImage, labelStartLink, lineEnding, thematicBreak, list, listItemPrefixWhitespaceConstruct, indentConstruct, setextUnderline, flow$1, resolver, string$1, text$1, constructs_exports, document, contentInitial, flowInitial, flow, string, text, insideSpan, attentionMarkers, disable, search, alignment, EditMap, DEFAULT_PRETTIER_OPTIONS, SVG_PLACEHOLDERS, SPINDLE_PLACEHOLDERS, PLACEHOLDER_PATTERN, DEFAULT_DEDENTING, WHITESPACE_AS_WRITTEN, LIST_ITEM_REGEX, CODE_FENCE_REGEX, NO_WRAP, NOT_MARKUP_TAGS;
21557
21708
  var init_story_variables = __esm({
21558
21709
  "node_modules/@rohal12/spindle/dist/pkg/story-variables.js"() {
21559
21710
  __defProp2 = Object.defineProperty;
@@ -21566,6 +21717,38 @@ var init_story_variables = __esm({
21566
21717
  if (!no_symbols) __defProp2(target, Symbol.toStringTag, { value: "Module" });
21567
21718
  return target;
21568
21719
  };
21720
+ NameMap = class extends Map {
21721
+ get(name) {
21722
+ return super.get(name.toLowerCase());
21723
+ }
21724
+ set(name, value) {
21725
+ return super.set(name.toLowerCase(), value);
21726
+ }
21727
+ has(name) {
21728
+ return super.has(name.toLowerCase());
21729
+ }
21730
+ delete(name) {
21731
+ return super.delete(name.toLowerCase());
21732
+ }
21733
+ };
21734
+ NameSet = class {
21735
+ constructor(names = []) {
21736
+ this.names = new NameMap();
21737
+ for (const name of names) this.add(name);
21738
+ }
21739
+ add(name) {
21740
+ this.names.set(name, true);
21741
+ }
21742
+ has(name) {
21743
+ return this.names.has(name);
21744
+ }
21745
+ delete(name) {
21746
+ this.names.delete(name);
21747
+ }
21748
+ clear() {
21749
+ this.names.clear();
21750
+ }
21751
+ };
21569
21752
  SIGIL_SCOPES = {
21570
21753
  $: "variable",
21571
21754
  _: "temporary",
@@ -21700,38 +21883,6 @@ var init_story_variables = __esm({
21700
21883
  return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
21701
21884
  }
21702
21885
  };
21703
- NameMap = class extends Map {
21704
- get(name) {
21705
- return super.get(name.toLowerCase());
21706
- }
21707
- set(name, value) {
21708
- return super.set(name.toLowerCase(), value);
21709
- }
21710
- has(name) {
21711
- return super.has(name.toLowerCase());
21712
- }
21713
- delete(name) {
21714
- return super.delete(name.toLowerCase());
21715
- }
21716
- };
21717
- NameSet = class {
21718
- constructor(names = []) {
21719
- this.names = new NameMap();
21720
- for (const name of names) this.add(name);
21721
- }
21722
- add(name) {
21723
- this.names.set(name, true);
21724
- }
21725
- has(name) {
21726
- return this.names.has(name);
21727
- }
21728
- delete(name) {
21729
- this.names.delete(name);
21730
- }
21731
- clear() {
21732
- this.names.clear();
21733
- }
21734
- };
21735
21886
  BLOCK_MACROS = new NameSet([
21736
21887
  "if",
21737
21888
  "for",
@@ -26999,6 +27150,11 @@ var init_story_variables = __esm({
26999
27150
  OFFSET_FIELD = /^(start|end|[A-Za-z]*(Start|End))$/;
27000
27151
  listeners = createListeners();
27001
27152
  listeners.subscribe;
27153
+ RESERVED_NAME = "__proto__";
27154
+ isArrayIndex = (key) => /^(?:0|[1-9]\d*)$/.test(key);
27155
+ hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
27156
+ createNamespace = (...sources) => ownEntries(sources);
27157
+ Object.freeze(createNamespace());
27002
27158
  TRAILING_OPERATOR_RE = /(?:[-+*/%&|^!~=<>?:,.]|(?:^|[^\w$@.])(?:typeof|void|new|delete|in|instanceof|await|yield))$/;
27003
27159
  slice = (src, [start, end]) => src.slice(start, end);
27004
27160
  MacroArgumentError = class extends Error {
@@ -27018,11 +27174,6 @@ var init_story_variables = __esm({
27018
27174
  holds: "text",
27019
27175
  required: true
27020
27176
  }]]]);
27021
- RESERVED_NAME = "__proto__";
27022
- isArrayIndex = (key) => /^(?:0|[1-9]\d*)$/.test(key);
27023
- hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
27024
- createNamespace = (...sources) => ownEntries(sources);
27025
- Object.freeze(createNamespace());
27026
27177
  parses = null;
27027
27178
  CONDITION_MACROS = /* @__PURE__ */ new Set([
27028
27179
  "if",
@@ -27435,7 +27586,6 @@ var init_story_variables = __esm({
27435
27586
  }
27436
27587
  };
27437
27588
  VAR_PATH_RE = /([$%])(\w+(?:\.\w+)*)/y;
27438
- QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
27439
27589
  VALID_VAR_TYPES = /* @__PURE__ */ new Set([
27440
27590
  "number",
27441
27591
  "string",
@@ -31487,7 +31637,7 @@ var init_tooling = __esm({
31487
31637
  // src/core/workspace/document-store.ts
31488
31638
  import { EventEmitter } from "node:events";
31489
31639
  import { readFileSync as readFileSync4 } from "node:fs";
31490
- import { pathToFileURL as pathToFileURL2 } from "node:url";
31640
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
31491
31641
  function stripBom(text3) {
31492
31642
  return text3.charCodeAt(0) === 65279 ? text3.slice(1) : text3;
31493
31643
  }
@@ -31535,7 +31685,7 @@ var init_document_store = __esm({
31535
31685
  for (const filePath of paths) {
31536
31686
  try {
31537
31687
  const text3 = readFileSync4(filePath, "utf-8");
31538
- const uri = pathToFileURL2(filePath).href;
31688
+ const uri = pathToFileURL3(filePath).href;
31539
31689
  this.open(uri, text3);
31540
31690
  } catch {
31541
31691
  }
@@ -31549,7 +31699,10 @@ var init_document_store = __esm({
31549
31699
  function buildLineStarts(text3) {
31550
31700
  const starts = [0];
31551
31701
  for (let i2 = 0; i2 < text3.length; i2++) {
31552
- if (text3[i2] === "\n") starts.push(i2 + 1);
31702
+ if (text3[i2] === "\r") {
31703
+ if (text3[i2 + 1] === "\n") i2++;
31704
+ starts.push(i2 + 1);
31705
+ } else if (text3[i2] === "\n") starts.push(i2 + 1);
31553
31706
  }
31554
31707
  return starts;
31555
31708
  }
@@ -31574,7 +31727,7 @@ function withoutCr(line) {
31574
31727
  return line.endsWith("\r") ? line.slice(0, -1) : line;
31575
31728
  }
31576
31729
  function splitLines(text3) {
31577
- return text3.split("\n").map(withoutCr);
31730
+ return text3.split(/\r\n|[\r\n]/);
31578
31731
  }
31579
31732
  var init_text = __esm({
31580
31733
  "src/core/text.ts"() {
@@ -31586,31 +31739,31 @@ var init_text = __esm({
31586
31739
  import { createRequire as createCompilerRequire } from "node:module";
31587
31740
  function parsePassageHeader(rawLine, lineNumber) {
31588
31741
  const line = withoutCr(rawLine);
31589
- const bomLength = lineNumber === 0 && line.charCodeAt(0) === 65279 ? 1 : 0;
31742
+ const bomLength = /^\uFEFF*/.exec(line)[0].length;
31590
31743
  const escaped = line.replace(/\\./g, "ec");
31591
- const prefix = /^::\s*/.exec(escaped.slice(bomLength));
31744
+ const prefix = new RegExp("^::\\p{White_Space}*", "u").exec(escaped.slice(bomLength));
31592
31745
  if (!prefix) return null;
31593
31746
  const nameStart = bomLength + prefix[0].length;
31594
31747
  const delimiter = /[\[\]{}]/g;
31595
31748
  delimiter.lastIndex = nameStart;
31596
31749
  const first = delimiter.exec(escaped);
31597
31750
  const boundary = first?.index ?? line.length;
31598
- const nameEnd = escaped.slice(0, boundary).trimEnd().length;
31599
- const name = line.slice(nameStart, nameEnd).replace(/\\(.|$)/g, "$1").trim();
31751
+ const nameEnd = escaped.slice(0, boundary).replace(new RegExp("\\p{White_Space}+$", "u"), "").length;
31752
+ const name = line.slice(nameStart, nameEnd).replace(/\\(.|$)/g, "$1").replace(new RegExp("^\\p{White_Space}+|\\p{White_Space}+$", "gu"), "");
31600
31753
  if (!name) return null;
31601
31754
  let rest = boundary;
31602
31755
  let tags = [];
31603
31756
  if (escaped[rest] === "[") {
31604
31757
  const end = escaped.indexOf("]", rest + 1);
31605
31758
  if (end === -1 || /[\[{}]/.test(escaped.slice(rest + 1, end))) return null;
31606
- const decoded = line.slice(rest + 1, end).replace(/\\(.)/g, "$1").trim();
31607
- tags = decoded ? decoded.split(/\s+/) : [];
31759
+ const decoded = line.slice(rest + 1, end).replace(/\\(.)/g, "$1");
31760
+ tags = decoded.match(/[^\p{White_Space}]+/gu) ?? [];
31608
31761
  rest = end + 1;
31609
- while (rest < line.length && /\s/.test(escaped[rest])) rest++;
31762
+ while (rest < line.length && new RegExp("\\p{White_Space}", "u").test(escaped[rest])) rest++;
31610
31763
  }
31611
31764
  let meta3;
31612
31765
  if (rest < line.length) {
31613
- const raw = line.slice(rest).trim();
31766
+ const raw = line.slice(rest).replace(new RegExp("\\p{White_Space}+$", "u"), "");
31614
31767
  if (escaped[rest] !== "{" || !raw.endsWith("}")) return null;
31615
31768
  try {
31616
31769
  meta3 = JSON.parse(raw);
@@ -31694,21 +31847,13 @@ var init_passage_parser = __esm({
31694
31847
 
31695
31848
  // src/core/workspace/passage-index.ts
31696
31849
  import { createRequire as createSettingsRequire } from "node:module";
31697
- function compareSourceUris(left, right) {
31698
- const a = left.split("/").map(decodeURIComponent);
31699
- const b = right.split("/").map(decodeURIComponent);
31700
- for (let i2 = 0; i2 < Math.min(a.length, b.length); i2++) {
31701
- const order = Buffer.compare(Buffer.from(a[i2], "utf8"), Buffer.from(b[i2], "utf8"));
31702
- if (order !== 0) return order;
31703
- }
31704
- return a.length - b.length;
31705
- }
31706
31850
  var PassageIndex;
31707
31851
  var init_passage_index = __esm({
31708
31852
  "src/core/workspace/passage-index.ts"() {
31709
31853
  "use strict";
31710
31854
  init_passage_parser();
31711
31855
  init_text();
31856
+ init_source_order();
31712
31857
  PassageIndex = class {
31713
31858
  /** Per-URI list of passages. */
31714
31859
  byUri = /* @__PURE__ */ new Map();
@@ -32334,20 +32479,18 @@ var init_parameter_validator = __esm({
32334
32479
  // and reads the rest as a dot path into the story variables.
32335
32480
  name: ["receiver"],
32336
32481
  validate(arg) {
32337
- let name;
32338
- if (arg.type === 0 /* String */) {
32339
- name = arg.text.slice(1, -1);
32340
- } else if (arg.type === 8 /* Bareword */) {
32341
- name = arg.text;
32342
- }
32343
- if (name === void 0) {
32482
+ if (arg.type !== 0 /* String */ && arg.type !== 8 /* Bareword */) {
32344
32483
  return new TypeError2("Argument is not a variable receiver");
32345
32484
  }
32346
- if (/^[_@%]/.test(name)) {
32347
- return new TypeError2(`Variable receiver must be a story variable ($name), got '${name}'`);
32348
- }
32349
- if (!/^(?:\$\w|[A-Za-z_])[\w$]*(?:\.[\w$]+)*$/.test(name)) {
32350
- return new TypeError2(`Argument is not a valid variable receiver: '${name}'`);
32485
+ try {
32486
+ const binding = parseBoundVariable(arg.text);
32487
+ if (!binding) return new TypeError2("Argument is not a variable receiver");
32488
+ const root = `${binding.explicitSigil ? "$" : ""}${binding.name}`;
32489
+ if (!/^(?:\$\w|[A-Za-z_])[\w$]*$/.test(root)) {
32490
+ return new TypeError2(`Argument is not a valid variable receiver: '${root}'`);
32491
+ }
32492
+ } catch (error48) {
32493
+ return new TypeError2(`Invalid story variable receiver: ${error48 instanceof Error ? error48.message : String(error48)}`);
32351
32494
  }
32352
32495
  return null;
32353
32496
  }
@@ -32779,7 +32922,8 @@ function collectVariableReferences2(markup) {
32779
32922
  name: ref2.name,
32780
32923
  path: [ref2.name, ...ref2.path].join("."),
32781
32924
  start: ref2.start,
32782
- end: ref2.end
32925
+ end: ref2.end,
32926
+ binding: ref2.binding
32783
32927
  }));
32784
32928
  }
32785
32929
  var init_executable_refs = __esm({
@@ -32805,13 +32949,13 @@ var init_passage_markup = __esm({
32805
32949
  this.bodyEnd = bodyEnd;
32806
32950
  const raw = doc.text.slice(bodyStart, bodyEnd);
32807
32951
  this.isMarkup = isMarkupPassage(passage);
32808
- if (raw.includes("\r\n")) {
32952
+ if (raw.includes("\r")) {
32809
32953
  const map2 = [];
32810
32954
  let content2 = "";
32811
32955
  for (let i2 = 0; i2 < raw.length; i2++) {
32812
32956
  if (raw[i2] === "\r" && raw[i2 + 1] === "\n") continue;
32813
32957
  map2.push(bodyStart + i2);
32814
- content2 += raw[i2];
32958
+ content2 += raw[i2] === "\r" ? "\n" : raw[i2];
32815
32959
  }
32816
32960
  map2.push(bodyEnd);
32817
32961
  this.content = content2;
@@ -32829,7 +32973,7 @@ var init_passage_markup = __esm({
32829
32973
  content;
32830
32974
  /** Whether Spindle reads the body as story markup (see isMarkupPassage). */
32831
32975
  isMarkup;
32832
- /** For each `content` offset, its offset in the document; null when the body has no CRLF. */
32976
+ /** For each `content` offset, its offset in the document; null when the body has no CR. */
32833
32977
  toDoc;
32834
32978
  cachedTokens;
32835
32979
  cachedPairing;
@@ -32870,7 +33014,7 @@ var init_passage_markup = __esm({
32870
33014
  }
32871
33015
  /** The flat tokens and the malformed tags; none for a passage that is not markup. */
32872
33016
  get tokenization() {
32873
- return this.cachedTokens ??= this.isMarkup ? tokenizeMarkupTolerant(this.content) : { tokens: [], errors: [] };
33017
+ return this.cachedTokens ??= this.isMarkup ? tokenizeMarkupTolerant(this.content, { macros: this.doc.context.macros }) : { tokens: [], errors: [] };
32874
33018
  }
32875
33019
  get tokens() {
32876
33020
  return this.tokenization.tokens;
@@ -33390,6 +33534,20 @@ var init_markup_index = __esm({
33390
33534
  }
33391
33535
  });
33392
33536
 
33537
+ // src/core/workspace/story-data.ts
33538
+ import { createRequire as createCompilerRequire2 } from "node:module";
33539
+ function readStoryData(content2) {
33540
+ const { StoryBuilder } = createCompilerRequire2(import.meta.url)("@rohal12/twee-ts");
33541
+ const builder = new StoryBuilder();
33542
+ builder.add({ name: "StoryData", tags: [], text: content2 }, []);
33543
+ return builder.build().twine2;
33544
+ }
33545
+ var init_story_data = __esm({
33546
+ "src/core/workspace/story-data.ts"() {
33547
+ "use strict";
33548
+ }
33549
+ });
33550
+
33393
33551
  // src/core/workspace/spindle-version.ts
33394
33552
  import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
33395
33553
  import { dirname as dirname2, join as join3, resolve as resolve4 } from "node:path";
@@ -33500,31 +33658,14 @@ function isSpindleFormatName(name) {
33500
33658
  return name.trim().toLowerCase() === "spindle";
33501
33659
  }
33502
33660
  function readStoryDataFormat(content2) {
33503
- let data2;
33504
- try {
33505
- data2 = JSON.parse(content2);
33506
- } catch {
33507
- return void 0;
33508
- }
33509
- if (typeof data2 !== "object" || data2 === null || Array.isArray(data2)) return void 0;
33510
- const format = data2.format;
33511
- if (typeof format !== "string") return void 0;
33512
- return format.trim() || void 0;
33661
+ return readStoryData(content2).format.trim() || void 0;
33513
33662
  }
33514
33663
  function readStoryDataFormatVersion(content2) {
33515
- let data2;
33516
- try {
33517
- data2 = JSON.parse(content2);
33518
- } catch {
33664
+ const metadata2 = readStoryData(content2);
33665
+ if (metadata2.format.trim() && !isSpindleFormatName(metadata2.format)) {
33519
33666
  return void 0;
33520
33667
  }
33521
- if (typeof data2 !== "object" || data2 === null || Array.isArray(data2)) return void 0;
33522
- const record2 = data2;
33523
- if (typeof record2.format === "string" && record2.format.trim() && !isSpindleFormatName(record2.format)) {
33524
- return void 0;
33525
- }
33526
- const version2 = record2["format-version"];
33527
- return typeof version2 === "string" && version2.trim() ? version2.trim() : void 0;
33668
+ return metadata2.formatVersion.trim() || void 0;
33528
33669
  }
33529
33670
  function resolveStoryFormat(declared) {
33530
33671
  let foreign;
@@ -33536,20 +33677,22 @@ function resolveStoryFormat(declared) {
33536
33677
  return foreign === void 0 ? UNDECLARED_STORY_FORMAT : { name: foreign, isSpindle: false };
33537
33678
  }
33538
33679
  function passageContent(passage, text3) {
33539
- return text3.split("\n").slice(passage.headerEnd.end.line + 1, passage.range.end.line + 1).join("\n");
33680
+ return splitLines(text3).slice(passage.headerEnd.end.line + 1, passage.range.end.line + 1).join("\n");
33681
+ }
33682
+ function effectiveStoryData(passages) {
33683
+ const passage = [...passages].filter((p) => p.name === "StoryData").at(-1);
33684
+ return passage ? [passage] : [];
33540
33685
  }
33541
33686
  function storyDataFormats(passages, getText) {
33542
33687
  const formats = [];
33543
- for (const passage of passages) {
33544
- if (passage.name !== "StoryData") continue;
33688
+ for (const passage of effectiveStoryData(passages)) {
33545
33689
  const text3 = getText(passage.uri);
33546
33690
  formats.push(text3 === void 0 ? void 0 : readStoryDataFormat(passageContent(passage, text3)));
33547
33691
  }
33548
33692
  return formats;
33549
33693
  }
33550
33694
  function storyDataFormatVersion(passages, getText) {
33551
- for (const passage of passages) {
33552
- if (passage.name !== "StoryData") continue;
33695
+ for (const passage of effectiveStoryData(passages)) {
33553
33696
  const text3 = getText(passage.uri);
33554
33697
  if (text3 === void 0) continue;
33555
33698
  const version2 = readStoryDataFormatVersion(passageContent(passage, text3));
@@ -33557,15 +33700,19 @@ function storyDataFormatVersion(passages, getText) {
33557
33700
  }
33558
33701
  return void 0;
33559
33702
  }
33560
- function storyFormatOfTexts(texts) {
33703
+ function indexStoryDataTexts(texts) {
33561
33704
  const index = new PassageIndex();
33562
33705
  const byUri = /* @__PURE__ */ new Map();
33563
33706
  for (const text3 of texts) {
33564
33707
  if (!text3.includes("StoryData")) continue;
33565
- const uri = `story-format:${byUri.size}`;
33708
+ const uri = `story-format:${String(byUri.size).padStart(16, "0")}`;
33566
33709
  byUri.set(uri, text3);
33567
33710
  index.rebuild(uri, text3);
33568
33711
  }
33712
+ return { index, byUri };
33713
+ }
33714
+ function storyFormatOfTexts(texts) {
33715
+ const { index, byUri } = indexStoryDataTexts(texts);
33569
33716
  const formats = storyDataFormats(index.getAllPassages(), (uri) => byUri.get(uri));
33570
33717
  return { ...resolveStoryFormat(formats), hasStoryData: formats.length > 0 };
33571
33718
  }
@@ -33583,14 +33730,7 @@ async function findStoryFormat(texts, dir) {
33583
33730
  return { name, isSpindle };
33584
33731
  }
33585
33732
  function storyFormatVersionOfTexts(texts) {
33586
- const index = new PassageIndex();
33587
- const byUri = /* @__PURE__ */ new Map();
33588
- for (const text3 of texts) {
33589
- if (!text3.includes("StoryData")) continue;
33590
- const uri = `story-format:${byUri.size}`;
33591
- byUri.set(uri, text3);
33592
- index.rebuild(uri, text3);
33593
- }
33733
+ const { index, byUri } = indexStoryDataTexts(texts);
33594
33734
  return storyDataFormatVersion(index.getAllPassages(), (uri) => byUri.get(uri));
33595
33735
  }
33596
33736
  async function findSpindleTarget(texts, dir) {
@@ -33617,8 +33757,10 @@ var UNDECLARED_STORY_FORMAT;
33617
33757
  var init_story_format = __esm({
33618
33758
  "src/core/workspace/story-format.ts"() {
33619
33759
  "use strict";
33760
+ init_text();
33620
33761
  init_document_store();
33621
33762
  init_passage_index();
33763
+ init_story_data();
33622
33764
  init_macro_sources();
33623
33765
  init_spindle_version();
33624
33766
  UNDECLARED_STORY_FORMAT = { name: void 0, isSpindle: true };
@@ -35515,7 +35657,7 @@ __export(check_exports, {
35515
35657
  runCheck: () => runCheck
35516
35658
  });
35517
35659
  import { resolve as resolve5 } from "node:path";
35518
- import { pathToFileURL as pathToFileURL3 } from "node:url";
35660
+ import { pathToFileURL as pathToFileURL4 } from "node:url";
35519
35661
  function parseArgs(args2) {
35520
35662
  const options = {
35521
35663
  format: "pretty",
@@ -35628,7 +35770,7 @@ async function runCheck(args2) {
35628
35770
  return 1;
35629
35771
  }
35630
35772
  }
35631
- const fileContents = new Map([...contents].map(([path2, text3]) => [pathToFileURL3(path2).toString(), text3]));
35773
+ const fileContents = new Map([...contents].map(([path2, text3]) => [pathToFileURL4(path2).toString(), text3]));
35632
35774
  const storyFormat = await findStoryFormat(fileContents.values(), commonDir);
35633
35775
  if (!storyFormat.isSpindle) {
35634
35776
  console.error(skippedFormatNote(storyFormat));
@@ -35696,7 +35838,7 @@ var init_check = __esm({
35696
35838
 
35697
35839
  // src/plugins/format/segment.ts
35698
35840
  function splitPassages(text3) {
35699
- const lines = text3.split("\n");
35841
+ const lines = splitLines(text3);
35700
35842
  if (lines.length > 0 && lines[lines.length - 1] === "") {
35701
35843
  lines.pop();
35702
35844
  }
@@ -35744,6 +35886,7 @@ function headerTags(header) {
35744
35886
  var init_segment = __esm({
35745
35887
  "src/plugins/format/segment.ts"() {
35746
35888
  "use strict";
35889
+ init_text();
35747
35890
  init_passage_parser();
35748
35891
  }
35749
35892
  });
@@ -37735,14 +37878,15 @@ function buildDefaultIsBlock(text3) {
37735
37878
  async function formatDocument(text3, options) {
37736
37879
  if (text3.charCodeAt(0) === 65279) return "\uFEFF" + await formatDocument(text3.slice(1), options);
37737
37880
  const eol = dominantEol2(text3);
37738
- const output = await formatLf2(text3.replace(/\r\n/g, "\n"), options);
37739
- return eol === "\r\n" ? output.replace(/\n/g, "\r\n") : output;
37881
+ const output = await formatLf2(splitLines(text3).join("\n"), options);
37882
+ return output.replace(/\n/g, eol);
37740
37883
  }
37741
37884
  function dominantEol2(text3) {
37742
- const crlf = (text3.match(/\r\n/g) ?? []).length;
37743
- const lf = (text3.match(/\n/g) ?? []).length - crlf;
37744
- if (crlf !== lf) return crlf > lf ? "\r\n" : "\n";
37745
- return text3.indexOf("\r\n") !== -1 && text3.indexOf("\r\n") < text3.indexOf("\n") ? "\r\n" : "\n";
37885
+ const counts = /* @__PURE__ */ new Map();
37886
+ for (const [ending] of text3.matchAll(/\r\n|[\r\n]/g)) {
37887
+ counts.set(ending, (counts.get(ending) ?? 0) + 1);
37888
+ }
37889
+ return [...counts].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "\n";
37746
37890
  }
37747
37891
  async function formatLf2(text3, options) {
37748
37892
  const isBlock = options?.isBlock ?? buildDefaultIsBlock(text3);
@@ -37793,6 +37937,7 @@ var DEFAULT_DEDENTING2, BUILTIN_CONTAINERS, formatPlugin;
37793
37937
  var init_format = __esm({
37794
37938
  "src/plugins/format.ts"() {
37795
37939
  "use strict";
37940
+ init_text();
37796
37941
  init_tooling();
37797
37942
  init_passage_parser();
37798
37943
  init_segment();
@@ -37816,7 +37961,7 @@ var init_format = __esm({
37816
37961
  if (text3 === void 0) return [];
37817
37962
  const formatted = await formatDocument(text3, { ...formatOpts, filePath: params.textDocument.uri });
37818
37963
  if (formatted === text3) return [];
37819
- const lines = text3.split("\n");
37964
+ const lines = splitLines(text3);
37820
37965
  return [{
37821
37966
  range: {
37822
37967
  start: { line: 0, character: 0 },
@@ -37834,7 +37979,7 @@ var init_format = __esm({
37834
37979
  };
37835
37980
  const formatted = await formatRange(text3, range2, { ...formatOpts, filePath: params.textDocument.uri });
37836
37981
  if (formatted === text3) return [];
37837
- const lines = text3.split("\n");
37982
+ const lines = splitLines(text3);
37838
37983
  return [{
37839
37984
  range: {
37840
37985
  start: { line: 0, character: 0 },
@@ -69579,7 +69724,7 @@ __export(server_exports, {
69579
69724
  startMcpServer: () => startMcpServer
69580
69725
  });
69581
69726
  import { writeFileSync as writeFileSync2 } from "node:fs";
69582
- import { pathToFileURL as pathToFileURL4 } from "node:url";
69727
+ import { pathToFileURL as pathToFileURL5 } from "node:url";
69583
69728
  function filterBySeverity2(diagnostics, minSeverity) {
69584
69729
  const minIdx = SEVERITY_ORDER2.indexOf(minSeverity);
69585
69730
  return diagnostics.filter((d) => {
@@ -69588,7 +69733,7 @@ function filterBySeverity2(diagnostics, minSeverity) {
69588
69733
  });
69589
69734
  }
69590
69735
  function readFiles(files) {
69591
- return new Map([...readFileContents(files)].map(([path2, text3]) => [pathToFileURL4(path2).toString(), text3]));
69736
+ return new Map([...readFileContents(files)].map(([path2, text3]) => [pathToFileURL5(path2).toString(), text3]));
69592
69737
  }
69593
69738
  async function skippedReason(fileContents, files) {
69594
69739
  const format = await findStoryFormat(fileContents.values(), commonDirectory(files));
@@ -69624,7 +69769,7 @@ async function checkProject(pattern, severity, cwd = process.cwd()) {
69624
69769
  try {
69625
69770
  const results = [];
69626
69771
  for (const filePath of files) {
69627
- const uri = pathToFileURL4(filePath).toString();
69772
+ const uri = pathToFileURL5(filePath).toString();
69628
69773
  let diags = computeDiagnostics(uri, workspace);
69629
69774
  if (severity) {
69630
69775
  diags = filterBySeverity2(diags, severity);
@@ -69656,7 +69801,7 @@ async function formatFiles(pattern, cwd = process.cwd()) {
69656
69801
  let unchanged = 0;
69657
69802
  const changedFiles = [];
69658
69803
  for (const filePath of files) {
69659
- const text3 = contents.get(pathToFileURL4(filePath).toString());
69804
+ const text3 = contents.get(pathToFileURL5(filePath).toString());
69660
69805
  if (text3 === void 0) continue;
69661
69806
  try {
69662
69807
  const result2 = await formatDocument(text3, { filePath });
@@ -69682,7 +69827,7 @@ async function checkFormatting(pattern, cwd = process.cwd()) {
69682
69827
  const needsFormatting = [];
69683
69828
  const alreadyFormatted = [];
69684
69829
  for (const filePath of files) {
69685
- const text3 = contents.get(pathToFileURL4(filePath).toString());
69830
+ const text3 = contents.get(pathToFileURL5(filePath).toString());
69686
69831
  if (text3 === void 0) continue;
69687
69832
  try {
69688
69833
  const result2 = await formatDocument(text3, { filePath });
@@ -79994,6 +80139,26 @@ var init_story_format_gate = __esm({
79994
80139
  }
79995
80140
  });
79996
80141
 
80142
+ // src/core/parsing/js-string-literal.ts
80143
+ function encodeStringLiteralBody(value, quote) {
80144
+ let out = "";
80145
+ for (let i2 = 0; i2 < value.length; i2++) {
80146
+ const ch = value[i2];
80147
+ if (ch === "\\" || ch === quote) out += "\\" + ch;
80148
+ else if (ch === "\n") out += "\\n";
80149
+ else if (ch === "\r") out += "\\r";
80150
+ else if (ch === "\u2028") out += "\\u2028";
80151
+ else if (ch === "\u2029") out += "\\u2029";
80152
+ else out += ch;
80153
+ }
80154
+ return out;
80155
+ }
80156
+ var init_js_string_literal = __esm({
80157
+ "src/core/parsing/js-string-literal.ts"() {
80158
+ "use strict";
80159
+ }
80160
+ });
80161
+
79997
80162
  // src/core/markup/tree.ts
79998
80163
  function* walkNodes(nodes) {
79999
80164
  for (const node of nodes) {
@@ -80247,7 +80412,7 @@ function getCompletions(uri, position, triggerChar, workspace) {
80247
80412
  ...item,
80248
80413
  textEdit: {
80249
80414
  range: passage.range(start, end),
80250
- newText: identifier ? item.label : encodeEnclosingMarkup(passage, start, end, `[${JSON.stringify(item.label)}]`)
80415
+ newText: identifier ? item.label : encodeCompletion(`[${JSON.stringify(item.label)}]`, context.containers)
80251
80416
  }
80252
80417
  };
80253
80418
  });
@@ -80270,6 +80435,12 @@ function getCompletions(uri, position, triggerChar, workspace) {
80270
80435
  }
80271
80436
  return [];
80272
80437
  }
80438
+ function encodeCompletion(spelling, containers = []) {
80439
+ for (const container of [...containers].reverse()) {
80440
+ if (container.kind === "string") spelling = encodeStringLiteralBody(spelling, container.quote);
80441
+ }
80442
+ return spelling;
80443
+ }
80273
80444
  function getClosingMacroCompletions(cursor, brace) {
80274
80445
  const names = [...new Set(cursor.openBlocks(brace).map((macro) => macro.name))];
80275
80446
  return names.map((name, idx) => ({
@@ -80362,7 +80533,7 @@ var init_completions = __esm({
80362
80533
  "use strict";
80363
80534
  init_tooling();
80364
80535
  init_widgets();
80365
- init_passage_refs();
80536
+ init_js_string_literal();
80366
80537
  init_link_runtime();
80367
80538
  init_passage_parser();
80368
80539
  init_markup_cursor();
@@ -80719,7 +80890,7 @@ var init_macro_heads = __esm({
80719
80890
  function getDefinition(uri, position, workspace) {
80720
80891
  const text3 = workspace.documents.getText(uri);
80721
80892
  if (text3 === void 0 || !workspace.hasPassages(uri)) return null;
80722
- const lines = text3.split("\n");
80893
+ const lines = splitLines(text3);
80723
80894
  if (position.line >= lines.length) return null;
80724
80895
  const passageResult = getPassageRefDefinition(uri, position, workspace);
80725
80896
  if (passageResult) return passageResult;
@@ -80731,7 +80902,7 @@ function getPassageRefDefinition(uri, position, workspace) {
80731
80902
  const doc = workspace.markup.get(uri);
80732
80903
  const ref2 = doc && passageRefAt(doc, position);
80733
80904
  if (!ref2) return null;
80734
- const passage = workspace.passages.getPassage(ref2.name);
80905
+ const passage = workspace.passages.getEffectivePassage(ref2.name);
80735
80906
  if (!passage) return null;
80736
80907
  return {
80737
80908
  uri: passage.uri,
@@ -80756,6 +80927,7 @@ var definitionPlugin;
80756
80927
  var init_definition = __esm({
80757
80928
  "src/plugins/definition.ts"() {
80758
80929
  "use strict";
80930
+ init_text();
80759
80931
  init_macro_heads();
80760
80932
  init_passage_refs();
80761
80933
  definitionPlugin = {
@@ -80785,7 +80957,7 @@ var init_definition = __esm({
80785
80957
  function findReferences(uri, position, workspace, includeDeclaration) {
80786
80958
  const text3 = workspace.documents.getText(uri);
80787
80959
  if (text3 === void 0 || !workspace.hasPassages(uri)) return [];
80788
- const lines = text3.split("\n");
80960
+ const lines = splitLines(text3);
80789
80961
  if (position.line >= lines.length) return [];
80790
80962
  const line = lines[position.line];
80791
80963
  const header = parsePassageHeader(line, position.line);
@@ -80895,6 +81067,7 @@ var referencesPlugin;
80895
81067
  var init_references = __esm({
80896
81068
  "src/plugins/references.ts"() {
80897
81069
  "use strict";
81070
+ init_text();
80898
81071
  init_macro_heads();
80899
81072
  init_passage_refs();
80900
81073
  init_passage_parser();
@@ -80921,26 +81094,6 @@ var init_references = __esm({
80921
81094
  }
80922
81095
  });
80923
81096
 
80924
- // src/core/parsing/js-string-literal.ts
80925
- function encodeStringLiteralBody(value, quote) {
80926
- let out = "";
80927
- for (let i2 = 0; i2 < value.length; i2++) {
80928
- const ch = value[i2];
80929
- if (ch === "\\" || ch === quote) out += "\\" + ch;
80930
- else if (ch === "\n") out += "\\n";
80931
- else if (ch === "\r") out += "\\r";
80932
- else if (ch === "\u2028") out += "\\u2028";
80933
- else if (ch === "\u2029") out += "\\u2029";
80934
- else out += ch;
80935
- }
80936
- return out;
80937
- }
80938
- var init_js_string_literal = __esm({
80939
- "src/core/parsing/js-string-literal.ts"() {
80940
- "use strict";
80941
- }
80942
- });
80943
-
80944
81097
  // src/plugins/rename.ts
80945
81098
  function prepareRename(uri, position, workspace) {
80946
81099
  const symbol2 = resolveSymbolAtCursor(uri, position, workspace);
@@ -81020,11 +81173,30 @@ function computeRename(uri, position, newName, workspace) {
81020
81173
  throw new RenameError(`Cannot rename to ${JSON.stringify(newName)}: a variable with that name already exists.`).at(uri, symbol2.range);
81021
81174
  }
81022
81175
  const refs = symbol2.sigil === "%" ? findTransientReferences(symbol2.name, workspace, true) : findVariableReferences(symbol2.name, workspace, true);
81176
+ const bindings = /* @__PURE__ */ new Map();
81023
81177
  for (const ref2 of refs) {
81024
81178
  const doc = workspace.markup.get(ref2.uri);
81025
81179
  const passage = doc.passageAt(ref2.range.start);
81026
81180
  const start = passage.contentOffset(positionToOffset(ref2.range.start, doc.lineStarts));
81027
81181
  const end = passage.contentOffset(positionToOffset(ref2.range.end, doc.lineStarts));
81182
+ let byStart = bindings.get(passage);
81183
+ if (!byStart) {
81184
+ byStart = new Map(collectVariableReferences2(passage).flatMap((ref3) => ref3.binding ? [[ref3.start, ref3.binding]] : []));
81185
+ bindings.set(passage, byStart);
81186
+ }
81187
+ const binding = byStart.get(start);
81188
+ if (binding && binding.end === end) {
81189
+ let spelling = bareName;
81190
+ if (!binding.explicitSigil) {
81191
+ try {
81192
+ parseBoundVariable(spelling);
81193
+ } catch {
81194
+ spelling = `$${spelling}`;
81195
+ }
81196
+ }
81197
+ addEdit(ref2.uri, passage.range(binding.nameStart, binding.nameEnd), spelling);
81198
+ continue;
81199
+ }
81028
81200
  const view = sourceView(passage, start, end);
81029
81201
  const index = viewIndex(view, start);
81030
81202
  let base;
@@ -81083,9 +81255,9 @@ function preserveEntryPassage(oldName, newName, workspace, addEdit) {
81083
81255
  } catch {
81084
81256
  throw new RenameError("Cannot preserve the entry passage: StoryData must contain a valid JSON object.").at(declaration.uri, declaration.nameRange);
81085
81257
  }
81086
- const start = typeof data2.start === "string" && data2.start ? data2.start : "Start";
81258
+ const start = readStoryData(passage.content).start || "Start";
81087
81259
  if (start !== oldName) continue;
81088
- if (Object.hasOwn(data2, "start") && typeof data2.start !== "string") {
81260
+ if (Object.entries(data2).some(([key, value]) => isEntryField(key) && typeof value !== "string")) {
81089
81261
  throw new RenameError("Cannot preserve the entry passage: StoryData.start must be a string.").at(declaration.uri, declaration.nameRange);
81090
81262
  }
81091
81263
  let depth = 0;
@@ -81100,7 +81272,7 @@ function preserveEntryPassage(oldName, newName, workspace, addEdit) {
81100
81272
  literal(text3, index) {
81101
81273
  if (depth !== 1 || !text3.startsWith('"')) return;
81102
81274
  const isKey = /^\s*:/.test(passage.content.slice(index + text3.length));
81103
- if (isKey) startValue = JSON.parse(text3) === "start";
81275
+ if (isKey) startValue = isEntryField(JSON.parse(text3));
81104
81276
  else if (startValue) {
81105
81277
  addEdit(declaration.uri, passage.range(index, index + text3.length), JSON.stringify(newName));
81106
81278
  replaced = true;
@@ -81116,10 +81288,13 @@ function preserveEntryPassage(oldName, newName, workspace, addEdit) {
81116
81288
  const declaration = workspace.passages.getPassage(oldName);
81117
81289
  const doc = workspace.markup.get(declaration.uri);
81118
81290
  const end = doc.passages.at(-1).position(doc.passages.at(-1).content.length);
81119
- const eol = doc.text.includes("\r\n") ? "\r\n" : "\n";
81291
+ const eol = /\r\n|[\r\n]/.exec(doc.text)?.[0] ?? "\n";
81120
81292
  addEdit(declaration.uri, { start: end, end }, `${eol}${eol}:: StoryData${eol}${JSON.stringify({ start: newName })}${eol}`);
81121
81293
  }
81122
81294
  }
81295
+ function isEntryField(key) {
81296
+ return readStoryData(JSON.stringify({ [key]: "Start" })).start === "Start";
81297
+ }
81123
81298
  function passageHeaderProblem(name) {
81124
81299
  if (name.trim() === "") return "a passage name cannot be empty";
81125
81300
  if (/[\r\n]/.test(name)) return "a passage header is a single line, so the name cannot contain a line break";
@@ -81195,7 +81370,7 @@ function escapePassageName(name) {
81195
81370
  function resolveSymbolAtCursor(uri, position, workspace) {
81196
81371
  const text3 = workspace.documents.getText(uri);
81197
81372
  if (text3 === void 0 || !workspace.hasPassages(uri)) return null;
81198
- const lines = text3.split("\n");
81373
+ const lines = splitLines(text3);
81199
81374
  if (position.line >= lines.length) return null;
81200
81375
  const line = lines[position.line];
81201
81376
  const header = parsePassageHeader(line, position.line);
@@ -81237,7 +81412,9 @@ var init_rename = __esm({
81237
81412
  "src/plugins/rename.ts"() {
81238
81413
  "use strict";
81239
81414
  import_vscode_languageserver2 = __toESM(require_api3(), 1);
81415
+ init_story_data();
81240
81416
  init_tooling();
81417
+ init_executable_refs();
81241
81418
  init_source_view();
81242
81419
  init_text();
81243
81420
  init_link_runtime();
@@ -81607,7 +81784,7 @@ var init_semantic_tokens = __esm({
81607
81784
  // src/plugins/code-actions.ts
81608
81785
  import { readFileSync as readFileSync7 } from "node:fs";
81609
81786
  import { basename as basename2, dirname as dirname5, join as join4 } from "node:path";
81610
- import { fileURLToPath as fileURLToPath4, pathToFileURL as pathToFileURL5 } from "node:url";
81787
+ import { fileURLToPath as fileURLToPath4, pathToFileURL as pathToFileURL6 } from "node:url";
81611
81788
  function computeCodeActions(uri, diagnostics, workspace, options = {}) {
81612
81789
  const actions = [];
81613
81790
  for (const diag of diagnostics) {
@@ -81658,7 +81835,7 @@ function addMacroToConfig(uri, macroName, workspace, options) {
81658
81835
  if (!root) return null;
81659
81836
  const existingPath = findConfigFile(root);
81660
81837
  const configPath = existingPath ?? join4(root, DEFAULT_CONFIG_FILENAME);
81661
- const configUri = pathToFileURL5(configPath).toString();
81838
+ const configUri = pathToFileURL6(configPath).toString();
81662
81839
  const action = {
81663
81840
  title: `Add '${macroName}' to ${basename2(configPath)}`,
81664
81841
  kind: "quickfix",
@@ -81732,7 +81909,7 @@ function addMacroToYamlConfig(text3, macroName) {
81732
81909
  const nl = text3.indexOf("\n", offset2);
81733
81910
  offset2 = nl === -1 ? text3.length : nl + 1;
81734
81911
  }
81735
- const lead = offset2 === text3.length && text3.length > 0 && !text3.endsWith("\n") ? "\n" : "";
81912
+ const lead = offset2 === text3.length && text3.length > 0 && !/[\r\n]$/.test(text3) ? "\n" : "";
81736
81913
  const pos = offsetToPosition2(text3, offset2);
81737
81914
  return { range: { start: pos, end: pos }, newText: lead + entry };
81738
81915
  }
@@ -81762,16 +81939,14 @@ function addMacroToJsonConfig(text3, macroName) {
81762
81939
  container.macros = { [macroName]: NEW_MACRO_ENTRY };
81763
81940
  }
81764
81941
  const indent = text3.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
81765
- const newline = text3.endsWith("\n") || !text3.trim() ? "\n" : "";
81942
+ const newline = /[\r\n]$/.test(text3) || !text3.trim() ? "\n" : "";
81766
81943
  return minimalEdit(text3, JSON.stringify(raw, null, indent) + newline);
81767
81944
  }
81768
81945
  function columnOf2(text3, offset2) {
81769
- return offset2 - (text3.lastIndexOf("\n", offset2 - 1) + 1);
81946
+ return offsetToPosition2(text3, offset2).character;
81770
81947
  }
81771
81948
  function offsetToPosition2(text3, offset2) {
81772
- const before = text3.slice(0, offset2);
81773
- const lineStart = before.lastIndexOf("\n") + 1;
81774
- return { line: before.split("\n").length - 1, character: offset2 - lineStart };
81949
+ return offsetToPosition(offset2, buildLineStarts(text3));
81775
81950
  }
81776
81951
  function minimalEdit(oldText, newText) {
81777
81952
  const max = Math.min(oldText.length, newText.length);
@@ -81788,19 +81963,19 @@ function minimalEdit(oldText, newText) {
81788
81963
  };
81789
81964
  }
81790
81965
  function insertLinesAt(text3, line, body, eol) {
81791
- const lines = text3.split("\n");
81966
+ const lines = splitLines(text3);
81792
81967
  const normalized = body.replace(/\r?\n/g, eol);
81793
81968
  if (line < lines.length) {
81794
81969
  const pos = { line, character: 0 };
81795
81970
  return { range: { start: pos, end: pos }, newText: normalized };
81796
81971
  }
81797
81972
  const end = offsetToPosition2(text3, text3.length);
81798
- const atBoundary = text3.length === 0 || text3.endsWith("\n");
81973
+ const atBoundary = text3.length === 0 || /[\r\n]$/.test(text3);
81799
81974
  return { range: { start: end, end }, newText: (atBoundary ? "" : eol) + normalized };
81800
81975
  }
81801
81976
  function lineEndingFor(workspace, uri) {
81802
81977
  const own2 = workspace.documents.getText(uri) ?? "";
81803
- const detect = (text3) => text3.includes("\r\n") ? "\r\n" : text3.includes("\n") ? "\n" : void 0;
81978
+ const detect = (text3) => text3.includes("\r\n") ? "\r\n" : text3.includes("\n") ? "\n" : text3.includes("\r") ? "\r" : void 0;
81804
81979
  const fromOwn = detect(own2);
81805
81980
  if (fromOwn) return fromOwn;
81806
81981
  for (const other of [...workspace.documents.getUris()].sort()) {
@@ -81819,7 +81994,7 @@ function declareVariable(diag, workspace, sigil) {
81819
81994
  if (text3 === void 0) return [];
81820
81995
  const markup = workspace.markup.get(passage.uri)?.passages.find((p) => p.passage === passage);
81821
81996
  if (!markup) return [];
81822
- const boundary = markup.bodyEnd === text3.length ? text3.split("\n").length : markup.position(markup.content.length).line;
81997
+ const boundary = markup.bodyEnd === text3.length ? splitLines(text3).length : markup.position(markup.content.length).line;
81823
81998
  const edit = insertLinesAt(text3, boundary, `${sigil}${name} = 0
81824
81999
  `, lineEndingFor(workspace, passage.uri));
81825
82000
  return [{
@@ -81835,7 +82010,7 @@ function fixNoStoryVariables(workspace) {
81835
82010
  if (targetUri === void 0) return null;
81836
82011
  const text3 = workspace.documents.getText(targetUri);
81837
82012
  if (text3 === void 0) return null;
81838
- const edit = insertLinesAt(text3, text3.split("\n").length, "\n:: StoryVariables\n", lineEndingFor(workspace, targetUri));
82013
+ const edit = insertLinesAt(text3, splitLines(text3).length, "\n:: StoryVariables\n", lineEndingFor(workspace, targetUri));
81839
82014
  return {
81840
82015
  title: "Create StoryVariables passage",
81841
82016
  kind: "quickfix",
@@ -81882,7 +82057,7 @@ function fixBrokenLink(uri, diag, workspace) {
81882
82057
  }
81883
82058
  const header = `:: ${name.replace(/[\\[\]{}]/g, "\\$&")}`;
81884
82059
  if (!workspace.passages.getPassage(name) && name.trim() === name && name !== "" && !/[\r\n]/.test(name)) {
81885
- const edit = insertLinesAt(text3, text3.split("\n").length, `
82060
+ const edit = insertLinesAt(text3, splitLines(text3).length, `
81886
82061
  ${header}
81887
82062
  `, lineEndingFor(workspace, uri));
81888
82063
  actions.push({
@@ -82231,7 +82406,7 @@ function computeDocumentLinks(uri, workspace) {
82231
82406
  const links = [];
82232
82407
  for (const ref2 of documentPassageRefs(doc)) {
82233
82408
  if (ref2.form !== "bracket") continue;
82234
- const targetPassage = workspace.passages.getPassage(ref2.name);
82409
+ const targetPassage = workspace.passages.getEffectivePassage(ref2.name);
82235
82410
  links.push({
82236
82411
  range: ref2.range,
82237
82412
  target: targetPassage ? `${targetPassage.uri}#L${targetPassage.range.start.line + 1}` : void 0
@@ -82317,7 +82492,7 @@ var server_exports2 = {};
82317
82492
  __export(server_exports2, {
82318
82493
  startServer: () => startServer
82319
82494
  });
82320
- import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL6 } from "node:url";
82495
+ import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL7 } from "node:url";
82321
82496
  import { readFileSync as readFileSync8 } from "node:fs";
82322
82497
  import { basename as basename3, dirname as dirname6 } from "node:path";
82323
82498
  function uriToFsPath(uri) {
@@ -82328,7 +82503,7 @@ function uriToFsPath(uri) {
82328
82503
  }
82329
82504
  }
82330
82505
  function fsPathToUri(fsPath) {
82331
- return pathToFileURL6(fsPath).toString();
82506
+ return pathToFileURL7(fsPath).toString();
82332
82507
  }
82333
82508
  function isConfigFileUri(uri) {
82334
82509
  const name = basename3(uriToFsPath(uri));