@lokascript/language-server 2.7.2 → 2.9.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/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@lokascript/language-server` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-02-05
9
+
10
+ ### Added
11
+
12
+ - **Go to Definition**: Jump to behavior and function definitions with Ctrl+Click or F12
13
+ - **Find References**: Find all usages of a symbol with Shift+F12
14
+ - **Document Formatting**: Format hyperscript code with consistent indentation
15
+ - **Single-quote support**: HTML attributes with `_='...'` now work correctly
16
+ - **CRLF support**: Windows line endings are handled correctly in position mapping
17
+ - **Error logging**: All catch blocks now log errors for easier debugging
18
+
19
+ ### Improved
20
+
21
+ - HTML extraction now supports both double and single quoted attributes
22
+ - Position mapping correctly handles all line ending types (LF, CRLF, CR)
23
+ - Better error messages in diagnostics
24
+ - Comprehensive test coverage (138 tests)
25
+
26
+ ### Fixed
27
+
28
+ - Position mapping for multiline hyperscript attributes
29
+ - Cursor position calculation with different line endings
30
+
31
+ ## [0.1.0] - Initial Release
32
+
33
+ ### Added
34
+
35
+ - **Diagnostics**: Real-time error detection and warnings
36
+ - **Completions**: Context-aware keyword and selector suggestions
37
+ - **Hover**: Documentation on hover for commands and keywords
38
+ - **Document Symbols**: Outline view showing event handlers, behaviors, and functions
39
+ - **Code Actions**: Quick fixes for common issues
40
+ - **Multilingual support**: Works with 21 human languages via `@lokascript/semantic`
41
+ - **HTML support**: Extracts hyperscript from `_="..."` attributes and `<script type="text/hyperscript">` tags
@@ -184,6 +184,26 @@ function filterBySeverity(diagnostics, severity) {
184
184
  return diagnostics.filter((d) => d.severity === severity);
185
185
  }
186
186
  function defineCommand(schema) {
187
+ if (typeof schema !== "object" || schema === null) {
188
+ throw new TypeError(`defineCommand: expected a schema object, got ${describeValue(schema)}.`);
189
+ }
190
+ if (typeof schema.action !== "string" || schema.action.length === 0) {
191
+ throw new TypeError(
192
+ `defineCommand: 'action' must be a non-empty string (got ${describeValue(schema.action)}).`
193
+ );
194
+ }
195
+ if (!Array.isArray(schema.roles)) {
196
+ throw new TypeError(
197
+ `defineCommand("${schema.action}"): 'roles' must be an array of RoleSpec (got ${describeValue(schema.roles)}). Build entries with defineRole().`
198
+ );
199
+ }
200
+ for (const [index, role] of schema.roles.entries()) {
201
+ if (typeof role !== "object" || role === null || typeof role.role !== "string") {
202
+ throw new TypeError(
203
+ `defineCommand("${schema.action}"): roles[${index}] must be a RoleSpec with a string 'role' field (got ${describeValue(role)}). Build it with defineRole().`
204
+ );
205
+ }
206
+ }
187
207
  return {
188
208
  description: schema.description || `${schema.action} command`,
189
209
  category: schema.category || "general",
@@ -203,8 +223,16 @@ function defineRole(role) {
203
223
  };
204
224
  }
205
225
  function getRoleSpec(schema, role) {
226
+ if (!schema || !Array.isArray(schema.roles)) return void 0;
206
227
  return schema.roles.find((r) => r.role === role);
207
228
  }
229
+ function describeValue(value) {
230
+ if (value === null) return "null";
231
+ if (Array.isArray(value)) return `an array of length ${value.length}`;
232
+ if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
233
+ if (typeof value === "object") return "an object";
234
+ return typeof value === "undefined" ? "undefined" : String(value);
235
+ }
208
236
  var DEFAULT_REFERENCES = /* @__PURE__ */ new Set([
209
237
  "me",
210
238
  "you",
@@ -212,7 +240,14 @@ var DEFAULT_REFERENCES = /* @__PURE__ */ new Set([
212
240
  "result",
213
241
  "event",
214
242
  "target",
215
- "body"
243
+ "body",
244
+ // DOM context globals — as reference-typed as `body`, and already advertised as
245
+ // references by core's REFERENCE_KEYWORDS and the parser's PROPERTY_ACCESS_BASES.
246
+ // Without them here, a foreign surface (ja ドキュメント, es documento) lexes as a
247
+ // keyword but fails isValidReference, degrades to a literal, and leaks verbatim.
248
+ "document",
249
+ "window",
250
+ "detail"
216
251
  ]);
217
252
  function isValidReference(value, referenceSet = DEFAULT_REFERENCES) {
218
253
  return referenceSet.has(value);
@@ -227,16 +262,6 @@ var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
227
262
  "catch",
228
263
  "finally"
229
264
  ]);
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
265
  function parseExplicit(input, options = {}) {
241
266
  const trimmed = input.trim();
242
267
  const collect = options.collectDiagnostics === true;
@@ -324,7 +349,7 @@ function parseExplicit(input, options = {}) {
324
349
  }
325
350
  const role = roleName;
326
351
  const valueStr = token.slice(colonIndex + 1);
327
- if (STRUCTURAL_ROLES.has(roleName) && valueStr.startsWith("[") && isNestedCommand(valueStr)) {
352
+ if (STRUCTURAL_ROLES.has(roleName) && valueStr.startsWith("[")) {
328
353
  const nestedEnd = findMatchingBracket(token, colonIndex + 1);
329
354
  const nestedSyntax = token.slice(colonIndex + 1, nestedEnd + 1);
330
355
  roles.set(role, { type: "expression", raw: nestedSyntax });
@@ -576,26 +601,63 @@ function extractStructuralBody(roles, roleName, options) {
576
601
  if (value.type === "expression") return [parseExplicit(value.raw, options)];
577
602
  return [];
578
603
  }
604
+ function hasUnquotedCombinator(value) {
605
+ let quote = "";
606
+ for (const ch of value) {
607
+ if (quote) {
608
+ if (ch === quote) quote = "";
609
+ continue;
610
+ }
611
+ if (ch === '"' || ch === "'") {
612
+ quote = ch;
613
+ continue;
614
+ }
615
+ if (ch === " " || ch === ">" || ch === "+" || ch === "~" || ch === ",") return true;
616
+ }
617
+ return false;
618
+ }
579
619
  function inferSelectorKind(value) {
580
620
  if (!value) return void 0;
621
+ if (hasUnquotedCombinator(value)) return "complex";
581
622
  if (value.startsWith("#")) return "id";
582
623
  if (value.startsWith(".")) return "class";
583
624
  if (value.startsWith("[")) return "attribute";
584
- if (value.startsWith("<") || value.startsWith("*")) return "element";
585
- if (/[>+~ ]/.test(value) && value.length > 1) return "complex";
625
+ if (value.startsWith("*")) return "element";
586
626
  return void 0;
587
627
  }
628
+ function unescapeString(s) {
629
+ if (!s.includes("\\")) return s;
630
+ let out = "";
631
+ for (let i = 0; i < s.length; i++) {
632
+ if (s[i] === "\\" && i + 1 < s.length) {
633
+ const next = s[i + 1];
634
+ const mapped = next === "\\" ? "\\" : next === '"' ? '"' : next === "'" ? "'" : next === "n" ? "\n" : next === "r" ? "\r" : next === "t" ? " " : "";
635
+ if (mapped) {
636
+ out += mapped;
637
+ i++;
638
+ continue;
639
+ }
640
+ }
641
+ out += s[i];
642
+ }
643
+ return out;
644
+ }
588
645
  function tokenizeExplicit(content) {
589
646
  const tokens = [];
590
647
  let current = "";
591
648
  let inString = false;
592
649
  let stringChar = "";
650
+ let inSelector = false;
593
651
  let bracketDepth = 0;
594
652
  for (let i = 0; i < content.length; i++) {
595
653
  const char = content[i];
596
654
  if (inString) {
597
655
  current += char;
598
- if (char === stringChar && content[i - 1] !== "\\") inString = false;
656
+ if (char === stringChar) {
657
+ let backslashes = 0;
658
+ for (let j = i - 1; j >= 0 && content[j] === "\\"; j--) backslashes++;
659
+ if (backslashes % 2 === 0) inString = false;
660
+ }
599
661
  continue;
600
662
  }
601
663
  if (char === '"' || char === "'") {
@@ -604,6 +666,16 @@ function tokenizeExplicit(content) {
604
666
  current += char;
605
667
  continue;
606
668
  }
669
+ if (inSelector) {
670
+ current += char;
671
+ if (char === ">" && current.endsWith("/>")) inSelector = false;
672
+ continue;
673
+ }
674
+ if (char === "<" && (current === "" || current.endsWith(":"))) {
675
+ inSelector = true;
676
+ current += char;
677
+ continue;
678
+ }
607
679
  if (char === "[") {
608
680
  bracketDepth++;
609
681
  current += char;
@@ -627,11 +699,19 @@ function tokenizeExplicit(content) {
627
699
  return tokens;
628
700
  }
629
701
  function parseExplicitValue(valueStr, referenceSet) {
702
+ if (valueStr.startsWith("<")) {
703
+ if (!valueStr.endsWith("/>")) {
704
+ throw new Error(`Unterminated selector literal: "${valueStr}". Expected a closing "/>"`);
705
+ }
706
+ const inner = valueStr.slice(1, -2);
707
+ if (!inner) throw new Error(`Empty selector literal: "${valueStr}"`);
708
+ return createSelector(inner, inferSelectorKind(inner));
709
+ }
630
710
  if (valueStr.startsWith("#") || valueStr.startsWith(".") || valueStr.startsWith("[") || valueStr.startsWith("@") || valueStr.startsWith("*")) {
631
711
  return createSelector(valueStr, inferSelectorKind(valueStr));
632
712
  }
633
713
  if (valueStr.startsWith('"') || valueStr.startsWith("'")) {
634
- return createLiteral(valueStr.slice(1, -1), "string");
714
+ return createLiteral(unescapeString(valueStr.slice(1, -1)), "string");
635
715
  }
636
716
  if (valueStr === "true") return createLiteral(true, "boolean");
637
717
  if (valueStr === "false") return createLiteral(false, "boolean");
@@ -730,16 +810,23 @@ function renderBracketCommand(node) {
730
810
  }
731
811
  return `[${parts.join(" ")}]`;
732
812
  }
813
+ function needsSelectorWrap(value) {
814
+ if (value.includes(" ")) return true;
815
+ const first = value[0];
816
+ return !(first === "#" || first === "." || first === "@" || first === "*");
817
+ }
733
818
  function valueToString(value) {
734
819
  switch (value.type) {
735
820
  case "literal":
736
821
  if (typeof value.value === "string") {
737
- if (value.dataType === "string" || /\s/.test(value.value)) return `"${value.value}"`;
822
+ if (value.dataType === "string" || /\s/.test(value.value))
823
+ return JSON.stringify(value.value);
738
824
  return value.value;
739
825
  }
740
826
  return String(value.value);
741
827
  case "selector":
742
- return value.value;
828
+ if (!value.value) return value.value;
829
+ return needsSelectorWrap(value.value) ? `<${value.value}/>` : value.value;
743
830
  case "reference":
744
831
  return value.value;
745
832
  case "property-path":
@@ -837,6 +924,7 @@ function toProtocolJSON(node) {
837
924
  if (node.diagnostics && node.diagnostics.length > 0) {
838
925
  result.diagnostics = node.diagnostics.map((d) => {
839
926
  const json = { level: d.severity, message: d.message };
927
+ if (d.role) json.role = d.role;
840
928
  if (d.code) json.code = d.code;
841
929
  if (d.source) json.source = d.source;
842
930
  if (d.suggestions && d.suggestions.length > 0) json.suggestions = [...d.suggestions];
@@ -921,6 +1009,7 @@ function fromProtocolJSON(json) {
921
1009
  const diagnostics = json.diagnostics?.map((d) => ({
922
1010
  severity: d.level,
923
1011
  message: d.message,
1012
+ ...d.role ? { role: d.role } : {},
924
1013
  ...d.code ? { code: d.code } : {},
925
1014
  ...d.source ? { source: d.source } : {},
926
1015
  ...d.suggestions && d.suggestions.length > 0 ? { suggestions: d.suggestions } : {}
@@ -2584,10 +2673,12 @@ function buildTokens(schema, profile, keyword, alternatives) {
2584
2673
  addRoleWithMarker(tokens, role, profile);
2585
2674
  }
2586
2675
  } else if (profile.wordOrder === "SOV") {
2676
+ const preVerb = [];
2677
+ const postVerb = [];
2587
2678
  for (const role of sortedRoles) {
2588
- addRoleWithMarker(tokens, role, profile);
2679
+ addRoleWithMarker(role.sovSlot === "postVerb" ? postVerb : preVerb, role, profile);
2589
2680
  }
2590
- tokens.push(keywordToken);
2681
+ tokens.push(...preVerb, keywordToken, ...postVerb);
2591
2682
  } else if (profile.wordOrder === "VSO") {
2592
2683
  tokens.push(keywordToken);
2593
2684
  for (const role of sortedRoles) {
@@ -2670,34 +2761,145 @@ function buildExtractionRules(schema, profile) {
2670
2761
  return rules;
2671
2762
  }
2672
2763
  function buildFormatString(schema, profile, keyword) {
2673
- const parts = [];
2674
- if (profile.wordOrder === "SVO" || profile.wordOrder === "VSO") {
2675
- parts.push(keyword);
2676
- }
2677
- for (const role of schema.roles) {
2764
+ const isSOV = profile.wordOrder === "SOV";
2765
+ const preVerb = [];
2766
+ const postVerb = [];
2767
+ const sortedRoles = sortRolesByWordOrder(schema.roles, profile.wordOrder);
2768
+ for (const role of sortedRoles) {
2678
2769
  const marker = getMarkerForRole(role, profile);
2679
2770
  const roleName = `{${role.role}}`;
2771
+ const bucket = isSOV && role.sovSlot === "postVerb" ? postVerb : preVerb;
2680
2772
  if (marker) {
2681
2773
  const markerInfo = profile.roleMarkers?.[role.role];
2682
2774
  if (markerInfo?.position === "after") {
2683
- parts.push(`${roleName} ${marker}`);
2775
+ bucket.push(`${roleName} ${marker}`);
2684
2776
  } else {
2685
- parts.push(`${marker} ${roleName}`);
2777
+ bucket.push(`${marker} ${roleName}`);
2686
2778
  }
2687
2779
  } else {
2688
- parts.push(roleName);
2780
+ bucket.push(roleName);
2689
2781
  }
2690
2782
  }
2691
- if (profile.wordOrder === "SOV") {
2692
- parts.push(keyword);
2693
- }
2694
- return parts.join(" ");
2783
+ return (isSOV ? [...preVerb, keyword, ...postVerb] : [keyword, ...preVerb, ...postVerb]).join(
2784
+ " "
2785
+ );
2695
2786
  }
2696
2787
  function generatePatternVariants(schema, profile, config = defaultConfig) {
2697
2788
  const patterns = [];
2698
2789
  patterns.push(generatePattern(schema, profile, config));
2699
2790
  return patterns;
2700
2791
  }
2792
+ function lookupKeyword(keywords, action, language) {
2793
+ return keywords[action]?.[language] ?? action;
2794
+ }
2795
+ function lookupMarker(markers, marker, language) {
2796
+ return markers[marker]?.[language] ?? marker;
2797
+ }
2798
+ function buildPhrase(...parts) {
2799
+ return parts.filter(Boolean).join(" ");
2800
+ }
2801
+ function buildTablesFromProfiles(schemas, profiles) {
2802
+ const keywords = {};
2803
+ const markers = {};
2804
+ for (const profile of profiles) {
2805
+ for (const [action, kw] of Object.entries(profile.keywords)) {
2806
+ if (!keywords[action]) keywords[action] = {};
2807
+ keywords[action][profile.code] = kw.primary;
2808
+ }
2809
+ }
2810
+ for (const schema of schemas) {
2811
+ for (const role of schema.roles) {
2812
+ if (role.markerOverride) {
2813
+ const markerKey = role.role;
2814
+ if (!markers[markerKey]) markers[markerKey] = {};
2815
+ for (const [lang, markerText] of Object.entries(role.markerOverride)) {
2816
+ markers[markerKey][lang] = markerText;
2817
+ }
2818
+ }
2819
+ }
2820
+ }
2821
+ for (const profile of profiles) {
2822
+ if (profile.roleMarkers) {
2823
+ for (const [markerKey, markerDef] of Object.entries(profile.roleMarkers)) {
2824
+ if (!markers[markerKey]) markers[markerKey] = {};
2825
+ markers[markerKey][profile.code] = markerDef.primary;
2826
+ }
2827
+ }
2828
+ }
2829
+ return { keywords, markers };
2830
+ }
2831
+ function detectWordOrders(profiles) {
2832
+ const sovLanguages = /* @__PURE__ */ new Set();
2833
+ const vsoLanguages = /* @__PURE__ */ new Set();
2834
+ for (const profile of profiles) {
2835
+ if (profile.wordOrder === "SOV") sovLanguages.add(profile.code);
2836
+ if (profile.wordOrder === "VSO") vsoLanguages.add(profile.code);
2837
+ }
2838
+ return { sovLanguages, vsoLanguages };
2839
+ }
2840
+ function createSchemaRenderer(schemas, profiles) {
2841
+ const tables = buildRenderTables(schemas, profiles);
2842
+ return {
2843
+ render(node, language) {
2844
+ return renderFromSchema(node, language, tables) ?? node.action;
2845
+ }
2846
+ };
2847
+ }
2848
+ function createDomainRenderer(config) {
2849
+ const { schemas, profiles, overrides } = config;
2850
+ let tables;
2851
+ return (node, language) => {
2852
+ const override = overrides?.[node.action];
2853
+ if (override) return override(node, language);
2854
+ tables ?? (tables = buildRenderTables(schemas, profiles));
2855
+ return renderFromSchema(node, language, tables);
2856
+ };
2857
+ }
2858
+ function buildRenderTables(schemas, profiles) {
2859
+ const { keywords, markers } = buildTablesFromProfiles(schemas, profiles);
2860
+ const { sovLanguages } = detectWordOrders(profiles);
2861
+ const markerPositions = {};
2862
+ for (const profile of profiles) {
2863
+ for (const [role, markerDef] of Object.entries(profile.roleMarkers ?? {})) {
2864
+ if (!markerDef.position) continue;
2865
+ markerPositions[role] ?? (markerPositions[role] = {});
2866
+ markerPositions[role][profile.code] = markerDef.position;
2867
+ }
2868
+ }
2869
+ const schemaMap = /* @__PURE__ */ new Map();
2870
+ for (const s of schemas) schemaMap.set(s.action, s);
2871
+ return { keywords, markers, markerPositions, sovLanguages, schemaMap };
2872
+ }
2873
+ function renderFromSchema(node, language, tables) {
2874
+ const schema = tables.schemaMap.get(node.action);
2875
+ if (!schema) return null;
2876
+ const keyword = lookupKeyword(tables.keywords, node.action, language);
2877
+ const isSOV = tables.sovLanguages.has(language);
2878
+ const ordered = sortRolesByWordOrder([...schema.roles], isSOV ? "SOV" : "SVO");
2879
+ const preVerb = [];
2880
+ const postVerb = [];
2881
+ for (const role of ordered) {
2882
+ let value = extractRoleValue(node, role.role);
2883
+ if (!value && role.default !== void 0) {
2884
+ value = String(extractValue(role.default));
2885
+ }
2886
+ if (!value) continue;
2887
+ if (role.quoteMultiword && /\s/.test(value) && !/^(["']).*\1$/.test(value)) {
2888
+ value = `"${value}"`;
2889
+ }
2890
+ const markerText = role.renderOverride?.[language] ?? role.markerOverride?.[language] ?? role.renderOverride?.["*"] ?? tables.markers[role.role]?.[language];
2891
+ const markerPosition = role.markerPositionOverride?.[language] ?? role.markerPosition ?? tables.markerPositions[role.role]?.[language] ?? (isSOV ? "after" : "before");
2892
+ const bucket = isSOV && role.sovSlot === "postVerb" ? postVerb : preVerb;
2893
+ if (markerText && markerPosition === "before") {
2894
+ bucket.push(markerText, value);
2895
+ } else if (markerText) {
2896
+ bucket.push(value, markerText);
2897
+ } else {
2898
+ bucket.push(value);
2899
+ }
2900
+ }
2901
+ return isSOV ? buildPhrase(...preVerb, keyword, ...postVerb) : buildPhrase(keyword, ...preVerb, ...postVerb);
2902
+ }
2701
2903
  function reorderRoles(roles, targetOrder) {
2702
2904
  const result = [];
2703
2905
  const usedRoles = /* @__PURE__ */ new Set();
@@ -3125,6 +3327,31 @@ var LatinExtendedIdentifierExtractor = class {
3125
3327
  return { value: input.slice(position, end), length: end - position };
3126
3328
  }
3127
3329
  };
3330
+ var SELECTOR_BODY_CHAR = /[\p{L}\p{N}_-]/u;
3331
+ var PARTICLE_SCRIPT_CHAR = /[\p{sc=Han}\p{sc=Hiragana}\p{sc=Katakana}\p{sc=Hangul}]/u;
3332
+ function isSelectorBodyChar(char) {
3333
+ return SELECTOR_BODY_CHAR.test(char) && !PARTICLE_SCRIPT_CHAR.test(char);
3334
+ }
3335
+ var CssSelectorExtractor = class {
3336
+ constructor() {
3337
+ this.name = "css-selector";
3338
+ }
3339
+ canExtract(input, position) {
3340
+ const char = input[position];
3341
+ if (char !== "#" && char !== ".") return false;
3342
+ const next = input[position + 1];
3343
+ if (next === void 0) return false;
3344
+ return next === "_" || next === "-" || /\p{L}/u.test(next) && !PARTICLE_SCRIPT_CHAR.test(next);
3345
+ }
3346
+ extract(input, position) {
3347
+ let end = position + 1;
3348
+ while (end < input.length && isSelectorBodyChar(input[end])) {
3349
+ end++;
3350
+ }
3351
+ if (end === position + 1) return null;
3352
+ return { value: input.slice(position, end), length: end - position };
3353
+ }
3354
+ };
3128
3355
  var WhitespaceExtractor = class {
3129
3356
  constructor() {
3130
3357
  this.name = "whitespace";
@@ -3844,10 +4071,17 @@ function semanticValueToAST(value) {
3844
4071
  }
3845
4072
  }
3846
4073
  var DSLRegistry = class {
3847
- constructor(config) {
4074
+ /**
4075
+ * @param partiallyTranslatedActions Actions permitted to lack a keyword in
4076
+ * some configured languages — extension commands, which may be added for a
4077
+ * subset of the DSL's languages. A built-in command missing a keyword stays
4078
+ * an error, since that is a domain-authoring mistake.
4079
+ */
4080
+ constructor(config, partiallyTranslatedActions = /* @__PURE__ */ new Set()) {
3848
4081
  this.patterns = /* @__PURE__ */ new Map();
3849
4082
  this.tokenizers = /* @__PURE__ */ new Map();
3850
4083
  this.schemas = config.schemas;
4084
+ this.partiallyTranslatedActions = partiallyTranslatedActions;
3851
4085
  for (const lang of config.languages) {
3852
4086
  this.registerLanguage(lang);
3853
4087
  }
@@ -3856,6 +4090,9 @@ var DSLRegistry = class {
3856
4090
  this.tokenizers.set(lang.code, lang.tokenizer);
3857
4091
  const patterns = [];
3858
4092
  for (const schema of this.schemas) {
4093
+ if (this.partiallyTranslatedActions.has(schema.action) && !lang.patternProfile.keywords[schema.action]) {
4094
+ continue;
4095
+ }
3859
4096
  const pattern = generatePattern(schema, lang.patternProfile);
3860
4097
  patterns.push(pattern);
3861
4098
  }
@@ -3872,13 +4109,25 @@ var DSLRegistry = class {
3872
4109
  }
3873
4110
  };
3874
4111
  var MultilingualDSLImpl = class {
3875
- constructor(config, registry, transformer) {
4112
+ constructor(config, registry, transformer, profileProvider) {
3876
4113
  this.registry = registry;
3877
4114
  this.matcher = new PatternMatcher();
3878
4115
  this.transformer = transformer;
4116
+ this.profileProvider = profileProvider;
3879
4117
  if (config.codeGenerator) {
3880
4118
  this.codeGenerator = config.codeGenerator;
3881
4119
  }
4120
+ if (config.renderer) {
4121
+ this.domainRenderer = config.renderer;
4122
+ }
4123
+ this.extensionRenderers = /* @__PURE__ */ new Map();
4124
+ for (const extension of config.extensions ?? []) {
4125
+ if (extension.render) this.extensionRenderers.set(extension.schema.action, extension.render);
4126
+ }
4127
+ this.schemaFallbackRenderer = createDomainRenderer({
4128
+ schemas: config.schemas,
4129
+ profiles: config.languages.map((l) => l.patternProfile)
4130
+ });
3882
4131
  const schemaMap = new Map(config.schemas.map((s) => [s.action, s]));
3883
4132
  this.schemaLookup = {
3884
4133
  getSchema: (action) => schemaMap.get(action)
@@ -3940,6 +4189,13 @@ var MultilingualDSLImpl = class {
3940
4189
  }
3941
4190
  translate(input, fromLanguage, toLanguage) {
3942
4191
  if (isExplicitSyntax(input)) return input;
4192
+ for (const language of [fromLanguage, toLanguage]) {
4193
+ if (!this.profileProvider.getProfile(language)) {
4194
+ throw new Error(
4195
+ `translate() requires a grammar profile for language "${language}", but none is configured. Set 'grammarProfile' on the LanguageConfig for "${language}" in createMultilingualDSL() (parse/validate/compile do not need it), or inject a custom 'profileProvider'.`
4196
+ );
4197
+ }
4198
+ }
3943
4199
  return this.transformer.transform(input, fromLanguage, toLanguage);
3944
4200
  }
3945
4201
  compile(input, language) {
@@ -3968,6 +4224,18 @@ var MultilingualDSLImpl = class {
3968
4224
  };
3969
4225
  }
3970
4226
  }
4227
+ render(node, language) {
4228
+ const extensionRenderer = this.extensionRenderers.get(node.action);
4229
+ if (extensionRenderer) {
4230
+ const rendered = extensionRenderer(node, language);
4231
+ if (rendered != null) return rendered;
4232
+ }
4233
+ if (this.domainRenderer) {
4234
+ const rendered = this.domainRenderer(node, language);
4235
+ if (rendered != null) return rendered;
4236
+ }
4237
+ return this.schemaFallbackRenderer(node, language);
4238
+ }
3971
4239
  getSupportedLanguages() {
3972
4240
  return this.registry.getSupportedLanguages();
3973
4241
  }
@@ -3992,15 +4260,80 @@ function createDefaultProfileProvider(config) {
3992
4260
  }
3993
4261
  return new InMemoryProfileProvider(profiles);
3994
4262
  }
4263
+ function applyExtensions(config) {
4264
+ const extensions = config.extensions;
4265
+ if (!extensions || extensions.length === 0) return config;
4266
+ const configuredLanguages = new Set(config.languages.map((l) => l.code));
4267
+ const actions = new Set(config.schemas.map((s) => s.action));
4268
+ for (const extension of extensions) {
4269
+ const { action } = extension.schema;
4270
+ if (actions.has(action)) {
4271
+ throw new Error(
4272
+ `Extension command "${action}" collides with a command this DSL already defines. Pick a different action name.`
4273
+ );
4274
+ }
4275
+ actions.add(action);
4276
+ for (const code of Object.keys(extension.vocabulary)) {
4277
+ if (!configuredLanguages.has(code)) {
4278
+ throw new Error(
4279
+ `Extension command "${action}" supplies vocabulary for language "${code}", which is not configured on this DSL. Configured languages: ${[...configuredLanguages].join(", ")}.`
4280
+ );
4281
+ }
4282
+ }
4283
+ }
4284
+ const schemas = [...config.schemas, ...extensions.map((e) => e.schema)];
4285
+ const languages = config.languages.map((lang) => {
4286
+ const keywords = { ...lang.patternProfile.keywords };
4287
+ let roleMarkers = lang.patternProfile.roleMarkers;
4288
+ for (const extension of extensions) {
4289
+ const vocabulary = extension.vocabulary[lang.code];
4290
+ if (!vocabulary) continue;
4291
+ keywords[extension.schema.action] = { ...vocabulary.keyword };
4292
+ if (vocabulary.roleMarkers) {
4293
+ roleMarkers = { ...roleMarkers, ...vocabulary.roleMarkers };
4294
+ }
4295
+ }
4296
+ return {
4297
+ ...lang,
4298
+ patternProfile: {
4299
+ ...lang.patternProfile,
4300
+ keywords,
4301
+ ...roleMarkers !== void 0 && { roleMarkers }
4302
+ }
4303
+ };
4304
+ });
4305
+ const extensionGenerators = new Map(
4306
+ extensions.filter((e) => e.generate).map((e) => [e.schema.action, e.generate])
4307
+ );
4308
+ const baseGenerator = config.codeGenerator;
4309
+ const codeGenerator = extensionGenerators.size > 0 ? {
4310
+ generate(node) {
4311
+ const generate = extensionGenerators.get(node.action);
4312
+ if (generate) return generate(node);
4313
+ if (!baseGenerator) {
4314
+ throw new Error(`No code generator for action "${node.action}"`);
4315
+ }
4316
+ return baseGenerator.generate(node);
4317
+ }
4318
+ } : baseGenerator;
4319
+ return {
4320
+ ...config,
4321
+ schemas,
4322
+ languages,
4323
+ ...codeGenerator !== void 0 && { codeGenerator }
4324
+ };
4325
+ }
3995
4326
  function createMultilingualDSL(config) {
3996
- const dictionary = config.dictionary ?? createDefaultDictionary(config);
3997
- const profileProvider = config.profileProvider ?? createDefaultProfileProvider(config);
4327
+ const effectiveConfig = applyExtensions(config);
4328
+ const extensionActions = new Set((config.extensions ?? []).map((e) => e.schema.action));
4329
+ const dictionary = effectiveConfig.dictionary ?? createDefaultDictionary(effectiveConfig);
4330
+ const profileProvider = effectiveConfig.profileProvider ?? createDefaultProfileProvider(effectiveConfig);
3998
4331
  const transformer = new GrammarTransformer({
3999
4332
  dictionary,
4000
4333
  profileProvider
4001
4334
  });
4002
- const registry = new DSLRegistry(config);
4003
- return new MultilingualDSLImpl(config, registry, transformer);
4335
+ const registry = new DSLRegistry(effectiveConfig, extensionActions);
4336
+ return new MultilingualDSLImpl(effectiveConfig, registry, transformer, profileProvider);
4004
4337
  }
4005
4338
  function generatePrompt(config) {
4006
4339
  const outputFormat = config.outputFormat ?? "both";
@@ -4085,7 +4418,9 @@ function buildProtocolSection() {
4085
4418
  3. No spaces around the colon in role:value pairs
4086
4419
  4. Strings with spaces must be quoted: \`patient:"hello world"\`
4087
4420
  5. Selectors start with \`#\`, \`.\`, \`[\`, \`@\`, or \`*\`
4088
- 6. Output must be valid bracket syntax: \`[action role:value ...]\``;
4421
+ 6. A selector containing a space, a combinator (\`>\` \`+\` \`~\`), or a comma must use a selector literal: \`patient:<ul > li/>\`, \`patient:<.a, .b/>\`
4422
+ 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]/>\`
4423
+ 8. Output must be valid bracket syntax: \`[action role:value ...]\``;
4089
4424
  return {
4090
4425
  id: "protocol",
4091
4426
  title: "LSE Protocol",
@@ -4098,6 +4433,7 @@ function buildValueTypeSection() {
4098
4433
 
4099
4434
  | Type | Syntax | Example |
4100
4435
  |------|--------|---------|
4436
+ | Selector literal | Delimited by \`<\` and \`/>\` | \`<ul > li/>\`, \`<.a, .b/>\`, \`<[data-id]/>\` |
4101
4437
  | Selector | Starts with \`#\` \`.\` \`[\` \`@\` \`*\` | \`#button\`, \`.active\`, \`[data-id]\` |
4102
4438
  | String | Quoted with \`"\` or \`'\` | \`"hello world"\`, \`'json'\` |
4103
4439
  | Boolean | Exact: \`true\` / \`false\` | \`visible:true\` |
@@ -4960,6 +5296,11 @@ var DomainRegistry = class {
4960
5296
  rendered = typeof renderer === "function" ? renderer(node, to) : renderer.render(node, to);
4961
5297
  } catch {
4962
5298
  }
5299
+ } else if (dsl.render) {
5300
+ try {
5301
+ rendered = dsl.render(node, to);
5302
+ } catch {
5303
+ }
4963
5304
  }
4964
5305
  const roles = {};
4965
5306
  for (const [key, value] of node.roles) {
@@ -5505,94 +5846,6 @@ async function registryToAOTBackends(registry) {
5505
5846
  }
5506
5847
  return backends;
5507
5848
  }
5508
- function lookupKeyword(keywords, action, language) {
5509
- return keywords[action]?.[language] ?? action;
5510
- }
5511
- function lookupMarker(markers, marker, language) {
5512
- return markers[marker]?.[language] ?? marker;
5513
- }
5514
- function buildPhrase(...parts) {
5515
- return parts.filter(Boolean).join(" ");
5516
- }
5517
- function buildTablesFromProfiles(schemas, profiles) {
5518
- const keywords = {};
5519
- const markers = {};
5520
- for (const profile of profiles) {
5521
- for (const [action, kw] of Object.entries(profile.keywords)) {
5522
- if (!keywords[action]) keywords[action] = {};
5523
- keywords[action][profile.code] = kw.primary;
5524
- }
5525
- }
5526
- for (const schema of schemas) {
5527
- for (const role of schema.roles) {
5528
- if (role.markerOverride) {
5529
- const markerKey = role.role;
5530
- if (!markers[markerKey]) markers[markerKey] = {};
5531
- for (const [lang, markerText] of Object.entries(role.markerOverride)) {
5532
- markers[markerKey][lang] = markerText;
5533
- }
5534
- }
5535
- }
5536
- }
5537
- for (const profile of profiles) {
5538
- if (profile.roleMarkers) {
5539
- for (const [markerKey, markerDef] of Object.entries(profile.roleMarkers)) {
5540
- if (!markers[markerKey]) markers[markerKey] = {};
5541
- markers[markerKey][profile.code] = markerDef.primary;
5542
- }
5543
- }
5544
- }
5545
- return { keywords, markers };
5546
- }
5547
- function detectWordOrders(profiles) {
5548
- const sovLanguages = /* @__PURE__ */ new Set();
5549
- const vsoLanguages = /* @__PURE__ */ new Set();
5550
- for (const profile of profiles) {
5551
- if (profile.wordOrder === "SOV") sovLanguages.add(profile.code);
5552
- if (profile.wordOrder === "VSO") vsoLanguages.add(profile.code);
5553
- }
5554
- return { sovLanguages, vsoLanguages };
5555
- }
5556
- function createSchemaRenderer(schemas, profiles) {
5557
- const { keywords, markers } = buildTablesFromProfiles(schemas, profiles);
5558
- const { sovLanguages } = detectWordOrders(profiles);
5559
- const schemaMap = /* @__PURE__ */ new Map();
5560
- for (const s of schemas) schemaMap.set(s.action, s);
5561
- return {
5562
- render(node, language) {
5563
- const schema = schemaMap.get(node.action);
5564
- if (!schema) return node.action;
5565
- const keyword = lookupKeyword(keywords, node.action, language);
5566
- const isSOV = sovLanguages.has(language);
5567
- const roleParts = [];
5568
- for (const role of schema.roles) {
5569
- const value = extractRoleValue(node, role.role);
5570
- if (!value && !role.required) continue;
5571
- const markerText = role.markerOverride?.[language] ?? markers[role.role]?.[language] ?? void 0;
5572
- roleParts.push({
5573
- ...markerText != null && { marker: markerText },
5574
- value: value || "",
5575
- role
5576
- });
5577
- }
5578
- const parts = [];
5579
- if (isSOV) {
5580
- for (const rp of roleParts) {
5581
- if (rp.value) parts.push(rp.value);
5582
- if (rp.marker) parts.push(rp.marker);
5583
- }
5584
- parts.push(keyword);
5585
- } else {
5586
- parts.push(keyword);
5587
- for (const rp of roleParts) {
5588
- if (rp.marker) parts.push(rp.marker);
5589
- if (rp.value) parts.push(rp.value);
5590
- }
5591
- }
5592
- return buildPhrase(...parts);
5593
- }
5594
- };
5595
- }
5596
5849
  var TokenStreamImpl = class {
5597
5850
  constructor(tokens, language) {
5598
5851
  this.pos = 0;
@@ -5698,6 +5951,9 @@ function isQuote(char) {
5698
5951
  function isDigit(char) {
5699
5952
  return /\d/.test(char);
5700
5953
  }
5954
+ function stripOptionalDiacritics(word) {
5955
+ return word.replace(/[ً-ْٰ]/g, "");
5956
+ }
5701
5957
  function isAsciiLetter(char) {
5702
5958
  return /[a-zA-Z]/.test(char);
5703
5959
  }
@@ -6238,7 +6494,39 @@ var _BaseTokenizer = class _BaseTokenizer2 {
6238
6494
  pos++;
6239
6495
  }
6240
6496
  }
6241
- return new TokenStreamImpl(tokens, this.language);
6497
+ return new TokenStreamImpl(this.mergeColonQualifiedNames(tokens), this.language);
6498
+ }
6499
+ /**
6500
+ * Fuse `name` + `:qualifier` into ONE identifier (`draggable:start`).
6501
+ *
6502
+ * `:name` is hyperscript's local-variable sigil, but a colon IMMEDIATELY
6503
+ * preceded by an identifier is a qualifier (custom event namespace), not a
6504
+ * sigil. The English tokenizer already merges these inside
6505
+ * EnglishKeywordExtractor; this post-pass gives the other 23 languages the
6506
+ * same stream. Strict position adjacency is the discriminator: whitespace
6507
+ * between the tokens (`trigger :start`) breaks `end === start`, so a spaced
6508
+ * local-variable reference survives untouched.
6509
+ *
6510
+ * Self-gating for non-hyperscript tokenizers (domain DSLs): their extractor
6511
+ * sets tokenize `:` as bare punctuation (length 1), which never matches
6512
+ * COLON_QUALIFIER, so this pass is a no-op for them.
6513
+ */
6514
+ mergeColonQualifiedNames(tokens) {
6515
+ const out = [];
6516
+ for (const tok of tokens) {
6517
+ const prev = out[out.length - 1];
6518
+ if (prev && _BaseTokenizer2.ASCII_WORD.test(prev.value) && _BaseTokenizer2.COLON_QUALIFIER.test(tok.value) && prev.position.end === tok.position.start) {
6519
+ const merged = prev.value + tok.value;
6520
+ out[out.length - 1] = createToken(
6521
+ merged,
6522
+ this.classifyToken(merged),
6523
+ createPosition(prev.position.start, tok.position.end)
6524
+ );
6525
+ continue;
6526
+ }
6527
+ out.push(tok);
6528
+ }
6529
+ return out;
6242
6530
  }
6243
6531
  /**
6244
6532
  * Classify an unknown character when no extractor matches.
@@ -6371,7 +6659,7 @@ var _BaseTokenizer = class _BaseTokenizer2 {
6371
6659
  * @returns Word without diacritics
6372
6660
  */
6373
6661
  removeDiacritics(word) {
6374
- return word.replace(/[\u064B-\u0652\u0670]/g, "");
6662
+ return stripOptionalDiacritics(word);
6375
6663
  }
6376
6664
  /**
6377
6665
  * Try to match a keyword from profile at the current position.
@@ -6462,24 +6750,40 @@ var _BaseTokenizer = class _BaseTokenizer2 {
6462
6750
  });
6463
6751
  }
6464
6752
  /**
6465
- * Look up a keyword by native word (case-insensitive).
6753
+ * Look up a keyword by native word (case-insensitive, diacritic-insensitive).
6466
6754
  * O(1) lookup using the keyword map.
6467
6755
  *
6756
+ * The map is INDEXED both with and without diacritics (see
6757
+ * `initializeKeywordsFromProfile`), so a stripped QUERY is the other half of
6758
+ * that: it lets a surface form carrying harakat the profile does not happen to
6759
+ * spell still find its entry. Only consulted after the exact lookup misses, so
6760
+ * every previously-matching word resolves byte-identically.
6761
+ *
6762
+ * Half-implementing this — indexing stripped but querying exact — is what made
6763
+ * diacritized `بَدِّل` (toggle) tokenize as `kind=particle normalized=with`:
6764
+ * `isKeyword` returned false, so the guard in `ArabicProcliticExtractor` that
6765
+ * exists to prevent exactly that handed the word on, and the single-char `ب`
6766
+ * bi- proclitic claimed it. A wrong CONCEPT, not a failed parse.
6767
+ *
6468
6768
  * @param native - Native word to look up
6469
6769
  * @returns KeywordEntry if found, undefined otherwise
6470
6770
  */
6471
6771
  lookupKeyword(native) {
6472
- return this.profileKeywordMap.get(native.toLowerCase());
6772
+ const exact = this.profileKeywordMap.get(native.toLowerCase());
6773
+ if (exact) return exact;
6774
+ const stripped = this.removeDiacritics(native);
6775
+ if (stripped === native) return void 0;
6776
+ return this.profileKeywordMap.get(stripped.toLowerCase());
6473
6777
  }
6474
6778
  /**
6475
- * Check if a word is a known keyword (case-insensitive).
6476
- * O(1) lookup using the keyword map.
6779
+ * Check if a word is a known keyword (case-insensitive, diacritic-insensitive).
6780
+ * O(1) lookup using the keyword map. See {@link lookupKeyword}.
6477
6781
  *
6478
6782
  * @param native - Native word to check
6479
6783
  * @returns true if the word is a keyword
6480
6784
  */
6481
6785
  isKeyword(native) {
6482
- return this.profileKeywordMap.has(native.toLowerCase());
6786
+ return this.lookupKeyword(native) !== void 0;
6483
6787
  }
6484
6788
  /**
6485
6789
  * Set the morphological normalizer for this tokenizer.
@@ -6744,6 +7048,8 @@ var _BaseTokenizer = class _BaseTokenizer2 {
6744
7048
  return null;
6745
7049
  }
6746
7050
  };
7051
+ _BaseTokenizer.ASCII_WORD = /^[A-Za-z_][A-Za-z0-9_]*$/;
7052
+ _BaseTokenizer.COLON_QUALIFIER = /^:[A-Za-z_][A-Za-z0-9_]*$/;
6747
7053
  _BaseTokenizer.STANDARD_TIME_UNITS = [
6748
7054
  { pattern: "ms", suffix: "ms", length: 2 },
6749
7055
  { pattern: "s", suffix: "s", length: 1, checkBoundary: true },
@@ -7428,6 +7734,7 @@ export {
7428
7734
  BaseMorphologicalNormalizer,
7429
7735
  BaseTokenizer,
7430
7736
  CrossDomainDispatcher,
7737
+ CssSelectorExtractor,
7431
7738
  DEFAULT_OPERATORS,
7432
7739
  DEFAULT_PUNCTUATION,
7433
7740
  DEFAULT_REFERENCES,
@@ -7464,6 +7771,7 @@ export {
7464
7771
  createCompoundNode,
7465
7772
  createConditionalNode,
7466
7773
  createDiagnosticCollector,
7774
+ createDomainRenderer,
7467
7775
  createEventHandlerNode,
7468
7776
  createExpression,
7469
7777
  createFlag,
@@ -7555,6 +7863,8 @@ export {
7555
7863
  semanticNodeToJSON,
7556
7864
  semanticNodeToRuntimeAST,
7557
7865
  semanticValueToAST,
7866
+ sortRolesByWordOrder,
7867
+ stripOptionalDiacritics,
7558
7868
  synthesizeFromSchemas,
7559
7869
  toEnvelopeJSON,
7560
7870
  toJSONL,
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-LIFKYPGW.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.2",
3
+ "version": "2.9.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.2",
35
- "@lokascript/semantic": "^2.7.2"
34
+ "@hyperfixi/core": "^2.9.0",
35
+ "@lokascript/semantic": "^2.9.0"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@lokascript/semantic": {
@@ -43,17 +43,18 @@
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
- "@hyperfixi/core": "^2.7.2",
47
- "@lokascript/semantic": "^2.7.2",
46
+ "@hyperfixi/core": "^2.9.0",
47
+ "@lokascript/semantic": "^2.9.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
  },
55
55
  "files": [
56
- "dist"
56
+ "dist",
57
+ "CHANGELOG.md"
57
58
  ],
58
59
  "publishConfig": {
59
60
  "access": "public"