@oml/language 0.14.17 → 0.16.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.
@@ -120,15 +120,72 @@ export function deref(node: any): any {
120
120
  return node;
121
121
  }
122
122
 
123
+ export const normalizeIri = (value: string): string =>
124
+ String(value ?? '').trim().replace(/^<|>$/g, '');
125
+
123
126
  export const normalizeNamespace = (raw: string): string =>
124
- raw.replace(/^<|>$/g, '').replace(/[#/]?$/, '');
127
+ normalizeIri(raw).replace(/[#/]?$/, '');
128
+
129
+ export const withNamespaceSeparator = (rawNamespace: string): string => {
130
+ const trimmed = normalizeIri(rawNamespace);
131
+ if (!trimmed) {
132
+ return trimmed;
133
+ }
134
+ return /[#/]$/.test(trimmed) ? trimmed : `${trimmed}#`;
135
+ };
136
+
137
+ export function resolveImportPrefix(imp: Import): string | undefined {
138
+ const explicitPrefix = typeof imp?.prefix === 'string' ? imp.prefix.trim() : '';
139
+ if (explicitPrefix) {
140
+ return explicitPrefix;
141
+ }
142
+ const resolvedPrefix = typeof (imp as any)?.imported?.ref?.prefix === 'string'
143
+ ? (imp as any).imported.ref.prefix.trim()
144
+ : '';
145
+ return resolvedPrefix || undefined;
146
+ }
147
+
148
+ export function resolveImportNamespaceRaw(imp: Import): string | undefined {
149
+ const resolvedNamespace = typeof (imp as any)?.imported?.ref?.namespace === 'string'
150
+ ? (imp as any).imported.ref.namespace
151
+ : '';
152
+ if (resolvedNamespace.trim().length > 0) {
153
+ return resolvedNamespace.trim();
154
+ }
155
+ const refText = typeof (imp as any)?.imported?.$refText === 'string'
156
+ ? (imp as any).imported.$refText.trim()
157
+ : '';
158
+ return refText || undefined;
159
+ }
125
160
 
126
161
  export const resolveImportNamespace = (imp: Import): string => {
127
- const raw =
128
- (imp.imported as any)?.$refText ??
129
- (imp as any)?.imported ??
130
- '';
131
- return raw.replace(/^<|>$/g, '');
162
+ return resolveImportNamespaceRaw(imp) ?? '';
163
+ };
164
+
165
+ export type IriParts = { namespace: string; fragment: string; separator: '#' | '/' };
166
+
167
+ export const parseIriParts = (iri: string): IriParts | undefined => {
168
+ const normalized = normalizeIri(iri);
169
+ if (!normalized) {
170
+ return undefined;
171
+ }
172
+ const hashIndex = normalized.lastIndexOf('#');
173
+ if (hashIndex > 0 && hashIndex < normalized.length - 1) {
174
+ return {
175
+ namespace: normalizeNamespace(normalized.slice(0, hashIndex)),
176
+ fragment: normalized.slice(hashIndex + 1),
177
+ separator: '#',
178
+ };
179
+ }
180
+ const slashIndex = normalized.lastIndexOf('/');
181
+ if (slashIndex > 0 && slashIndex < normalized.length - 1) {
182
+ return {
183
+ namespace: normalizeNamespace(normalized.slice(0, slashIndex)),
184
+ fragment: normalized.slice(slashIndex + 1),
185
+ separator: '/',
186
+ };
187
+ }
188
+ return undefined;
132
189
  };
133
190
 
134
191
  export const getAllLangiumDocuments = (documents: LangiumDocuments): LangiumDocument[] => {
@@ -164,37 +221,153 @@ const OML_LS_IGNORED_DOCUMENT_SCHEMES = new Set([
164
221
  'chat-editing-text-model',
165
222
  'chat-editing-snapshot-text-model',
166
223
  'vscode-chat-code-block',
224
+ 'copilot-chat-code-block',
225
+ 'claude-code-chat-code-block',
167
226
  ]);
168
227
 
169
228
  export function isIgnoredByOmlLsDocumentUri(uri: string): boolean {
170
229
  try {
171
- return OML_LS_IGNORED_DOCUMENT_SCHEMES.has(URI.parse(uri).scheme);
230
+ const scheme = URI.parse(uri).scheme.toLowerCase();
231
+ if (OML_LS_IGNORED_DOCUMENT_SCHEMES.has(scheme)) {
232
+ return true;
233
+ }
234
+ return scheme.includes('chat') || scheme.includes('copilot') || scheme.includes('claude');
172
235
  } catch {
173
236
  return false;
174
237
  }
175
238
  }
176
239
 
177
240
  export const splitIri = (iri: string): { base: string; fragment: string; separator: string } | null => {
178
- const trimmed = iri.trim();
179
- if (!trimmed) return null;
180
- const hashIndex = trimmed.lastIndexOf('#');
181
- if (hashIndex >= 0) {
182
- return {
183
- base: trimmed.slice(0, hashIndex),
184
- fragment: trimmed.slice(hashIndex + 1),
185
- separator: '#',
186
- };
241
+ const parts = parseIriParts(iri);
242
+ return parts
243
+ ? { base: parts.namespace, fragment: parts.fragment, separator: parts.separator }
244
+ : null;
245
+ };
246
+
247
+ export function getRefText(ref: any): string | undefined {
248
+ if (typeof ref === 'string') {
249
+ return ref;
187
250
  }
188
- const slashIndex = trimmed.lastIndexOf('/');
189
- if (slashIndex >= 0) {
190
- return {
191
- base: trimmed.slice(0, slashIndex),
192
- fragment: trimmed.slice(slashIndex + 1),
193
- separator: '/',
194
- };
251
+ if (typeof ref?.$refText === 'string') {
252
+ return ref.$refText;
195
253
  }
196
- return null;
197
- };
254
+ if (typeof ref?.$refNode?.text === 'string') {
255
+ return ref.$refNode.text;
256
+ }
257
+ return undefined;
258
+ }
259
+
260
+ export function getLocalNameForNamespace(iri: string, rawNamespace: string | undefined): string | undefined {
261
+ const namespace = normalizeNamespace(String(rawNamespace ?? ''));
262
+ if (!namespace) {
263
+ return undefined;
264
+ }
265
+ const parts = parseIriParts(iri);
266
+ return parts?.namespace === namespace ? parts.fragment : undefined;
267
+ }
268
+
269
+ export function expandLocalRefText(rawNamespace: string | undefined, localName: string): string | undefined {
270
+ const local = String(localName ?? '').trim();
271
+ const namespace = withNamespaceSeparator(String(rawNamespace ?? ''));
272
+ if (!namespace || !local) {
273
+ return undefined;
274
+ }
275
+ return `${namespace}${local}`;
276
+ }
277
+
278
+ export function expandRefTextToIri(ontology: any, refText: string): string | undefined {
279
+ const trimmed = String(refText ?? '').trim();
280
+ if (!trimmed) {
281
+ return undefined;
282
+ }
283
+ if (trimmed === 'a') {
284
+ return 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
285
+ }
286
+ if (trimmed.startsWith('<') && trimmed.endsWith('>')) {
287
+ return normalizeIri(trimmed);
288
+ }
289
+ if (trimmed.includes('://')) {
290
+ return normalizeIri(trimmed);
291
+ }
292
+ const localInCurrentOntology = expandLocalRefText(ontology?.namespace, trimmed);
293
+ if (localInCurrentOntology) {
294
+ return localInCurrentOntology;
295
+ }
296
+ const separatorIndex = trimmed.indexOf(':');
297
+ if (separatorIndex <= 0) {
298
+ return undefined;
299
+ }
300
+ const prefix = trimmed.slice(0, separatorIndex);
301
+ const local = trimmed.slice(separatorIndex + 1);
302
+ if (!local) {
303
+ return undefined;
304
+ }
305
+ if (typeof ontology?.prefix === 'string' && ontology.prefix.trim() === prefix) {
306
+ return expandLocalRefText(ontology.namespace, local);
307
+ }
308
+ const imports = Array.isArray(ontology?.ownedImports) ? ontology.ownedImports : [];
309
+ for (const imp of imports) {
310
+ if (resolveImportPrefix(imp) !== prefix) {
311
+ continue;
312
+ }
313
+ return expandLocalRefText(resolveImportNamespaceRaw(imp), local);
314
+ }
315
+ return undefined;
316
+ }
317
+
318
+ export function localNameFromRefText(refText: string): string | undefined {
319
+ const trimmed = normalizeIri(refText);
320
+ if (!trimmed) {
321
+ return undefined;
322
+ }
323
+ const iriParts = parseIriParts(trimmed);
324
+ if (iriParts) {
325
+ return iriParts.fragment;
326
+ }
327
+ const colonIndex = trimmed.indexOf(':');
328
+ if (colonIndex > 0 && colonIndex < trimmed.length - 1) {
329
+ return trimmed.slice(colonIndex + 1);
330
+ }
331
+ return trimmed;
332
+ }
333
+
334
+ export function isSameIriTarget(left: string, right: string): boolean {
335
+ const leftNorm = normalizeIri(left);
336
+ const rightNorm = normalizeIri(right);
337
+ if (leftNorm === rightNorm) {
338
+ return true;
339
+ }
340
+ const leftParts = parseIriParts(leftNorm);
341
+ const rightParts = parseIriParts(rightNorm);
342
+ if (!leftParts || !rightParts) {
343
+ return false;
344
+ }
345
+ return leftParts.namespace === rightParts.namespace
346
+ && leftParts.fragment === rightParts.fragment;
347
+ }
348
+
349
+ export function pickImportPrefix(namespace: string, usedPrefixes: ReadonlySet<string>): string {
350
+ const normalizedNamespace = normalizeNamespace(namespace);
351
+ const pieces = normalizedNamespace.split('/').filter((piece) => piece.length > 0);
352
+ const tail = (pieces[pieces.length - 1] ?? normalizedNamespace)
353
+ .replace(/[^A-Za-z0-9_]/g, '')
354
+ .toLowerCase();
355
+ const base = /^[A-Za-z_]/.test(tail) ? tail : `ns${tail}`;
356
+ const candidateBase = (base || 'ns').slice(0, 32);
357
+ if (!usedPrefixes.has(candidateBase)) {
358
+ return candidateBase;
359
+ }
360
+ let suffix = 1;
361
+ while (usedPrefixes.has(`${candidateBase}${suffix}`)) {
362
+ suffix += 1;
363
+ }
364
+ return `${candidateBase}${suffix}`;
365
+ }
366
+
367
+ export function normalizeUriPath(path: string): string {
368
+ const normalized = path.replace(/\/+$/, '');
369
+ return normalized || '/';
370
+ }
198
371
 
199
372
  export const resolveIndent = (text: string, rootOffset: number, insertOffset: number): string => {
200
373
  const slice = text.slice(rootOffset, insertOffset);
@@ -239,7 +412,7 @@ export function getIriOwningElement(node: any): IriOwningElement | undefined {
239
412
  }
240
413
 
241
414
  export function getNamespaceSeparator(rawNamespace: string | undefined): '#' | '/' {
242
- const value = (rawNamespace ?? '').trim();
415
+ const value = normalizeIri(rawNamespace ?? '');
243
416
  if (value.endsWith('/')) return '/';
244
417
  if (value.endsWith('#')) return '#';
245
418
  return '#';
@@ -3,6 +3,7 @@
3
3
  import { URI, type LangiumSharedCoreServices } from 'langium';
4
4
  import { DefaultWorkspaceManager } from 'langium';
5
5
  import type { Connection } from 'vscode-languageserver';
6
+ import { normalizeUriPath } from './oml-utils.js';
6
7
 
7
8
  // LSP request name — must match the handler registered in node-client.ts
8
9
  const GET_WORKSPACE_FOLDERS_REQUEST = 'oml/fs/getWorkspaceFolders';
@@ -75,7 +76,3 @@ function isUriPathWithinWorkspace(candidate: URI, workspace: URI): boolean {
75
76
  return candidatePath === workspacePath || candidatePath.startsWith(`${workspacePath}/`);
76
77
  }
77
78
 
78
- function normalizeUriPath(path: string): string {
79
- const normalized = path.replace(/\/+$/, '');
80
- return normalized || '/';
81
- }