@rohal12/spindle-lsp 0.10.0 → 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.
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
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;
@@ -6838,17 +7135,19 @@ function pairMacros(macros, isBlock, passageStartLines = []) {
6838
7135
  }
6839
7136
  }
6840
7137
  }
6841
- var variableInterpolationRegex, SELECTOR_PATTERN, 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
7146
  SELECTOR_PATTERN = String.raw`(?:[.#](?:[\w-]|\{[$_@][\w.]*\})*)+`;
6847
7147
  macroHeadRegex = new RegExp(
6848
7148
  String.raw`(?<!\\)\{(?:(\/)|(${SELECTOR_PATTERN}) )?([A-Za-z][\w-]*)(?=[\s}]|\{[$_@%][A-Za-z_$][\w$.]*\})`,
6849
7149
  "gi"
6850
7150
  );
6851
- NON_STRING_QUOTE_PREFIX = /[\p{L}\p{N}_\\]/u;
6852
7151
  }
6853
7152
  });
6854
7153
 
@@ -16107,6 +16406,7 @@ var init_diagnostic_codes = __esm({
16107
16406
  UndefinedMacro: "SP100",
16108
16407
  MalformedContainer: "SP101",
16109
16408
  MalformedElement: "SP102",
16409
+ UnevaluatedAttributeBlock: "SP103",
16110
16410
  IllegalClosingTag: "SP104",
16111
16411
  InvalidChildren: "SP107",
16112
16412
  ExpectedNoArguments: "SP108",
@@ -16136,6 +16436,7 @@ var init_diagnostic_codes = __esm({
16136
16436
  SP100: "warning",
16137
16437
  SP101: "error",
16138
16438
  SP102: "error",
16439
+ SP103: "warning",
16139
16440
  SP104: "error",
16140
16441
  SP107: "error",
16141
16442
  SP108: "error",
@@ -16434,255 +16735,159 @@ var init_argument_lexer = __esm({
16434
16735
  }
16435
16736
  });
16436
16737
 
16437
- // src/core/parsing/html-scanner.ts
16438
- function plainBraceMatches(input) {
16439
- const match2 = new Int32Array(input.length).fill(-1);
16440
- const open = [];
16441
- for (let i = 0; i < input.length; i++) {
16442
- if (input[i] === "{") open.push(i);
16443
- else if (input[i] === "}" && open.length > 0) match2[open.pop()] = i;
16444
- }
16445
- return match2;
16446
- }
16447
- function scanHtmlTags(text) {
16448
- const n = text.length;
16449
- const tags = [];
16450
- const macros = [];
16451
- const code = createCodeScanner(text);
16452
- const plain = plainBraceMatches(text);
16453
- const rawBodies = [];
16454
- let doCloses;
16455
- let nextDoClose = 0;
16456
- const rawBody = (end) => {
16457
- doCloses ??= [...text.matchAll(/\{\/do\s*\}/gi)].map((match2) => match2.index);
16458
- while (nextDoClose < doCloses.length && doCloses[nextDoClose] < end) nextDoClose++;
16459
- if (nextDoClose < doCloses.length) rawBodies.push([end, doCloses[nextDoClose]]);
16460
- };
16461
- const rawBodyLimit = (limit2) => {
16462
- if (rawBodies.length === 0) return limit2;
16463
- const macroStarts = new Set(macros);
16464
- let t = 0;
16465
- for (const [start, close] of rawBodies) {
16466
- if (start >= limit2) break;
16467
- if (close < limit2 && !macroStarts.has(close)) {
16468
- limit2 = start;
16469
- break;
16470
- }
16471
- while (t < tags.length && tags[t].start < start) t++;
16472
- if (t < tags.length && tags[t].start < Math.min(close, limit2)) limit2 = tags[t].start;
16473
- }
16474
- return limit2;
16475
- };
16476
- const braceEnd = (open) => {
16477
- const close = code.closeBrace(open + 1);
16478
- if (close !== plain[open]) return GIVE_UP;
16479
- return close === -1 ? open + 1 : close + 1;
16480
- };
16481
- const displayEnd = (open, sigil) => {
16482
- let j = sigil + 1;
16483
- while (j < n && DISPLAY_NAME_CHAR.test(text[j])) j++;
16484
- return text[j] === "}" ? j + 1 : braceEnd(open);
16485
- };
16486
- const afterBrace = (i2) => {
16487
- let at = i2 + 1;
16488
- const selectors = text[at] === "." || text[at] === "#";
16489
- if (selectors) {
16490
- at = selectorsEnd(text, at);
16491
- if (text[at] === " ") at++;
16492
- }
16493
- const c3 = text[at];
16494
- if (c3 === "$" || c3 === "_" || c3 === "@" || c3 === "%") return displayEnd(i2, at);
16495
- if (c3 === void 0) return i2 + 1;
16496
- if (LETTER.test(c3) || c3 === "/" && !selectors) {
16497
- const end = braceEnd(i2);
16498
- if (end > i2 + 1) {
16499
- macros.push(i2);
16500
- if (/^do$/i.test(text.slice(at, end - 1).trim().split(/\s/)[0])) rawBody(end);
16501
- }
16502
- return end;
16503
- }
16504
- return i2 + 1;
16505
- };
16506
- const readTag = (i2) => {
16507
- let j = i2 + 1;
16508
- const isClose = text[j] === "/";
16509
- if (isClose) j++;
16510
- const nameStart = j;
16511
- while (j < n && TAG_NAME_CHAR.test(text[j])) j++;
16512
- const name = text.slice(nameStart, j);
16513
- if (!name || !LETTER.test(name[0])) return i2 + 1;
16514
- const isVoid = VOID_ELEMENTS.has(name.toLowerCase());
16515
- if (isClose) {
16516
- while (j < n && WHITESPACE.test(text[j])) j++;
16517
- if (text[j] !== ">") return i2 + 1;
16518
- if (!isVoid) tags.push({ name, kind: "close", start: i2, end: j + 1 });
16519
- return j + 1;
16520
- }
16521
- const attrs = readAttributes(j);
16522
- if (attrs.end === GIVE_UP) return GIVE_UP;
16523
- j = attrs.end;
16524
- let selfClosing = isVoid;
16525
- if (text[j] === "/") {
16526
- selfClosing = true;
16527
- j++;
16528
- }
16529
- if (text[j] === ">") {
16530
- tags.push({ name, kind: selfClosing ? "void" : "open", start: i2, end: j + 1 });
16531
- return j + 1;
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;
16532
16751
  }
16533
- return attrs.count > 0 ? GIVE_UP : i2 + 1;
16534
- };
16535
- const readAttributes = (j) => {
16536
- let count = 0;
16537
- while (j < n) {
16538
- while (j < n && WHITESPACE.test(text[j])) j++;
16539
- if (j >= n || text[j] === ">" || text[j] === "/" && text[j + 1] === ">") break;
16540
- const nameStart = j;
16541
- while (j < n && ATTR_NAME_CHAR.test(text[j])) j++;
16542
- if (j === nameStart) break;
16543
- count++;
16544
- if (text[j] !== "=") continue;
16545
- j++;
16546
- const quote = text[j];
16547
- if (quote === '"' || quote === "'") {
16548
- const close = quotedValueEnd(j + 1, quote);
16549
- if (close === GIVE_UP) return { end: GIVE_UP, count };
16550
- j = close < n ? close + 1 : close;
16551
- } else {
16552
- while (j < n && !WHITESPACE.test(text[j]) && text[j] !== ">") j++;
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;
16553
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++;
16554
16767
  }
16555
- return { end: j, count };
16556
- };
16557
- const quotedValueEnd = (from, quote) => {
16558
- let depth = 0;
16559
- let sawBrace = false;
16560
- let k = from;
16561
- for (; k < n; k++) {
16562
- const c3 = text[k];
16563
- if (c3 === "{") {
16564
- depth++;
16565
- sawBrace = true;
16566
- } else if (c3 === "}") {
16567
- depth--;
16568
- sawBrace = true;
16569
- } else if (c3 === quote && depth <= 0) {
16570
- break;
16571
- }
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;
16572
16799
  }
16573
- if (!sawBrace) return k;
16574
- let d = from;
16575
- while (d < n) {
16576
- if (text[d] === "{") {
16577
- const close = code.closeBrace(d + 1);
16578
- if (close !== -1) {
16579
- d = close + 1;
16580
- continue;
16581
- }
16582
- } else if (text[d] === quote) {
16583
- break;
16584
- }
16585
- d++;
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++;
16586
16807
  }
16587
- return d === k ? k : GIVE_UP;
16588
- };
16808
+ }
16809
+ return void 0;
16810
+ }
16811
+ function usesVariable(inner) {
16812
+ const code = createCodeScanner(inner);
16813
+ let masked = "";
16589
16814
  let i = 0;
16590
- let stoppedAt = -1;
16591
- while (i < n) {
16592
- const c3 = text[i];
16593
- let next;
16594
- if (c3 === "\\") {
16595
- let k = i + 1;
16596
- while (text[k] === "\\") k++;
16597
- if (text[k] === "{" || text[k] === "}") next = (k - i) % 2 === 1 ? k + 1 : GIVE_UP;
16598
- else next = k;
16599
- } else if (c3 === "[" && text[i + 1] === "[") {
16600
- next = linkEnd(text, i);
16601
- } else if (c3 === "{") {
16602
- next = afterBrace(i);
16603
- } else if (c3 === "<") {
16604
- next = readTag(i);
16605
- } else {
16606
- next = i + 1;
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;
16607
16821
  }
16608
- if (next === GIVE_UP) {
16609
- stoppedAt = i;
16610
- break;
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
+ }
16611
16829
  }
16612
- i = next;
16830
+ masked += inner[i];
16831
+ i++;
16613
16832
  }
16614
- const limit = rawBodyLimit(stoppedAt === -1 ? n : stoppedAt);
16615
- if (limit === stoppedAt || limit === n) return { tags, stoppedAt, macros };
16616
- return {
16617
- tags: tags.filter((tag) => tag.start < limit),
16618
- stoppedAt: limit,
16619
- macros: macros.filter((at) => at < limit)
16620
- };
16833
+ return VARIABLE_REFERENCE.test(masked);
16621
16834
  }
16622
- function selectorsEnd(text, i) {
16623
- const n = text.length;
16624
- while (i < n && (text[i] === "." || text[i] === "#")) {
16625
- i++;
16626
- while (i < n) {
16627
- if (SELECTOR_NAME_CHAR.test(text[i])) {
16628
- i++;
16629
- } else if (text[i] === "{" && (text[i + 1] === "$" || text[i + 1] === "_" || text[i + 1] === "@")) {
16630
- let j = i + 2;
16631
- while (j < n && DISPLAY_NAME_CHAR.test(text[j])) j++;
16632
- if (text[j] !== "}") break;
16633
- i = j + 1;
16634
- } else {
16635
- break;
16636
- }
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;
16637
16849
  }
16850
+ operators += condition[i];
16851
+ i++;
16638
16852
  }
16639
- return i;
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)}}`;
16640
16858
  }
16641
- function linkEnd(text, i) {
16642
- const n = text.length;
16643
- let depth = 1;
16644
- let j = i + 2;
16645
- while (j < n) {
16646
- if (text[j] === "[" && text[j + 1] === "[") {
16647
- depth++;
16648
- j += 2;
16649
- } else if (text[j] === "]" && text[j + 1] === "]") {
16650
- if (--depth === 0) return j + 2;
16651
- j += 2;
16652
- } else {
16653
- j++;
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;
16654
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++;
16655
16877
  }
16656
- return GIVE_UP;
16878
+ return SIMPLE_PATH.test(expr) && expr.includes(".") ? `{${expr} ?? ''}` : `{${expr}}`;
16657
16879
  }
16658
- var VOID_ELEMENTS, TAG_NAME_CHAR, LETTER, ATTR_NAME_CHAR, WHITESPACE, SELECTOR_NAME_CHAR, DISPLAY_NAME_CHAR, GIVE_UP;
16659
- var init_html_scanner = __esm({
16660
- "src/core/parsing/html-scanner.ts"() {
16880
+ var SIGILS, MACRO_HEAD, VARIABLE_REFERENCE, IF_ELSE, SIMPLE_PATH;
16881
+ var init_attribute_blocks = __esm({
16882
+ "src/core/parsing/attribute-blocks.ts"() {
16661
16883
  "use strict";
16884
+ init_code_scanner();
16662
16885
  init_macro_parser();
16663
- VOID_ELEMENTS = /* @__PURE__ */ new Set([
16664
- "area",
16665
- "base",
16666
- "br",
16667
- "col",
16668
- "embed",
16669
- "hr",
16670
- "img",
16671
- "input",
16672
- "link",
16673
- "meta",
16674
- "param",
16675
- "source",
16676
- "track",
16677
- "wbr"
16678
- ]);
16679
- TAG_NAME_CHAR = /[a-zA-Z0-9-]/;
16680
- LETTER = /[a-zA-Z]/;
16681
- ATTR_NAME_CHAR = /[a-zA-Z0-9_\-:@]/;
16682
- WHITESPACE = /[^\S ]/;
16683
- SELECTOR_NAME_CHAR = /[a-zA-Z0-9_-]/;
16684
- DISPLAY_NAME_CHAR = /[\w.]/;
16685
- GIVE_UP = -2;
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.]+$/;
16686
16891
  }
16687
16892
  });
16688
16893
 
@@ -17544,6 +17749,10 @@ function computeDiagnostics(uri, workspace, options) {
17544
17749
  diagnostics.push(...replay.errors);
17545
17750
  } catch {
17546
17751
  }
17752
+ try {
17753
+ validateAttributeBlocks(markupText, passages, workspace, diagnostics);
17754
+ } catch {
17755
+ }
17547
17756
  try {
17548
17757
  validateMacros(macros, elements, workspace, diagnostics);
17549
17758
  } catch {
@@ -17771,6 +17980,40 @@ function replayPassage(macros, from, to, scan, base, lineStarts, workspace, repl
17771
17980
  if ("element" in entry) report2(tagRange(entry.element), `unclosed <${entry.element.name}>`);
17772
17981
  }
17773
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
+ }
17774
18017
  function isInsideParent(macros, index, parents, workspace) {
17775
18018
  const parentSet = new Set(parents.map((p) => p.toLowerCase()));
17776
18019
  for (let i = index - 1; i >= 0; i--) {
@@ -18264,7 +18507,7 @@ function toLspDiagnostic(d) {
18264
18507
  message: d.message
18265
18508
  };
18266
18509
  }
18267
- var DIRECT_CHILD_MACROS, NON_MARKUP_PASSAGES, WILL_NOT_START, 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;
18268
18511
  var init_diagnostics = __esm({
18269
18512
  "src/plugins/diagnostics.ts"() {
18270
18513
  "use strict";
@@ -18272,6 +18515,7 @@ var init_diagnostics = __esm({
18272
18515
  init_macro_parser();
18273
18516
  init_argument_lexer();
18274
18517
  init_html_scanner();
18518
+ init_attribute_blocks();
18275
18519
  init_widget_arguments();
18276
18520
  init_parameter_validator();
18277
18521
  init_link_parser();
@@ -18285,6 +18529,9 @@ var init_diagnostics = __esm({
18285
18529
  "StoryTransients",
18286
18530
  "SaveTitle"
18287
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'}";
18288
18535
  WILL_NOT_START = "Spindle will not start the story.";
18289
18536
  DEFERRED_CONTAINERS = /* @__PURE__ */ new Set(["link", "button"]);
18290
18537
  EXPRESSION_BUILTINS2 = /* @__PURE__ */ new Set([
@@ -60030,8 +60277,9 @@ function getCompletions(uri, position, triggerChar, workspace) {
60030
60277
  const lines = text.split("\n");
60031
60278
  if (position.line >= lines.length) return [];
60032
60279
  const lineText = lines[position.line].substring(0, position.character);
60280
+ const inAttribute = () => inAttributeValue(text, (buildLineStarts(text)[position.line] ?? 0) + position.character - 1);
60033
60281
  if (/\{\/[A-Za-z\w-]*$/.test(lineText)) {
60034
- return getClosingMacroCompletions(uri, text, position, workspace);
60282
+ return inAttribute() ? [] : getClosingMacroCompletions(uri, text, position, workspace);
60035
60283
  }
60036
60284
  const transientDotPathMatch = /%([\w$]+)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
60037
60285
  if (transientDotPathMatch) {
@@ -60057,7 +60305,7 @@ function getCompletions(uri, position, triggerChar, workspace) {
60057
60305
  return getPassageNameCompletions(workspace);
60058
60306
  }
60059
60307
  if (/(?:^|[^\\])\{[A-Za-z\w-]*$/.test(lineText)) {
60060
- return getMacroCompletions(workspace);
60308
+ return inAttribute() ? [] : getMacroCompletions(workspace);
60061
60309
  }
60062
60310
  return [];
60063
60311
  }
@@ -60229,6 +60477,7 @@ var init_completions = __esm({
60229
60477
  "src/plugins/completions.ts"() {
60230
60478
  "use strict";
60231
60479
  init_macro_parser();
60480
+ init_html_scanner();
60232
60481
  completionsPlugin = {
60233
60482
  id: "completions",
60234
60483
  capabilities: {
@@ -60467,11 +60716,13 @@ function getHoverInfo(uri, position, workspace) {
60467
60716
  const lines = text.split("\n");
60468
60717
  if (position.line >= lines.length) return null;
60469
60718
  const line = lines[position.line];
60470
- 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);
60471
60722
  if (macroResult) return macroResult;
60472
60723
  const varResult = getVariableHover(uri, line, position, workspace);
60473
60724
  if (varResult) return varResult;
60474
- const widgetResult = getWidgetHover(line, position, workspace);
60725
+ const widgetResult = inAttribute ? null : getWidgetHover(line, position, workspace);
60475
60726
  if (widgetResult) return widgetResult;
60476
60727
  return null;
60477
60728
  }
@@ -60633,6 +60884,7 @@ var init_hover = __esm({
60633
60884
  "use strict";
60634
60885
  init_macro_parser();
60635
60886
  init_references();
60887
+ init_html_scanner();
60636
60888
  hoverPlugin = {
60637
60889
  id: "hover",
60638
60890
  capabilities: {
@@ -61457,6 +61709,11 @@ function computeCodeActions(uri, diagnostics, workspace, options = {}) {
61457
61709
  if (action) actions.push(action);
61458
61710
  break;
61459
61711
  }
61712
+ case DiagnosticCode.UnevaluatedAttributeBlock: {
61713
+ const action = fixAttributeMacro(uri, diag, workspace);
61714
+ if (action) actions.push(action);
61715
+ break;
61716
+ }
61460
61717
  }
61461
61718
  }
61462
61719
  return actions;
@@ -61699,6 +61956,21 @@ function fixNullVariableValue(uri, diag) {
61699
61956
  }]
61700
61957
  };
61701
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
+ }
61702
61974
  function toLspRange6(r) {
61703
61975
  return {
61704
61976
  start: { line: r.start.line, character: r.start.character },
@@ -61713,6 +61985,8 @@ var init_code_actions = __esm({
61713
61985
  init_diagnostic_codes();
61714
61986
  init_config_loader();
61715
61987
  init_macro_sources();
61988
+ init_attribute_blocks();
61989
+ init_macro_parser();
61716
61990
  DEFAULT_CONFIG_FILENAME = "spindle.config.yaml";
61717
61991
  NEW_MACRO_ENTRY = { description: "" };
61718
61992
  codeActionsPlugin = {
@@ -62346,7 +62620,7 @@ var init_server4 = __esm({
62346
62620
 
62347
62621
  // src/bin.ts
62348
62622
  var args = process.argv.slice(2);
62349
- var VERSION = "0.10.0";
62623
+ var VERSION = "0.11.0";
62350
62624
  var HELP = `spindle-lsp v${VERSION}
62351
62625
  Language Server Protocol server for Spindle story format
62352
62626