@rohal12/spindle-lsp 0.9.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/bin.js +1829 -406
  2. package/dist/bin.js.map +4 -4
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -6694,7 +6694,7 @@ var init_macro_registry = __esm({
6694
6694
  }
6695
6695
  });
6696
6696
 
6697
- // src/core/parsing/macro-parser.ts
6697
+ // src/core/parsing/code-scanner.ts
6698
6698
  function buildCodeScanner(input, from) {
6699
6699
  const n = input.length;
6700
6700
  const close = new Int32Array(n + 2).fill(-1);
@@ -6745,6 +6745,293 @@ function buildCodeScanner(input, from) {
6745
6745
  function createCodeScanner(input) {
6746
6746
  return buildCodeScanner(input, 0);
6747
6747
  }
6748
+ var NON_STRING_QUOTE_PREFIX;
6749
+ var init_code_scanner = __esm({
6750
+ "src/core/parsing/code-scanner.ts"() {
6751
+ "use strict";
6752
+ NON_STRING_QUOTE_PREFIX = /[\p{L}\p{N}_\\]/u;
6753
+ }
6754
+ });
6755
+
6756
+ // src/core/parsing/html-scanner.ts
6757
+ function plainBraceMatches(input) {
6758
+ const match2 = new Int32Array(input.length).fill(-1);
6759
+ const open = [];
6760
+ for (let i = 0; i < input.length; i++) {
6761
+ if (input[i] === "{") open.push(i);
6762
+ else if (input[i] === "}" && open.length > 0) match2[open.pop()] = i;
6763
+ }
6764
+ return match2;
6765
+ }
6766
+ function scanHtmlTags(text) {
6767
+ const n = text.length;
6768
+ const tags = [];
6769
+ const macros = [];
6770
+ const code = createCodeScanner(text);
6771
+ const plain = plainBraceMatches(text);
6772
+ const rawBodies = [];
6773
+ let doCloses;
6774
+ let nextDoClose = 0;
6775
+ const rawBody = (end) => {
6776
+ doCloses ??= [...text.matchAll(/\{\/do\s*\}/gi)].map((match2) => match2.index);
6777
+ while (nextDoClose < doCloses.length && doCloses[nextDoClose] < end) nextDoClose++;
6778
+ if (nextDoClose < doCloses.length) rawBodies.push([end, doCloses[nextDoClose]]);
6779
+ };
6780
+ const rawBodyLimit = (limit2) => {
6781
+ if (rawBodies.length === 0) return limit2;
6782
+ const macroStarts = new Set(macros);
6783
+ let t = 0;
6784
+ for (const [start, close] of rawBodies) {
6785
+ if (start >= limit2) break;
6786
+ if (close < limit2 && !macroStarts.has(close)) {
6787
+ limit2 = start;
6788
+ break;
6789
+ }
6790
+ while (t < tags.length && tags[t].start < start) t++;
6791
+ if (t < tags.length && tags[t].start < Math.min(close, limit2)) limit2 = tags[t].start;
6792
+ }
6793
+ return limit2;
6794
+ };
6795
+ const braceEnd = (open) => {
6796
+ const close = code.closeBrace(open + 1);
6797
+ if (close !== plain[open]) return GIVE_UP;
6798
+ return close === -1 ? open + 1 : close + 1;
6799
+ };
6800
+ const displayEnd = (open, sigil) => {
6801
+ let j = sigil + 1;
6802
+ while (j < n && DISPLAY_NAME_CHAR.test(text[j])) j++;
6803
+ return text[j] === "}" ? j + 1 : braceEnd(open);
6804
+ };
6805
+ const afterBrace = (i2) => {
6806
+ let at = i2 + 1;
6807
+ const selectors = text[at] === "." || text[at] === "#";
6808
+ if (selectors) {
6809
+ at = selectorsEnd(text, at);
6810
+ if (text[at] === " ") at++;
6811
+ }
6812
+ const c3 = text[at];
6813
+ if (c3 === "$" || c3 === "_" || c3 === "@" || c3 === "%") return displayEnd(i2, at);
6814
+ if (c3 === void 0) return i2 + 1;
6815
+ if (LETTER.test(c3) || c3 === "/" && !selectors) {
6816
+ const end = braceEnd(i2);
6817
+ if (end > i2 + 1) {
6818
+ macros.push(i2);
6819
+ if (/^do$/i.test(text.slice(at, end - 1).trim().split(/\s/)[0])) rawBody(end);
6820
+ }
6821
+ return end;
6822
+ }
6823
+ return i2 + 1;
6824
+ };
6825
+ const readTag = (i2) => {
6826
+ let j = i2 + 1;
6827
+ const isClose = text[j] === "/";
6828
+ if (isClose) j++;
6829
+ const nameStart = j;
6830
+ while (j < n && TAG_NAME_CHAR.test(text[j])) j++;
6831
+ const name = text.slice(nameStart, j);
6832
+ if (!name || !LETTER.test(name[0])) return i2 + 1;
6833
+ const isVoid = VOID_ELEMENTS.has(name.toLowerCase());
6834
+ if (isClose) {
6835
+ while (j < n && WHITESPACE.test(text[j])) j++;
6836
+ if (text[j] !== ">") return i2 + 1;
6837
+ if (!isVoid) tags.push({ name, kind: "close", start: i2, end: j + 1 });
6838
+ return j + 1;
6839
+ }
6840
+ const attrs = readAttributes(j);
6841
+ if (attrs.end === GIVE_UP) return GIVE_UP;
6842
+ j = attrs.end;
6843
+ let selfClosing = isVoid;
6844
+ if (text[j] === "/") {
6845
+ selfClosing = true;
6846
+ j++;
6847
+ }
6848
+ if (text[j] === ">") {
6849
+ tags.push({ name, kind: selfClosing ? "void" : "open", start: i2, end: j + 1, values: attrs.values });
6850
+ return j + 1;
6851
+ }
6852
+ return attrs.count > 0 ? GIVE_UP : i2 + 1;
6853
+ };
6854
+ const readAttributes = (j) => {
6855
+ let count = 0;
6856
+ const values = [];
6857
+ while (j < n) {
6858
+ while (j < n && WHITESPACE.test(text[j])) j++;
6859
+ if (j >= n || text[j] === ">" || text[j] === "/" && text[j + 1] === ">") break;
6860
+ const nameStart = j;
6861
+ while (j < n && ATTR_NAME_CHAR.test(text[j])) j++;
6862
+ if (j === nameStart) break;
6863
+ count++;
6864
+ if (text[j] !== "=") continue;
6865
+ j++;
6866
+ const quote = text[j];
6867
+ if (quote === '"' || quote === "'") {
6868
+ const close = quotedValueEnd(j + 1, quote);
6869
+ if (close === GIVE_UP) return { end: GIVE_UP, count, values };
6870
+ values.push([j + 1, close]);
6871
+ j = close < n ? close + 1 : close;
6872
+ } else {
6873
+ const start = j;
6874
+ while (j < n && !WHITESPACE.test(text[j]) && text[j] !== ">") j++;
6875
+ values.push([start, j]);
6876
+ }
6877
+ }
6878
+ return { end: j, count, values };
6879
+ };
6880
+ const quotedValueEnd = (from, quote) => {
6881
+ let depth = 0;
6882
+ let sawBrace = false;
6883
+ let k = from;
6884
+ for (; k < n; k++) {
6885
+ const c3 = text[k];
6886
+ if (c3 === "{") {
6887
+ depth++;
6888
+ sawBrace = true;
6889
+ } else if (c3 === "}") {
6890
+ depth--;
6891
+ sawBrace = true;
6892
+ } else if (c3 === quote && depth <= 0) {
6893
+ break;
6894
+ }
6895
+ }
6896
+ if (!sawBrace) return k;
6897
+ let d = from;
6898
+ while (d < n) {
6899
+ if (text[d] === "{") {
6900
+ const close = code.closeBrace(d + 1);
6901
+ if (close !== -1) {
6902
+ d = close + 1;
6903
+ continue;
6904
+ }
6905
+ } else if (text[d] === quote) {
6906
+ break;
6907
+ }
6908
+ d++;
6909
+ }
6910
+ return d === k ? k : GIVE_UP;
6911
+ };
6912
+ let i = 0;
6913
+ let stoppedAt = -1;
6914
+ while (i < n) {
6915
+ const c3 = text[i];
6916
+ let next;
6917
+ if (c3 === "\\") {
6918
+ let k = i + 1;
6919
+ while (text[k] === "\\") k++;
6920
+ if (text[k] === "{" || text[k] === "}") next = (k - i) % 2 === 1 ? k + 1 : GIVE_UP;
6921
+ else next = k;
6922
+ } else if (c3 === "[" && text[i + 1] === "[") {
6923
+ next = linkEnd(text, i);
6924
+ } else if (c3 === "{") {
6925
+ next = afterBrace(i);
6926
+ } else if (c3 === "<") {
6927
+ next = readTag(i);
6928
+ } else {
6929
+ next = i + 1;
6930
+ }
6931
+ if (next === GIVE_UP) {
6932
+ stoppedAt = i;
6933
+ break;
6934
+ }
6935
+ i = next;
6936
+ }
6937
+ const limit = rawBodyLimit(stoppedAt === -1 ? n : stoppedAt);
6938
+ if (limit === stoppedAt || limit === n) return { tags, stoppedAt, macros };
6939
+ return {
6940
+ tags: tags.filter((tag) => tag.start < limit),
6941
+ stoppedAt: limit,
6942
+ macros: macros.filter((at) => at < limit)
6943
+ };
6944
+ }
6945
+ function selectorsEnd(text, i) {
6946
+ const n = text.length;
6947
+ while (i < n && (text[i] === "." || text[i] === "#")) {
6948
+ i++;
6949
+ while (i < n) {
6950
+ if (SELECTOR_NAME_CHAR.test(text[i])) {
6951
+ i++;
6952
+ } else if (text[i] === "{" && (text[i + 1] === "$" || text[i + 1] === "_" || text[i + 1] === "@")) {
6953
+ let j = i + 2;
6954
+ while (j < n && DISPLAY_NAME_CHAR.test(text[j])) j++;
6955
+ if (text[j] !== "}") break;
6956
+ i = j + 1;
6957
+ } else {
6958
+ break;
6959
+ }
6960
+ }
6961
+ }
6962
+ return i;
6963
+ }
6964
+ function linkEnd(text, i) {
6965
+ const n = text.length;
6966
+ let depth = 1;
6967
+ let j = i + 2;
6968
+ while (j < n) {
6969
+ if (text[j] === "[" && text[j + 1] === "[") {
6970
+ depth++;
6971
+ j += 2;
6972
+ } else if (text[j] === "]" && text[j + 1] === "]") {
6973
+ if (--depth === 0) return j + 2;
6974
+ j += 2;
6975
+ } else {
6976
+ j++;
6977
+ }
6978
+ }
6979
+ return GIVE_UP;
6980
+ }
6981
+ function attributeValueSpans(text) {
6982
+ const spans = [];
6983
+ if (!text.includes("<")) return spans;
6984
+ const headers = [...text.matchAll(/^::.*$/gm)];
6985
+ const scan = (from2, to) => {
6986
+ const content = text.slice(from2, to);
6987
+ if (!content.includes("<")) return;
6988
+ for (const tag of scanHtmlTags(content).tags) {
6989
+ for (const [start, end] of tag.values ?? []) spans.push([from2 + start, from2 + end]);
6990
+ }
6991
+ };
6992
+ let from = 0;
6993
+ for (const header of headers) {
6994
+ scan(from, header.index);
6995
+ from = header.index + header[0].length;
6996
+ }
6997
+ scan(from, text.length);
6998
+ return spans;
6999
+ }
7000
+ function inAttributeValue(text, offset) {
7001
+ return attributeValueSpans(text).some(([start, end]) => start <= offset && offset < end);
7002
+ }
7003
+ var VOID_ELEMENTS, TAG_NAME_CHAR, LETTER, ATTR_NAME_CHAR, WHITESPACE, SELECTOR_NAME_CHAR, DISPLAY_NAME_CHAR, GIVE_UP;
7004
+ var init_html_scanner = __esm({
7005
+ "src/core/parsing/html-scanner.ts"() {
7006
+ "use strict";
7007
+ init_code_scanner();
7008
+ VOID_ELEMENTS = /* @__PURE__ */ new Set([
7009
+ "area",
7010
+ "base",
7011
+ "br",
7012
+ "col",
7013
+ "embed",
7014
+ "hr",
7015
+ "img",
7016
+ "input",
7017
+ "link",
7018
+ "meta",
7019
+ "param",
7020
+ "source",
7021
+ "track",
7022
+ "wbr"
7023
+ ]);
7024
+ TAG_NAME_CHAR = /[a-zA-Z0-9-]/;
7025
+ LETTER = /[a-zA-Z]/;
7026
+ ATTR_NAME_CHAR = /[a-zA-Z0-9_\-:@]/;
7027
+ WHITESPACE = /[^\S ]/;
7028
+ SELECTOR_NAME_CHAR = /[a-zA-Z0-9_-]/;
7029
+ DISPLAY_NAME_CHAR = /[\w.]/;
7030
+ GIVE_UP = -2;
7031
+ }
7032
+ });
7033
+
7034
+ // src/core/parsing/macro-parser.ts
6748
7035
  function buildLineStarts(text) {
6749
7036
  const starts = [0];
6750
7037
  for (let i = 0; i < text.length; i++) {
@@ -6775,10 +7062,20 @@ function parseMacros(text) {
6775
7062
  const scanner = createCodeScanner(cleaned);
6776
7063
  const macros = [];
6777
7064
  let id = 0;
7065
+ const attributeValues = attributeValueSpans(text);
7066
+ let value = 0;
7067
+ const inAttributeValue2 = (offset) => {
7068
+ while (value < attributeValues.length && attributeValues[value][1] <= offset) value++;
7069
+ return value < attributeValues.length && attributeValues[value][0] <= offset;
7070
+ };
6778
7071
  macroHeadRegex.lastIndex = 0;
6779
7072
  let match2;
6780
- while ((match2 = macroHeadRegex.exec(cleaned)) !== null) {
7073
+ while ((match2 = macroHeadRegex.exec(text)) !== null) {
6781
7074
  const matchStart = match2.index;
7075
+ if (inAttributeValue2(matchStart)) {
7076
+ macroHeadRegex.lastIndex = matchStart + 1;
7077
+ continue;
7078
+ }
6782
7079
  const closeIdx = scanner.closeBrace(matchStart + 1);
6783
7080
  if (closeIdx === -1) {
6784
7081
  macroHeadRegex.lastIndex = matchStart + 1;
@@ -6787,7 +7084,7 @@ function parseMacros(text) {
6787
7084
  const matchEnd = closeIdx + 1;
6788
7085
  macroHeadRegex.lastIndex = matchEnd;
6789
7086
  const closeSlash = match2[1];
6790
- const cssPrefix = (match2[2] || "").trim();
7087
+ const cssPrefix = match2[2] || "";
6791
7088
  const macroName = match2[3];
6792
7089
  const rawArgs = cleaned.slice(matchStart + match2[0].length, closeIdx).replace(/^\s+/, "");
6793
7090
  const open = closeSlash !== "/";
@@ -6838,13 +7135,345 @@ function pairMacros(macros, isBlock, passageStartLines = []) {
6838
7135
  }
6839
7136
  }
6840
7137
  }
6841
- var variableInterpolationRegex, macroHeadRegex, NON_STRING_QUOTE_PREFIX;
7138
+ var variableInterpolationRegex, SELECTOR_PATTERN, macroHeadRegex;
6842
7139
  var init_macro_parser = __esm({
6843
7140
  "src/core/parsing/macro-parser.ts"() {
6844
7141
  "use strict";
7142
+ init_code_scanner();
7143
+ init_html_scanner();
7144
+ init_code_scanner();
6845
7145
  variableInterpolationRegex = /(?<!\\)\{([$_@%][A-Za-z_$][\w$.]*)\}/g;
6846
- macroHeadRegex = /(?<!\\)\{(\/)?(?:((?:[#.][a-zA-Z][\w-]*\s*)*)([A-Za-z][\w-]*))(?=[\s}])/gi;
6847
- NON_STRING_QUOTE_PREFIX = /[\p{L}\p{N}_\\]/u;
7146
+ SELECTOR_PATTERN = String.raw`(?:[.#](?:[\w-]|\{[$_@][\w.]*\})*)+`;
7147
+ macroHeadRegex = new RegExp(
7148
+ String.raw`(?<!\\)\{(?:(\/)|(${SELECTOR_PATTERN}) )?([A-Za-z][\w-]*)(?=[\s}]|\{[$_@%][A-Za-z_$][\w$.]*\})`,
7149
+ "gi"
7150
+ );
7151
+ }
7152
+ });
7153
+
7154
+ // src/core/workspace/variable-schema.ts
7155
+ function inferDefaultSchema(expr) {
7156
+ const parser = new LiteralParser(expr.trim());
7157
+ try {
7158
+ const schema = parser.value();
7159
+ parser.skipSpace();
7160
+ return schema && parser.atEnd() ? schema : void 0;
7161
+ } catch (err) {
7162
+ if (err instanceof Unparsable) return void 0;
7163
+ throw err;
7164
+ }
7165
+ }
7166
+ function findUnsupportedValue(expr) {
7167
+ const offset = expr.length - expr.trimStart().length;
7168
+ const parser = new LiteralParser(expr.trim());
7169
+ try {
7170
+ const read = parser.read();
7171
+ parser.skipSpace();
7172
+ const value = read.unsupported;
7173
+ if (!parser.atEnd() || !value) return void 0;
7174
+ return { ...value, start: value.start + offset, end: value.end + offset };
7175
+ } catch (err) {
7176
+ if (err instanceof Unparsable) return void 0;
7177
+ throw err;
7178
+ }
7179
+ }
7180
+ function bigintString(literal2) {
7181
+ const digits = literal2.slice(0, -1).replace(/_/g, "");
7182
+ const negative = digits.startsWith("-");
7183
+ try {
7184
+ const value = BigInt(negative ? digits.slice(1) : digits);
7185
+ return String(negative ? -value : value);
7186
+ } catch {
7187
+ return digits;
7188
+ }
7189
+ }
7190
+ function findPrimitiveFieldAccess(schema, fields) {
7191
+ let current = schema;
7192
+ for (let i = 0; i < fields.length; i++) {
7193
+ if (current.type === "array") return void 0;
7194
+ if (current.type !== "object") return { index: i, type: current.type };
7195
+ const next = current.fields?.get(fields[i]);
7196
+ if (!next) return void 0;
7197
+ current = next;
7198
+ }
7199
+ return void 0;
7200
+ }
7201
+ var Unparsable, IDENTIFIER_RE, BIGINT_RE, NUMBER_RE, INDEX_KEY_RE, UNKNOWN2, LiteralParser;
7202
+ var init_variable_schema = __esm({
7203
+ "src/core/workspace/variable-schema.ts"() {
7204
+ "use strict";
7205
+ Unparsable = class extends Error {
7206
+ };
7207
+ IDENTIFIER_RE = /[A-Za-z_$][\w$]*/y;
7208
+ BIGINT_RE = /-?(?:0[xX][\da-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+|0|[1-9][\d_]*)n(?![\w$])/y;
7209
+ NUMBER_RE = /[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?/y;
7210
+ INDEX_KEY_RE = /(?:0|[1-9]\d{0,14})(?![\w$.])/y;
7211
+ UNKNOWN2 = { schema: null };
7212
+ LiteralParser = class {
7213
+ constructor(text) {
7214
+ this.text = text;
7215
+ }
7216
+ pos = 0;
7217
+ atEnd() {
7218
+ return this.pos >= this.text.length;
7219
+ }
7220
+ skipSpace() {
7221
+ while (/\s/.test(this.text[this.pos] ?? "")) this.pos++;
7222
+ }
7223
+ /** A value up to the `,` / `}` / `]` that ends it; null when its type is unknown. */
7224
+ value() {
7225
+ return this.read().schema;
7226
+ }
7227
+ /** A value up to the `,` / `}` / `]` that ends it. */
7228
+ read() {
7229
+ this.skipSpace();
7230
+ const start = this.pos;
7231
+ const read = this.literal();
7232
+ this.skipSpace();
7233
+ const next = this.text[this.pos];
7234
+ if (read && (next === void 0 || next === "," || next === "}" || next === "]")) return read;
7235
+ this.pos = start;
7236
+ this.skipExpression();
7237
+ return UNKNOWN2;
7238
+ }
7239
+ /** A literal, consumed; undefined when the code here is not one. */
7240
+ literal() {
7241
+ const start = this.pos;
7242
+ const ch = this.text[this.pos];
7243
+ if (ch === "{") return this.object();
7244
+ if (ch === "[") {
7245
+ this.pos++;
7246
+ this.skipList("]");
7247
+ return { schema: { type: "array" } };
7248
+ }
7249
+ if (ch === '"' || ch === "'" || ch === "`") {
7250
+ this.stringLiteral();
7251
+ return { schema: { type: "string" } };
7252
+ }
7253
+ if (ch === "(") return this.functionLiteral(start);
7254
+ const word = this.match(IDENTIFIER_RE);
7255
+ if (word === "true" || word === "false") return { schema: { type: "boolean" } };
7256
+ if (word === "null") return this.unsupported("object", start);
7257
+ if (word === "undefined") return this.unsupported("undefined", start);
7258
+ if (word === "void") {
7259
+ this.skipSpace();
7260
+ if (this.match(NUMBER_RE) === void 0) return void 0;
7261
+ return this.unsupported("undefined", start, "undefined");
7262
+ }
7263
+ if (word !== void 0) return this.functionLiteral(start);
7264
+ if (this.match(BIGINT_RE) !== void 0) {
7265
+ return this.unsupported("bigint", start, bigintString(this.text.slice(start, this.pos)));
7266
+ }
7267
+ if (this.match(NUMBER_RE) !== void 0) return { schema: { type: "number" } };
7268
+ return void 0;
7269
+ }
7270
+ /** The value from `start` to here, which Spindle rejects. */
7271
+ unsupported(type, start, text) {
7272
+ const end = this.pos;
7273
+ return { schema: null, unsupported: { type, text: text ?? this.text.slice(start, end), path: [], start, end } };
7274
+ }
7275
+ /**
7276
+ * A function, arrow function or class expression at `start`, consumed;
7277
+ * undefined, with nothing consumed, for any other code.
7278
+ */
7279
+ functionLiteral(start) {
7280
+ this.pos = start;
7281
+ try {
7282
+ if (this.functionSyntax()) return this.unsupported("function", start);
7283
+ } catch (err) {
7284
+ if (!(err instanceof Unparsable)) throw err;
7285
+ }
7286
+ this.pos = start;
7287
+ return void 0;
7288
+ }
7289
+ functionSyntax() {
7290
+ let word = this.match(IDENTIFIER_RE);
7291
+ if (word === "async") {
7292
+ this.skipSpace();
7293
+ word = this.match(IDENTIFIER_RE);
7294
+ }
7295
+ this.skipSpace();
7296
+ if (word === "function") {
7297
+ if (this.text[this.pos] === "*") this.pos++;
7298
+ this.skipSpace();
7299
+ this.match(IDENTIFIER_RE);
7300
+ this.skipSpace();
7301
+ if (!this.skipBracketed("(", ")")) return false;
7302
+ this.skipSpace();
7303
+ return this.skipBracketed("{", "}");
7304
+ }
7305
+ if (word === "class") {
7306
+ if (this.match(IDENTIFIER_RE) !== void 0) this.skipSpace();
7307
+ return this.skipBracketed("{", "}");
7308
+ }
7309
+ if (word === void 0 && !this.skipBracketed("(", ")")) return false;
7310
+ this.skipSpace();
7311
+ if (!this.text.startsWith("=>", this.pos)) return false;
7312
+ this.pos += 2;
7313
+ this.skipSpace();
7314
+ if (this.text[this.pos] === "{") return this.skipBracketed("{", "}");
7315
+ const bodyStart = this.pos;
7316
+ this.skipExpression();
7317
+ while (this.pos > bodyStart && /\s/.test(this.text[this.pos - 1])) this.pos--;
7318
+ return this.pos > bodyStart;
7319
+ }
7320
+ /** Skip a bracketed list that opens here; false if none does. */
7321
+ skipBracketed(open, close) {
7322
+ if (this.text[this.pos] !== open) return false;
7323
+ this.pos++;
7324
+ this.skipList(close);
7325
+ return true;
7326
+ }
7327
+ object() {
7328
+ this.pos++;
7329
+ let fields = /* @__PURE__ */ new Map();
7330
+ const unsupported = /* @__PURE__ */ new Map();
7331
+ for (; ; ) {
7332
+ this.skipSpace();
7333
+ if (this.text[this.pos] === "}") {
7334
+ this.pos++;
7335
+ break;
7336
+ }
7337
+ const key = this.propertyKey();
7338
+ this.skipSpace();
7339
+ if (key !== void 0 && key !== "__proto__" && this.text[this.pos] === ":") {
7340
+ this.pos++;
7341
+ const read = this.read();
7342
+ fields?.set(key, read.schema);
7343
+ unsupported.delete(key);
7344
+ if (read.unsupported) {
7345
+ unsupported.set(key, { ...read.unsupported, path: [key, ...read.unsupported.path] });
7346
+ }
7347
+ } else {
7348
+ fields = void 0;
7349
+ this.skipExpression();
7350
+ }
7351
+ this.skipSpace();
7352
+ const sep2 = this.text[this.pos++];
7353
+ if (sep2 === "}") break;
7354
+ if (sep2 !== ",") throw new Unparsable();
7355
+ }
7356
+ if (!fields) return { schema: { type: "object" } };
7357
+ const [first] = unsupported.values();
7358
+ return { schema: { type: "object", fields }, unsupported: first };
7359
+ }
7360
+ /** A plain property key, consumed; undefined (possibly consumed) for any other. */
7361
+ propertyKey() {
7362
+ const ch = this.text[this.pos];
7363
+ if (ch === '"' || ch === "'") {
7364
+ const raw = this.stringLiteral();
7365
+ return raw.includes("\\") ? void 0 : raw;
7366
+ }
7367
+ return this.match(IDENTIFIER_RE) ?? this.match(INDEX_KEY_RE);
7368
+ }
7369
+ /** Consume a string or template literal and return its raw text. */
7370
+ stringLiteral() {
7371
+ const quote = this.text[this.pos];
7372
+ const start = ++this.pos;
7373
+ while (this.pos < this.text.length) {
7374
+ const ch = this.text[this.pos];
7375
+ if (ch === "\\") {
7376
+ this.pos += 2;
7377
+ continue;
7378
+ }
7379
+ if (quote === "`" && ch === "$" && this.text[this.pos + 1] === "{") throw new Unparsable();
7380
+ if (ch === quote) return this.text.slice(start, this.pos++);
7381
+ this.pos++;
7382
+ }
7383
+ throw new Unparsable();
7384
+ }
7385
+ /** Skip comma-separated expressions up to and including `close`. */
7386
+ skipList(close) {
7387
+ for (; ; ) {
7388
+ this.skipExpression();
7389
+ const ch = this.text[this.pos++];
7390
+ if (ch === close) return;
7391
+ if (ch !== ",") throw new Unparsable();
7392
+ }
7393
+ }
7394
+ /** Skip code up to a `,`, `)`, `]` or `}` that closes nothing, or the end. */
7395
+ skipExpression() {
7396
+ let depth = 0;
7397
+ while (this.pos < this.text.length) {
7398
+ const ch = this.text[this.pos];
7399
+ if (ch === '"' || ch === "'" || ch === "`") {
7400
+ this.stringLiteral();
7401
+ continue;
7402
+ }
7403
+ if (ch === "/") throw new Unparsable();
7404
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
7405
+ else if (ch === ")" || ch === "]" || ch === "}") {
7406
+ if (depth === 0) return;
7407
+ depth--;
7408
+ } else if (ch === "," && depth === 0) return;
7409
+ this.pos++;
7410
+ }
7411
+ if (depth > 0) throw new Unparsable();
7412
+ }
7413
+ match(re) {
7414
+ re.lastIndex = this.pos;
7415
+ const m = re.exec(this.text);
7416
+ if (!m) return void 0;
7417
+ this.pos += m[0].length;
7418
+ return m[0];
7419
+ }
7420
+ };
7421
+ }
7422
+ });
7423
+
7424
+ // src/core/workspace/declaration-check.ts
7425
+ function declaredName(line, sigil) {
7426
+ return DECLARATION_RE[sigil].exec(line.trim())?.[1];
7427
+ }
7428
+ function checkDeclaration(rawLine, sigil) {
7429
+ const line = rawLine.trim();
7430
+ if (!line || line.includes("\r") || line.includes("\xA0")) return void 0;
7431
+ const passage = PASSAGE[sigil];
7432
+ const match2 = DECLARATION_RE[sigil].exec(line);
7433
+ if (!match2) {
7434
+ const comment = /^(?:\/\/|\/\*|<!--|#)/.test(line) ? ` ${passage} has no comment syntax.` : "";
7435
+ return {
7436
+ kind: "invalid",
7437
+ message: `${passage}: Invalid declaration: "${line}". Expected: ${sigil}name = value.${comment}`
7438
+ };
7439
+ }
7440
+ const [, name, expr] = match2;
7441
+ const error48 = compileError(expr);
7442
+ if (error48 !== void 0) {
7443
+ const comment = compileError(expr + "\n") === void 0 ? ` A comment runs to the end of the line and hides the ")" Spindle closes the value with; ${passage} has no comment syntax.` : "";
7444
+ return {
7445
+ kind: "evaluate",
7446
+ message: `${passage}: Failed to evaluate "${sigil}${name} = ${expr}": ${error48}.${comment}`
7447
+ };
7448
+ }
7449
+ const value = findUnsupportedValue(expr);
7450
+ if (!value) return void 0;
7451
+ const exprStart = rawLine.length - rawLine.trimStart().length + line.length - expr.length;
7452
+ if (value.type === "object") {
7453
+ return { kind: "null", field: value.path, start: exprStart + value.start, end: exprStart + value.end };
7454
+ }
7455
+ const where = value.path.length > 0 ? ` (${sigil}${[name, ...value.path].join(".")})` : "";
7456
+ return {
7457
+ kind: "unsupported",
7458
+ message: `${passage}: Unsupported type "${value.type}" for value ${value.text}${where}. Expected number, string, boolean, array, or object.`
7459
+ };
7460
+ }
7461
+ function compileError(expr) {
7462
+ try {
7463
+ new Function("return (" + expr + ")");
7464
+ return void 0;
7465
+ } catch (err) {
7466
+ if (!(err instanceof SyntaxError) || /regular expression/i.test(err.message)) return void 0;
7467
+ return err.message;
7468
+ }
7469
+ }
7470
+ var DECLARATION_RE, PASSAGE;
7471
+ var init_declaration_check = __esm({
7472
+ "src/core/workspace/declaration-check.ts"() {
7473
+ "use strict";
7474
+ init_variable_schema();
7475
+ DECLARATION_RE = { "$": /^\$(\w+)\s*=\s*(.+)$/, "%": /^%(\w+)\s*=\s*(.+)$/ };
7476
+ PASSAGE = { "$": "StoryVariables", "%": "StoryTransients" };
6848
7477
  }
6849
7478
  });
6850
7479
 
@@ -6853,6 +7482,14 @@ function replaceCodeLiterals(text, replace) {
6853
7482
  const scanner = createCodeScanner(text);
6854
7483
  let result = "";
6855
7484
  let copied = 0;
7485
+ for (const [open, close] of codeBlocks(text, scanner)) {
7486
+ result += text.slice(copied, open + 1) + replaceLiterals(text, scanner, open + 1, close, replace);
7487
+ copied = close;
7488
+ }
7489
+ return result + text.slice(copied);
7490
+ }
7491
+ function codeBlocks(text, scanner) {
7492
+ const blocks = [];
6856
7493
  for (let i = 0; i < text.length; i++) {
6857
7494
  if (text[i] === "\\" && (text[i + 1] === "{" || text[i + 1] === "}")) {
6858
7495
  i++;
@@ -6861,11 +7498,10 @@ function replaceCodeLiterals(text, replace) {
6861
7498
  if (text[i] !== "{") continue;
6862
7499
  const close = scanner.closeBrace(i + 1);
6863
7500
  if (close === -1) continue;
6864
- result += text.slice(copied, i + 1) + replaceLiterals(text, scanner, i + 1, close, replace);
6865
- copied = close;
7501
+ blocks.push([i, close]);
6866
7502
  i = close;
6867
7503
  }
6868
- return result + text.slice(copied);
7504
+ return blocks;
6869
7505
  }
6870
7506
  function replaceLiterals(text, scanner, from, to, replace) {
6871
7507
  let result = "";
@@ -6891,7 +7527,7 @@ function blankLiteralText(literal2) {
6891
7527
  for (let i = 1; i < literal2.length - 1; i++) {
6892
7528
  if (literal2[i] !== "{") continue;
6893
7529
  const templateCode = literal2[0] === "`" && literal2[i - 1] === "$" && literal2[i - 2] !== "\\";
6894
- if (!templateCode && !/^[$%][A-Za-z_$]/.test(literal2.slice(i + 1, i + 3))) continue;
7530
+ if (!templateCode && !/^[$%][\w$]/.test(literal2.slice(i + 1, i + 3))) continue;
6895
7531
  let depth = 0;
6896
7532
  for (let j = i; j < literal2.length - 1; j++) {
6897
7533
  if (literal2[j] === "{") depth++;
@@ -6904,6 +7540,43 @@ function blankLiteralText(literal2) {
6904
7540
  }
6905
7541
  return literal2.replace(/[^\r\n]/g, (ch, i) => keep[i] ? ch : " ");
6906
7542
  }
7543
+ function checkDeclarations(lines, contentStartLine, sigil) {
7544
+ const check2 = { problems: [], nestedNulls: [], names: /* @__PURE__ */ new Map() };
7545
+ for (let i = 0; i < lines.length; i++) {
7546
+ const line = lines[i];
7547
+ if (line.startsWith("::")) break;
7548
+ const absLine = contentStartLine + i;
7549
+ const problem = checkDeclaration(line, sigil);
7550
+ if (!problem) {
7551
+ const name = /[\r\u00a0]/.test(line.trim()) ? void 0 : declaredName(line, sigil);
7552
+ if (name !== void 0 && !check2.names.has(name)) {
7553
+ check2.names.set(name, declarationLocation(line, name, absLine, void 0).declarationRange);
7554
+ }
7555
+ } else if (problem.kind === "null") {
7556
+ if (problem.field.length > 0) {
7557
+ check2.nestedNulls.push({
7558
+ name: declaredName(line, sigil),
7559
+ sigil,
7560
+ field: problem.field,
7561
+ range: {
7562
+ start: { line: absLine, character: problem.start },
7563
+ end: { line: absLine, character: problem.end }
7564
+ }
7565
+ });
7566
+ }
7567
+ } else {
7568
+ const start = line.length - line.trimStart().length;
7569
+ check2.problems.push({
7570
+ message: problem.message,
7571
+ range: {
7572
+ start: { line: absLine, character: start },
7573
+ end: { line: absLine, character: line.trimEnd().length }
7574
+ }
7575
+ });
7576
+ }
7577
+ }
7578
+ return check2;
7579
+ }
6907
7580
  function findLiteralEnd(text, start) {
6908
7581
  const open = text[start];
6909
7582
  if (open === '"' || open === "'" || open === "`") {
@@ -6960,14 +7633,48 @@ function declarationLocation(line, name, absLine, uri) {
6960
7633
  if (uri !== void 0) location.declarationUri = uri;
6961
7634
  return location;
6962
7635
  }
6963
- var varRefRegex, transientRefRegex, EXCLUDED_PASSAGES, UNCHECKED_PASSAGES, COMMENT_PATTERNS, BUILTIN_STORE_VAR_MACROS, QUOTED_RECEIVER_RE, ARRAY_MEMBERS, VariableTracker;
7636
+ function validatedReferences(lines, passages) {
7637
+ const refs = [];
7638
+ for (let pi = 0; pi < passages.length; pi++) {
7639
+ const passage = passages[pi];
7640
+ if (UNVALIDATED_PASSAGES.has(passage.name) || isScriptOrStylesheetPassage(passage)) continue;
7641
+ const contentStartLine = passage.startLine + 1;
7642
+ const contentEndLine = pi + 1 < passages.length ? passages[pi + 1].startLine : lines.length;
7643
+ const contentLines = lines.slice(contentStartLine, contentEndLine);
7644
+ const forLocals = /* @__PURE__ */ new Set();
7645
+ for (const m of contentLines.join("\n").matchAll(VALIDATED_FOR_LOCAL_RE)) {
7646
+ forLocals.add(m[1]);
7647
+ if (m[2]) forLocals.add(m[2]);
7648
+ }
7649
+ for (let i = 0; i < contentLines.length; i++) {
7650
+ for (const m of contentLines[i].matchAll(VALIDATED_REF_RE)) {
7651
+ const baseName = m[1].split(".")[0];
7652
+ if (forLocals.has(baseName)) continue;
7653
+ const line = contentStartLine + i;
7654
+ refs.push({
7655
+ baseName,
7656
+ path: m[1],
7657
+ range: {
7658
+ start: { line, character: m.index },
7659
+ end: { line, character: m.index + m[0].length }
7660
+ }
7661
+ });
7662
+ }
7663
+ }
7664
+ }
7665
+ return refs;
7666
+ }
7667
+ var varRefRegex, transientRefRegex, DECLARATION_RE2, EXCLUDED_PASSAGES, VALIDATED_REF_RE, VALIDATED_FOR_LOCAL_RE, UNVALIDATED_PASSAGES, COMMENT_PATTERNS, BUILTIN_STORE_VAR_MACROS, QUOTED_RECEIVER_RE, NO_DECLARATIONS, ARRAY_MEMBERS, VariableTracker;
6964
7668
  var init_variable_tracker = __esm({
6965
7669
  "src/core/workspace/variable-tracker.ts"() {
6966
7670
  "use strict";
6967
7671
  init_passage_parser();
6968
7672
  init_macro_parser();
6969
- varRefRegex = /\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
6970
- transientRefRegex = /(?<!\w)%([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
7673
+ init_variable_schema();
7674
+ init_declaration_check();
7675
+ varRefRegex = /\$([\w$]+(?:\.[A-Za-z_$][\w$]*)*)/g;
7676
+ transientRefRegex = /(?<!\w)%([\w$]+(?:\.[A-Za-z_$][\w$]*)*)/g;
7677
+ DECLARATION_RE2 = { "$": /^\$(\w+)\s*=\s*(.*)$/, "%": /^%(\w+)\s*=\s*(.*)$/ };
6971
7678
  EXCLUDED_PASSAGES = /* @__PURE__ */ new Set([
6972
7679
  "StoryVariables",
6973
7680
  "StoryTransients",
@@ -6975,7 +7682,14 @@ var init_variable_tracker = __esm({
6975
7682
  "StoryScript",
6976
7683
  "StoryInterface"
6977
7684
  ]);
6978
- UNCHECKED_PASSAGES = /* @__PURE__ */ new Set(["StoryInit"]);
7685
+ VALIDATED_REF_RE = /\$(\w+(?:\.\w+)*)/g;
7686
+ VALIDATED_FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
7687
+ UNVALIDATED_PASSAGES = /* @__PURE__ */ new Set([
7688
+ "StoryVariables",
7689
+ "StoryTransients",
7690
+ "StoryData",
7691
+ "StoryTitle"
7692
+ ]);
6979
7693
  COMMENT_PATTERNS = [
6980
7694
  /<!--[\s\S]*?-->/g,
6981
7695
  // HTML comments
@@ -6993,7 +7707,11 @@ var init_variable_tracker = __esm({
6993
7707
  "textarea",
6994
7708
  "textbox"
6995
7709
  ]);
6996
- QUOTED_RECEIVER_RE = /(?<!\\)(\{(?:[#.][a-zA-Z][\w-]*\s*)*([A-Za-z][\w-]*)\s+(["']))\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\3?(?=[\s}])/g;
7710
+ QUOTED_RECEIVER_RE = new RegExp(
7711
+ String.raw`(?<!\\)(\{(?:${SELECTOR_PATTERN} )?([A-Za-z][\w-]*)\s+(["']))\$([\w$]+(?:\.[A-Za-z_$][\w$]*)*)\3?(?=[\s}])`,
7712
+ "g"
7713
+ );
7714
+ NO_DECLARATIONS = { problems: [], nestedNulls: [], names: /* @__PURE__ */ new Map() };
6997
7715
  ARRAY_MEMBERS = /* @__PURE__ */ new Set([
6998
7716
  ...Object.getOwnPropertyNames(Array.prototype),
6999
7717
  ...Object.getOwnPropertyNames(Object.prototype),
@@ -7012,13 +7730,17 @@ var init_variable_tracker = __esm({
7012
7730
  declared = /* @__PURE__ */ new Map();
7013
7731
  _hasStoryVariables = false;
7014
7732
  _nullDeclarations = [];
7733
+ _variablesCheck = NO_DECLARATIONS;
7015
7734
  declaredTransient = /* @__PURE__ */ new Map();
7016
7735
  _hasStoryTransients = false;
7017
7736
  _nullTransientDeclarations = [];
7737
+ _transientsCheck = NO_DECLARATIONS;
7018
7738
  /** Per-URI list of variable usages. */
7019
7739
  usagesByUri = /* @__PURE__ */ new Map();
7020
7740
  /** Per-URI list of transient variable usages. */
7021
7741
  transientUsagesByUri = /* @__PURE__ */ new Map();
7742
+ /** Per-URI list of the `$` references Spindle validates at startup. */
7743
+ validatedRefsByUri = /* @__PURE__ */ new Map();
7022
7744
  /**
7023
7745
  * Parse the StoryVariables passage content for declarations.
7024
7746
  * Each line like `$name = value` becomes a declaration.
@@ -7028,10 +7750,11 @@ var init_variable_tracker = __esm({
7028
7750
  this._hasStoryVariables = true;
7029
7751
  this._nullDeclarations = [];
7030
7752
  const lines = content.split("\n");
7753
+ this._variablesCheck = checkDeclarations(lines, contentStartLine, "$");
7031
7754
  for (let i = 0; i < lines.length; i++) {
7032
7755
  const trimmed2 = lines[i].trim();
7033
7756
  if (!trimmed2 || trimmed2.startsWith("//") || trimmed2.startsWith("<!--")) continue;
7034
- const match2 = /^\$([A-Za-z_$][\w$]*)\s*=\s*(.*)$/.exec(trimmed2);
7757
+ const match2 = DECLARATION_RE2.$.exec(trimmed2);
7035
7758
  if (!match2) continue;
7036
7759
  const name = match2[1];
7037
7760
  const expr = match2[2].trim();
@@ -7053,6 +7776,8 @@ var init_variable_tracker = __esm({
7053
7776
  const decl = { name, sigil: "$", ...location };
7054
7777
  const type = inferLiteralType(expr);
7055
7778
  if (type) decl.type = type;
7779
+ const schema = inferDefaultSchema(expr);
7780
+ if (schema) decl.schema = schema;
7056
7781
  if (expr.startsWith("{")) {
7057
7782
  const fieldRegex = /(\w+)\s*:/g;
7058
7783
  let fieldMatch;
@@ -7072,12 +7797,14 @@ var init_variable_tracker = __esm({
7072
7797
  this.declared.clear();
7073
7798
  this._hasStoryVariables = false;
7074
7799
  this._nullDeclarations = [];
7800
+ this._variablesCheck = NO_DECLARATIONS;
7075
7801
  }
7076
7802
  /** Forget StoryTransients declarations, e.g. after the passage was removed. */
7077
7803
  clearStoryTransients() {
7078
7804
  this.declaredTransient.clear();
7079
7805
  this._hasStoryTransients = false;
7080
7806
  this._nullTransientDeclarations = [];
7807
+ this._transientsCheck = NO_DECLARATIONS;
7081
7808
  }
7082
7809
  /**
7083
7810
  * Parse the StoryTransients passage content for declarations.
@@ -7088,10 +7815,11 @@ var init_variable_tracker = __esm({
7088
7815
  this._hasStoryTransients = true;
7089
7816
  this._nullTransientDeclarations = [];
7090
7817
  const lines = content.split("\n");
7818
+ this._transientsCheck = checkDeclarations(lines, contentStartLine, "%");
7091
7819
  for (let i = 0; i < lines.length; i++) {
7092
7820
  const trimmed2 = lines[i].trim();
7093
7821
  if (!trimmed2 || trimmed2.startsWith("//") || trimmed2.startsWith("<!--")) continue;
7094
- const match2 = /^%([A-Za-z_$][\w$]*)\s*=\s*(.*)$/.exec(trimmed2);
7822
+ const match2 = DECLARATION_RE2["%"].exec(trimmed2);
7095
7823
  if (!match2) continue;
7096
7824
  const name = match2[1];
7097
7825
  const expr = match2[2].trim();
@@ -7136,6 +7864,7 @@ var init_variable_tracker = __esm({
7136
7864
  scanDocument(uri, text, _macros, storeVarMacros = BUILTIN_STORE_VAR_MACROS) {
7137
7865
  this.usagesByUri.delete(uri);
7138
7866
  this.transientUsagesByUri.delete(uri);
7867
+ this.validatedRefsByUri.delete(uri);
7139
7868
  const lines = text.split("\n");
7140
7869
  const usages = [];
7141
7870
  const transientUsages = [];
@@ -7170,14 +7899,15 @@ var init_variable_tracker = __esm({
7170
7899
  for (let i = 0; i < cleaned.length; i++) {
7171
7900
  if (cleaned[i] === "\n") lineOffsets.push(i + 1);
7172
7901
  }
7173
- const checkedPassage = !UNCHECKED_PASSAGES.has(passage.name);
7902
+ const blocks = codeBlocks(uncommented, createCodeScanner(uncommented));
7903
+ const inCode = (offset) => blocks.some(([open, close]) => open < offset && offset < close);
7174
7904
  const scans = [
7175
- [varRefRegex, usages],
7176
- [transientRefRegex, transientUsages]
7905
+ [varRefRegex, usages, false],
7906
+ [transientRefRegex, transientUsages, true]
7177
7907
  ];
7178
- for (const [regex, out] of scans) {
7908
+ for (const [regex, out, digitsOnlyInCode] of scans) {
7179
7909
  const seen = /* @__PURE__ */ new Set();
7180
- for (const [source, checked] of [[cleaned, checkedPassage], [referenced, false]]) {
7910
+ for (const source of [cleaned, referenced]) {
7181
7911
  const re = new RegExp(regex.source, "g");
7182
7912
  let match2;
7183
7913
  while ((match2 = re.exec(source)) !== null) {
@@ -7186,6 +7916,7 @@ var init_variable_tracker = __esm({
7186
7916
  seen.add(charOffset);
7187
7917
  const fullName = match2[1];
7188
7918
  const baseName = fullName.split(".")[0];
7919
+ if (digitsOnlyInCode && /^\d/.test(baseName) && !inCode(charOffset)) continue;
7189
7920
  let localLine = 0;
7190
7921
  for (let i = 0; i < lineOffsets.length; i++) {
7191
7922
  if (lineOffsets[i] > charOffset) break;
@@ -7197,7 +7928,7 @@ var init_variable_tracker = __esm({
7197
7928
  start: { line: absoluteLine, character },
7198
7929
  end: { line: absoluteLine, character: character + match2[0].length }
7199
7930
  };
7200
- out.push({ uri, baseName, fullName, range: range2, checked });
7931
+ out.push({ uri, baseName, fullName, range: range2 });
7201
7932
  }
7202
7933
  }
7203
7934
  }
@@ -7208,11 +7939,16 @@ var init_variable_tracker = __esm({
7208
7939
  if (transientUsages.length > 0) {
7209
7940
  this.transientUsagesByUri.set(uri, transientUsages);
7210
7941
  }
7942
+ const validated = validatedReferences(lines, passageBoundaries);
7943
+ if (validated.length > 0) {
7944
+ this.validatedRefsByUri.set(uri, validated);
7945
+ }
7211
7946
  }
7212
7947
  /** Forget the usages recorded for a document, e.g. after it was deleted. */
7213
7948
  removeDocument(uri) {
7214
7949
  this.usagesByUri.delete(uri);
7215
7950
  this.transientUsagesByUri.delete(uri);
7951
+ this.validatedRefsByUri.delete(uri);
7216
7952
  }
7217
7953
  /** Get all declared variables. */
7218
7954
  getDeclared() {
@@ -7230,20 +7966,52 @@ var init_variable_tracker = __esm({
7230
7966
  }
7231
7967
  return results;
7232
7968
  }
7233
- /** Get undeclared variable usages in a specific document. */
7969
+ /**
7970
+ * Get undeclared variable references in a specific document: the ones
7971
+ * Spindle rejects when the story starts, wherever they are in the text.
7972
+ */
7234
7973
  getUndeclared(uri) {
7235
- const usages = this.usagesByUri.get(uri);
7236
- if (!usages) return [];
7974
+ const refs = this.validatedRefsByUri.get(uri);
7975
+ if (!refs) return [];
7237
7976
  const results = [];
7238
7977
  const seen = /* @__PURE__ */ new Set();
7239
- for (const u of usages) {
7240
- if (u.checked && !this.declared.has(u.baseName) && !seen.has(u.baseName)) {
7978
+ for (const u of refs) {
7979
+ if (!this.declared.has(u.baseName) && !seen.has(u.baseName)) {
7241
7980
  seen.add(u.baseName);
7242
7981
  results.push({ name: u.baseName, range: u.range });
7243
7982
  }
7244
7983
  }
7245
7984
  return results;
7246
7985
  }
7986
+ /**
7987
+ * Get the `$var.a.b` paths in a document that Spindle rejects at startup
7988
+ * because they access a field of a number, string or boolean, judged by
7989
+ * the StoryVariables defaults as Spindle's validateRef() does. Every
7990
+ * occurrence is reported; defaults that are not literals are not checked.
7991
+ */
7992
+ getPrimitiveFieldAccesses(uri) {
7993
+ const results = [];
7994
+ for (const ref of this.validatedRefsByUri.get(uri) ?? []) {
7995
+ const schema = this.declared.get(ref.baseName)?.schema;
7996
+ if (!schema) continue;
7997
+ const parts = ref.path.split(".");
7998
+ const found = findPrimitiveFieldAccess(schema, parts.slice(1));
7999
+ if (!found) continue;
8000
+ const owner = parts.slice(0, found.index + 1).join(".");
8001
+ const field = parts[found.index + 1];
8002
+ const start = ref.range.start.character + 1 + owner.length + 1;
8003
+ results.push({
8004
+ path: `$${owner}`,
8005
+ field,
8006
+ type: found.type,
8007
+ range: {
8008
+ start: { line: ref.range.start.line, character: start },
8009
+ end: { line: ref.range.start.line, character: start + field.length }
8010
+ }
8011
+ });
8012
+ }
8013
+ return results;
8014
+ }
7247
8015
  /** Whether a StoryVariables passage has been parsed. */
7248
8016
  hasStoryVariables() {
7249
8017
  return this._hasStoryVariables;
@@ -7271,7 +8039,7 @@ var init_variable_tracker = __esm({
7271
8039
  const results = [];
7272
8040
  const seen = /* @__PURE__ */ new Set();
7273
8041
  for (const u of usages) {
7274
- if (u.checked && !this.declaredTransient.has(u.baseName) && !seen.has(u.baseName)) {
8042
+ if (!this.declaredTransient.has(u.baseName) && !seen.has(u.baseName)) {
7275
8043
  seen.add(u.baseName);
7276
8044
  results.push({ name: u.baseName, range: u.range });
7277
8045
  }
@@ -7296,7 +8064,6 @@ var init_variable_tracker = __esm({
7296
8064
  for (const [sigil, usages, declared] of sources) {
7297
8065
  if (!usages) continue;
7298
8066
  for (const u of usages) {
7299
- if (!u.checked) continue;
7300
8067
  const member = u.fullName.split(".")[1];
7301
8068
  if (member === void 0) continue;
7302
8069
  if (declared.get(u.baseName)?.type !== "array") continue;
@@ -7308,11 +8075,32 @@ var init_variable_tracker = __esm({
7308
8075
  }
7309
8076
  /** Get variables declared with null values in StoryVariables. */
7310
8077
  getNullDeclarations() {
7311
- return this._nullDeclarations;
8078
+ return [...this._nullDeclarations, ...this._variablesCheck.nestedNulls];
8079
+ }
8080
+ /** StoryVariables lines that stop Spindle from starting, apart from null values. */
8081
+ getInvalidDeclarations() {
8082
+ return this._variablesCheck.problems;
8083
+ }
8084
+ /**
8085
+ * StoryTransients lines that stop Spindle from starting, apart from null
8086
+ * values, and the names it declares that StoryVariables declares too.
8087
+ */
8088
+ getInvalidTransientDeclarations() {
8089
+ const collisions = [];
8090
+ if (this._hasStoryVariables) {
8091
+ for (const [name, range2] of this._transientsCheck.names) {
8092
+ if (!this._variablesCheck.names.has(name)) continue;
8093
+ collisions.push({
8094
+ message: `StoryTransients: Variable "${name}" is already declared in StoryVariables. Names must be unique across scopes.`,
8095
+ range: range2
8096
+ });
8097
+ }
8098
+ }
8099
+ return [...this._transientsCheck.problems, ...collisions].sort((a, b) => a.range.start.line - b.range.start.line);
7312
8100
  }
7313
8101
  /** Get transient variables declared with null values in StoryTransients. */
7314
8102
  getNullTransientDeclarations() {
7315
- return this._nullTransientDeclarations;
8103
+ return [...this._nullTransientDeclarations, ...this._transientsCheck.nestedNulls];
7316
8104
  }
7317
8105
  };
7318
8106
  }
@@ -7513,11 +8301,11 @@ function discoverMacrosFromSource(source) {
7513
8301
  }
7514
8302
  return macros;
7515
8303
  }
7516
- function discoverMacrosFromStoryInit(passageContent2) {
8304
+ function discoverMacrosFromStoryInit(passageContent3) {
7517
8305
  const macros = [];
7518
8306
  doBlockRegex.lastIndex = 0;
7519
8307
  let doMatch;
7520
- while ((doMatch = doBlockRegex.exec(passageContent2)) !== null) {
8308
+ while ((doMatch = doBlockRegex.exec(passageContent3)) !== null) {
7521
8309
  const jsContent = doMatch[1];
7522
8310
  const found = discoverMacrosFromSource(jsContent);
7523
8311
  macros.push(...found);
@@ -14924,8 +15712,11 @@ function isExcludedMacroSource(path2, root) {
14924
15712
  return (root ? relative(root, path2) : path2).split(/[\\/]/).some(isExcludedSegment);
14925
15713
  }
14926
15714
  function findMacroSourceFiles(root) {
15715
+ return findProjectFiles(root, MACRO_SOURCE_GLOB);
15716
+ }
15717
+ function findProjectFiles(root, pattern) {
14927
15718
  const excluded = (p) => isExcludedMacroSource(p.fullpath(), root);
14928
- return glob(MACRO_SOURCE_GLOB, {
15719
+ return glob(pattern, {
14929
15720
  cwd: root,
14930
15721
  absolute: true,
14931
15722
  nodir: true,
@@ -14975,6 +15766,82 @@ var init_macro_sources = __esm({
14975
15766
  }
14976
15767
  });
14977
15768
 
15769
+ // src/core/workspace/story-format.ts
15770
+ import { readFileSync as readFileSync5 } from "node:fs";
15771
+ function isSpindleFormatName(name) {
15772
+ return name.trim().toLowerCase() === "spindle";
15773
+ }
15774
+ function readStoryDataFormat(content) {
15775
+ let data;
15776
+ try {
15777
+ data = JSON.parse(content);
15778
+ } catch {
15779
+ return void 0;
15780
+ }
15781
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
15782
+ const format = data.format;
15783
+ if (typeof format !== "string") return void 0;
15784
+ return format.trim() || void 0;
15785
+ }
15786
+ function resolveStoryFormat(declared) {
15787
+ let foreign;
15788
+ for (const name of declared) {
15789
+ if (name === void 0) continue;
15790
+ if (isSpindleFormatName(name)) return { name: name.trim(), isSpindle: true };
15791
+ foreign ??= name.trim();
15792
+ }
15793
+ return foreign === void 0 ? UNDECLARED_STORY_FORMAT : { name: foreign, isSpindle: false };
15794
+ }
15795
+ function passageContent(passage, text) {
15796
+ return text.split("\n").slice(passage.headerEnd.end.line + 1, passage.range.end.line + 1).join("\n");
15797
+ }
15798
+ function storyDataFormats(passages, getText) {
15799
+ const formats = [];
15800
+ for (const passage of passages) {
15801
+ if (passage.name !== "StoryData") continue;
15802
+ const text = getText(passage.uri);
15803
+ formats.push(text === void 0 ? void 0 : readStoryDataFormat(passageContent(passage, text)));
15804
+ }
15805
+ return formats;
15806
+ }
15807
+ function storyFormatOfTexts(texts) {
15808
+ const index = new PassageIndex();
15809
+ const byUri = /* @__PURE__ */ new Map();
15810
+ for (const text of texts) {
15811
+ if (!text.includes("StoryData")) continue;
15812
+ const uri = `story-format:${byUri.size}`;
15813
+ byUri.set(uri, text);
15814
+ index.rebuild(uri, text);
15815
+ }
15816
+ const formats = storyDataFormats(index.getAllPassages(), (uri) => byUri.get(uri));
15817
+ return { ...resolveStoryFormat(formats), hasStoryData: formats.length > 0 };
15818
+ }
15819
+ async function findStoryFormat(texts, dir) {
15820
+ const own = storyFormatOfTexts(texts);
15821
+ if (own.hasStoryData) return { name: own.name, isSpindle: own.isSpindle };
15822
+ const projectTexts = [];
15823
+ for (const file2 of await findProjectFiles(findProjectRoot(dir), "**/*.{tw,twee}")) {
15824
+ try {
15825
+ projectTexts.push(readFileSync5(file2, "utf-8"));
15826
+ } catch {
15827
+ }
15828
+ }
15829
+ const { name, isSpindle } = storyFormatOfTexts(projectTexts);
15830
+ return { name, isSpindle };
15831
+ }
15832
+ function skippedFormatNote(format) {
15833
+ return `Skipped: story format is ${format.name}, not Spindle`;
15834
+ }
15835
+ var UNDECLARED_STORY_FORMAT;
15836
+ var init_story_format = __esm({
15837
+ "src/core/workspace/story-format.ts"() {
15838
+ "use strict";
15839
+ init_passage_index();
15840
+ init_macro_sources();
15841
+ UNDECLARED_STORY_FORMAT = { name: void 0, isSpindle: true };
15842
+ }
15843
+ });
15844
+
14978
15845
  // src/macro-supplements.json
14979
15846
  var macro_supplements_default;
14980
15847
  var init_macro_supplements = __esm({
@@ -15004,9 +15871,6 @@ var init_macro_supplements = __esm({
15004
15871
  },
15005
15872
  for: {
15006
15873
  name: "for",
15007
- children: [
15008
- { name: "stop" }
15009
- ],
15010
15874
  container: true,
15011
15875
  description: "Loop block. Usage: `{for @item of $list}...{/for}` or `{for @item, @index of $list}...{/for}`",
15012
15876
  skipArgs: true
@@ -15019,8 +15883,8 @@ var init_macro_supplements = __esm({
15019
15883
  },
15020
15884
  stop: {
15021
15885
  name: "stop",
15022
- parents: ["for", "repeat"],
15023
- description: "Breaks out of a `{for}` or `{repeat}` loop.\n\nUsage: `{stop}`",
15886
+ parents: ["repeat"],
15887
+ description: "Stops the enclosing `{repeat}` loop. It has no effect anywhere else, including in a `{for}` loop.\n\nUsage: `{stop}`",
15024
15888
  parameters: []
15025
15889
  },
15026
15890
  switch: {
@@ -15089,8 +15953,8 @@ var init_macro_supplements = __esm({
15089
15953
  { name: "next" }
15090
15954
  ],
15091
15955
  container: true,
15092
- description: "Displays its contents after a specified delay.\n\nUsage:\n\n```\n{timed delay}\n ...\n {next delay}\n ...\n{/timed}\n```",
15093
- parameters: ["text"]
15956
+ description: "Displays its contents after a specified delay (immediately when omitted).\n\nUsage:\n\n```\n{timed delay}\n ...\n {next delay}\n ...\n{/timed}\n```",
15957
+ parameters: ["text", ""]
15094
15958
  },
15095
15959
  repeat: {
15096
15960
  name: "repeat",
@@ -15284,6 +16148,7 @@ var init_workspace_model = __esm({
15284
16148
  init_macro_parser();
15285
16149
  init_macro_discovery();
15286
16150
  init_macro_sources();
16151
+ init_story_format();
15287
16152
  init_macro_supplements();
15288
16153
  WorkspaceModel = class extends EventEmitter3 {
15289
16154
  documents;
@@ -15293,6 +16158,8 @@ var init_workspace_model = __esm({
15293
16158
  widgets;
15294
16159
  /** True after initialize() has completed (full workspace scan done). */
15295
16160
  initialized = false;
16161
+ /** The story format declared by StoryData, updated with the passages. */
16162
+ format = UNDECLARED_STORY_FORMAT;
15296
16163
  debounceTimer = null;
15297
16164
  DEBOUNCE_MS = 200;
15298
16165
  /** Bound listeners for cleanup in dispose(). */
@@ -15347,6 +16214,22 @@ var init_workspace_model = __esm({
15347
16214
  isContainer(name) {
15348
16215
  return this.macros.isBlock(name) || (this.widgets.getWidget(name)?.block ?? false);
15349
16216
  }
16217
+ /**
16218
+ * The story format named by the project's StoryData passage (trimmed), or
16219
+ * undefined if no StoryData passage names one.
16220
+ */
16221
+ get storyFormat() {
16222
+ return this.format.name;
16223
+ }
16224
+ /**
16225
+ * Whether Spindle semantics apply to this project. False only if a
16226
+ * StoryData passage names another story format (SugarCube, Harlowe, ...)
16227
+ * and none names Spindle; a missing or unreadable StoryData counts as
16228
+ * Spindle.
16229
+ */
16230
+ isSpindleProject() {
16231
+ return this.format.isSpindle;
16232
+ }
15350
16233
  /** Clean up listeners and timers. */
15351
16234
  dispose() {
15352
16235
  this.documents.removeListener("documentChanged", this.onDocumentChanged);
@@ -15366,6 +16249,7 @@ var init_workspace_model = __esm({
15366
16249
  }
15367
16250
  this.refreshDiscoveredMacros();
15368
16251
  this.cascade();
16252
+ this.updateStoryFormat();
15369
16253
  this.emit("documentChanged", uri);
15370
16254
  this.emit("passagesUpdated", uri);
15371
16255
  this.scheduleModelReady();
@@ -15376,6 +16260,7 @@ var init_workspace_model = __esm({
15376
16260
  this.variables.removeDocument(uri);
15377
16261
  this.refreshDiscoveredMacros();
15378
16262
  this.cascade();
16263
+ this.updateStoryFormat();
15379
16264
  this.emit("documentClosed", uri);
15380
16265
  this.emit("passagesUpdated", uri);
15381
16266
  this.scheduleModelReady();
@@ -15390,6 +16275,21 @@ var init_workspace_model = __esm({
15390
16275
  }
15391
16276
  this.refreshDiscoveredMacros();
15392
16277
  this.cascade();
16278
+ this.updateStoryFormat();
16279
+ }
16280
+ /**
16281
+ * Re-read the story format from the StoryData passages after the passage
16282
+ * index changed, emitting 'storyFormatChanged' if it differs. Called after
16283
+ * the cascade, so listeners see up-to-date indices.
16284
+ */
16285
+ updateStoryFormat() {
16286
+ const format = resolveStoryFormat(storyDataFormats(
16287
+ this.passages.getAllPassages(),
16288
+ (uri) => this.documents.getText(uri)
16289
+ ));
16290
+ if (format.name === this.format.name && format.isSpindle === this.format.isSpindle) return;
16291
+ this.format = format;
16292
+ this.emit("storyFormatChanged", format);
15393
16293
  }
15394
16294
  /**
15395
16295
  * Re-run static macro discovery over the workspace and replace the
@@ -15505,6 +16405,8 @@ var init_diagnostic_codes = __esm({
15505
16405
  DiagnosticCode = {
15506
16406
  UndefinedMacro: "SP100",
15507
16407
  MalformedContainer: "SP101",
16408
+ MalformedElement: "SP102",
16409
+ UnevaluatedAttributeBlock: "SP103",
15508
16410
  IllegalClosingTag: "SP104",
15509
16411
  InvalidChildren: "SP107",
15510
16412
  ExpectedNoArguments: "SP108",
@@ -15515,12 +16417,13 @@ var init_diagnostic_codes = __esm({
15515
16417
  ChildMaxExceeded: "SP114",
15516
16418
  ChildMinNotMet: "SP115",
15517
16419
  UndeclaredVariable: "SP200",
15518
- UndeclaredField: "SP201",
16420
+ PrimitiveFieldAccess: "SP201",
15519
16421
  NoStoryVariables: "SP202",
15520
16422
  UndeclaredTransient: "SP203",
15521
16423
  NullVariableValue: "SP204",
15522
16424
  TemporaryAssignedInLoop: "SP205",
15523
16425
  ArrayMemberAccess: "SP206",
16426
+ InvalidDeclaration: "SP207",
15524
16427
  BrokenPassageLink: "SP300",
15525
16428
  WidgetArgCountMismatch: "SP301",
15526
16429
  IncludeWidgetPassage: "SP302",
@@ -15532,6 +16435,8 @@ var init_diagnostic_codes = __esm({
15532
16435
  severityMap = {
15533
16436
  SP100: "warning",
15534
16437
  SP101: "error",
16438
+ SP102: "error",
16439
+ SP103: "warning",
15535
16440
  SP104: "error",
15536
16441
  SP107: "error",
15537
16442
  SP108: "error",
@@ -15542,12 +16447,13 @@ var init_diagnostic_codes = __esm({
15542
16447
  SP114: "error",
15543
16448
  SP115: "error",
15544
16449
  SP200: "error",
15545
- SP201: "warning",
16450
+ SP201: "error",
15546
16451
  SP202: "info",
15547
16452
  SP203: "warning",
15548
16453
  SP204: "error",
15549
16454
  SP205: "warning",
15550
16455
  SP206: "warning",
16456
+ SP207: "error",
15551
16457
  SP300: "warning",
15552
16458
  SP301: "warning",
15553
16459
  SP302: "warning",
@@ -15829,6 +16735,162 @@ var init_argument_lexer = __esm({
15829
16735
  }
15830
16736
  });
15831
16737
 
16738
+ // src/core/parsing/attribute-blocks.ts
16739
+ function findUnevaluatedBlocks(value, lookup) {
16740
+ const blocks = [];
16741
+ if (!value.includes("{")) return blocks;
16742
+ const code = createCodeScanner(value);
16743
+ let i = 0;
16744
+ while (i < value.length) {
16745
+ i = value.indexOf("{", i);
16746
+ if (i === -1) break;
16747
+ const block = readBlock(value, code, i);
16748
+ if (block.kind === "skip") {
16749
+ i = block.next;
16750
+ continue;
16751
+ }
16752
+ const inner = value.slice(i + 1, block.close);
16753
+ const head = MACRO_HEAD.exec(inner);
16754
+ if (head && lookup.isMacro(head[2])) {
16755
+ const closing = head[1] === "/";
16756
+ let end = block.close + 1;
16757
+ if (!closing && lookup.isContainer(head[2])) {
16758
+ end = containerEnd(value, code, end, head[2]) ?? end;
16759
+ }
16760
+ blocks.push({ start: i, end, kind: "macro", macro: closing ? `/${head[2]}` : head[2] });
16761
+ i = end;
16762
+ } else if (usesVariable(inner)) {
16763
+ blocks.push({ start: i, end: block.close + 1, kind: "expression" });
16764
+ i = block.close + 1;
16765
+ } else {
16766
+ i++;
16767
+ }
16768
+ }
16769
+ return blocks;
16770
+ }
16771
+ function readBlock(value, code, i) {
16772
+ if (i > 0 && value[i - 1] === "\\") return { kind: "skip", next: i + 1 };
16773
+ if (SIGILS.has(value[i + 1])) {
16774
+ const end = Math.max(plainClose(value, i + 1), code.closeBrace(i + 1));
16775
+ return { kind: "skip", next: end === -1 ? i + 1 : end + 1 };
16776
+ }
16777
+ const close = code.closeBrace(i + 1);
16778
+ return close === -1 ? { kind: "skip", next: i + 1 } : { kind: "text", close };
16779
+ }
16780
+ function plainClose(value, i) {
16781
+ let depth = 1;
16782
+ for (; i < value.length; i++) {
16783
+ if (value[i] === "{") depth++;
16784
+ else if (value[i] === "}" && --depth === 0) return i;
16785
+ }
16786
+ return -1;
16787
+ }
16788
+ function containerEnd(value, code, from, name) {
16789
+ const lower = name.toLowerCase();
16790
+ let depth = 1;
16791
+ let i = from;
16792
+ while (i < value.length) {
16793
+ i = value.indexOf("{", i);
16794
+ if (i === -1) break;
16795
+ const block = readBlock(value, code, i);
16796
+ if (block.kind === "skip") {
16797
+ i = block.next;
16798
+ continue;
16799
+ }
16800
+ const head = MACRO_HEAD.exec(value.slice(i + 1, block.close));
16801
+ if (head && head[2].toLowerCase() === lower) {
16802
+ depth += head[1] === "/" ? -1 : 1;
16803
+ if (depth === 0) return block.close + 1;
16804
+ i = block.close + 1;
16805
+ } else {
16806
+ i++;
16807
+ }
16808
+ }
16809
+ return void 0;
16810
+ }
16811
+ function usesVariable(inner) {
16812
+ const code = createCodeScanner(inner);
16813
+ let masked = "";
16814
+ let i = 0;
16815
+ while (i < inner.length) {
16816
+ const literal2 = code.literalEnd(i);
16817
+ if (literal2 !== -1) {
16818
+ masked += " ".repeat(literal2 - i);
16819
+ i = literal2;
16820
+ continue;
16821
+ }
16822
+ if (inner[i] === "{" && SIGILS.has(inner[i + 1])) {
16823
+ const close = code.closeBrace(i + 1);
16824
+ if (close !== -1) {
16825
+ masked += " ".repeat(close + 1 - i);
16826
+ i = close + 1;
16827
+ continue;
16828
+ }
16829
+ }
16830
+ masked += inner[i];
16831
+ i++;
16832
+ }
16833
+ return VARIABLE_REFERENCE.test(masked);
16834
+ }
16835
+ function conditionalExpression(source) {
16836
+ const match2 = IF_ELSE.exec(source);
16837
+ if (!match2) return null;
16838
+ const condition = match2[1].trim();
16839
+ const [whenTrue, whenFalse = ""] = [match2[2], match2[3]];
16840
+ if (!/^[$_@%]\w/.test(condition)) return null;
16841
+ if (/[`\r\n]/.test(condition) || /[\r\n]/.test(whenTrue + whenFalse)) return null;
16842
+ const code = createCodeScanner(condition);
16843
+ let operators = "";
16844
+ for (let i = 0; i < condition.length; ) {
16845
+ const literal2 = code.literalEnd(i);
16846
+ if (literal2 !== -1) {
16847
+ i = literal2;
16848
+ continue;
16849
+ }
16850
+ operators += condition[i];
16851
+ i++;
16852
+ }
16853
+ if (/(?:<<|>>)=/.test(operators)) return null;
16854
+ const rest = operators.replace(/\?\?|\?\./g, "").replace(/[!=]==?|[<>]=/g, "");
16855
+ if (/[?=,;]/.test(rest)) return null;
16856
+ const quote = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
16857
+ return `{${condition} ? ${quote(whenTrue)} : ${quote(whenFalse)}}`;
16858
+ }
16859
+ function printExpression(source) {
16860
+ const match2 = /^\{print\s+([^{}\r\n]*)\}$/.exec(source);
16861
+ if (!match2) return null;
16862
+ const expr = match2[1].trim();
16863
+ if (!/^[$_@%]\w/.test(expr)) return null;
16864
+ const code = createCodeScanner(expr);
16865
+ let depth = 0;
16866
+ for (let i = 0; i < expr.length; ) {
16867
+ const literal2 = code.literalEnd(i);
16868
+ if (literal2 !== -1) {
16869
+ i = literal2;
16870
+ continue;
16871
+ }
16872
+ const c3 = expr[i];
16873
+ if (c3 === "(" || c3 === "[") depth++;
16874
+ else if (c3 === ")" || c3 === "]") depth--;
16875
+ else if (c3 === ";" || c3 === "," && depth <= 0) return null;
16876
+ i++;
16877
+ }
16878
+ return SIMPLE_PATH.test(expr) && expr.includes(".") ? `{${expr} ?? ''}` : `{${expr}}`;
16879
+ }
16880
+ var SIGILS, MACRO_HEAD, VARIABLE_REFERENCE, IF_ELSE, SIMPLE_PATH;
16881
+ var init_attribute_blocks = __esm({
16882
+ "src/core/parsing/attribute-blocks.ts"() {
16883
+ "use strict";
16884
+ init_code_scanner();
16885
+ init_macro_parser();
16886
+ SIGILS = /* @__PURE__ */ new Set(["$", "_", "@", "%"]);
16887
+ MACRO_HEAD = new RegExp(String.raw`^(?:(\/)|${SELECTOR_PATTERN} )?([A-Za-z][\w-]*)(?=\s|$|\{[$_@%])`);
16888
+ VARIABLE_REFERENCE = /(?:^|[^\w$.@%])[$_@%][A-Za-z_$]/;
16889
+ IF_ELSE = /^\{if\s+([^{}]*)\}([^{}]*?)(?:\{else\s*\}([^{}]*))?\{\/if\s*\}$/;
16890
+ SIMPLE_PATH = /^[$_@%][\w.]+$/;
16891
+ }
16892
+ });
16893
+
15832
16894
  // src/core/parsing/widget-arguments.ts
15833
16895
  function splitWidgetArguments(raw) {
15834
16896
  const args2 = [];
@@ -16331,7 +17393,7 @@ var init_parameter_validator = __esm({
16331
17393
  if (/^[_@%]/.test(name)) {
16332
17394
  return new TypeError2(`Variable receiver must be a story variable ($name), got '${name}'`);
16333
17395
  }
16334
- if (!/^\$?[A-Za-z_][\w$]*(?:\.[\w$]+)*$/.test(name)) {
17396
+ if (!/^(?:\$\w|[A-Za-z_])[\w$]*(?:\.[\w$]+)*$/.test(name)) {
16335
17397
  return new TypeError2(`Argument is not a valid variable receiver: '${name}'`);
16336
17398
  }
16337
17399
  return null;
@@ -16664,6 +17726,7 @@ var init_link_parser = __esm({
16664
17726
  // src/plugins/diagnostics.ts
16665
17727
  function computeDiagnostics(uri, workspace, options) {
16666
17728
  if (!workspace.initialized) return [];
17729
+ if (!workspace.isSpindleProject()) return [];
16667
17730
  try {
16668
17731
  const text = workspace.documents.getText(uri);
16669
17732
  if (text === void 0) return [];
@@ -16679,8 +17742,19 @@ function computeDiagnostics(uri, workspace, options) {
16679
17742
  );
16680
17743
  const allPassages = workspace.passages.getAllPassages();
16681
17744
  const passageNames = new Set(allPassages.map((p) => p.name));
17745
+ let elements = /* @__PURE__ */ new Map();
16682
17746
  try {
16683
- validateMacros(macros, workspace, diagnostics);
17747
+ const replay = replayElements(markupText, macros, passages, workspace);
17748
+ elements = replay.elements;
17749
+ diagnostics.push(...replay.errors);
17750
+ } catch {
17751
+ }
17752
+ try {
17753
+ validateAttributeBlocks(markupText, passages, workspace, diagnostics);
17754
+ } catch {
17755
+ }
17756
+ try {
17757
+ validateMacros(macros, elements, workspace, diagnostics);
16684
17758
  } catch {
16685
17759
  }
16686
17760
  try {
@@ -16738,7 +17812,7 @@ function maskScriptAndStylesheetPassages(text, passages) {
16738
17812
  }
16739
17813
  return lines.join("\n");
16740
17814
  }
16741
- function validateMacros(macros, workspace, diagnostics) {
17815
+ function validateMacros(macros, elements, workspace, diagnostics) {
16742
17816
  for (let curIndex = 0; curIndex < macros.length; curIndex++) {
16743
17817
  const macro = macros[curIndex];
16744
17818
  const info = workspace.macros.getMacro(macro.name);
@@ -16767,7 +17841,7 @@ function validateMacros(macros, workspace, diagnostics) {
16767
17841
  ));
16768
17842
  }
16769
17843
  if (info?.children && info.children.length > 0 && macro.open && macro.pair !== -1) {
16770
- validateChildren(macros, curIndex, macro, info.children, workspace, diagnostics);
17844
+ validateChildren(macros, curIndex, macro, info.children, elements, workspace, diagnostics);
16771
17845
  }
16772
17846
  } else if (!macro.open) {
16773
17847
  diagnostics.push(makeDiag(
@@ -16777,8 +17851,20 @@ function validateMacros(macros, workspace, diagnostics) {
16777
17851
  ));
16778
17852
  }
16779
17853
  if (info?.parents && info.parents.length > 0 && macro.open) {
16780
- if (!isInsideParent(macros, curIndex, info.parents, workspace)) {
16781
- const parentList = info.parents.join(", ");
17854
+ const parentList = info.parents.join(", ");
17855
+ if (DIRECT_CHILD_MACROS.has(macro.name.toLowerCase())) {
17856
+ const element = elements.get(curIndex);
17857
+ const enclosing = element === void 0 ? innermostContainer(macros, curIndex, workspace) : void 0;
17858
+ const parentSet = new Set(info.parents.map((p) => p.toLowerCase()));
17859
+ if (element !== void 0 || !enclosing || !parentSet.has(enclosing.name.toLowerCase())) {
17860
+ const where = element !== void 0 ? `, not inside <${element}>` : enclosing ? `, not inside {${enclosing.name}}` : "";
17861
+ diagnostics.push(makeDiag(
17862
+ macro.range,
17863
+ DiagnosticCode.InvalidChildren,
17864
+ `Invalid: {${macro.name}} can only be directly inside {${parentList}}${where}`
17865
+ ));
17866
+ }
17867
+ } else if (!isInsideParent(macros, curIndex, info.parents, workspace)) {
16782
17868
  diagnostics.push(makeDiag(
16783
17869
  macro.range,
16784
17870
  DiagnosticCode.InvalidChildren,
@@ -16788,6 +17874,146 @@ function validateMacros(macros, workspace, diagnostics) {
16788
17874
  }
16789
17875
  }
16790
17876
  }
17877
+ function innermostContainer(macros, index, workspace) {
17878
+ for (let i = index - 1; i >= 0; i--) {
17879
+ const candidate = macros[i];
17880
+ if (!candidate.open || candidate.pair <= index) continue;
17881
+ if (workspace.isContainer(candidate.name)) return candidate;
17882
+ }
17883
+ return void 0;
17884
+ }
17885
+ function isMarkupPassage(passage) {
17886
+ return !NON_MARKUP_PASSAGES.has(passage.name) && !isScriptOrStylesheetPassage(passage);
17887
+ }
17888
+ function replayElements(text, macros, passages, workspace) {
17889
+ const replay = { elements: /* @__PURE__ */ new Map(), errors: [] };
17890
+ if (!text.includes("<")) return replay;
17891
+ const lineStarts = buildLineStarts(text);
17892
+ const lineOffset = (line) => lineStarts[line] ?? text.length;
17893
+ const ordered = [...passages].sort((a, b) => a.range.start.line - b.range.start.line);
17894
+ let m = 0;
17895
+ for (const passage of ordered) {
17896
+ const contentStart = lineOffset(passage.range.start.line + 1);
17897
+ const contentEnd = lineOffset(passage.range.end.line + 1);
17898
+ while (m < macros.length && positionToOffset(macros[m].range.start, lineStarts) < contentStart) m++;
17899
+ const first = m;
17900
+ while (m < macros.length && positionToOffset(macros[m].range.start, lineStarts) < contentEnd) m++;
17901
+ const content = text.slice(contentStart, contentEnd);
17902
+ if (!content.includes("<") || !isMarkupPassage(passage)) continue;
17903
+ replayPassage(macros, first, m, scanHtmlTags(content), contentStart, lineStarts, workspace, replay);
17904
+ }
17905
+ return replay;
17906
+ }
17907
+ function replayPassage(macros, from, to, scan, base, lineStarts, workspace, replay) {
17908
+ const { tags } = scan;
17909
+ const macroStart = (k) => positionToOffset(macros[k].range.start, lineStarts) - base;
17910
+ let certainUntil = scan.stoppedAt === -1 ? Infinity : scan.stoppedAt;
17911
+ const parsed = /* @__PURE__ */ new Set();
17912
+ for (let k = from; k < to; k++) parsed.add(macroStart(k));
17913
+ const unparsed = scan.macros.find((at) => !parsed.has(at));
17914
+ if (unparsed !== void 0) certainUntil = Math.min(certainUntil, unparsed);
17915
+ const stack = [];
17916
+ let t = 0;
17917
+ let s = 0;
17918
+ let lastMacroEnd = -1;
17919
+ const report2 = (range2, message) => {
17920
+ replay.errors.push(makeDiag(range2, DiagnosticCode.MalformedElement, `Malformed element: ${message}`));
17921
+ };
17922
+ const tagRange = (tag) => ({
17923
+ start: offsetToPosition(base + tag.start, lineStarts),
17924
+ end: offsetToPosition(base + tag.end, lineStarts)
17925
+ });
17926
+ const applyTags = (until) => {
17927
+ for (; t < tags.length; t++) {
17928
+ const tag = tags[t];
17929
+ const at = tag.kind === "close" ? tag.start : tag.end;
17930
+ if (at > until) break;
17931
+ if (tag.start >= certainUntil) return false;
17932
+ if (tag.start < lastMacroEnd) return false;
17933
+ if (tag.kind === "open") {
17934
+ stack.push({ element: tag });
17935
+ } else if (tag.kind === "close") {
17936
+ const top = stack[stack.length - 1];
17937
+ const found = `</${tag.name}>`;
17938
+ if (!top) {
17939
+ report2(tagRange(tag), `unexpected closing ${found}`);
17940
+ return false;
17941
+ }
17942
+ if ("macro" in top) {
17943
+ report2(tagRange(tag), `expected {/${macros[top.macro].name}} but found ${found}`);
17944
+ return false;
17945
+ }
17946
+ if (top.element.name.toLowerCase() !== tag.name.toLowerCase()) {
17947
+ report2(tagRange(tag), `expected </${top.element.name}> but found ${found}`);
17948
+ return false;
17949
+ }
17950
+ stack.pop();
17951
+ }
17952
+ }
17953
+ return true;
17954
+ };
17955
+ for (let k = from; k < to; k++) {
17956
+ const macro = macros[k];
17957
+ const start = macroStart(k);
17958
+ if (!applyTags(start)) return;
17959
+ if (start >= certainUntil) return;
17960
+ while (s < scan.macros.length && scan.macros[s] < start) s++;
17961
+ if (scan.macros[s] !== start) return;
17962
+ lastMacroEnd = positionToOffset(macro.range.end, lineStarts) - base;
17963
+ const top = stack[stack.length - 1];
17964
+ if (top && "element" in top) replay.elements.set(k, top.element.name);
17965
+ if (!workspace.isContainer(macro.name)) continue;
17966
+ if (macro.pair === -1) return;
17967
+ if (macro.open) {
17968
+ stack.push({ macro: k });
17969
+ continue;
17970
+ }
17971
+ const opener = stack.pop();
17972
+ if (opener && "element" in opener) {
17973
+ report2(macro.range, `expected </${opener.element.name}> but found {/${macro.name}}`);
17974
+ return;
17975
+ }
17976
+ if (!opener || opener.macro !== macro.pair) return;
17977
+ }
17978
+ if (!applyTags(Infinity) || certainUntil !== Infinity) return;
17979
+ for (const entry of stack) {
17980
+ if ("element" in entry) report2(tagRange(entry.element), `unclosed <${entry.element.name}>`);
17981
+ }
17982
+ }
17983
+ function validateAttributeBlocks(text, passages, workspace, diagnostics) {
17984
+ if (!text.includes("<")) return;
17985
+ const lineStarts = buildLineStarts(text);
17986
+ const lineOffset = (line) => lineStarts[line] ?? text.length;
17987
+ const lookup = {
17988
+ isMacro: (name) => workspace.macros.getMacro(name) !== void 0 || workspace.widgets.getWidget(name) !== void 0,
17989
+ isContainer: (name) => workspace.isContainer(name)
17990
+ };
17991
+ for (const passage of passages) {
17992
+ if (!isMarkupPassage(passage)) continue;
17993
+ const base = lineOffset(passage.range.start.line + 1);
17994
+ const content = text.slice(base, lineOffset(passage.range.end.line + 1));
17995
+ if (!content.includes("<") || !content.includes("{")) continue;
17996
+ let rawText;
17997
+ for (const tag of scanHtmlTags(content).tags) {
17998
+ const name = tag.name.toLowerCase();
17999
+ if (rawText !== void 0) {
18000
+ if (tag.kind === "close" && name === rawText) rawText = void 0;
18001
+ continue;
18002
+ }
18003
+ for (const [start, end] of tag.values ?? []) {
18004
+ for (const block of findUnevaluatedBlocks(content.slice(start, end), lookup)) {
18005
+ const range2 = {
18006
+ start: offsetToPosition(base + start + block.start, lineStarts),
18007
+ end: offsetToPosition(base + start + block.end, lineStarts)
18008
+ };
18009
+ const message = block.kind === "macro" ? MACRO_IN_ATTRIBUTE(block.macro) : EXPRESSION_IN_ATTRIBUTE;
18010
+ diagnostics.push(makeDiag(range2, DiagnosticCode.UnevaluatedAttributeBlock, message));
18011
+ }
18012
+ }
18013
+ if (tag.kind === "open" && RAW_TEXT_ELEMENTS.has(name)) rawText = name;
18014
+ }
18015
+ }
18016
+ }
16791
18017
  function isInsideParent(macros, index, parents, workspace) {
16792
18018
  const parentSet = new Set(parents.map((p) => p.toLowerCase()));
16793
18019
  for (let i = index - 1; i >= 0; i--) {
@@ -16803,18 +18029,18 @@ function isInsideParent(macros, index, parents, workspace) {
16803
18029
  }
16804
18030
  return false;
16805
18031
  }
16806
- function validateChildren(macros, curIndex, parentMacro, childConstraints, workspace, diagnostics) {
18032
+ function validateChildren(macros, curIndex, parentMacro, childConstraints, elements, workspace, diagnostics) {
16807
18033
  const children = /* @__PURE__ */ Object.create(null);
16808
18034
  const startIndex = curIndex + 1;
16809
18035
  const endIndex = parentMacro.pair;
16810
18036
  for (let i = startIndex; i < endIndex; i++) {
16811
18037
  const child = macros[i];
16812
- const childInfo = workspace.macros.getMacro(child.name);
16813
- if (!childInfo) continue;
16814
- if (childInfo.block && child.open && child.pair !== -1) {
18038
+ if (child.open && child.pair !== -1 && workspace.isContainer(child.name)) {
16815
18039
  i = child.pair;
16816
18040
  continue;
16817
18041
  }
18042
+ if (elements.has(i)) continue;
18043
+ if (!workspace.macros.getMacro(child.name)) continue;
16818
18044
  const childKey = child.name.toLowerCase();
16819
18045
  if (childConstraints.some((c3) => c3.name.toLowerCase() === childKey)) {
16820
18046
  children[childKey] = (children[childKey] ?? 0) + 1;
@@ -16929,15 +18155,25 @@ function validateVariables(uri, workspace, diagnostics) {
16929
18155
  `Variable '$${u.name}' is not declared in StoryVariables`
16930
18156
  ));
16931
18157
  }
18158
+ for (const a of workspace.variables.getPrimitiveFieldAccesses(uri)) {
18159
+ diagnostics.push(makeDiag(
18160
+ a.range,
18161
+ DiagnosticCode.PrimitiveFieldAccess,
18162
+ `Cannot access field "${a.field}" on ${a.path} (type: ${a.type}). Spindle checks field access against the StoryVariables defaults and will not start the story.`
18163
+ ));
18164
+ }
16932
18165
  const storyVars = workspace.passages.getStoryVariables();
16933
18166
  if (storyVars && storyVars.uri === uri) {
16934
18167
  for (const nd of workspace.variables.getNullDeclarations()) {
16935
18168
  diagnostics.push(makeDiag(
16936
18169
  nd.range,
16937
18170
  DiagnosticCode.NullVariableValue,
16938
- `Variable '$${nd.name}' is set to null. Spindle does not support null \u2014 use a valid default (number, string, boolean, array, or object).`
18171
+ `${nullSubject(nd)} is set to null. Spindle does not support null \u2014 use a valid default (number, string, boolean, array, or object).`
16939
18172
  ));
16940
18173
  }
18174
+ for (const d of workspace.variables.getInvalidDeclarations()) {
18175
+ diagnostics.push(makeDiag(d.range, DiagnosticCode.InvalidDeclaration, `${d.message} ${WILL_NOT_START}`));
18176
+ }
16941
18177
  }
16942
18178
  }
16943
18179
  if (workspace.variables.hasStoryTransients()) {
@@ -16955,12 +18191,20 @@ function validateVariables(uri, workspace, diagnostics) {
16955
18191
  diagnostics.push(makeDiag(
16956
18192
  nd.range,
16957
18193
  DiagnosticCode.NullVariableValue,
16958
- `Transient variable '%${nd.name}' is set to null. Spindle does not support null \u2014 use a valid default (number, string, boolean, array, or object).`
18194
+ `${nullSubject(nd)} is set to null. Spindle does not support null \u2014 use a valid default (number, string, boolean, array, or object).`
16959
18195
  ));
16960
18196
  }
18197
+ for (const d of workspace.variables.getInvalidTransientDeclarations()) {
18198
+ diagnostics.push(makeDiag(d.range, DiagnosticCode.InvalidDeclaration, `${d.message} ${WILL_NOT_START}`));
18199
+ }
16961
18200
  }
16962
18201
  }
16963
18202
  }
18203
+ function nullSubject(nd) {
18204
+ const variable = nd.sigil === "%" ? "Transient variable" : "Variable";
18205
+ if (nd.field?.length) return `Field '${nd.sigil}${[nd.name, ...nd.field].join(".")}'`;
18206
+ return `${variable} '${nd.sigil}${nd.name}'`;
18207
+ }
16964
18208
  function validateLinks(text, passages, passageNames, diagnostics) {
16965
18209
  const links = parseLinks(text);
16966
18210
  for (const link of links) {
@@ -17177,7 +18421,7 @@ function widgetDefinitionNames(content) {
17177
18421
  while ((m = re.exec(content)) !== null) names.push(m[2]);
17178
18422
  return names;
17179
18423
  }
17180
- function passageContent(passage, workspace) {
18424
+ function passageContent2(passage, workspace) {
17181
18425
  const text = workspace.documents.getText(passage.uri) ?? "";
17182
18426
  return text.split("\n").slice(passage.range.start.line + 1, passage.range.end.line + 1).join("\n");
17183
18427
  }
@@ -17190,7 +18434,7 @@ function validateWidgetIncludes(text, macros, workspace, diagnostics) {
17190
18434
  if (target === null) continue;
17191
18435
  const passage = workspace.passages.getPassage(target);
17192
18436
  if (!passage?.tags?.includes("widget")) continue;
17193
- const widgets = widgetDefinitionNames(passageContent(passage, workspace));
18437
+ const widgets = widgetDefinitionNames(passageContent2(passage, workspace));
17194
18438
  if (widgets.length === 0) continue;
17195
18439
  const list = widgets.map((w) => `{${w}}`).join(", ");
17196
18440
  diagnostics.push(makeDiag(
@@ -17263,18 +18507,32 @@ function toLspDiagnostic(d) {
17263
18507
  message: d.message
17264
18508
  };
17265
18509
  }
17266
- var DEFERRED_CONTAINERS, EXPRESSION_BUILTINS2, diagnosticsPlugin;
18510
+ var DIRECT_CHILD_MACROS, NON_MARKUP_PASSAGES, RAW_TEXT_ELEMENTS, MACRO_IN_ATTRIBUTE, EXPRESSION_IN_ATTRIBUTE, WILL_NOT_START, DEFERRED_CONTAINERS, EXPRESSION_BUILTINS2, diagnosticsPlugin;
17267
18511
  var init_diagnostics = __esm({
17268
18512
  "src/plugins/diagnostics.ts"() {
17269
18513
  "use strict";
17270
18514
  init_diagnostic_codes();
17271
18515
  init_macro_parser();
17272
18516
  init_argument_lexer();
18517
+ init_html_scanner();
18518
+ init_attribute_blocks();
17273
18519
  init_widget_arguments();
17274
18520
  init_parameter_validator();
17275
18521
  init_link_parser();
17276
18522
  init_passage_parser();
17277
18523
  init_macro_sources();
18524
+ DIRECT_CHILD_MACROS = /* @__PURE__ */ new Set(["elseif", "else", "case", "default", "next", "option"]);
18525
+ NON_MARKUP_PASSAGES = /* @__PURE__ */ new Set([
18526
+ "StoryTitle",
18527
+ "StoryData",
18528
+ "StoryVariables",
18529
+ "StoryTransients",
18530
+ "SaveTitle"
18531
+ ]);
18532
+ RAW_TEXT_ELEMENTS = /* @__PURE__ */ new Set(["script", "style"]);
18533
+ MACRO_IN_ATTRIBUTE = (macro) => `Macros are not evaluated inside HTML attributes, so Spindle outputs this {${macro}} as text. Use an expression such as {$x ? 'a' : 'b'}`;
18534
+ EXPRESSION_IN_ATTRIBUTE = "Spindle evaluates {\u2026} in an HTML attribute only when $, _, @ or % follows the brace, so it outputs this block as text. Start the expression with a variable, as in {$x ? 'a' : 'b'}";
18535
+ WILL_NOT_START = "Spindle will not start the story.";
17278
18536
  DEFERRED_CONTAINERS = /* @__PURE__ */ new Set(["link", "button"]);
17279
18537
  EXPRESSION_BUILTINS2 = /* @__PURE__ */ new Set([
17280
18538
  "currentPassage",
@@ -17302,12 +18560,14 @@ var init_diagnostics = __esm({
17302
18560
  diagnostics: diags.filter(isEnabled).map((d) => toLspDiagnostic(d))
17303
18561
  });
17304
18562
  };
17305
- ctx.workspace.on("modelReady", () => {
18563
+ const publishAll = () => {
17306
18564
  for (const uri of ctx.workspace.documents.getUris()) {
17307
18565
  if (isMacroSource(uri)) continue;
17308
18566
  publishFor(uri);
17309
18567
  }
17310
- });
18568
+ };
18569
+ ctx.workspace.on("modelReady", publishAll);
18570
+ ctx.workspace.on("storyFormatChanged", publishAll);
17311
18571
  ctx.workspace.on("documentChanged", publishFor);
17312
18572
  ctx.workspace.on("documentClosed", (uri) => {
17313
18573
  ctx.connection.sendDiagnostics({ uri, diagnostics: [] });
@@ -17484,7 +18744,7 @@ var check_exports = {};
17484
18744
  __export(check_exports, {
17485
18745
  runCheck: () => runCheck
17486
18746
  });
17487
- import { readFileSync as readFileSync5 } from "node:fs";
18747
+ import { readFileSync as readFileSync6 } from "node:fs";
17488
18748
  import { resolve as resolve2 } from "node:path";
17489
18749
  import { pathToFileURL as pathToFileURL3 } from "node:url";
17490
18750
  function parseArgs(args2) {
@@ -17605,16 +18865,22 @@ async function runCheck(args2) {
17605
18865
  }
17606
18866
  projectConfig = loadConfigFromDisk(configRoot);
17607
18867
  }
17608
- const workspace = new WorkspaceModel({ workspaceRoot: commonDir });
17609
18868
  const fileContents = /* @__PURE__ */ new Map();
17610
18869
  for (const filePath of uniqueFiles) {
17611
18870
  try {
17612
- const text = readFileSync5(filePath, "utf-8");
18871
+ const text = readFileSync6(filePath, "utf-8");
17613
18872
  const uri = pathToFileURL3(filePath).toString();
17614
18873
  fileContents.set(uri, text);
17615
18874
  } catch {
17616
18875
  }
17617
18876
  }
18877
+ const storyFormat = await findStoryFormat(fileContents.values(), commonDir);
18878
+ if (!storyFormat.isSpindle) {
18879
+ console.error(skippedFormatNote(storyFormat));
18880
+ report(options.format, []);
18881
+ return 0;
18882
+ }
18883
+ const workspace = new WorkspaceModel({ workspaceRoot: commonDir });
17618
18884
  const workspaceContents = new Map(fileContents);
17619
18885
  await addProjectMacroSources(workspaceContents, commonDir);
17620
18886
  workspace.initialize(workspaceContents);
@@ -17639,19 +18905,22 @@ async function runCheck(args2) {
17639
18905
  }
17640
18906
  }
17641
18907
  }
17642
- switch (options.format) {
18908
+ report(options.format, allResults);
18909
+ workspace.dispose();
18910
+ return hasErrors ? 1 : 0;
18911
+ }
18912
+ function report(format, results) {
18913
+ switch (format) {
17643
18914
  case "pretty":
17644
- console.log(formatPretty(allResults));
18915
+ console.log(formatPretty(results));
17645
18916
  break;
17646
18917
  case "json":
17647
- console.log(formatJson(allResults));
18918
+ console.log(formatJson(results));
17648
18919
  break;
17649
18920
  case "sarif":
17650
- console.log(formatSarif(allResults));
18921
+ console.log(formatSarif(results));
17651
18922
  break;
17652
18923
  }
17653
- workspace.dispose();
17654
- return hasErrors ? 1 : 0;
17655
18924
  }
17656
18925
  var SEVERITY_ORDER;
17657
18926
  var init_check = __esm({
@@ -17662,6 +18931,7 @@ var init_check = __esm({
17662
18931
  init_diagnostics();
17663
18932
  init_config_loader();
17664
18933
  init_macro_sources();
18934
+ init_story_format();
17665
18935
  init_pretty();
17666
18936
  init_json();
17667
18937
  init_sarif();
@@ -17705,13 +18975,17 @@ function splitPassages(text) {
17705
18975
  return passages;
17706
18976
  }
17707
18977
  function classifyPassage(header) {
17708
- const tagMatch = header.match(/\[([^\]]*)\]/g);
17709
- if (!tagMatch) return "normal";
17710
- const tags = tagMatch.flatMap((t) => t.slice(1, -1).toLowerCase().split(/\s+/));
18978
+ const tags = headerTags(header).map((t) => t.toLowerCase());
17711
18979
  if (tags.includes("script")) return "script";
17712
18980
  if (tags.includes("stylesheet")) return "stylesheet";
17713
18981
  return "normal";
17714
18982
  }
18983
+ function headerTags(header) {
18984
+ const parsed = parsePassageHeader(header, 0);
18985
+ if (parsed) return parsed.tags;
18986
+ const unescaped = header.replace(/\\./g, "ec");
18987
+ return (unescaped.match(/\[[^\]]*\]/g) ?? []).flatMap((t) => t.slice(1, -1).split(/\s+/));
18988
+ }
17715
18989
  function segmentRegions(body) {
17716
18990
  const lines = body.split("\n");
17717
18991
  if (lines.length > 0 && lines[lines.length - 1] === "") {
@@ -17758,7 +19032,7 @@ function segmentRegions(body) {
17758
19032
  if (/^<([a-zA-Z][\w-]*)/.test(trimmed2)) {
17759
19033
  const tagMatch = trimmed2.match(/^<([a-zA-Z][\w-]*)/);
17760
19034
  const tagName = tagMatch[1].toLowerCase();
17761
- if (VOID_ELEMENTS.has(tagName) && !lines[i + 1]?.trim().startsWith("<")) {
19035
+ if (VOID_ELEMENTS2.has(tagName) && !lines[i + 1]?.trim().startsWith("<")) {
17762
19036
  pushSpindleLine(regions, line);
17763
19037
  i++;
17764
19038
  continue;
@@ -17795,12 +19069,13 @@ function pushSpindleLine(regions, line) {
17795
19069
  regions.push({ type: "spindle", lines: [line] });
17796
19070
  }
17797
19071
  }
17798
- var PASSAGE_HEADER_REGEX, VOID_ELEMENTS;
19072
+ var PASSAGE_HEADER_REGEX, VOID_ELEMENTS2;
17799
19073
  var init_segment = __esm({
17800
19074
  "src/plugins/format/segment.ts"() {
17801
19075
  "use strict";
19076
+ init_passage_parser();
17802
19077
  PASSAGE_HEADER_REGEX = /^::\s+/;
17803
- VOID_ELEMENTS = /* @__PURE__ */ new Set([
19078
+ VOID_ELEMENTS2 = /* @__PURE__ */ new Set([
17804
19079
  "area",
17805
19080
  "base",
17806
19081
  "br",
@@ -18287,11 +19562,12 @@ var init_format = __esm({
18287
19562
  "src/plugins/format.ts"() {
18288
19563
  "use strict";
18289
19564
  init_macro_supplements();
19565
+ init_macro_parser();
18290
19566
  init_segment();
18291
19567
  init_prettier_bridge();
18292
19568
  init_placeholders();
18293
19569
  PASSAGE_HEADER_REGEX2 = /^(::)\s+/;
18294
- MACRO_OPEN_REGEX = /^\{(?:[#.][a-zA-Z][\w-]*\s*)*([A-Za-z][\w-]*)\b/;
19570
+ MACRO_OPEN_REGEX = new RegExp(String.raw`^\{(?:${SELECTOR_PATTERN} )?([A-Za-z][\w-]*)\b`);
18295
19571
  MACRO_CLOSE_REGEX = /^\{\/([A-Za-z][\w-]*)\b/;
18296
19572
  DEFAULT_DEDENTING = /* @__PURE__ */ new Set(["else", "elseif", "next", "case", "default"]);
18297
19573
  LIST_ITEM_REGEX = /^(?:[-+*]|\d{1,9}[.)])(?:[ \t]|$)/;
@@ -18304,7 +19580,8 @@ var init_format = __esm({
18304
19580
  },
18305
19581
  initialize(ctx) {
18306
19582
  const formatOpts = {
18307
- isBlock: (name) => ctx.workspace.macros.isBlock(name),
19583
+ // Block widgets (whose body renders {@children}) are containers too
19584
+ isBlock: (name) => ctx.workspace.isContainer(name),
18308
19585
  isDedentingSubMacro: (name) => DEFAULT_DEDENTING.has(name.toLowerCase())
18309
19586
  };
18310
19587
  ctx.connection.onDocumentFormatting(async (params) => {
@@ -18349,7 +19626,7 @@ var format_exports = {};
18349
19626
  __export(format_exports, {
18350
19627
  runFormat: () => runFormat
18351
19628
  });
18352
- import { readFileSync as readFileSync6, writeFileSync } from "node:fs";
19629
+ import { readFileSync as readFileSync7, writeFileSync } from "node:fs";
18353
19630
  function parseArgs2(args2) {
18354
19631
  const options = {
18355
19632
  check: false,
@@ -18399,14 +19676,25 @@ async function runFormat(args2) {
18399
19676
  console.log("No files found");
18400
19677
  return 0;
18401
19678
  }
19679
+ const texts = /* @__PURE__ */ new Map();
19680
+ for (const filePath of uniqueFiles) {
19681
+ try {
19682
+ texts.set(filePath, readFileSync7(filePath, "utf-8"));
19683
+ } catch {
19684
+ }
19685
+ }
19686
+ const storyFormat = await findStoryFormat(texts.values(), commonDirectory(uniqueFiles));
19687
+ if (!storyFormat.isSpindle) {
19688
+ console.error(skippedFormatNote(storyFormat));
19689
+ return 0;
19690
+ }
18402
19691
  const formatOpts = {};
18403
19692
  if (options.maxLineLength !== null) {
18404
19693
  formatOpts.maxLineLength = options.maxLineLength;
18405
19694
  }
18406
19695
  const unformatted = [];
18407
- for (const filePath of uniqueFiles) {
19696
+ for (const [filePath, text] of texts) {
18408
19697
  try {
18409
- const text = readFileSync6(filePath, "utf-8");
18410
19698
  const formatted = await formatDocument(text, formatOpts);
18411
19699
  if (formatted !== text) {
18412
19700
  if (options.check) {
@@ -18437,6 +19725,8 @@ var init_format2 = __esm({
18437
19725
  "use strict";
18438
19726
  init_esm6();
18439
19727
  init_format();
19728
+ init_macro_sources();
19729
+ init_story_format();
18440
19730
  }
18441
19731
  });
18442
19732
 
@@ -49584,9 +50874,12 @@ var init_stdio2 = __esm({
49584
50874
  var server_exports = {};
49585
50875
  __export(server_exports, {
49586
50876
  checkFiles: () => checkFiles,
50877
+ checkFormatting: () => checkFormatting,
50878
+ checkProject: () => checkProject,
50879
+ formatFiles: () => formatFiles,
49587
50880
  startMcpServer: () => startMcpServer
49588
50881
  });
49589
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
50882
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
49590
50883
  import { resolve as resolve3 } from "node:path";
49591
50884
  import { pathToFileURL as pathToFileURL4 } from "node:url";
49592
50885
  function filterBySeverity2(diagnostics, minSeverity) {
@@ -49618,20 +50911,28 @@ function findConfigRoot(commonDir) {
49618
50911
  }
49619
50912
  return configRoot;
49620
50913
  }
49621
- async function createWorkspace(files) {
49622
- const commonDir = commonDirectory(files);
49623
- const configRoot = findConfigRoot(commonDir);
49624
- const projectConfig = loadConfigFromDisk(configRoot);
49625
- const workspace = new WorkspaceModel({ workspaceRoot: configRoot });
50914
+ function readFiles(files) {
49626
50915
  const fileContents = /* @__PURE__ */ new Map();
49627
50916
  for (const filePath of files) {
49628
50917
  try {
49629
- const text = readFileSync7(filePath, "utf-8");
50918
+ const text = readFileSync8(filePath, "utf-8");
49630
50919
  const uri = pathToFileURL4(filePath).toString();
49631
50920
  fileContents.set(uri, text);
49632
50921
  } catch {
49633
50922
  }
49634
50923
  }
50924
+ return fileContents;
50925
+ }
50926
+ async function skippedReason(fileContents, files) {
50927
+ const format = await findStoryFormat(fileContents.values(), commonDirectory(files));
50928
+ return format.isSpindle ? void 0 : skippedFormatNote(format);
50929
+ }
50930
+ async function createWorkspace(files, fileContents) {
50931
+ const commonDir = commonDirectory(files);
50932
+ const configRoot = findConfigRoot(commonDir);
50933
+ const projectConfig = loadConfigFromDisk(configRoot);
50934
+ const workspace = new WorkspaceModel({ workspaceRoot: configRoot });
50935
+ fileContents = new Map(fileContents);
49635
50936
  await addProjectMacroSources(fileContents, commonDir);
49636
50937
  workspace.initialize(fileContents);
49637
50938
  if (Object.keys(projectConfig.macros).length > 0) {
@@ -49640,9 +50941,15 @@ async function createWorkspace(files) {
49640
50941
  return workspace;
49641
50942
  }
49642
50943
  async function checkFiles(pattern, severity, cwd = process.cwd()) {
50944
+ return (await checkProject(pattern, severity, cwd)).results;
50945
+ }
50946
+ async function checkProject(pattern, severity, cwd = process.cwd()) {
49643
50947
  const files = await resolveFiles(pattern, cwd);
49644
- if (files.length === 0) return [];
49645
- const workspace = await createWorkspace(files);
50948
+ if (files.length === 0) return { results: [] };
50949
+ const fileContents = readFiles(files);
50950
+ const skipped = await skippedReason(fileContents, files);
50951
+ if (skipped) return { results: [], skipped };
50952
+ const workspace = await createWorkspace(files, fileContents);
49646
50953
  try {
49647
50954
  const results = [];
49648
50955
  for (const filePath of files) {
@@ -49664,11 +50971,57 @@ async function checkFiles(pattern, severity, cwd = process.cwd()) {
49664
50971
  });
49665
50972
  }
49666
50973
  }
49667
- return results;
50974
+ return { results };
49668
50975
  } finally {
49669
50976
  workspace.dispose();
49670
50977
  }
49671
50978
  }
50979
+ function relativeTo(cwd, filePath) {
50980
+ return filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
50981
+ }
50982
+ async function formatFiles(pattern, cwd = process.cwd()) {
50983
+ const files = await resolveFiles(pattern, cwd);
50984
+ const contents = readFiles(files);
50985
+ const skipped = files.length > 0 ? await skippedReason(contents, files) : void 0;
50986
+ if (skipped) return { formatted: 0, unchanged: 0, files: [], skipped };
50987
+ let formatted = 0;
50988
+ let unchanged = 0;
50989
+ const changedFiles = [];
50990
+ for (const filePath of files) {
50991
+ const text = contents.get(pathToFileURL4(filePath).toString());
50992
+ if (text === void 0) continue;
50993
+ try {
50994
+ const result = await formatDocument(text);
50995
+ if (result !== text) {
50996
+ writeFileSync2(filePath, result, "utf-8");
50997
+ formatted++;
50998
+ changedFiles.push(relativeTo(cwd, filePath));
50999
+ } else {
51000
+ unchanged++;
51001
+ }
51002
+ } catch {
51003
+ }
51004
+ }
51005
+ return { formatted, unchanged, files: changedFiles };
51006
+ }
51007
+ async function checkFormatting(pattern, cwd = process.cwd()) {
51008
+ const files = await resolveFiles(pattern, cwd);
51009
+ const contents = readFiles(files);
51010
+ const skipped = files.length > 0 ? await skippedReason(contents, files) : void 0;
51011
+ if (skipped) return { needsFormatting: [], alreadyFormatted: [], skipped };
51012
+ const needsFormatting = [];
51013
+ const alreadyFormatted = [];
51014
+ for (const filePath of files) {
51015
+ const text = contents.get(pathToFileURL4(filePath).toString());
51016
+ if (text === void 0) continue;
51017
+ try {
51018
+ const result = await formatDocument(text);
51019
+ (result !== text ? needsFormatting : alreadyFormatted).push(relativeTo(cwd, filePath));
51020
+ } catch {
51021
+ }
51022
+ }
51023
+ return { needsFormatting, alreadyFormatted };
51024
+ }
49672
51025
  async function startMcpServer() {
49673
51026
  const server = new McpServer({
49674
51027
  name: "spindle-lsp",
@@ -49682,9 +51035,12 @@ async function startMcpServer() {
49682
51035
  severity: external_exports3.enum(["error", "warning", "info", "hint"]).optional().describe("Minimum severity to include")
49683
51036
  },
49684
51037
  async (args2) => {
49685
- const results = await checkFiles(args2.path, args2.severity);
51038
+ const { results, skipped } = await checkProject(args2.path, args2.severity);
49686
51039
  return {
49687
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
51040
+ content: [
51041
+ { type: "text", text: JSON.stringify(results, null, 2) },
51042
+ ...skipped ? [{ type: "text", text: skipped }] : []
51043
+ ]
49688
51044
  };
49689
51045
  }
49690
51046
  );
@@ -49695,27 +51051,7 @@ async function startMcpServer() {
49695
51051
  path: external_exports3.string().default("**/*.{tw,twee}").describe("Glob pattern or directory to format")
49696
51052
  },
49697
51053
  async (args2) => {
49698
- const cwd = process.cwd();
49699
- const files = await resolveFiles(args2.path, cwd);
49700
- let formatted = 0;
49701
- let unchanged = 0;
49702
- const changedFiles = [];
49703
- for (const filePath of files) {
49704
- try {
49705
- const text = readFileSync7(filePath, "utf-8");
49706
- const result = await formatDocument(text);
49707
- if (result !== text) {
49708
- writeFileSync2(filePath, result, "utf-8");
49709
- formatted++;
49710
- const relativePath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
49711
- changedFiles.push(relativePath);
49712
- } else {
49713
- unchanged++;
49714
- }
49715
- } catch {
49716
- }
49717
- }
49718
- const output = { formatted, unchanged, files: changedFiles };
51054
+ const output = await formatFiles(args2.path);
49719
51055
  return {
49720
51056
  content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
49721
51057
  };
@@ -49728,24 +51064,7 @@ async function startMcpServer() {
49728
51064
  path: external_exports3.string().default("**/*.{tw,twee}").describe("Glob pattern or directory to check")
49729
51065
  },
49730
51066
  async (args2) => {
49731
- const cwd = process.cwd();
49732
- const files = await resolveFiles(args2.path, cwd);
49733
- const needsFormatting = [];
49734
- const alreadyFormatted = [];
49735
- for (const filePath of files) {
49736
- try {
49737
- const text = readFileSync7(filePath, "utf-8");
49738
- const result = await formatDocument(text);
49739
- const relativePath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
49740
- if (result !== text) {
49741
- needsFormatting.push(relativePath);
49742
- } else {
49743
- alreadyFormatted.push(relativePath);
49744
- }
49745
- } catch {
49746
- }
49747
- }
49748
- const output = { needsFormatting, alreadyFormatted };
51067
+ const output = await checkFormatting(args2.path);
49749
51068
  return {
49750
51069
  content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
49751
51070
  };
@@ -49767,6 +51086,7 @@ var init_server3 = __esm({
49767
51086
  init_macro_sources();
49768
51087
  init_diagnostics();
49769
51088
  init_format();
51089
+ init_story_format();
49770
51090
  SEVERITY_ORDER2 = ["hint", "info", "warning", "error"];
49771
51091
  }
49772
51092
  });
@@ -58894,6 +60214,62 @@ var init_capabilities = __esm({
58894
60214
  }
58895
60215
  });
58896
60216
 
60217
+ // src/server/story-format-gate.ts
60218
+ function gateRegistrations(target, methods, enabled, nested = {}) {
60219
+ return new Proxy(target, {
60220
+ get(obj, prop, receiver) {
60221
+ const value = Reflect.get(obj, prop, receiver);
60222
+ if (typeof prop !== "string") return value;
60223
+ if (Object.hasOwn(nested, prop) && value !== null && (typeof value === "object" || typeof value === "function")) {
60224
+ return nested[prop](value);
60225
+ }
60226
+ if (methods.includes(prop) && typeof value === "function") {
60227
+ return (handler, ...rest) => Reflect.apply(value, obj, [
60228
+ (...args2) => enabled() ? handler(...args2) : null,
60229
+ ...rest
60230
+ ]);
60231
+ }
60232
+ return value;
60233
+ }
60234
+ });
60235
+ }
60236
+ function gateConnection(connection, enabled) {
60237
+ const languageFeatures = {};
60238
+ for (const [feature, methods] of Object.entries(GATED_LANGUAGE_FEATURES)) {
60239
+ languageFeatures[feature] = (value) => gateRegistrations(value, methods, enabled);
60240
+ }
60241
+ return gateRegistrations(connection, GATED_REQUESTS, enabled, {
60242
+ languages: (value) => gateRegistrations(value, [], enabled, languageFeatures)
60243
+ });
60244
+ }
60245
+ var GATED_REQUESTS, GATED_LANGUAGE_FEATURES;
60246
+ var init_story_format_gate = __esm({
60247
+ "src/server/story-format-gate.ts"() {
60248
+ "use strict";
60249
+ GATED_REQUESTS = [
60250
+ "onCompletion",
60251
+ "onHover",
60252
+ "onSignatureHelp",
60253
+ "onDefinition",
60254
+ "onReferences",
60255
+ "onPrepareRename",
60256
+ "onRenameRequest",
60257
+ "onCodeLens",
60258
+ "onCodeAction",
60259
+ "onDocumentFormatting",
60260
+ "onDocumentRangeFormatting",
60261
+ "onDocumentSymbol",
60262
+ "onWorkspaceSymbol",
60263
+ "onFoldingRanges",
60264
+ "onDocumentLinks"
60265
+ ];
60266
+ GATED_LANGUAGE_FEATURES = {
60267
+ semanticTokens: ["on"],
60268
+ inlayHint: ["on"]
60269
+ };
60270
+ }
60271
+ });
60272
+
58897
60273
  // src/plugins/completions.ts
58898
60274
  function getCompletions(uri, position, triggerChar, workspace) {
58899
60275
  const text = workspace.documents.getText(uri);
@@ -58901,14 +60277,15 @@ function getCompletions(uri, position, triggerChar, workspace) {
58901
60277
  const lines = text.split("\n");
58902
60278
  if (position.line >= lines.length) return [];
58903
60279
  const lineText = lines[position.line].substring(0, position.character);
60280
+ const inAttribute = () => inAttributeValue(text, (buildLineStarts(text)[position.line] ?? 0) + position.character - 1);
58904
60281
  if (/\{\/[A-Za-z\w-]*$/.test(lineText)) {
58905
- return getClosingMacroCompletions(uri, text, position, workspace);
60282
+ return inAttribute() ? [] : getClosingMacroCompletions(uri, text, position, workspace);
58906
60283
  }
58907
- const transientDotPathMatch = /%([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
60284
+ const transientDotPathMatch = /%([\w$]+)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
58908
60285
  if (transientDotPathMatch) {
58909
60286
  return getTransientDotPathCompletions(transientDotPathMatch[1], workspace);
58910
60287
  }
58911
- const dotPathMatch = /\$([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
60288
+ const dotPathMatch = /\$([\w$]+)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
58912
60289
  if (dotPathMatch) {
58913
60290
  return getDotPathCompletions(dotPathMatch[1], workspace);
58914
60291
  }
@@ -58928,7 +60305,7 @@ function getCompletions(uri, position, triggerChar, workspace) {
58928
60305
  return getPassageNameCompletions(workspace);
58929
60306
  }
58930
60307
  if (/(?:^|[^\\])\{[A-Za-z\w-]*$/.test(lineText)) {
58931
- return getMacroCompletions(workspace);
60308
+ return inAttribute() ? [] : getMacroCompletions(workspace);
58932
60309
  }
58933
60310
  return [];
58934
60311
  }
@@ -59100,6 +60477,7 @@ var init_completions = __esm({
59100
60477
  "src/plugins/completions.ts"() {
59101
60478
  "use strict";
59102
60479
  init_macro_parser();
60480
+ init_html_scanner();
59103
60481
  completionsPlugin = {
59104
60482
  id: "completions",
59105
60483
  capabilities: {
@@ -59122,6 +60500,215 @@ var init_completions = __esm({
59122
60500
  }
59123
60501
  });
59124
60502
 
60503
+ // src/plugins/references.ts
60504
+ function findReferences(uri, position, workspace, includeDeclaration) {
60505
+ const text = workspace.documents.getText(uri);
60506
+ if (text === void 0) return [];
60507
+ const lines = text.split("\n");
60508
+ if (position.line >= lines.length) return [];
60509
+ const line = lines[position.line];
60510
+ const header = parsePassageHeader(line, position.line);
60511
+ if (header) {
60512
+ const { start, end } = header.nameRange;
60513
+ if (position.character >= start.character && position.character <= end.character) {
60514
+ return findPassageReferences(header.name, workspace, includeDeclaration);
60515
+ }
60516
+ }
60517
+ {
60518
+ const varRegex = /\$([\w$]+)/g;
60519
+ let match2;
60520
+ while ((match2 = varRegex.exec(line)) !== null) {
60521
+ const start = match2.index;
60522
+ const end = start + match2[0].length;
60523
+ if (position.character >= start && position.character <= end) {
60524
+ const varName = match2[1];
60525
+ return findVariableReferences(varName, workspace, includeDeclaration);
60526
+ }
60527
+ }
60528
+ }
60529
+ {
60530
+ const transRegex = /(?<!\w)%([\w$]+)/g;
60531
+ let match2;
60532
+ while ((match2 = transRegex.exec(line)) !== null) {
60533
+ const start = match2.index;
60534
+ const end = start + match2[0].length;
60535
+ if (position.character >= start && position.character <= end) {
60536
+ const varName = match2[1];
60537
+ if (!isTransientAt(varName, uri, position.line, start, workspace)) break;
60538
+ return findTransientReferences(varName, workspace, includeDeclaration);
60539
+ }
60540
+ }
60541
+ }
60542
+ {
60543
+ const widgetRegex = /\{\/?([A-Za-z_$][\w$]*)/g;
60544
+ let match2;
60545
+ while ((match2 = widgetRegex.exec(line)) !== null) {
60546
+ const name = match2[1];
60547
+ const nameStart = match2.index + match2[0].length - name.length;
60548
+ const nameEnd = nameStart + name.length;
60549
+ if (position.character >= nameStart && position.character <= nameEnd) {
60550
+ const widget = workspace.widgets.getWidget(name);
60551
+ const isClosing = match2[0][1] === "/";
60552
+ if (!workspace.macros.getMacro(name) && widget && (!isClosing || widget.block)) {
60553
+ return findWidgetReferences(name, workspace, includeDeclaration);
60554
+ }
60555
+ }
60556
+ }
60557
+ }
60558
+ const passageRef = findPassageRefAt(text, position);
60559
+ if (passageRef) {
60560
+ return findPassageReferences(passageRef.name, workspace, includeDeclaration);
60561
+ }
60562
+ {
60563
+ const allPassages = workspace.passages.getAllPassages();
60564
+ const passageNames = new Set(allPassages.map((p) => p.name));
60565
+ const wordRegex = /[A-Za-z_$][\w$\s]*/g;
60566
+ let match2;
60567
+ while ((match2 = wordRegex.exec(line)) !== null) {
60568
+ const start = match2.index;
60569
+ const end = start + match2[0].length;
60570
+ if (position.character >= start && position.character <= end) {
60571
+ const word = match2[0].trim();
60572
+ if (passageNames.has(word)) {
60573
+ return findPassageReferences(word, workspace, includeDeclaration);
60574
+ }
60575
+ }
60576
+ }
60577
+ }
60578
+ return [];
60579
+ }
60580
+ function findPassageReferences(passageName, workspace, includeDeclaration) {
60581
+ const locations = [];
60582
+ if (includeDeclaration) {
60583
+ const passage = workspace.passages.getPassage(passageName);
60584
+ if (passage) {
60585
+ locations.push({
60586
+ uri: passage.uri,
60587
+ range: passage.nameRange
60588
+ });
60589
+ }
60590
+ }
60591
+ for (const docUri of workspace.documents.getUris()) {
60592
+ if (isMacroSource(docUri)) continue;
60593
+ const docText = workspace.documents.getText(docUri);
60594
+ if (!docText) continue;
60595
+ for (const ref of [...parseLinks(docText), ...parseMacroPassageRefs(docText)]) {
60596
+ if (ref.name === passageName) {
60597
+ locations.push({ uri: docUri, range: ref.range });
60598
+ }
60599
+ }
60600
+ }
60601
+ return locations;
60602
+ }
60603
+ function findVariableReferences(varName, workspace, includeDeclaration) {
60604
+ const locations = [];
60605
+ if (includeDeclaration) {
60606
+ const decl = workspace.variables.getDeclared().get(varName);
60607
+ if (decl?.declarationRange && decl.declarationUri) {
60608
+ locations.push({
60609
+ uri: decl.declarationUri,
60610
+ range: decl.declarationRange
60611
+ });
60612
+ }
60613
+ }
60614
+ const usages = workspace.variables.getUsages(varName);
60615
+ for (const u of usages) {
60616
+ locations.push({ uri: u.uri, range: u.range });
60617
+ }
60618
+ return locations;
60619
+ }
60620
+ function isTransientAt(name, uri, line, character, workspace) {
60621
+ if (!/^\d/.test(name)) return true;
60622
+ const at = (u) => u.uri === uri && u.range?.start.line === line && u.range.start.character === character;
60623
+ const decl = workspace.variables.getDeclaredTransient().get(name);
60624
+ if (decl && at({ uri: decl.declarationUri, range: decl.declarationRange })) return true;
60625
+ return workspace.variables.getTransientUsages(name).some(at);
60626
+ }
60627
+ function findTransientReferences(varName, workspace, includeDeclaration) {
60628
+ const results = [];
60629
+ if (includeDeclaration) {
60630
+ const decl = workspace.variables.getDeclaredTransient().get(varName);
60631
+ if (decl?.declarationUri && decl.declarationRange) {
60632
+ results.push({ uri: decl.declarationUri, range: decl.declarationRange });
60633
+ }
60634
+ }
60635
+ const usages = workspace.variables.getTransientUsages(varName);
60636
+ for (const u of usages) {
60637
+ results.push({ uri: u.uri, range: u.range });
60638
+ }
60639
+ return results;
60640
+ }
60641
+ function findWidgetReferences(widgetName, workspace, includeDeclaration) {
60642
+ const locations = [];
60643
+ const widget = workspace.widgets.getWidget(widgetName);
60644
+ if (includeDeclaration && widget) {
60645
+ locations.push({ uri: widget.uri, range: widget.range });
60646
+ }
60647
+ const widgetInvocationRegex = /\{(\/)?([A-Za-z_$][\w$]*)\b/g;
60648
+ const lowerName = widgetName.toLowerCase();
60649
+ const isBlock = widget?.block ?? false;
60650
+ for (const docUri of workspace.documents.getUris()) {
60651
+ if (isMacroSource(docUri)) continue;
60652
+ const docText = workspace.documents.getText(docUri);
60653
+ if (!docText) continue;
60654
+ const lines = docText.split("\n");
60655
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
60656
+ const line = lines[lineNum];
60657
+ widgetInvocationRegex.lastIndex = 0;
60658
+ let match2;
60659
+ while ((match2 = widgetInvocationRegex.exec(line)) !== null) {
60660
+ if (match2[1] && !isBlock) continue;
60661
+ if (match2[2].toLowerCase() === lowerName) {
60662
+ const nameStart = match2.index + 1 + (match2[1] ? 1 : 0);
60663
+ locations.push({
60664
+ uri: docUri,
60665
+ range: {
60666
+ start: { line: lineNum, character: nameStart },
60667
+ end: { line: lineNum, character: nameStart + match2[2].length }
60668
+ }
60669
+ });
60670
+ }
60671
+ }
60672
+ }
60673
+ }
60674
+ return locations;
60675
+ }
60676
+ function toLspRange(r) {
60677
+ return {
60678
+ start: { line: r.start.line, character: r.start.character },
60679
+ end: { line: r.end.line, character: r.end.character }
60680
+ };
60681
+ }
60682
+ var referencesPlugin;
60683
+ var init_references = __esm({
60684
+ "src/plugins/references.ts"() {
60685
+ "use strict";
60686
+ init_link_parser();
60687
+ init_passage_parser();
60688
+ init_macro_sources();
60689
+ referencesPlugin = {
60690
+ id: "references",
60691
+ capabilities: {
60692
+ referencesProvider: true
60693
+ },
60694
+ initialize(ctx) {
60695
+ ctx.connection.onReferences((params) => {
60696
+ const refs = findReferences(
60697
+ params.textDocument.uri,
60698
+ { line: params.position.line, character: params.position.character },
60699
+ ctx.workspace,
60700
+ params.context.includeDeclaration
60701
+ );
60702
+ return refs.map((r) => ({
60703
+ uri: r.uri,
60704
+ range: toLspRange(r.range)
60705
+ }));
60706
+ });
60707
+ }
60708
+ };
60709
+ }
60710
+ });
60711
+
59125
60712
  // src/plugins/hover.ts
59126
60713
  function getHoverInfo(uri, position, workspace) {
59127
60714
  const text = workspace.documents.getText(uri);
@@ -59129,20 +60716,22 @@ function getHoverInfo(uri, position, workspace) {
59129
60716
  const lines = text.split("\n");
59130
60717
  if (position.line >= lines.length) return null;
59131
60718
  const line = lines[position.line];
59132
- const macroResult = getMacroHover(line, position, workspace);
60719
+ const offset = (buildLineStarts(text)[position.line] ?? 0) + position.character;
60720
+ const inAttribute = inAttributeValue(text, offset);
60721
+ const macroResult = inAttribute ? null : getMacroHover(line, position, workspace);
59133
60722
  if (macroResult) return macroResult;
59134
- const varResult = getVariableHover(line, position, workspace);
60723
+ const varResult = getVariableHover(uri, line, position, workspace);
59135
60724
  if (varResult) return varResult;
59136
- const widgetResult = getWidgetHover(line, position, workspace);
60725
+ const widgetResult = inAttribute ? null : getWidgetHover(line, position, workspace);
59137
60726
  if (widgetResult) return widgetResult;
59138
60727
  return null;
59139
60728
  }
59140
60729
  function getMacroHover(line, position, workspace) {
59141
- const macroRegex = /\{(\/)?(?:(?:[#.][a-zA-Z][\w-]*\s*)*)([A-Za-z][\w-]*)/g;
60730
+ const macroRegex = new RegExp(String.raw`\{(?:(\/)|${SELECTOR_PATTERN} )?([A-Za-z][\w-]*)`, "g");
59142
60731
  let match2;
59143
60732
  while ((match2 = macroRegex.exec(line)) !== null) {
59144
60733
  const name = match2[2];
59145
- const nameStart = match2.index + match2[0].indexOf(name);
60734
+ const nameStart = match2.index + match2[0].length - name.length;
59146
60735
  const nameEnd = nameStart + name.length;
59147
60736
  if (position.character >= nameStart && position.character <= nameEnd) {
59148
60737
  const info = workspace.macros.getMacro(name);
@@ -59171,9 +60760,9 @@ function getMacroHover(line, position, workspace) {
59171
60760
  }
59172
60761
  return null;
59173
60762
  }
59174
- function getVariableHover(line, position, workspace) {
60763
+ function getVariableHover(uri, line, position, workspace) {
59175
60764
  {
59176
- const re = /\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
60765
+ const re = /\$([\w$]+(?:\.[A-Za-z_$][\w$]*)*)/g;
59177
60766
  let match2;
59178
60767
  while ((match2 = re.exec(line)) !== null) {
59179
60768
  const start = match2.index;
@@ -59229,13 +60818,14 @@ Fields: ${decl.fields.map((f) => `\`${f}\``).join(", ")}` : "";
59229
60818
  }
59230
60819
  }
59231
60820
  {
59232
- const re = /(?<!\w)%([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
60821
+ const re = /(?<!\w)%([\w$]+(?:\.[A-Za-z_$][\w$]*)*)/g;
59233
60822
  let match2;
59234
60823
  while ((match2 = re.exec(line)) !== null) {
59235
60824
  const start = match2.index;
59236
60825
  const end = start + match2[0].length;
59237
60826
  if (position.character >= start && position.character <= end) {
59238
60827
  const baseName = match2[1].split(".")[0];
60828
+ if (!isTransientAt(baseName, uri, position.line, start, workspace)) break;
59239
60829
  const decl = workspace.variables.getDeclaredTransient().get(baseName);
59240
60830
  const typeInfo = decl?.fields && decl.fields.length > 0 ? `
59241
60831
 
@@ -59282,7 +60872,7 @@ Defined in: \`${widget.uri}\``,
59282
60872
  }
59283
60873
  };
59284
60874
  }
59285
- function toLspRange(r) {
60875
+ function toLspRange2(r) {
59286
60876
  return {
59287
60877
  start: { line: r.start.line, character: r.start.character },
59288
60878
  end: { line: r.end.line, character: r.end.character }
@@ -59292,6 +60882,9 @@ var hoverPlugin;
59292
60882
  var init_hover = __esm({
59293
60883
  "src/plugins/hover.ts"() {
59294
60884
  "use strict";
60885
+ init_macro_parser();
60886
+ init_references();
60887
+ init_html_scanner();
59295
60888
  hoverPlugin = {
59296
60889
  id: "hover",
59297
60890
  capabilities: {
@@ -59307,7 +60900,7 @@ var init_hover = __esm({
59307
60900
  if (!result) return null;
59308
60901
  return {
59309
60902
  contents: { kind: "markdown", value: result.contents },
59310
- range: toLspRange(result.range)
60903
+ range: toLspRange2(result.range)
59311
60904
  };
59312
60905
  });
59313
60906
  }
@@ -59374,7 +60967,7 @@ var init_signature = __esm({
59374
60967
  init_argument_lexer();
59375
60968
  init_widget_arguments();
59376
60969
  init_macro_parser();
59377
- macroHeadRegex2 = /(?<!\\)\{(?:[#.][a-zA-Z][\w-]*\s*)*([A-Za-z][\w-]*)\s+/g;
60970
+ macroHeadRegex2 = new RegExp(String.raw`(?<!\\)\{(?:${SELECTOR_PATTERN} )?([A-Za-z][\w-]*)\s+`, "g");
59378
60971
  signaturePlugin = {
59379
60972
  id: "signature",
59380
60973
  capabilities: {
@@ -59449,7 +61042,7 @@ function getWidgetDefinition(line, position, workspace) {
59449
61042
  }
59450
61043
  return null;
59451
61044
  }
59452
- function toLspRange2(r) {
61045
+ function toLspRange3(r) {
59453
61046
  return {
59454
61047
  start: { line: r.start.line, character: r.start.character },
59455
61048
  end: { line: r.end.line, character: r.end.character }
@@ -59475,7 +61068,7 @@ var init_definition = __esm({
59475
61068
  if (!result) return null;
59476
61069
  return {
59477
61070
  uri: result.uri,
59478
- range: toLspRange2(result.range)
61071
+ range: toLspRange3(result.range)
59479
61072
  };
59480
61073
  });
59481
61074
  }
@@ -59483,211 +61076,6 @@ var init_definition = __esm({
59483
61076
  }
59484
61077
  });
59485
61078
 
59486
- // src/plugins/references.ts
59487
- function findReferences(uri, position, workspace, includeDeclaration) {
59488
- const text = workspace.documents.getText(uri);
59489
- if (text === void 0) return [];
59490
- const lines = text.split("\n");
59491
- if (position.line >= lines.length) return [];
59492
- const line = lines[position.line];
59493
- const passageHeaderRegex2 = /^::\s*(\S.*?)(?:\s*\[|\s*\{|\s*$)/;
59494
- if (line.trimStart().startsWith("::")) {
59495
- const headerMatch = passageHeaderRegex2.exec(line);
59496
- if (headerMatch) {
59497
- const passageName = headerMatch[1].trim();
59498
- const nameStart = line.indexOf(passageName);
59499
- const nameEnd = nameStart + passageName.length;
59500
- if (position.character >= nameStart && position.character <= nameEnd) {
59501
- return findPassageReferences(passageName, workspace, includeDeclaration);
59502
- }
59503
- }
59504
- }
59505
- {
59506
- const varRegex = /\$([A-Za-z_$][\w$]*)/g;
59507
- let match2;
59508
- while ((match2 = varRegex.exec(line)) !== null) {
59509
- const start = match2.index;
59510
- const end = start + match2[0].length;
59511
- if (position.character >= start && position.character <= end) {
59512
- const varName = match2[1];
59513
- return findVariableReferences(varName, workspace, includeDeclaration);
59514
- }
59515
- }
59516
- }
59517
- {
59518
- const transRegex = /(?<!\w)%([A-Za-z_$][\w$]*)/g;
59519
- let match2;
59520
- while ((match2 = transRegex.exec(line)) !== null) {
59521
- const start = match2.index;
59522
- const end = start + match2[0].length;
59523
- if (position.character >= start && position.character <= end) {
59524
- const varName = match2[1];
59525
- return findTransientReferences(varName, workspace, includeDeclaration);
59526
- }
59527
- }
59528
- }
59529
- {
59530
- const widgetRegex = /\{\/?([A-Za-z_$][\w$]*)/g;
59531
- let match2;
59532
- while ((match2 = widgetRegex.exec(line)) !== null) {
59533
- const name = match2[1];
59534
- const nameStart = match2.index + match2[0].length - name.length;
59535
- const nameEnd = nameStart + name.length;
59536
- if (position.character >= nameStart && position.character <= nameEnd) {
59537
- const widget = workspace.widgets.getWidget(name);
59538
- const isClosing = match2[0][1] === "/";
59539
- if (!workspace.macros.getMacro(name) && widget && (!isClosing || widget.block)) {
59540
- return findWidgetReferences(name, workspace, includeDeclaration);
59541
- }
59542
- }
59543
- }
59544
- }
59545
- const passageRef = findPassageRefAt(text, position);
59546
- if (passageRef) {
59547
- return findPassageReferences(passageRef.name, workspace, includeDeclaration);
59548
- }
59549
- {
59550
- const allPassages = workspace.passages.getAllPassages();
59551
- const passageNames = new Set(allPassages.map((p) => p.name));
59552
- const wordRegex = /[A-Za-z_$][\w$\s]*/g;
59553
- let match2;
59554
- while ((match2 = wordRegex.exec(line)) !== null) {
59555
- const start = match2.index;
59556
- const end = start + match2[0].length;
59557
- if (position.character >= start && position.character <= end) {
59558
- const word = match2[0].trim();
59559
- if (passageNames.has(word)) {
59560
- return findPassageReferences(word, workspace, includeDeclaration);
59561
- }
59562
- }
59563
- }
59564
- }
59565
- return [];
59566
- }
59567
- function findPassageReferences(passageName, workspace, includeDeclaration) {
59568
- const locations = [];
59569
- if (includeDeclaration) {
59570
- const passage = workspace.passages.getPassage(passageName);
59571
- if (passage) {
59572
- locations.push({
59573
- uri: passage.uri,
59574
- range: passage.nameRange
59575
- });
59576
- }
59577
- }
59578
- for (const docUri of workspace.documents.getUris()) {
59579
- if (isMacroSource(docUri)) continue;
59580
- const docText = workspace.documents.getText(docUri);
59581
- if (!docText) continue;
59582
- for (const ref of [...parseLinks(docText), ...parseMacroPassageRefs(docText)]) {
59583
- if (ref.name === passageName) {
59584
- locations.push({ uri: docUri, range: ref.range });
59585
- }
59586
- }
59587
- }
59588
- return locations;
59589
- }
59590
- function findVariableReferences(varName, workspace, includeDeclaration) {
59591
- const locations = [];
59592
- if (includeDeclaration) {
59593
- const decl = workspace.variables.getDeclared().get(varName);
59594
- if (decl?.declarationRange && decl.declarationUri) {
59595
- locations.push({
59596
- uri: decl.declarationUri,
59597
- range: decl.declarationRange
59598
- });
59599
- }
59600
- }
59601
- const usages = workspace.variables.getUsages(varName);
59602
- for (const u of usages) {
59603
- locations.push({ uri: u.uri, range: u.range });
59604
- }
59605
- return locations;
59606
- }
59607
- function findTransientReferences(varName, workspace, includeDeclaration) {
59608
- const results = [];
59609
- if (includeDeclaration) {
59610
- const decl = workspace.variables.getDeclaredTransient().get(varName);
59611
- if (decl?.declarationUri && decl.declarationRange) {
59612
- results.push({ uri: decl.declarationUri, range: decl.declarationRange });
59613
- }
59614
- }
59615
- const usages = workspace.variables.getTransientUsages(varName);
59616
- for (const u of usages) {
59617
- results.push({ uri: u.uri, range: u.range });
59618
- }
59619
- return results;
59620
- }
59621
- function findWidgetReferences(widgetName, workspace, includeDeclaration) {
59622
- const locations = [];
59623
- const widget = workspace.widgets.getWidget(widgetName);
59624
- if (includeDeclaration && widget) {
59625
- locations.push({ uri: widget.uri, range: widget.range });
59626
- }
59627
- const widgetInvocationRegex = /\{(\/)?([A-Za-z_$][\w$]*)\b/g;
59628
- const lowerName = widgetName.toLowerCase();
59629
- const isBlock = widget?.block ?? false;
59630
- for (const docUri of workspace.documents.getUris()) {
59631
- if (isMacroSource(docUri)) continue;
59632
- const docText = workspace.documents.getText(docUri);
59633
- if (!docText) continue;
59634
- const lines = docText.split("\n");
59635
- for (let lineNum = 0; lineNum < lines.length; lineNum++) {
59636
- const line = lines[lineNum];
59637
- widgetInvocationRegex.lastIndex = 0;
59638
- let match2;
59639
- while ((match2 = widgetInvocationRegex.exec(line)) !== null) {
59640
- if (match2[1] && !isBlock) continue;
59641
- if (match2[2].toLowerCase() === lowerName) {
59642
- const nameStart = match2.index + 1 + (match2[1] ? 1 : 0);
59643
- locations.push({
59644
- uri: docUri,
59645
- range: {
59646
- start: { line: lineNum, character: nameStart },
59647
- end: { line: lineNum, character: nameStart + match2[2].length }
59648
- }
59649
- });
59650
- }
59651
- }
59652
- }
59653
- }
59654
- return locations;
59655
- }
59656
- function toLspRange3(r) {
59657
- return {
59658
- start: { line: r.start.line, character: r.start.character },
59659
- end: { line: r.end.line, character: r.end.character }
59660
- };
59661
- }
59662
- var referencesPlugin;
59663
- var init_references = __esm({
59664
- "src/plugins/references.ts"() {
59665
- "use strict";
59666
- init_link_parser();
59667
- init_macro_sources();
59668
- referencesPlugin = {
59669
- id: "references",
59670
- capabilities: {
59671
- referencesProvider: true
59672
- },
59673
- initialize(ctx) {
59674
- ctx.connection.onReferences((params) => {
59675
- const refs = findReferences(
59676
- params.textDocument.uri,
59677
- { line: params.position.line, character: params.position.character },
59678
- ctx.workspace,
59679
- params.context.includeDeclaration
59680
- );
59681
- return refs.map((r) => ({
59682
- uri: r.uri,
59683
- range: toLspRange3(r.range)
59684
- }));
59685
- });
59686
- }
59687
- };
59688
- }
59689
- });
59690
-
59691
61079
  // src/plugins/rename.ts
59692
61080
  function prepareRename(uri, position, workspace) {
59693
61081
  const symbol2 = resolveSymbolAtCursor(uri, position, workspace);
@@ -59705,8 +61093,11 @@ function computeRename(uri, position, newName, workspace) {
59705
61093
  }
59706
61094
  switch (symbol2.kind) {
59707
61095
  case "passage": {
59708
- const refs = findPassageReferences(symbol2.name, workspace, true);
59709
- for (const ref of refs) {
61096
+ const declaration = workspace.passages.getPassage(symbol2.name);
61097
+ if (declaration) {
61098
+ addEdit(declaration.uri, declaration.nameRange, escapePassageName(newName));
61099
+ }
61100
+ for (const ref of findPassageReferences(symbol2.name, workspace, false)) {
59710
61101
  addEdit(ref.uri, ref.range, newName);
59711
61102
  }
59712
61103
  break;
@@ -59737,33 +61128,24 @@ function computeRename(uri, position, newName, workspace) {
59737
61128
  }
59738
61129
  return edits;
59739
61130
  }
61131
+ function escapePassageName(name) {
61132
+ return name.replace(/[[\]{}\\]/g, "\\$&");
61133
+ }
59740
61134
  function resolveSymbolAtCursor(uri, position, workspace) {
59741
61135
  const text = workspace.documents.getText(uri);
59742
61136
  if (text === void 0) return null;
59743
61137
  const lines = text.split("\n");
59744
61138
  if (position.line >= lines.length) return null;
59745
61139
  const line = lines[position.line];
59746
- const passageHeaderRegex2 = /^::\s*(\S.*?)(?:\s*\[|\s*\{|\s*$)/;
59747
- if (line.trimStart().startsWith("::")) {
59748
- const headerMatch = passageHeaderRegex2.exec(line);
59749
- if (headerMatch) {
59750
- const passageName = headerMatch[1].trim();
59751
- const nameStart = line.indexOf(passageName);
59752
- const nameEnd = nameStart + passageName.length;
59753
- if (position.character >= nameStart && position.character <= nameEnd) {
59754
- return {
59755
- kind: "passage",
59756
- name: passageName,
59757
- range: {
59758
- start: { line: position.line, character: nameStart },
59759
- end: { line: position.line, character: nameEnd }
59760
- }
59761
- };
59762
- }
61140
+ const header = parsePassageHeader(line, position.line);
61141
+ if (header) {
61142
+ const { start, end } = header.nameRange;
61143
+ if (position.character >= start.character && position.character <= end.character) {
61144
+ return { kind: "passage", name: header.name, range: header.nameRange };
59763
61145
  }
59764
61146
  }
59765
61147
  {
59766
- const varRegex = /\$([A-Za-z_$][\w$]*)/g;
61148
+ const varRegex = /\$([\w$]+)/g;
59767
61149
  let match2;
59768
61150
  while ((match2 = varRegex.exec(line)) !== null) {
59769
61151
  const start = match2.index;
@@ -59782,12 +61164,13 @@ function resolveSymbolAtCursor(uri, position, workspace) {
59782
61164
  }
59783
61165
  }
59784
61166
  {
59785
- const transRegex = /(?<!\w)%([A-Za-z_$][\w$]*)/g;
61167
+ const transRegex = /(?<!\w)%([\w$]+)/g;
59786
61168
  let match2;
59787
61169
  while ((match2 = transRegex.exec(line)) !== null) {
59788
61170
  const start = match2.index;
59789
61171
  const end = start + match2[0].length;
59790
61172
  if (position.character >= start && position.character <= end) {
61173
+ if (!isTransientAt(match2[1], uri, position.line, start, workspace)) break;
59791
61174
  return {
59792
61175
  kind: "variable",
59793
61176
  name: match2[1],
@@ -59846,6 +61229,7 @@ var init_rename = __esm({
59846
61229
  "src/plugins/rename.ts"() {
59847
61230
  "use strict";
59848
61231
  init_link_parser();
61232
+ init_passage_parser();
59849
61233
  init_references();
59850
61234
  renamePlugin = {
59851
61235
  id: "rename",
@@ -59898,9 +61282,9 @@ function computeCodeLenses(uri, workspace) {
59898
61282
  const isStoryVarsFile = storyVarsPassage && storyVarsPassage.uri === uri;
59899
61283
  for (let lineNum = 0; lineNum < lines.length; lineNum++) {
59900
61284
  const line = lines[lineNum];
59901
- const passageMatch = line.match(/^::\s*(\S.*?)(?:\s*\[|\s*\{|\s*$)/);
59902
- if (passageMatch) {
59903
- const passageName = passageMatch[1].trim();
61285
+ const header = parsePassageHeader(line, lineNum);
61286
+ if (header) {
61287
+ const passageName = header.name;
59904
61288
  if (passageName === "StoryData") continue;
59905
61289
  const refs = findPassageReferences(passageName, workspace, true);
59906
61290
  const refCount = Math.max(0, refs.length - 1);
@@ -59931,7 +61315,7 @@ function computeCodeLenses(uri, workspace) {
59931
61315
  });
59932
61316
  }
59933
61317
  if (isStoryVarsFile) {
59934
- const varDeclMatch = line.match(/^\$([A-Za-z_$][\w$]*)\s*=/);
61318
+ const varDeclMatch = line.match(/^\$(\w+)\s*=/);
59935
61319
  if (varDeclMatch) {
59936
61320
  const varName = varDeclMatch[1];
59937
61321
  const refs = findVariableReferences(varName, workspace, true);
@@ -59960,6 +61344,7 @@ var codeLensPlugin;
59960
61344
  var init_code_lens = __esm({
59961
61345
  "src/plugins/code-lens.ts"() {
59962
61346
  "use strict";
61347
+ init_passage_parser();
59963
61348
  init_references();
59964
61349
  codeLensPlugin = {
59965
61350
  id: "code-lens",
@@ -60025,7 +61410,7 @@ function addVariableTypeHints(uri, text, range2, workspace, hints) {
60025
61410
  const endLine = Math.min(range2.end.line, lines.length - 1);
60026
61411
  for (let lineNum = startLine; lineNum <= endLine; lineNum++) {
60027
61412
  const line = lines[lineNum];
60028
- const declMatch = line.match(/^\$([A-Za-z_$][\w$]*)\s*=\s*(.+)$/);
61413
+ const declMatch = line.match(/^\$(\w+)\s*=\s*(.+)$/);
60029
61414
  if (!declMatch) continue;
60030
61415
  const valueStr = declMatch[2].trim();
60031
61416
  const inferredType = inferType(valueStr);
@@ -60047,7 +61432,7 @@ function addTransientTypeHints(uri, text, range2, workspace, hints) {
60047
61432
  const endLine = Math.min(range2.end.line, lines.length - 1);
60048
61433
  for (let lineNum = startLine; lineNum <= endLine; lineNum++) {
60049
61434
  const line = lines[lineNum];
60050
- const declMatch = line.match(/^%([A-Za-z_$][\w$]*)\s*=\s*(.+)$/);
61435
+ const declMatch = line.match(/^%(\w+)\s*=\s*(.+)$/);
60051
61436
  if (!declMatch) continue;
60052
61437
  const valueStr = declMatch[2].trim();
60053
61438
  const inferredType = inferType(valueStr);
@@ -60123,7 +61508,6 @@ function computeSemanticTokensAbsolute(uri, workspace) {
60123
61508
  for (const passage of passages) {
60124
61509
  const headerLine = passage.headerEnd.start.line;
60125
61510
  headerLines.add(headerLine);
60126
- const rawLine = lines[headerLine] ?? "";
60127
61511
  tokens.push({
60128
61512
  line: headerLine,
60129
61513
  startChar: 0,
@@ -60131,18 +61515,14 @@ function computeSemanticTokensAbsolute(uri, workspace) {
60131
61515
  tokenType: encodeType("namespace"),
60132
61516
  tokenModifiers: 0
60133
61517
  });
60134
- const nameMatch = rawLine.match(/^::\s*(\S.*?)(?:\s*\[|\s*\{|\s*$)/);
60135
- if (nameMatch) {
60136
- const name = nameMatch[1].trim();
60137
- const nameStart = rawLine.indexOf(name);
60138
- tokens.push({
60139
- line: headerLine,
60140
- startChar: nameStart,
60141
- length: name.length,
60142
- tokenType: encodeType("namespace"),
60143
- tokenModifiers: encodeModifiers(["declaration"])
60144
- });
60145
- }
61518
+ const { start, end } = passage.nameRange;
61519
+ tokens.push({
61520
+ line: headerLine,
61521
+ startChar: start.character,
61522
+ length: end.character - start.character,
61523
+ tokenType: encodeType("namespace"),
61524
+ tokenModifiers: encodeModifiers(["declaration"])
61525
+ });
60146
61526
  }
60147
61527
  const macros = parseMacros(text);
60148
61528
  for (const macro of macros) {
@@ -60161,10 +61541,10 @@ function computeSemanticTokensAbsolute(uri, workspace) {
60161
61541
  tokenModifiers: encodeModifiers(isDefined ? ["defaultLibrary"] : [])
60162
61542
  });
60163
61543
  }
60164
- const storyVarRegex = /(?<!\w)\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
61544
+ const storyVarRegex = /(?<!\w)\$([\w$]+(?:\.[A-Za-z_$][\w$]*)*)/g;
60165
61545
  const tempVarRegex = /(?<!\w)_([A-Za-z_$][\w$]*)/g;
60166
61546
  const localVarRegex = /(?<!\w)@([A-Za-z_$][\w$]*)/g;
60167
- const transientVarRegex = /(?<!\w)%([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
61547
+ const transientVarRegex = /(?<!\w)%([\w$]+(?:\.[A-Za-z_$][\w$]*)*)/g;
60168
61548
  const sugarKeywordRegex = /\b(to|is|isnot|eq|neq|gt|gte|lt|lte|and|or|not|def|ndef)\b/g;
60169
61549
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
60170
61550
  if (headerLines.has(lineIndex)) continue;
@@ -60202,6 +61582,7 @@ function computeSemanticTokensAbsolute(uri, workspace) {
60202
61582
  }
60203
61583
  transientVarRegex.lastIndex = 0;
60204
61584
  while ((m = transientVarRegex.exec(line)) !== null) {
61585
+ if (!isTransientAt(m[1].split(".")[0], uri, lineIndex, m.index, workspace)) continue;
60205
61586
  tokens.push({
60206
61587
  line: lineIndex,
60207
61588
  startChar: m.index,
@@ -60246,6 +61627,7 @@ var init_semantic_tokens = __esm({
60246
61627
  "src/plugins/semantic-tokens.ts"() {
60247
61628
  "use strict";
60248
61629
  init_macro_parser();
61630
+ init_references();
60249
61631
  tokenTypesLegend = [
60250
61632
  "function",
60251
61633
  "variable",
@@ -60295,7 +61677,7 @@ var init_semantic_tokens = __esm({
60295
61677
  });
60296
61678
 
60297
61679
  // src/plugins/code-actions.ts
60298
- import { readFileSync as readFileSync8 } from "node:fs";
61680
+ import { readFileSync as readFileSync9 } from "node:fs";
60299
61681
  import { basename, dirname as dirname3, join as join4 } from "node:path";
60300
61682
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL5 } from "node:url";
60301
61683
  function computeCodeActions(uri, diagnostics, workspace, options = {}) {
@@ -60327,6 +61709,11 @@ function computeCodeActions(uri, diagnostics, workspace, options = {}) {
60327
61709
  if (action) actions.push(action);
60328
61710
  break;
60329
61711
  }
61712
+ case DiagnosticCode.UnevaluatedAttributeBlock: {
61713
+ const action = fixAttributeMacro(uri, diag, workspace);
61714
+ if (action) actions.push(action);
61715
+ break;
61716
+ }
60330
61717
  }
60331
61718
  }
60332
61719
  return actions;
@@ -60360,7 +61747,7 @@ function fixUndefinedMacro(uri, diag, workspace, options) {
60360
61747
  let text = workspace.documents.getText(configUri);
60361
61748
  if (text === void 0) {
60362
61749
  try {
60363
- text = readFileSync8(existingPath, "utf-8");
61750
+ text = readFileSync9(existingPath, "utf-8");
60364
61751
  } catch {
60365
61752
  return null;
60366
61753
  }
@@ -60569,6 +61956,21 @@ function fixNullVariableValue(uri, diag) {
60569
61956
  }]
60570
61957
  };
60571
61958
  }
61959
+ function fixAttributeMacro(uri, diag, workspace) {
61960
+ const text = workspace.documents.getText(uri);
61961
+ if (text === void 0) return null;
61962
+ const lineStarts = buildLineStarts(text);
61963
+ const offset = (p) => (lineStarts[p.line] ?? text.length) + p.character;
61964
+ const source = text.slice(offset(diag.range.start), offset(diag.range.end));
61965
+ const expression = conditionalExpression(source) ?? printExpression(source);
61966
+ if (!expression) return null;
61967
+ return {
61968
+ title: `Rewrite as ${expression}`,
61969
+ kind: "quickfix",
61970
+ diagnosticCodes: [DiagnosticCode.UnevaluatedAttributeBlock],
61971
+ edits: [{ uri, range: diag.range, newText: expression }]
61972
+ };
61973
+ }
60572
61974
  function toLspRange6(r) {
60573
61975
  return {
60574
61976
  start: { line: r.start.line, character: r.start.character },
@@ -60583,6 +61985,8 @@ var init_code_actions = __esm({
60583
61985
  init_diagnostic_codes();
60584
61986
  init_config_loader();
60585
61987
  init_macro_sources();
61988
+ init_attribute_blocks();
61989
+ init_macro_parser();
60586
61990
  DEFAULT_CONFIG_FILENAME = "spindle.config.yaml";
60587
61991
  NEW_MACRO_ENTRY = { description: "" };
60588
61992
  codeActionsPlugin = {
@@ -60691,7 +62095,7 @@ function computeDocumentSymbols(uri, workspace) {
60691
62095
  const contentStart = passage.range.start.line + 1;
60692
62096
  const contentEnd = passage.range.end.line + 1;
60693
62097
  for (let i = contentStart; i < contentEnd && i < lines.length; i++) {
60694
- const varMatch = lines[i].match(/^\$([A-Za-z_$][\w$]*)\s*=/);
62098
+ const varMatch = lines[i].match(/^\$(\w+)\s*=/);
60695
62099
  if (varMatch) {
60696
62100
  const charEnd = varMatch[0].length;
60697
62101
  passageSymbol.children.push({
@@ -60990,7 +62394,7 @@ __export(server_exports2, {
60990
62394
  startServer: () => startServer
60991
62395
  });
60992
62396
  import { fileURLToPath as fileURLToPath6, pathToFileURL as pathToFileURL6 } from "node:url";
60993
- import { readFileSync as readFileSync9 } from "node:fs";
62397
+ import { readFileSync as readFileSync10 } from "node:fs";
60994
62398
  import { basename as basename2 } from "node:path";
60995
62399
  function uriToFsPath(uri) {
60996
62400
  try {
@@ -61028,9 +62432,27 @@ function startServer(_args) {
61028
62432
  for (const warning of workspace.macros.warnings) console.error("[spindle-lsp]", warning);
61029
62433
  activePlugins = loadPlugins(allPlugins, config2);
61030
62434
  console.error("[spindle-lsp] plugins loaded:", activePlugins.map((p) => p.id).join(", "));
62435
+ const model = workspace;
62436
+ const pluginConnection = gateConnection(connection, () => model.isSpindleProject());
61031
62437
  for (const plugin of activePlugins) {
61032
- plugin.initialize({ connection, workspace, config: config2 });
61033
- }
62438
+ plugin.initialize({ connection: pluginConnection, workspace, config: config2 });
62439
+ }
62440
+ const refresh = params.capabilities.workspace;
62441
+ let spindle = workspace.isSpindleProject();
62442
+ workspace.on("storyFormatChanged", (format) => {
62443
+ if (format.isSpindle === spindle) return;
62444
+ spindle = format.isSpindle;
62445
+ console.error(format.isSpindle ? "[spindle-lsp] story format is Spindle; language features enabled" : `[spindle-lsp] story format "${format.name}" is not Spindle; language features disabled`);
62446
+ const requests = [
62447
+ refresh?.semanticTokens?.refreshSupport && "workspace/semanticTokens/refresh",
62448
+ refresh?.inlayHint?.refreshSupport && "workspace/inlayHint/refresh",
62449
+ refresh?.codeLens?.refreshSupport && "workspace/codeLens/refresh"
62450
+ ];
62451
+ for (const method of requests) {
62452
+ if (method) connection.sendRequest(method).catch(() => {
62453
+ });
62454
+ }
62455
+ });
61034
62456
  if (Object.keys(projectConfig.macros).length > 0) {
61035
62457
  workspace.macros.loadConfig(projectConfig.macros);
61036
62458
  console.error("[spindle-lsp] loaded config macros:", Object.keys(projectConfig.macros).length);
@@ -61097,7 +62519,7 @@ function startServer(_args) {
61097
62519
  if (workspace) {
61098
62520
  try {
61099
62521
  const fsPath = uriToFsPath(textDocument.uri);
61100
- const text = readFileSync9(fsPath, "utf-8");
62522
+ const text = readFileSync10(fsPath, "utf-8");
61101
62523
  workspace.documents.update(textDocument.uri, text);
61102
62524
  } catch {
61103
62525
  workspace.documents.close(textDocument.uri);
@@ -61133,7 +62555,7 @@ function startServer(_args) {
61133
62555
  } else {
61134
62556
  try {
61135
62557
  const fsPath = uriToFsPath(change.uri);
61136
- const text = readFileSync9(fsPath, "utf-8");
62558
+ const text = readFileSync10(fsPath, "utf-8");
61137
62559
  if (workspace.documents.has(change.uri)) {
61138
62560
  workspace.documents.update(change.uri, text);
61139
62561
  } else {
@@ -61169,7 +62591,7 @@ async function scanWorkspaceFiles(root) {
61169
62591
  ];
61170
62592
  for (const filePath of files) {
61171
62593
  try {
61172
- const text = readFileSync9(filePath, "utf-8");
62594
+ const text = readFileSync10(filePath, "utf-8");
61173
62595
  const uri = fsPathToUri(filePath);
61174
62596
  contents.set(uri, text);
61175
62597
  } catch {
@@ -61189,6 +62611,7 @@ var init_server4 = __esm({
61189
62611
  init_workspace_model();
61190
62612
  init_plugin_loader();
61191
62613
  init_capabilities();
62614
+ init_story_format_gate();
61192
62615
  init_plugins();
61193
62616
  init_config_loader();
61194
62617
  init_macro_sources();
@@ -61197,7 +62620,7 @@ var init_server4 = __esm({
61197
62620
 
61198
62621
  // src/bin.ts
61199
62622
  var args = process.argv.slice(2);
61200
- var VERSION = "0.9.3";
62623
+ var VERSION = "0.11.0";
61201
62624
  var HELP = `spindle-lsp v${VERSION}
61202
62625
  Language Server Protocol server for Spindle story format
61203
62626