@lokascript/language-server 2.8.0 → 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",
@@ -2645,10 +2673,12 @@ function buildTokens(schema, profile, keyword, alternatives) {
2645
2673
  addRoleWithMarker(tokens, role, profile);
2646
2674
  }
2647
2675
  } else if (profile.wordOrder === "SOV") {
2676
+ const preVerb = [];
2677
+ const postVerb = [];
2648
2678
  for (const role of sortedRoles) {
2649
- addRoleWithMarker(tokens, role, profile);
2679
+ addRoleWithMarker(role.sovSlot === "postVerb" ? postVerb : preVerb, role, profile);
2650
2680
  }
2651
- tokens.push(keywordToken);
2681
+ tokens.push(...preVerb, keywordToken, ...postVerb);
2652
2682
  } else if (profile.wordOrder === "VSO") {
2653
2683
  tokens.push(keywordToken);
2654
2684
  for (const role of sortedRoles) {
@@ -2731,35 +2761,145 @@ function buildExtractionRules(schema, profile) {
2731
2761
  return rules;
2732
2762
  }
2733
2763
  function buildFormatString(schema, profile, keyword) {
2734
- const parts = [];
2735
- if (profile.wordOrder === "SVO" || profile.wordOrder === "VSO") {
2736
- parts.push(keyword);
2737
- }
2764
+ const isSOV = profile.wordOrder === "SOV";
2765
+ const preVerb = [];
2766
+ const postVerb = [];
2738
2767
  const sortedRoles = sortRolesByWordOrder(schema.roles, profile.wordOrder);
2739
2768
  for (const role of sortedRoles) {
2740
2769
  const marker = getMarkerForRole(role, profile);
2741
2770
  const roleName = `{${role.role}}`;
2771
+ const bucket = isSOV && role.sovSlot === "postVerb" ? postVerb : preVerb;
2742
2772
  if (marker) {
2743
2773
  const markerInfo = profile.roleMarkers?.[role.role];
2744
2774
  if (markerInfo?.position === "after") {
2745
- parts.push(`${roleName} ${marker}`);
2775
+ bucket.push(`${roleName} ${marker}`);
2746
2776
  } else {
2747
- parts.push(`${marker} ${roleName}`);
2777
+ bucket.push(`${marker} ${roleName}`);
2748
2778
  }
2749
2779
  } else {
2750
- parts.push(roleName);
2780
+ bucket.push(roleName);
2751
2781
  }
2752
2782
  }
2753
- if (profile.wordOrder === "SOV") {
2754
- parts.push(keyword);
2755
- }
2756
- return parts.join(" ");
2783
+ return (isSOV ? [...preVerb, keyword, ...postVerb] : [keyword, ...preVerb, ...postVerb]).join(
2784
+ " "
2785
+ );
2757
2786
  }
2758
2787
  function generatePatternVariants(schema, profile, config = defaultConfig) {
2759
2788
  const patterns = [];
2760
2789
  patterns.push(generatePattern(schema, profile, config));
2761
2790
  return patterns;
2762
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
+ }
2763
2903
  function reorderRoles(roles, targetOrder) {
2764
2904
  const result = [];
2765
2905
  const usedRoles = /* @__PURE__ */ new Set();
@@ -3187,6 +3327,31 @@ var LatinExtendedIdentifierExtractor = class {
3187
3327
  return { value: input.slice(position, end), length: end - position };
3188
3328
  }
3189
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
+ };
3190
3355
  var WhitespaceExtractor = class {
3191
3356
  constructor() {
3192
3357
  this.name = "whitespace";
@@ -3906,10 +4071,17 @@ function semanticValueToAST(value) {
3906
4071
  }
3907
4072
  }
3908
4073
  var DSLRegistry = class {
3909
- 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()) {
3910
4081
  this.patterns = /* @__PURE__ */ new Map();
3911
4082
  this.tokenizers = /* @__PURE__ */ new Map();
3912
4083
  this.schemas = config.schemas;
4084
+ this.partiallyTranslatedActions = partiallyTranslatedActions;
3913
4085
  for (const lang of config.languages) {
3914
4086
  this.registerLanguage(lang);
3915
4087
  }
@@ -3918,6 +4090,9 @@ var DSLRegistry = class {
3918
4090
  this.tokenizers.set(lang.code, lang.tokenizer);
3919
4091
  const patterns = [];
3920
4092
  for (const schema of this.schemas) {
4093
+ if (this.partiallyTranslatedActions.has(schema.action) && !lang.patternProfile.keywords[schema.action]) {
4094
+ continue;
4095
+ }
3921
4096
  const pattern = generatePattern(schema, lang.patternProfile);
3922
4097
  patterns.push(pattern);
3923
4098
  }
@@ -3934,13 +4109,25 @@ var DSLRegistry = class {
3934
4109
  }
3935
4110
  };
3936
4111
  var MultilingualDSLImpl = class {
3937
- constructor(config, registry, transformer) {
4112
+ constructor(config, registry, transformer, profileProvider) {
3938
4113
  this.registry = registry;
3939
4114
  this.matcher = new PatternMatcher();
3940
4115
  this.transformer = transformer;
4116
+ this.profileProvider = profileProvider;
3941
4117
  if (config.codeGenerator) {
3942
4118
  this.codeGenerator = config.codeGenerator;
3943
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
+ });
3944
4131
  const schemaMap = new Map(config.schemas.map((s) => [s.action, s]));
3945
4132
  this.schemaLookup = {
3946
4133
  getSchema: (action) => schemaMap.get(action)
@@ -4002,6 +4189,13 @@ var MultilingualDSLImpl = class {
4002
4189
  }
4003
4190
  translate(input, fromLanguage, toLanguage) {
4004
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
+ }
4005
4199
  return this.transformer.transform(input, fromLanguage, toLanguage);
4006
4200
  }
4007
4201
  compile(input, language) {
@@ -4030,6 +4224,18 @@ var MultilingualDSLImpl = class {
4030
4224
  };
4031
4225
  }
4032
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
+ }
4033
4239
  getSupportedLanguages() {
4034
4240
  return this.registry.getSupportedLanguages();
4035
4241
  }
@@ -4054,15 +4260,80 @@ function createDefaultProfileProvider(config) {
4054
4260
  }
4055
4261
  return new InMemoryProfileProvider(profiles);
4056
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
+ }
4057
4326
  function createMultilingualDSL(config) {
4058
- const dictionary = config.dictionary ?? createDefaultDictionary(config);
4059
- 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);
4060
4331
  const transformer = new GrammarTransformer({
4061
4332
  dictionary,
4062
4333
  profileProvider
4063
4334
  });
4064
- const registry = new DSLRegistry(config);
4065
- return new MultilingualDSLImpl(config, registry, transformer);
4335
+ const registry = new DSLRegistry(effectiveConfig, extensionActions);
4336
+ return new MultilingualDSLImpl(effectiveConfig, registry, transformer, profileProvider);
4066
4337
  }
4067
4338
  function generatePrompt(config) {
4068
4339
  const outputFormat = config.outputFormat ?? "both";
@@ -5025,6 +5296,11 @@ var DomainRegistry = class {
5025
5296
  rendered = typeof renderer === "function" ? renderer(node, to) : renderer.render(node, to);
5026
5297
  } catch {
5027
5298
  }
5299
+ } else if (dsl.render) {
5300
+ try {
5301
+ rendered = dsl.render(node, to);
5302
+ } catch {
5303
+ }
5028
5304
  }
5029
5305
  const roles = {};
5030
5306
  for (const [key, value] of node.roles) {
@@ -5570,94 +5846,6 @@ async function registryToAOTBackends(registry) {
5570
5846
  }
5571
5847
  return backends;
5572
5848
  }
5573
- function lookupKeyword(keywords, action, language) {
5574
- return keywords[action]?.[language] ?? action;
5575
- }
5576
- function lookupMarker(markers, marker, language) {
5577
- return markers[marker]?.[language] ?? marker;
5578
- }
5579
- function buildPhrase(...parts) {
5580
- return parts.filter(Boolean).join(" ");
5581
- }
5582
- function buildTablesFromProfiles(schemas, profiles) {
5583
- const keywords = {};
5584
- const markers = {};
5585
- for (const profile of profiles) {
5586
- for (const [action, kw] of Object.entries(profile.keywords)) {
5587
- if (!keywords[action]) keywords[action] = {};
5588
- keywords[action][profile.code] = kw.primary;
5589
- }
5590
- }
5591
- for (const schema of schemas) {
5592
- for (const role of schema.roles) {
5593
- if (role.markerOverride) {
5594
- const markerKey = role.role;
5595
- if (!markers[markerKey]) markers[markerKey] = {};
5596
- for (const [lang, markerText] of Object.entries(role.markerOverride)) {
5597
- markers[markerKey][lang] = markerText;
5598
- }
5599
- }
5600
- }
5601
- }
5602
- for (const profile of profiles) {
5603
- if (profile.roleMarkers) {
5604
- for (const [markerKey, markerDef] of Object.entries(profile.roleMarkers)) {
5605
- if (!markers[markerKey]) markers[markerKey] = {};
5606
- markers[markerKey][profile.code] = markerDef.primary;
5607
- }
5608
- }
5609
- }
5610
- return { keywords, markers };
5611
- }
5612
- function detectWordOrders(profiles) {
5613
- const sovLanguages = /* @__PURE__ */ new Set();
5614
- const vsoLanguages = /* @__PURE__ */ new Set();
5615
- for (const profile of profiles) {
5616
- if (profile.wordOrder === "SOV") sovLanguages.add(profile.code);
5617
- if (profile.wordOrder === "VSO") vsoLanguages.add(profile.code);
5618
- }
5619
- return { sovLanguages, vsoLanguages };
5620
- }
5621
- function createSchemaRenderer(schemas, profiles) {
5622
- const { keywords, markers } = buildTablesFromProfiles(schemas, profiles);
5623
- const { sovLanguages } = detectWordOrders(profiles);
5624
- const schemaMap = /* @__PURE__ */ new Map();
5625
- for (const s of schemas) schemaMap.set(s.action, s);
5626
- return {
5627
- render(node, language) {
5628
- const schema = schemaMap.get(node.action);
5629
- if (!schema) return node.action;
5630
- const keyword = lookupKeyword(keywords, node.action, language);
5631
- const isSOV = sovLanguages.has(language);
5632
- const roleParts = [];
5633
- for (const role of schema.roles) {
5634
- const value = extractRoleValue(node, role.role);
5635
- if (!value && !role.required) continue;
5636
- const markerText = role.markerOverride?.[language] ?? markers[role.role]?.[language] ?? void 0;
5637
- roleParts.push({
5638
- ...markerText != null && { marker: markerText },
5639
- value: value || "",
5640
- role
5641
- });
5642
- }
5643
- const parts = [];
5644
- if (isSOV) {
5645
- for (const rp of roleParts) {
5646
- if (rp.value) parts.push(rp.value);
5647
- if (rp.marker) parts.push(rp.marker);
5648
- }
5649
- parts.push(keyword);
5650
- } else {
5651
- parts.push(keyword);
5652
- for (const rp of roleParts) {
5653
- if (rp.marker) parts.push(rp.marker);
5654
- if (rp.value) parts.push(rp.value);
5655
- }
5656
- }
5657
- return buildPhrase(...parts);
5658
- }
5659
- };
5660
- }
5661
5849
  var TokenStreamImpl = class {
5662
5850
  constructor(tokens, language) {
5663
5851
  this.pos = 0;
@@ -5763,6 +5951,9 @@ function isQuote(char) {
5763
5951
  function isDigit(char) {
5764
5952
  return /\d/.test(char);
5765
5953
  }
5954
+ function stripOptionalDiacritics(word) {
5955
+ return word.replace(/[ً-ْٰ]/g, "");
5956
+ }
5766
5957
  function isAsciiLetter(char) {
5767
5958
  return /[a-zA-Z]/.test(char);
5768
5959
  }
@@ -6468,7 +6659,7 @@ var _BaseTokenizer = class _BaseTokenizer2 {
6468
6659
  * @returns Word without diacritics
6469
6660
  */
6470
6661
  removeDiacritics(word) {
6471
- return word.replace(/[\u064B-\u0652\u0670]/g, "");
6662
+ return stripOptionalDiacritics(word);
6472
6663
  }
6473
6664
  /**
6474
6665
  * Try to match a keyword from profile at the current position.
@@ -6559,24 +6750,40 @@ var _BaseTokenizer = class _BaseTokenizer2 {
6559
6750
  });
6560
6751
  }
6561
6752
  /**
6562
- * Look up a keyword by native word (case-insensitive).
6753
+ * Look up a keyword by native word (case-insensitive, diacritic-insensitive).
6563
6754
  * O(1) lookup using the keyword map.
6564
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
+ *
6565
6768
  * @param native - Native word to look up
6566
6769
  * @returns KeywordEntry if found, undefined otherwise
6567
6770
  */
6568
6771
  lookupKeyword(native) {
6569
- 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());
6570
6777
  }
6571
6778
  /**
6572
- * Check if a word is a known keyword (case-insensitive).
6573
- * 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}.
6574
6781
  *
6575
6782
  * @param native - Native word to check
6576
6783
  * @returns true if the word is a keyword
6577
6784
  */
6578
6785
  isKeyword(native) {
6579
- return this.profileKeywordMap.has(native.toLowerCase());
6786
+ return this.lookupKeyword(native) !== void 0;
6580
6787
  }
6581
6788
  /**
6582
6789
  * Set the morphological normalizer for this tokenizer.
@@ -7527,6 +7734,7 @@ export {
7527
7734
  BaseMorphologicalNormalizer,
7528
7735
  BaseTokenizer,
7529
7736
  CrossDomainDispatcher,
7737
+ CssSelectorExtractor,
7530
7738
  DEFAULT_OPERATORS,
7531
7739
  DEFAULT_PUNCTUATION,
7532
7740
  DEFAULT_REFERENCES,
@@ -7563,6 +7771,7 @@ export {
7563
7771
  createCompoundNode,
7564
7772
  createConditionalNode,
7565
7773
  createDiagnosticCollector,
7774
+ createDomainRenderer,
7566
7775
  createEventHandlerNode,
7567
7776
  createExpression,
7568
7777
  createFlag,
@@ -7654,6 +7863,8 @@ export {
7654
7863
  semanticNodeToJSON,
7655
7864
  semanticNodeToRuntimeAST,
7656
7865
  semanticValueToAST,
7866
+ sortRolesByWordOrder,
7867
+ stripOptionalDiacritics,
7657
7868
  synthesizeFromSchemas,
7658
7869
  toEnvelopeJSON,
7659
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-6KATWM5A.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.8.0",
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.8.0",
35
- "@lokascript/semantic": "^2.8.0"
34
+ "@hyperfixi/core": "^2.9.0",
35
+ "@lokascript/semantic": "^2.9.0"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@lokascript/semantic": {
@@ -43,8 +43,8 @@
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
- "@hyperfixi/core": "^2.8.0",
47
- "@lokascript/semantic": "^2.8.0",
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",
@@ -53,7 +53,8 @@
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"