@lokascript/language-server 2.7.1 → 2.8.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.
@@ -212,7 +212,14 @@ var DEFAULT_REFERENCES = /* @__PURE__ */ new Set([
212
212
  "result",
213
213
  "event",
214
214
  "target",
215
- "body"
215
+ "body",
216
+ // DOM context globals — as reference-typed as `body`, and already advertised as
217
+ // references by core's REFERENCE_KEYWORDS and the parser's PROPERTY_ACCESS_BASES.
218
+ // Without them here, a foreign surface (ja ドキュメント, es documento) lexes as a
219
+ // keyword but fails isValidReference, degrades to a literal, and leaks verbatim.
220
+ "document",
221
+ "window",
222
+ "detail"
216
223
  ]);
217
224
  function isValidReference(value, referenceSet = DEFAULT_REFERENCES) {
218
225
  return referenceSet.has(value);
@@ -227,16 +234,6 @@ var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
227
234
  "catch",
228
235
  "finally"
229
236
  ]);
230
- function isNestedCommand(value) {
231
- const inner = value.slice(1, -1);
232
- let depth = 0;
233
- for (const ch of inner) {
234
- if (ch === "[") depth++;
235
- else if (ch === "]") depth--;
236
- else if (depth === 0 && (ch === " " || ch === ":")) return true;
237
- }
238
- return false;
239
- }
240
237
  function parseExplicit(input, options = {}) {
241
238
  const trimmed = input.trim();
242
239
  const collect = options.collectDiagnostics === true;
@@ -324,7 +321,7 @@ function parseExplicit(input, options = {}) {
324
321
  }
325
322
  const role = roleName;
326
323
  const valueStr = token.slice(colonIndex + 1);
327
- if (STRUCTURAL_ROLES.has(roleName) && valueStr.startsWith("[") && isNestedCommand(valueStr)) {
324
+ if (STRUCTURAL_ROLES.has(roleName) && valueStr.startsWith("[")) {
328
325
  const nestedEnd = findMatchingBracket(token, colonIndex + 1);
329
326
  const nestedSyntax = token.slice(colonIndex + 1, nestedEnd + 1);
330
327
  roles.set(role, { type: "expression", raw: nestedSyntax });
@@ -576,26 +573,63 @@ function extractStructuralBody(roles, roleName, options) {
576
573
  if (value.type === "expression") return [parseExplicit(value.raw, options)];
577
574
  return [];
578
575
  }
576
+ function hasUnquotedCombinator(value) {
577
+ let quote = "";
578
+ for (const ch of value) {
579
+ if (quote) {
580
+ if (ch === quote) quote = "";
581
+ continue;
582
+ }
583
+ if (ch === '"' || ch === "'") {
584
+ quote = ch;
585
+ continue;
586
+ }
587
+ if (ch === " " || ch === ">" || ch === "+" || ch === "~" || ch === ",") return true;
588
+ }
589
+ return false;
590
+ }
579
591
  function inferSelectorKind(value) {
580
592
  if (!value) return void 0;
593
+ if (hasUnquotedCombinator(value)) return "complex";
581
594
  if (value.startsWith("#")) return "id";
582
595
  if (value.startsWith(".")) return "class";
583
596
  if (value.startsWith("[")) return "attribute";
584
- if (value.startsWith("<") || value.startsWith("*")) return "element";
585
- if (/[>+~ ]/.test(value) && value.length > 1) return "complex";
597
+ if (value.startsWith("*")) return "element";
586
598
  return void 0;
587
599
  }
600
+ function unescapeString(s) {
601
+ if (!s.includes("\\")) return s;
602
+ let out = "";
603
+ for (let i = 0; i < s.length; i++) {
604
+ if (s[i] === "\\" && i + 1 < s.length) {
605
+ const next = s[i + 1];
606
+ const mapped = next === "\\" ? "\\" : next === '"' ? '"' : next === "'" ? "'" : next === "n" ? "\n" : next === "r" ? "\r" : next === "t" ? " " : "";
607
+ if (mapped) {
608
+ out += mapped;
609
+ i++;
610
+ continue;
611
+ }
612
+ }
613
+ out += s[i];
614
+ }
615
+ return out;
616
+ }
588
617
  function tokenizeExplicit(content) {
589
618
  const tokens = [];
590
619
  let current = "";
591
620
  let inString = false;
592
621
  let stringChar = "";
622
+ let inSelector = false;
593
623
  let bracketDepth = 0;
594
624
  for (let i = 0; i < content.length; i++) {
595
625
  const char = content[i];
596
626
  if (inString) {
597
627
  current += char;
598
- if (char === stringChar && content[i - 1] !== "\\") inString = false;
628
+ if (char === stringChar) {
629
+ let backslashes = 0;
630
+ for (let j = i - 1; j >= 0 && content[j] === "\\"; j--) backslashes++;
631
+ if (backslashes % 2 === 0) inString = false;
632
+ }
599
633
  continue;
600
634
  }
601
635
  if (char === '"' || char === "'") {
@@ -604,6 +638,16 @@ function tokenizeExplicit(content) {
604
638
  current += char;
605
639
  continue;
606
640
  }
641
+ if (inSelector) {
642
+ current += char;
643
+ if (char === ">" && current.endsWith("/>")) inSelector = false;
644
+ continue;
645
+ }
646
+ if (char === "<" && (current === "" || current.endsWith(":"))) {
647
+ inSelector = true;
648
+ current += char;
649
+ continue;
650
+ }
607
651
  if (char === "[") {
608
652
  bracketDepth++;
609
653
  current += char;
@@ -627,11 +671,19 @@ function tokenizeExplicit(content) {
627
671
  return tokens;
628
672
  }
629
673
  function parseExplicitValue(valueStr, referenceSet) {
674
+ if (valueStr.startsWith("<")) {
675
+ if (!valueStr.endsWith("/>")) {
676
+ throw new Error(`Unterminated selector literal: "${valueStr}". Expected a closing "/>"`);
677
+ }
678
+ const inner = valueStr.slice(1, -2);
679
+ if (!inner) throw new Error(`Empty selector literal: "${valueStr}"`);
680
+ return createSelector(inner, inferSelectorKind(inner));
681
+ }
630
682
  if (valueStr.startsWith("#") || valueStr.startsWith(".") || valueStr.startsWith("[") || valueStr.startsWith("@") || valueStr.startsWith("*")) {
631
683
  return createSelector(valueStr, inferSelectorKind(valueStr));
632
684
  }
633
685
  if (valueStr.startsWith('"') || valueStr.startsWith("'")) {
634
- return createLiteral(valueStr.slice(1, -1), "string");
686
+ return createLiteral(unescapeString(valueStr.slice(1, -1)), "string");
635
687
  }
636
688
  if (valueStr === "true") return createLiteral(true, "boolean");
637
689
  if (valueStr === "false") return createLiteral(false, "boolean");
@@ -730,16 +782,23 @@ function renderBracketCommand(node) {
730
782
  }
731
783
  return `[${parts.join(" ")}]`;
732
784
  }
785
+ function needsSelectorWrap(value) {
786
+ if (value.includes(" ")) return true;
787
+ const first = value[0];
788
+ return !(first === "#" || first === "." || first === "@" || first === "*");
789
+ }
733
790
  function valueToString(value) {
734
791
  switch (value.type) {
735
792
  case "literal":
736
793
  if (typeof value.value === "string") {
737
- if (value.dataType === "string" || /\s/.test(value.value)) return `"${value.value}"`;
794
+ if (value.dataType === "string" || /\s/.test(value.value))
795
+ return JSON.stringify(value.value);
738
796
  return value.value;
739
797
  }
740
798
  return String(value.value);
741
799
  case "selector":
742
- return value.value;
800
+ if (!value.value) return value.value;
801
+ return needsSelectorWrap(value.value) ? `<${value.value}/>` : value.value;
743
802
  case "reference":
744
803
  return value.value;
745
804
  case "property-path":
@@ -837,6 +896,7 @@ function toProtocolJSON(node) {
837
896
  if (node.diagnostics && node.diagnostics.length > 0) {
838
897
  result.diagnostics = node.diagnostics.map((d) => {
839
898
  const json = { level: d.severity, message: d.message };
899
+ if (d.role) json.role = d.role;
840
900
  if (d.code) json.code = d.code;
841
901
  if (d.source) json.source = d.source;
842
902
  if (d.suggestions && d.suggestions.length > 0) json.suggestions = [...d.suggestions];
@@ -921,6 +981,7 @@ function fromProtocolJSON(json) {
921
981
  const diagnostics = json.diagnostics?.map((d) => ({
922
982
  severity: d.level,
923
983
  message: d.message,
984
+ ...d.role ? { role: d.role } : {},
924
985
  ...d.code ? { code: d.code } : {},
925
986
  ...d.source ? { source: d.source } : {},
926
987
  ...d.suggestions && d.suggestions.length > 0 ? { suggestions: d.suggestions } : {}
@@ -2674,7 +2735,8 @@ function buildFormatString(schema, profile, keyword) {
2674
2735
  if (profile.wordOrder === "SVO" || profile.wordOrder === "VSO") {
2675
2736
  parts.push(keyword);
2676
2737
  }
2677
- for (const role of schema.roles) {
2738
+ const sortedRoles = sortRolesByWordOrder(schema.roles, profile.wordOrder);
2739
+ for (const role of sortedRoles) {
2678
2740
  const marker = getMarkerForRole(role, profile);
2679
2741
  const roleName = `{${role.role}}`;
2680
2742
  if (marker) {
@@ -4085,7 +4147,9 @@ function buildProtocolSection() {
4085
4147
  3. No spaces around the colon in role:value pairs
4086
4148
  4. Strings with spaces must be quoted: \`patient:"hello world"\`
4087
4149
  5. Selectors start with \`#\`, \`.\`, \`[\`, \`@\`, or \`*\`
4088
- 6. Output must be valid bracket syntax: \`[action role:value ...]\``;
4150
+ 6. A selector containing a space, a combinator (\`>\` \`+\` \`~\`), or a comma must use a selector literal: \`patient:<ul > li/>\`, \`patient:<.a, .b/>\`
4151
+ 7. Inside a structural role (\`body\`, \`then\`, \`else\`, \`condition\`, \`loop-body\`, \`variable\`, \`catch\`, \`finally\`) a \`[...]\` value is always a nested command. Write an attribute selector there as \`condition:<[data-active]/>\`
4152
+ 8. Output must be valid bracket syntax: \`[action role:value ...]\``;
4089
4153
  return {
4090
4154
  id: "protocol",
4091
4155
  title: "LSE Protocol",
@@ -4098,6 +4162,7 @@ function buildValueTypeSection() {
4098
4162
 
4099
4163
  | Type | Syntax | Example |
4100
4164
  |------|--------|---------|
4165
+ | Selector literal | Delimited by \`<\` and \`/>\` | \`<ul > li/>\`, \`<.a, .b/>\`, \`<[data-id]/>\` |
4101
4166
  | Selector | Starts with \`#\` \`.\` \`[\` \`@\` \`*\` | \`#button\`, \`.active\`, \`[data-id]\` |
4102
4167
  | String | Quoted with \`"\` or \`'\` | \`"hello world"\`, \`'json'\` |
4103
4168
  | Boolean | Exact: \`true\` / \`false\` | \`visible:true\` |
@@ -6238,7 +6303,39 @@ var _BaseTokenizer = class _BaseTokenizer2 {
6238
6303
  pos++;
6239
6304
  }
6240
6305
  }
6241
- return new TokenStreamImpl(tokens, this.language);
6306
+ return new TokenStreamImpl(this.mergeColonQualifiedNames(tokens), this.language);
6307
+ }
6308
+ /**
6309
+ * Fuse `name` + `:qualifier` into ONE identifier (`draggable:start`).
6310
+ *
6311
+ * `:name` is hyperscript's local-variable sigil, but a colon IMMEDIATELY
6312
+ * preceded by an identifier is a qualifier (custom event namespace), not a
6313
+ * sigil. The English tokenizer already merges these inside
6314
+ * EnglishKeywordExtractor; this post-pass gives the other 23 languages the
6315
+ * same stream. Strict position adjacency is the discriminator: whitespace
6316
+ * between the tokens (`trigger :start`) breaks `end === start`, so a spaced
6317
+ * local-variable reference survives untouched.
6318
+ *
6319
+ * Self-gating for non-hyperscript tokenizers (domain DSLs): their extractor
6320
+ * sets tokenize `:` as bare punctuation (length 1), which never matches
6321
+ * COLON_QUALIFIER, so this pass is a no-op for them.
6322
+ */
6323
+ mergeColonQualifiedNames(tokens) {
6324
+ const out = [];
6325
+ for (const tok of tokens) {
6326
+ const prev = out[out.length - 1];
6327
+ if (prev && _BaseTokenizer2.ASCII_WORD.test(prev.value) && _BaseTokenizer2.COLON_QUALIFIER.test(tok.value) && prev.position.end === tok.position.start) {
6328
+ const merged = prev.value + tok.value;
6329
+ out[out.length - 1] = createToken(
6330
+ merged,
6331
+ this.classifyToken(merged),
6332
+ createPosition(prev.position.start, tok.position.end)
6333
+ );
6334
+ continue;
6335
+ }
6336
+ out.push(tok);
6337
+ }
6338
+ return out;
6242
6339
  }
6243
6340
  /**
6244
6341
  * Classify an unknown character when no extractor matches.
@@ -6744,6 +6841,8 @@ var _BaseTokenizer = class _BaseTokenizer2 {
6744
6841
  return null;
6745
6842
  }
6746
6843
  };
6844
+ _BaseTokenizer.ASCII_WORD = /^[A-Za-z_][A-Za-z0-9_]*$/;
6845
+ _BaseTokenizer.COLON_QUALIFIER = /^:[A-Za-z_][A-Za-z0-9_]*$/;
6747
6846
  _BaseTokenizer.STANDARD_TIME_UNITS = [
6748
6847
  { pattern: "ms", suffix: "ms", length: 2 },
6749
6848
  { pattern: "s", suffix: "s", length: 1, checkBoundary: true },
package/dist/server.js CHANGED
@@ -1134,7 +1134,7 @@ try {
1134
1134
  }
1135
1135
  var frameworkIR = null;
1136
1136
  try {
1137
- const fw = await import("./dist-MRVDB2YM.js");
1137
+ const fw = await import("./dist-6KATWM5A.js");
1138
1138
  frameworkIR = { fromInterchangeNode: fw.fromInterchangeNode, renderExplicit: fw.renderExplicit };
1139
1139
  console.error(
1140
1140
  "[lokascript-ls] @lokascript/framework loaded \u2014 LSE bracket notation in hover enabled"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokascript/language-server",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "description": "Language Server Protocol implementation for LokaScript/hyperscript with 21 language support",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -31,8 +31,8 @@
31
31
  "vscode-languageserver-textdocument": "^1.0.11"
32
32
  },
33
33
  "peerDependencies": {
34
- "@hyperfixi/core": "^2.7.1",
35
- "@lokascript/semantic": "^2.7.1"
34
+ "@hyperfixi/core": "^2.8.0",
35
+ "@lokascript/semantic": "^2.8.0"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@lokascript/semantic": {
@@ -43,12 +43,12 @@
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
- "@hyperfixi/core": "^2.7.1",
47
- "@lokascript/semantic": "^2.7.1",
46
+ "@hyperfixi/core": "^2.8.0",
47
+ "@lokascript/semantic": "^2.8.0",
48
48
  "@types/node": "^20.0.0",
49
49
  "@vitest/coverage-v8": "^4.0.0",
50
50
  "tsup": "^8.0.0",
51
- "tsx": "^4.0.0",
51
+ "tsx": "^4.23.1",
52
52
  "typescript": "^5.0.0",
53
53
  "vitest": "^4.1.5"
54
54
  },