@oml/server 0.14.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/README.md +94 -0
- package/package.json +34 -0
- package/src/cli.ts +189 -0
- package/src/index.ts +9 -0
- package/src/lsp/diagram-server.ts +48 -0
- package/src/lsp/language-server.ts +423 -0
- package/src/lsp/protocol/browser-fs-protocol.ts +21 -0
- package/src/lsp/protocol/reasoner-protocol.ts +86 -0
- package/src/lsp/providers/browser-fs-provider.ts +85 -0
- package/src/lsp/providers/hybrid-fs-provider.ts +134 -0
- package/src/rest/export.ts +118 -0
- package/src/rest/routes.ts +117 -0
- package/src/rest/server.ts +2517 -0
- package/src/rest/template.ts +276 -0
- package/src/rest/validation.ts +555 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,2517 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import * as http from 'node:http';
|
|
4
|
+
import * as fsSync from 'node:fs';
|
|
5
|
+
import * as fs from 'node:fs/promises';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { PassThrough } from 'node:stream';
|
|
10
|
+
import { URL, pathToFileURL } from 'node:url';
|
|
11
|
+
import { URI } from 'langium';
|
|
12
|
+
import { NodeFileSystem } from 'langium/node';
|
|
13
|
+
import { createConnection, type Connection } from 'vscode-languageserver/node.js';
|
|
14
|
+
import { DataFactory, Writer } from 'n3';
|
|
15
|
+
import uFuzzy from '@leeoniya/ufuzzy';
|
|
16
|
+
import {
|
|
17
|
+
MarkdownHandlerRegistry,
|
|
18
|
+
MarkdownPreviewRuntime,
|
|
19
|
+
buildTemplateCatalog as buildNavigationTemplateCatalog,
|
|
20
|
+
extractLeadingFrontMatter,
|
|
21
|
+
resolveTemplateForNavigation,
|
|
22
|
+
renderTemplate,
|
|
23
|
+
type TemplateInvocation,
|
|
24
|
+
MarkdownExecutor,
|
|
25
|
+
type MdBlockKind,
|
|
26
|
+
type MdExecutableBlock,
|
|
27
|
+
type MdBlockExecutionResult,
|
|
28
|
+
} from '@oml/markdown';
|
|
29
|
+
import { STATIC_MARKDOWN_RUNTIME_BUNDLE_FILE, STATIC_MARKDOWN_RUNTIME_CSS } from '@oml/markdown/static';
|
|
30
|
+
import {
|
|
31
|
+
applyOmlUpdate,
|
|
32
|
+
collectOntologyMembers,
|
|
33
|
+
getIriForNode,
|
|
34
|
+
getOntologyModelIndex,
|
|
35
|
+
iriFragment,
|
|
36
|
+
isDescription,
|
|
37
|
+
isOntology,
|
|
38
|
+
isVocabulary,
|
|
39
|
+
tokenizeForFuzzy,
|
|
40
|
+
type OmlEditRequest,
|
|
41
|
+
type OmlEditResponse,
|
|
42
|
+
type OmlFuzzyIndexedEntry,
|
|
43
|
+
} from '@oml/language';
|
|
44
|
+
import { detectSparqlKind } from '@oml/owl';
|
|
45
|
+
import { exportAssertedWorkspace, exportWorkspace, type RestExportContext } from './export.js';
|
|
46
|
+
import { startOmlLanguageServer, type OmlLanguageServerRuntime } from '../lsp/language-server.js';
|
|
47
|
+
import { createOpenApiSpec, dispatchRestRoute } from './routes.js';
|
|
48
|
+
import {
|
|
49
|
+
buildTemplateCatalog,
|
|
50
|
+
expandTemplateComposeBlocks,
|
|
51
|
+
findFilesByExtension,
|
|
52
|
+
frontMatterString,
|
|
53
|
+
isTemplateMarkdownFile,
|
|
54
|
+
normalizeContextOntologyIri,
|
|
55
|
+
} from './template.js';
|
|
56
|
+
import { lintWorkspace, reasonWorkspace, type RestLintResult, type RestValidationContext, validateWorkspace } from './validation.js';
|
|
57
|
+
|
|
58
|
+
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
59
|
+
const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
60
|
+
const DEFAULT_BODY_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
61
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
62
|
+
|
|
63
|
+
const SUPPORTED_MD_BLOCK_KINDS = new Set<MdBlockKind>([
|
|
64
|
+
'table',
|
|
65
|
+
'tree',
|
|
66
|
+
'graph',
|
|
67
|
+
'chart',
|
|
68
|
+
'diagram',
|
|
69
|
+
'list',
|
|
70
|
+
'text',
|
|
71
|
+
'matrix',
|
|
72
|
+
'table-editor'
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
export interface OmlRestServerOptions {
|
|
76
|
+
host: string;
|
|
77
|
+
port: number;
|
|
78
|
+
workspaceRoot?: string;
|
|
79
|
+
watchWorkspace?: boolean;
|
|
80
|
+
requestTimeoutMs?: number;
|
|
81
|
+
authToken?: string;
|
|
82
|
+
runtime?: OmlLanguageServerRuntime;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface WorkspaceModelFileEntry {
|
|
86
|
+
path: string;
|
|
87
|
+
uri: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const BUILT_IN_ONTOLOGIES = new Set([
|
|
91
|
+
'http://www.w3.org/2001/XMLSchema',
|
|
92
|
+
'http://www.w3.org/1999/02/22-rdf-syntax-ns',
|
|
93
|
+
'http://www.w3.org/2000/01/rdf-schema',
|
|
94
|
+
'http://www.w3.org/2002/07/owl',
|
|
95
|
+
'http://www.w3.org/2003/11/swrl',
|
|
96
|
+
'http://www.w3.org/2003/11/swrlb',
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
function jsonResponse(res: http.ServerResponse, status: number, payload: unknown): void {
|
|
101
|
+
const body = JSON.stringify(payload);
|
|
102
|
+
res.statusCode = status;
|
|
103
|
+
res.setHeader('content-type', JSON_CONTENT_TYPE);
|
|
104
|
+
res.setHeader('content-length', Buffer.byteLength(body, 'utf-8'));
|
|
105
|
+
res.end(body);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function htmlResponse(res: http.ServerResponse, status: number, body: string): void {
|
|
109
|
+
res.statusCode = status;
|
|
110
|
+
res.setHeader('content-type', HTML_CONTENT_TYPE);
|
|
111
|
+
res.setHeader('content-length', Buffer.byteLength(body, 'utf-8'));
|
|
112
|
+
res.end(body);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function escapeHtml(value: string): string {
|
|
116
|
+
return value
|
|
117
|
+
.replace(/&/g, '&')
|
|
118
|
+
.replace(/</g, '<')
|
|
119
|
+
.replace(/>/g, '>')
|
|
120
|
+
.replace(/"/g, '"')
|
|
121
|
+
.replace(/'/g, ''');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function listWorkspaceModelFiles(workspaceRoot: string): Promise<WorkspaceModelFileEntry[]> {
|
|
125
|
+
const entries: WorkspaceModelFileEntry[] = [];
|
|
126
|
+
const ignoredNames = new Set(['.git', 'node_modules', 'dist', 'build', 'out']);
|
|
127
|
+
|
|
128
|
+
const visit = async (currentDir: string): Promise<void> => {
|
|
129
|
+
const children = await fs.readdir(currentDir, { withFileTypes: true });
|
|
130
|
+
for (const child of children) {
|
|
131
|
+
const childPath = path.join(currentDir, child.name);
|
|
132
|
+
if (child.isDirectory()) {
|
|
133
|
+
if (ignoredNames.has(child.name) || child.name.startsWith('.')) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
await visit(childPath);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!child.isFile() || !child.name.toLowerCase().endsWith('.oml')) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const relativePath = path.relative(workspaceRoot, childPath).split(path.sep).join('/');
|
|
143
|
+
entries.push({
|
|
144
|
+
path: relativePath,
|
|
145
|
+
uri: pathToFileURL(childPath).toString(),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
await visit(workspaceRoot);
|
|
151
|
+
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
152
|
+
return entries;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createSparqlWorkbenchPage(defaultWorkspaceRoot: string): string {
|
|
156
|
+
const escapedWorkspaceRoot = escapeHtml(defaultWorkspaceRoot);
|
|
157
|
+
return `<!DOCTYPE html>
|
|
158
|
+
<html lang="en">
|
|
159
|
+
<head>
|
|
160
|
+
<meta charset="UTF-8">
|
|
161
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
162
|
+
<title>OML SPARQL Workbench</title>
|
|
163
|
+
<style>
|
|
164
|
+
:root {
|
|
165
|
+
--bg: #f3f5f9;
|
|
166
|
+
--bg-soft: #eef1f7;
|
|
167
|
+
--card: rgba(255, 255, 255, 0.94);
|
|
168
|
+
--card-strong: #ffffff;
|
|
169
|
+
--ink: #101828;
|
|
170
|
+
--muted: #475467;
|
|
171
|
+
--line: #d0d5dd;
|
|
172
|
+
--line-soft: #e4e7ec;
|
|
173
|
+
--primary: #155eef;
|
|
174
|
+
--primary-strong: #004eea;
|
|
175
|
+
--primary-soft: rgba(21, 94, 239, 0.14);
|
|
176
|
+
--success: #067647;
|
|
177
|
+
--danger: #b42318;
|
|
178
|
+
--shadow-xl: 0 20px 48px rgba(16, 24, 40, 0.12);
|
|
179
|
+
--shadow-md: 0 8px 20px rgba(16, 24, 40, 0.08);
|
|
180
|
+
--radius-lg: 18px;
|
|
181
|
+
--radius-md: 12px;
|
|
182
|
+
--mono: "SFMono-Regular", "Menlo", "Monaco", "Consolas", monospace;
|
|
183
|
+
--sans: "Inter", "Segoe UI", "SF Pro Text", sans-serif;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
* { box-sizing: border-box; }
|
|
187
|
+
|
|
188
|
+
body {
|
|
189
|
+
margin: 0;
|
|
190
|
+
min-height: 100vh;
|
|
191
|
+
background:
|
|
192
|
+
radial-gradient(1200px 600px at 100% -10%, rgba(21, 94, 239, 0.12), transparent 60%),
|
|
193
|
+
radial-gradient(900px 460px at -5% -20%, rgba(2, 132, 199, 0.08), transparent 58%),
|
|
194
|
+
linear-gradient(180deg, #f8fafc 0%, var(--bg) 100%);
|
|
195
|
+
color: var(--ink);
|
|
196
|
+
font-family: var(--sans);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.shell {
|
|
200
|
+
width: min(1520px, calc(100vw - 36px));
|
|
201
|
+
margin: 20px auto 28px;
|
|
202
|
+
display: grid;
|
|
203
|
+
gap: 16px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.hero {
|
|
207
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.62));
|
|
208
|
+
border: 1px solid rgba(255, 255, 255, 0.65);
|
|
209
|
+
border-radius: var(--radius-lg);
|
|
210
|
+
box-shadow: var(--shadow-xl);
|
|
211
|
+
backdrop-filter: blur(6px);
|
|
212
|
+
padding: 20px 24px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.page-title {
|
|
216
|
+
margin: 0;
|
|
217
|
+
font-size: clamp(1.9rem, 3.4vw, 2.7rem);
|
|
218
|
+
font-weight: 800;
|
|
219
|
+
letter-spacing: -0.035em;
|
|
220
|
+
color: #0f172a;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.page-subtitle {
|
|
224
|
+
margin: 8px 0 0;
|
|
225
|
+
color: var(--muted);
|
|
226
|
+
font-size: 0.98rem;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.row {
|
|
230
|
+
background: var(--card);
|
|
231
|
+
border: 1px solid var(--line-soft);
|
|
232
|
+
border-radius: var(--radius-lg);
|
|
233
|
+
box-shadow: var(--shadow-md);
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
backdrop-filter: blur(4px);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.row-body {
|
|
239
|
+
padding: 16px 18px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.top-row {
|
|
243
|
+
display: grid;
|
|
244
|
+
grid-template-columns: minmax(280px, 0.95fr) minmax(440px, 1.45fr);
|
|
245
|
+
gap: 16px;
|
|
246
|
+
align-items: end;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.workspace-label,
|
|
250
|
+
.field-label,
|
|
251
|
+
.section-title {
|
|
252
|
+
color: #344054;
|
|
253
|
+
font-size: 0.78rem;
|
|
254
|
+
font-weight: 700;
|
|
255
|
+
letter-spacing: 0.08em;
|
|
256
|
+
text-transform: uppercase;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.workspace-value {
|
|
260
|
+
margin-top: 6px;
|
|
261
|
+
padding: 11px 12px;
|
|
262
|
+
border-radius: var(--radius-md);
|
|
263
|
+
border: 1px solid var(--line);
|
|
264
|
+
background: var(--card-strong);
|
|
265
|
+
font-family: var(--mono);
|
|
266
|
+
font-size: 0.9rem;
|
|
267
|
+
color: #1d2939;
|
|
268
|
+
word-break: break-all;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.file-row {
|
|
272
|
+
display: grid;
|
|
273
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
274
|
+
gap: 10px;
|
|
275
|
+
align-items: end;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.field {
|
|
279
|
+
display: grid;
|
|
280
|
+
gap: 6px;
|
|
281
|
+
min-width: 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.query-row {
|
|
285
|
+
display: grid;
|
|
286
|
+
gap: 12px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.query-editor {
|
|
290
|
+
position: relative;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.query-header,
|
|
294
|
+
.results-header,
|
|
295
|
+
.json-header {
|
|
296
|
+
display: flex;
|
|
297
|
+
justify-content: space-between;
|
|
298
|
+
align-items: center;
|
|
299
|
+
gap: 14px;
|
|
300
|
+
flex-wrap: wrap;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.status {
|
|
304
|
+
min-height: 1.3em;
|
|
305
|
+
color: var(--muted);
|
|
306
|
+
font-size: 0.92rem;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.status.error {
|
|
310
|
+
color: var(--danger);
|
|
311
|
+
font-weight: 600;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.status.success {
|
|
315
|
+
color: var(--success);
|
|
316
|
+
font-weight: 600;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
input,
|
|
320
|
+
textarea,
|
|
321
|
+
button,
|
|
322
|
+
pre,
|
|
323
|
+
table {
|
|
324
|
+
font: inherit;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
input,
|
|
328
|
+
textarea {
|
|
329
|
+
width: 100%;
|
|
330
|
+
border: 1px solid var(--line);
|
|
331
|
+
border-radius: var(--radius-md);
|
|
332
|
+
padding: 12px 13px;
|
|
333
|
+
background: #ffffff;
|
|
334
|
+
color: #101828;
|
|
335
|
+
font-family: var(--mono);
|
|
336
|
+
font-size: 0.9rem;
|
|
337
|
+
transition: border-color 140ms ease, box-shadow 140ms ease;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
input:focus,
|
|
341
|
+
textarea:focus {
|
|
342
|
+
outline: none;
|
|
343
|
+
border-color: var(--primary);
|
|
344
|
+
box-shadow: 0 0 0 4px var(--primary-soft);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
textarea {
|
|
348
|
+
min-height: 250px;
|
|
349
|
+
resize: vertical;
|
|
350
|
+
line-height: 1.55;
|
|
351
|
+
padding-right: 70px;
|
|
352
|
+
padding-top: 16px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
button {
|
|
356
|
+
border: 0;
|
|
357
|
+
border-radius: var(--radius-md);
|
|
358
|
+
padding: 11px 16px;
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
font-weight: 700;
|
|
361
|
+
transition: transform 130ms ease, box-shadow 130ms ease, opacity 130ms ease;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
button:hover {
|
|
365
|
+
transform: translateY(-1px);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
button:disabled {
|
|
369
|
+
cursor: wait;
|
|
370
|
+
opacity: 0.62;
|
|
371
|
+
transform: none;
|
|
372
|
+
box-shadow: none;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.primary {
|
|
376
|
+
background: linear-gradient(180deg, var(--primary), var(--primary-strong));
|
|
377
|
+
color: #f8faff;
|
|
378
|
+
box-shadow: 0 10px 24px rgba(21, 94, 239, 0.3);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.play-button {
|
|
382
|
+
position: absolute;
|
|
383
|
+
top: 12px;
|
|
384
|
+
right: 12px;
|
|
385
|
+
width: 44px;
|
|
386
|
+
height: 44px;
|
|
387
|
+
padding: 0;
|
|
388
|
+
display: grid;
|
|
389
|
+
place-items: center;
|
|
390
|
+
border-radius: 999px;
|
|
391
|
+
z-index: 1;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.ghost {
|
|
395
|
+
background: #ffffff;
|
|
396
|
+
color: #344054;
|
|
397
|
+
border: 1px solid var(--line);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.ghost:hover {
|
|
401
|
+
background: #f8fafc;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.table-frame {
|
|
405
|
+
border-top: 1px solid var(--line-soft);
|
|
406
|
+
background: #ffffff;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.table-viewport {
|
|
410
|
+
height: 390px;
|
|
411
|
+
overflow: auto;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
table {
|
|
415
|
+
width: 100%;
|
|
416
|
+
border-collapse: collapse;
|
|
417
|
+
min-width: 100%;
|
|
418
|
+
font-family: var(--mono);
|
|
419
|
+
font-size: 0.86rem;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
thead th {
|
|
423
|
+
position: sticky;
|
|
424
|
+
top: 0;
|
|
425
|
+
z-index: 1;
|
|
426
|
+
padding: 10px 12px;
|
|
427
|
+
border-bottom: 1px solid var(--line-soft);
|
|
428
|
+
background: #f9fafb;
|
|
429
|
+
color: #344054;
|
|
430
|
+
text-align: left;
|
|
431
|
+
white-space: nowrap;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
tbody td {
|
|
435
|
+
padding: 10px 12px;
|
|
436
|
+
border-top: 1px solid #f2f4f7;
|
|
437
|
+
color: #101828;
|
|
438
|
+
vertical-align: top;
|
|
439
|
+
word-break: break-word;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
tbody tr:nth-child(even) {
|
|
443
|
+
background: #fcfcfd;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.empty {
|
|
447
|
+
min-height: 100%;
|
|
448
|
+
display: grid;
|
|
449
|
+
place-items: center;
|
|
450
|
+
padding: 24px;
|
|
451
|
+
color: var(--muted);
|
|
452
|
+
font-style: italic;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.pager {
|
|
456
|
+
display: flex;
|
|
457
|
+
justify-content: space-between;
|
|
458
|
+
align-items: center;
|
|
459
|
+
gap: 10px;
|
|
460
|
+
padding: 12px 16px;
|
|
461
|
+
border-top: 1px solid var(--line-soft);
|
|
462
|
+
background: #fcfcfd;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.pager-meta {
|
|
466
|
+
color: #475467;
|
|
467
|
+
font-size: 0.88rem;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.pager-actions {
|
|
471
|
+
display: flex;
|
|
472
|
+
gap: 8px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.raw {
|
|
476
|
+
margin: 0;
|
|
477
|
+
min-height: 230px;
|
|
478
|
+
max-height: 340px;
|
|
479
|
+
overflow: auto;
|
|
480
|
+
padding: 16px;
|
|
481
|
+
border-top: 1px solid var(--line-soft);
|
|
482
|
+
background: #0f172a;
|
|
483
|
+
color: #e2e8f0;
|
|
484
|
+
font-family: var(--mono);
|
|
485
|
+
font-size: 0.86rem;
|
|
486
|
+
line-height: 1.55;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#browseDialog {
|
|
490
|
+
width: min(900px, calc(100vw - 40px));
|
|
491
|
+
border: 0;
|
|
492
|
+
border-radius: 20px;
|
|
493
|
+
padding: 0;
|
|
494
|
+
overflow: hidden;
|
|
495
|
+
box-shadow: 0 34px 90px rgba(16, 24, 40, 0.32);
|
|
496
|
+
background: #ffffff;
|
|
497
|
+
color: var(--ink);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#browseDialog::backdrop {
|
|
501
|
+
background: rgba(2, 6, 23, 0.56);
|
|
502
|
+
backdrop-filter: blur(2px);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.dialog-header {
|
|
506
|
+
padding: 18px 22px 14px;
|
|
507
|
+
border-bottom: 1px solid var(--line-soft);
|
|
508
|
+
display: flex;
|
|
509
|
+
justify-content: space-between;
|
|
510
|
+
align-items: center;
|
|
511
|
+
gap: 16px;
|
|
512
|
+
background: linear-gradient(180deg, #f8fafc, #f2f4f7);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.dialog-title {
|
|
516
|
+
font-size: 1.02rem;
|
|
517
|
+
font-weight: 800;
|
|
518
|
+
color: #0f172a;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.dialog-subtitle {
|
|
522
|
+
margin-top: 4px;
|
|
523
|
+
font-size: 0.9rem;
|
|
524
|
+
color: var(--muted);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.dialog-body {
|
|
528
|
+
padding: 16px 22px 22px;
|
|
529
|
+
display: grid;
|
|
530
|
+
gap: 10px;
|
|
531
|
+
background: #ffffff;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.browse-list {
|
|
535
|
+
display: grid;
|
|
536
|
+
gap: 8px;
|
|
537
|
+
max-height: 440px;
|
|
538
|
+
overflow: auto;
|
|
539
|
+
padding-right: 2px;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.browse-entry {
|
|
543
|
+
text-align: left;
|
|
544
|
+
border: 1px solid var(--line-soft);
|
|
545
|
+
background: #ffffff;
|
|
546
|
+
border-radius: 12px;
|
|
547
|
+
padding: 11px 12px;
|
|
548
|
+
transition: border-color 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.browse-entry:hover {
|
|
552
|
+
border-color: #b2ccff;
|
|
553
|
+
background: #f8fbff;
|
|
554
|
+
box-shadow: 0 6px 16px rgba(21, 94, 239, 0.12);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.browse-entry.selected {
|
|
558
|
+
border-color: var(--primary);
|
|
559
|
+
background: #eef4ff;
|
|
560
|
+
box-shadow: 0 0 0 3px var(--primary-soft);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.browse-entry-path,
|
|
564
|
+
.browse-entry-uri {
|
|
565
|
+
font-family: var(--mono);
|
|
566
|
+
white-space: nowrap;
|
|
567
|
+
overflow: hidden;
|
|
568
|
+
text-overflow: ellipsis;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.browse-entry-path {
|
|
572
|
+
color: #101828;
|
|
573
|
+
font-size: 0.9rem;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.browse-entry-uri {
|
|
577
|
+
color: #667085;
|
|
578
|
+
font-size: 0.78rem;
|
|
579
|
+
margin-top: 4px;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
@media (max-width: 920px) {
|
|
583
|
+
.shell {
|
|
584
|
+
width: min(100vw - 16px, 1520px);
|
|
585
|
+
margin: 10px auto 16px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.hero {
|
|
589
|
+
padding: 16px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.file-row {
|
|
593
|
+
grid-template-columns: 1fr;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.top-row {
|
|
597
|
+
grid-template-columns: 1fr;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.query-header,
|
|
601
|
+
.results-header,
|
|
602
|
+
.json-header,
|
|
603
|
+
.pager {
|
|
604
|
+
align-items: start;
|
|
605
|
+
flex-direction: column;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.table-viewport {
|
|
609
|
+
height: 320px;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
</style>
|
|
613
|
+
</head>
|
|
614
|
+
<body>
|
|
615
|
+
<div class="shell">
|
|
616
|
+
<section class="hero">
|
|
617
|
+
<h1 class="page-title">OML Language Server</h1>
|
|
618
|
+
<p class="page-subtitle">Run SPARQL queries against workspace models with fast browsing, paged table results, and raw JSON diagnostics.</p>
|
|
619
|
+
</section>
|
|
620
|
+
|
|
621
|
+
<section class="row">
|
|
622
|
+
<div class="row-body top-row">
|
|
623
|
+
<div class="workspace-row field">
|
|
624
|
+
<div class="workspace-label">Workspace</div>
|
|
625
|
+
<div class="workspace-value">${escapedWorkspaceRoot}</div>
|
|
626
|
+
</div>
|
|
627
|
+
<div class="file-row">
|
|
628
|
+
<label class="field">
|
|
629
|
+
<span class="field-label">OML File</span>
|
|
630
|
+
<input id="modelUri" type="text" spellcheck="false" placeholder="Select an OML file from Browse" readonly>
|
|
631
|
+
</label>
|
|
632
|
+
<button id="browseModelsButton" class="ghost" type="button">Browse</button>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
</section>
|
|
636
|
+
|
|
637
|
+
<section class="row">
|
|
638
|
+
<div class="row-body query-row">
|
|
639
|
+
<div class="query-header">
|
|
640
|
+
<div class="section-title">Query</div>
|
|
641
|
+
<div id="status" class="status">Ready.</div>
|
|
642
|
+
</div>
|
|
643
|
+
<div class="query-editor">
|
|
644
|
+
<button id="runQueryButton" class="primary play-button" type="button" aria-label="Run Query" title="Run Query">▶</button>
|
|
645
|
+
<textarea id="sparql" spellcheck="false">SELECT * WHERE {
|
|
646
|
+
?s ?p ?o
|
|
647
|
+
}
|
|
648
|
+
LIMIT 25</textarea>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</section>
|
|
652
|
+
|
|
653
|
+
<section class="row">
|
|
654
|
+
<div class="row-body results-header">
|
|
655
|
+
<div class="section-title">Results Table</div>
|
|
656
|
+
<div id="tableStatus" class="status">Run a SELECT query to populate the table.</div>
|
|
657
|
+
</div>
|
|
658
|
+
<div class="table-frame">
|
|
659
|
+
<div class="table-viewport" id="tableViewport"><div class="empty" id="tableShell">Run a SELECT query to populate the table.</div></div>
|
|
660
|
+
<div class="pager">
|
|
661
|
+
<div id="pagerMeta" class="pager-meta">No rows.</div>
|
|
662
|
+
<div class="pager-actions">
|
|
663
|
+
<button id="prevPageButton" class="ghost" type="button" disabled>Previous</button>
|
|
664
|
+
<button id="nextPageButton" class="ghost" type="button" disabled>Next</button>
|
|
665
|
+
</div>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</section>
|
|
669
|
+
|
|
670
|
+
<section class="row">
|
|
671
|
+
<div class="row-body json-header">
|
|
672
|
+
<div class="section-title">JSON Result</div>
|
|
673
|
+
</div>
|
|
674
|
+
<pre id="rawOutput" class="raw">{
|
|
675
|
+
"ok": true,
|
|
676
|
+
"result": {
|
|
677
|
+
"success": true,
|
|
678
|
+
"kind": "select",
|
|
679
|
+
"warnings": []
|
|
680
|
+
}
|
|
681
|
+
}</pre>
|
|
682
|
+
</section>
|
|
683
|
+
</div>
|
|
684
|
+
|
|
685
|
+
<dialog id="browseDialog">
|
|
686
|
+
<form method="dialog">
|
|
687
|
+
<div class="dialog-header">
|
|
688
|
+
<div>
|
|
689
|
+
<div class="dialog-title">Workspace OML Files</div>
|
|
690
|
+
<div class="dialog-subtitle">Select the model file to run the query against.</div>
|
|
691
|
+
</div>
|
|
692
|
+
<button id="closeBrowseDialog" type="submit" class="ghost">Close</button>
|
|
693
|
+
</div>
|
|
694
|
+
<div class="dialog-body">
|
|
695
|
+
<input id="browseFilter" type="text" placeholder="Filter files by path" spellcheck="false">
|
|
696
|
+
<div id="browseStatus" class="status">Loading...</div>
|
|
697
|
+
<div id="browseList" class="browse-list"></div>
|
|
698
|
+
</div>
|
|
699
|
+
</form>
|
|
700
|
+
</dialog>
|
|
701
|
+
|
|
702
|
+
<script>
|
|
703
|
+
const modelUriInput = document.getElementById('modelUri');
|
|
704
|
+
const sparqlInput = document.getElementById('sparql');
|
|
705
|
+
const runButton = document.getElementById('runQueryButton');
|
|
706
|
+
const browseModelsButton = document.getElementById('browseModelsButton');
|
|
707
|
+
const statusNode = document.getElementById('status');
|
|
708
|
+
const tableStatus = document.getElementById('tableStatus');
|
|
709
|
+
const tableViewport = document.getElementById('tableViewport');
|
|
710
|
+
const tableShell = document.getElementById('tableShell');
|
|
711
|
+
const rawOutput = document.getElementById('rawOutput');
|
|
712
|
+
const pagerMeta = document.getElementById('pagerMeta');
|
|
713
|
+
const prevPageButton = document.getElementById('prevPageButton');
|
|
714
|
+
const nextPageButton = document.getElementById('nextPageButton');
|
|
715
|
+
const browseDialog = document.getElementById('browseDialog');
|
|
716
|
+
const browseStatus = document.getElementById('browseStatus');
|
|
717
|
+
const browseList = document.getElementById('browseList');
|
|
718
|
+
const browseFilter = document.getElementById('browseFilter');
|
|
719
|
+
|
|
720
|
+
let browseEntries = [];
|
|
721
|
+
let selectedModelUri = '';
|
|
722
|
+
let currentTableColumns = [];
|
|
723
|
+
let currentTableRows = [];
|
|
724
|
+
let currentPageIndex = 0;
|
|
725
|
+
const pageSize = 12;
|
|
726
|
+
|
|
727
|
+
function escapeClientHtml(value) {
|
|
728
|
+
return String(value)
|
|
729
|
+
.replace(/&/g, '&')
|
|
730
|
+
.replace(/</g, '<')
|
|
731
|
+
.replace(/>/g, '>')
|
|
732
|
+
.replace(/"/g, '"')
|
|
733
|
+
.replace(/'/g, ''');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function setStatus(message, kind) {
|
|
737
|
+
statusNode.textContent = message;
|
|
738
|
+
statusNode.className = kind ? 'status ' + kind : 'status';
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function setTableStatus(message) {
|
|
742
|
+
tableStatus.textContent = message;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function showTableMessage(message) {
|
|
746
|
+
tableShell.innerHTML = '<div class="empty">' + escapeClientHtml(message) + '</div>';
|
|
747
|
+
tableViewport.scrollTop = 0;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function formatTerm(term) {
|
|
751
|
+
if (!term || typeof term !== 'object') {
|
|
752
|
+
return '';
|
|
753
|
+
}
|
|
754
|
+
if (term.termType === 'Literal') {
|
|
755
|
+
const suffix = term.language ? '@' + term.language : (term.datatype ? '^^' + term.datatype : '');
|
|
756
|
+
return '"' + term.value + '"' + suffix;
|
|
757
|
+
}
|
|
758
|
+
return String(term.value ?? '');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function resetTable(message) {
|
|
762
|
+
currentTableColumns = [];
|
|
763
|
+
currentTableRows = [];
|
|
764
|
+
currentPageIndex = 0;
|
|
765
|
+
showTableMessage(message);
|
|
766
|
+
pagerMeta.textContent = 'No rows.';
|
|
767
|
+
prevPageButton.disabled = true;
|
|
768
|
+
nextPageButton.disabled = true;
|
|
769
|
+
setTableStatus(message);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function renderTablePage() {
|
|
773
|
+
if (currentTableRows.length === 0 || currentTableColumns.length === 0) {
|
|
774
|
+
resetTable('The SELECT query returned no rows.');
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const totalPages = Math.max(1, Math.ceil(currentTableRows.length / pageSize));
|
|
779
|
+
if (currentPageIndex >= totalPages) {
|
|
780
|
+
currentPageIndex = totalPages - 1;
|
|
781
|
+
}
|
|
782
|
+
const startIndex = currentPageIndex * pageSize;
|
|
783
|
+
const pageRows = currentTableRows.slice(startIndex, startIndex + pageSize);
|
|
784
|
+
const head = '<thead><tr>' + currentTableColumns.map((column) => '<th>' + escapeClientHtml(column) + '</th>').join('') + '</tr></thead>';
|
|
785
|
+
const body = '<tbody>' + pageRows.map((row) => {
|
|
786
|
+
return '<tr>' + currentTableColumns.map((column) => '<td>' + escapeClientHtml(formatTerm(row[column])) + '</td>').join('') + '</tr>';
|
|
787
|
+
}).join('') + '</tbody>';
|
|
788
|
+
tableShell.innerHTML = '<table>' + head + body + '</table>';
|
|
789
|
+
tableViewport.scrollTop = 0;
|
|
790
|
+
|
|
791
|
+
const from = startIndex + 1;
|
|
792
|
+
const to = startIndex + pageRows.length;
|
|
793
|
+
pagerMeta.textContent = 'Rows ' + from + '-' + to + ' of ' + currentTableRows.length + ' | Page ' + (currentPageIndex + 1) + ' of ' + totalPages;
|
|
794
|
+
prevPageButton.disabled = currentPageIndex === 0;
|
|
795
|
+
nextPageButton.disabled = currentPageIndex >= totalPages - 1;
|
|
796
|
+
setTableStatus('');
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function renderSelectTable(result) {
|
|
800
|
+
const rows = Array.isArray(result.rows) ? result.rows : [];
|
|
801
|
+
if (rows.length === 0) {
|
|
802
|
+
resetTable('The SELECT query returned no rows.');
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
currentTableColumns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
806
|
+
currentTableRows = rows;
|
|
807
|
+
currentPageIndex = 0;
|
|
808
|
+
renderTablePage();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function renderBrowseEntries(entries) {
|
|
812
|
+
if (entries.length === 0) {
|
|
813
|
+
browseList.innerHTML = '<div class="empty">No OML files found for the current filter.</div>';
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
browseList.innerHTML = entries.map((entry) => {
|
|
817
|
+
const isSelected = selectedModelUri && selectedModelUri === entry.uri;
|
|
818
|
+
const selectedClass = isSelected ? ' selected' : '';
|
|
819
|
+
return '<button type="button" class="browse-entry' + selectedClass + '" data-uri="' + escapeClientHtml(entry.uri) + '" data-path="' + escapeClientHtml(entry.path) + '">'
|
|
820
|
+
+ '<div class="browse-entry-path" title="' + escapeClientHtml(entry.path) + '">' + escapeClientHtml(entry.path) + '</div>'
|
|
821
|
+
+ '</button>';
|
|
822
|
+
}).join('');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function applyBrowseFilter() {
|
|
826
|
+
const filter = (browseFilter.value || '').trim().toLowerCase();
|
|
827
|
+
const filtered = filter.length === 0
|
|
828
|
+
? browseEntries
|
|
829
|
+
: browseEntries.filter((entry) => entry.path.toLowerCase().includes(filter));
|
|
830
|
+
renderBrowseEntries(filtered);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async function loadWorkspaceModels() {
|
|
834
|
+
browseStatus.textContent = 'Loading workspace OML files...';
|
|
835
|
+
browseStatus.className = 'status';
|
|
836
|
+
try {
|
|
837
|
+
const response = await fetch('/v0/models');
|
|
838
|
+
const payload = await response.json();
|
|
839
|
+
if (!response.ok) {
|
|
840
|
+
throw new Error(payload.error || 'Unable to list workspace OML files.');
|
|
841
|
+
}
|
|
842
|
+
browseEntries = Array.isArray(payload.files) ? payload.files : [];
|
|
843
|
+
browseStatus.textContent = 'Loaded ' + browseEntries.length + ' workspace OML file(s).';
|
|
844
|
+
browseStatus.className = 'status success';
|
|
845
|
+
applyBrowseFilter();
|
|
846
|
+
} catch (error) {
|
|
847
|
+
browseEntries = [];
|
|
848
|
+
browseList.innerHTML = '<div class="empty">Unable to load workspace OML files.</div>';
|
|
849
|
+
browseStatus.textContent = error instanceof Error ? error.message : String(error);
|
|
850
|
+
browseStatus.className = 'status error';
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function validateModelUri(modelUri) {
|
|
855
|
+
if (!modelUri) {
|
|
856
|
+
return 'OML file is required.';
|
|
857
|
+
}
|
|
858
|
+
if (!modelUri.startsWith('file://')) {
|
|
859
|
+
return 'OML file must be a file:// URI for an .oml file under the server workspace.';
|
|
860
|
+
}
|
|
861
|
+
if (modelUri.toLowerCase().endsWith('.md')) {
|
|
862
|
+
return 'Markdown files are not queryable directly. Choose an .oml file or resolve the markdown document\\'s ontology context first.';
|
|
863
|
+
}
|
|
864
|
+
if (!modelUri.toLowerCase().endsWith('.oml')) {
|
|
865
|
+
return 'Choose an .oml model file.';
|
|
866
|
+
}
|
|
867
|
+
return undefined;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function executeQuery() {
|
|
871
|
+
const modelUri = selectedModelUri.trim();
|
|
872
|
+
const sparql = sparqlInput.value.trim();
|
|
873
|
+
const modelUriError = validateModelUri(modelUri);
|
|
874
|
+
if (modelUriError) {
|
|
875
|
+
setStatus(modelUriError, 'error');
|
|
876
|
+
rawOutput.textContent = JSON.stringify({ error: modelUriError }, null, 2);
|
|
877
|
+
resetTable('Choose a valid OML model file to run a query.');
|
|
878
|
+
browseModelsButton.focus();
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (!sparql) {
|
|
882
|
+
setStatus('Query is required.', 'error');
|
|
883
|
+
rawOutput.textContent = JSON.stringify({ error: 'Query is required.' }, null, 2);
|
|
884
|
+
resetTable('Table rendering is available only for successful SELECT queries.');
|
|
885
|
+
sparqlInput.focus();
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
runButton.disabled = true;
|
|
890
|
+
setStatus('Running query...', '');
|
|
891
|
+
|
|
892
|
+
try {
|
|
893
|
+
const response = await fetch('/v0/query', {
|
|
894
|
+
method: 'POST',
|
|
895
|
+
headers: { 'content-type': 'application/json' },
|
|
896
|
+
body: JSON.stringify({ modelUri, sparql })
|
|
897
|
+
});
|
|
898
|
+
const payload = await response.json();
|
|
899
|
+
rawOutput.textContent = JSON.stringify(payload, null, 2);
|
|
900
|
+
|
|
901
|
+
if (!response.ok) {
|
|
902
|
+
resetTable('Raw response is shown below.');
|
|
903
|
+
setStatus(payload.error || 'Request failed.', 'error');
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const result = payload && typeof payload === 'object' ? payload.result : undefined;
|
|
908
|
+
const kind = result && typeof result.kind === 'string' ? result.kind : 'unknown';
|
|
909
|
+
if (kind === 'select') {
|
|
910
|
+
renderSelectTable(result);
|
|
911
|
+
} else {
|
|
912
|
+
resetTable('Raw response is shown below. Table rendering is available only for SELECT queries.');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (result && result.success === false) {
|
|
916
|
+
setStatus(result.error || 'Query completed with an error.', 'error');
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const warnings = Array.isArray(result && result.warnings) ? result.warnings.length : 0;
|
|
921
|
+
const warningSuffix = warnings > 0 ? ' ' + warnings + ' warning(s).' : '';
|
|
922
|
+
setStatus('Query completed as ' + kind.toUpperCase() + '.' + warningSuffix, 'success');
|
|
923
|
+
} catch (error) {
|
|
924
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
925
|
+
resetTable('Raw response is shown below.');
|
|
926
|
+
rawOutput.textContent = JSON.stringify({ error: errorMessage }, null, 2);
|
|
927
|
+
setStatus('Request failed. See JSON result.', 'error');
|
|
928
|
+
} finally {
|
|
929
|
+
runButton.disabled = false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
browseModelsButton.addEventListener('click', async () => {
|
|
934
|
+
if (typeof browseDialog.showModal === 'function') {
|
|
935
|
+
browseDialog.showModal();
|
|
936
|
+
}
|
|
937
|
+
await loadWorkspaceModels();
|
|
938
|
+
browseFilter.focus();
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
browseFilter.addEventListener('input', () => {
|
|
942
|
+
applyBrowseFilter();
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
browseList.addEventListener('click', (event) => {
|
|
946
|
+
const target = event.target && event.target.closest ? event.target.closest('button[data-uri]') : null;
|
|
947
|
+
if (!target) {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const uri = target.getAttribute('data-uri');
|
|
951
|
+
const relativePath = target.getAttribute('data-path');
|
|
952
|
+
if (!uri) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
selectedModelUri = uri;
|
|
956
|
+
modelUriInput.value = relativePath || '';
|
|
957
|
+
setStatus('Selected workspace OML file.', 'success');
|
|
958
|
+
applyBrowseFilter();
|
|
959
|
+
if (browseDialog.open) {
|
|
960
|
+
browseDialog.close();
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
prevPageButton.addEventListener('click', () => {
|
|
965
|
+
if (currentPageIndex === 0) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
currentPageIndex -= 1;
|
|
969
|
+
renderTablePage();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
nextPageButton.addEventListener('click', () => {
|
|
973
|
+
if ((currentPageIndex + 1) * pageSize >= currentTableRows.length) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
currentPageIndex += 1;
|
|
977
|
+
renderTablePage();
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
runButton.addEventListener('click', () => {
|
|
981
|
+
void executeQuery();
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
sparqlInput.addEventListener('keydown', (event) => {
|
|
985
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
986
|
+
event.preventDefault();
|
|
987
|
+
void executeQuery();
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
</script>
|
|
991
|
+
</body>
|
|
992
|
+
</html>`;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async function readJsonBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
|
|
996
|
+
const chunks: Buffer[] = [];
|
|
997
|
+
let total = 0;
|
|
998
|
+
for await (const chunk of req) {
|
|
999
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1000
|
+
total += buffer.length;
|
|
1001
|
+
if (total > DEFAULT_BODY_LIMIT_BYTES) {
|
|
1002
|
+
throw new Error(`Request body exceeds ${DEFAULT_BODY_LIMIT_BYTES} bytes.`);
|
|
1003
|
+
}
|
|
1004
|
+
chunks.push(buffer);
|
|
1005
|
+
}
|
|
1006
|
+
if (chunks.length === 0) {
|
|
1007
|
+
return {};
|
|
1008
|
+
}
|
|
1009
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
|
|
1010
|
+
return typeof parsed === 'object' && parsed !== null ? parsed as Record<string, unknown> : {};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, method: string): Promise<T> {
|
|
1014
|
+
return new Promise<T>((resolve, reject) => {
|
|
1015
|
+
const timer = setTimeout(() => {
|
|
1016
|
+
reject(new Error(`LSP request timeout for '${method}' after ${timeoutMs}ms.`));
|
|
1017
|
+
}, timeoutMs);
|
|
1018
|
+
void promise.then(
|
|
1019
|
+
(value) => {
|
|
1020
|
+
clearTimeout(timer);
|
|
1021
|
+
resolve(value);
|
|
1022
|
+
},
|
|
1023
|
+
(error: unknown) => {
|
|
1024
|
+
clearTimeout(timer);
|
|
1025
|
+
reject(error);
|
|
1026
|
+
},
|
|
1027
|
+
);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function normalizeOntologyNamespace(value: string | undefined): string | undefined {
|
|
1032
|
+
if (!value) {
|
|
1033
|
+
return undefined;
|
|
1034
|
+
}
|
|
1035
|
+
const normalized = value.replace(/^<|>$/g, '').replace(/[\/#]+$/, '');
|
|
1036
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function resolveOutputPathFromOntologyIriString(ontologyIri: string, format: 'ttl' | 'trig' | 'nt' | 'nq' | 'n3'): string {
|
|
1040
|
+
const iri = new URL(ontologyIri);
|
|
1041
|
+
const rawPath = iri.pathname.replace(/\/+$/, '');
|
|
1042
|
+
const pathSegments = rawPath.split('/').filter(Boolean);
|
|
1043
|
+
const fileStem = pathSegments.at(-1) || 'index';
|
|
1044
|
+
const directorySegments = iri.host
|
|
1045
|
+
? [iri.host, ...pathSegments.slice(0, -1)]
|
|
1046
|
+
: pathSegments.slice(0, -1);
|
|
1047
|
+
return path.join(...directorySegments, `${fileStem}.${format}`);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async function serializeQuads(
|
|
1051
|
+
quads: any[],
|
|
1052
|
+
format: 'ttl' | 'trig' | 'nt' | 'nq' | 'n3',
|
|
1053
|
+
pretty = false,
|
|
1054
|
+
): Promise<string> {
|
|
1055
|
+
const writer = new Writer({
|
|
1056
|
+
format: format === 'ttl'
|
|
1057
|
+
? 'text/turtle'
|
|
1058
|
+
: (format === 'trig'
|
|
1059
|
+
? 'application/trig'
|
|
1060
|
+
: (format === 'nt'
|
|
1061
|
+
? 'N-Triples'
|
|
1062
|
+
: (format === 'nq' ? 'N-Quads' : 'application/n3'))),
|
|
1063
|
+
});
|
|
1064
|
+
writer.addQuads(quads);
|
|
1065
|
+
const serialized = await new Promise<string>((resolve, reject) => {
|
|
1066
|
+
writer.end((error, result) => {
|
|
1067
|
+
if (error) {
|
|
1068
|
+
reject(error);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
resolve(result);
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
return pretty ? prettyPrintRdf(serialized, format) : serialized;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function prettyPrintRdf(serialized: string, format: 'ttl' | 'trig' | 'nt' | 'nq' | 'n3'): string {
|
|
1078
|
+
if (format !== 'ttl' && format !== 'trig') {
|
|
1079
|
+
return serialized;
|
|
1080
|
+
}
|
|
1081
|
+
const lines = serialized.split('\n');
|
|
1082
|
+
const out: string[] = [];
|
|
1083
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1084
|
+
const line = lines[i];
|
|
1085
|
+
out.push(line);
|
|
1086
|
+
if (line.trim() === '.') {
|
|
1087
|
+
const next = lines[i + 1];
|
|
1088
|
+
if (next !== undefined && next.trim().length > 0) {
|
|
1089
|
+
out.push('');
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return out.join('\n');
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function toSparqlRowDto(row: Map<string, any>): Record<string, {
|
|
1097
|
+
termType: 'NamedNode' | 'Literal' | 'BlankNode';
|
|
1098
|
+
value: string;
|
|
1099
|
+
datatype?: string;
|
|
1100
|
+
language?: string;
|
|
1101
|
+
}> {
|
|
1102
|
+
const result: Record<string, {
|
|
1103
|
+
termType: 'NamedNode' | 'Literal' | 'BlankNode';
|
|
1104
|
+
value: string;
|
|
1105
|
+
datatype?: string;
|
|
1106
|
+
language?: string;
|
|
1107
|
+
}> = {};
|
|
1108
|
+
for (const [key, term] of row.entries()) {
|
|
1109
|
+
if (!term) {
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
result[key] = {
|
|
1113
|
+
termType: term.termType,
|
|
1114
|
+
value: term.value,
|
|
1115
|
+
datatype: term.datatype?.value,
|
|
1116
|
+
language: term.language,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
return result;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1123
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
type RenderedBlockResult = MdBlockExecutionResult & {
|
|
1127
|
+
options?: Record<string, unknown>;
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
function toMdBlockKind(language: string): MdBlockKind | undefined {
|
|
1131
|
+
return SUPPORTED_MD_BLOCK_KINDS.has(language as MdBlockKind) ? (language as MdBlockKind) : undefined;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function toExecutableBlocks(codeBlocks: ReadonlyArray<{
|
|
1135
|
+
id: string;
|
|
1136
|
+
language: string;
|
|
1137
|
+
content: string;
|
|
1138
|
+
meta?: string;
|
|
1139
|
+
options?: Record<string, unknown>;
|
|
1140
|
+
lineStart: number;
|
|
1141
|
+
lineEnd: number;
|
|
1142
|
+
}>): MdExecutableBlock[] {
|
|
1143
|
+
const executable: MdExecutableBlock[] = [];
|
|
1144
|
+
for (const block of codeBlocks) {
|
|
1145
|
+
const kind = toMdBlockKind(block.language);
|
|
1146
|
+
if (!kind) {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
executable.push({
|
|
1150
|
+
id: block.id,
|
|
1151
|
+
kind,
|
|
1152
|
+
source: block.content,
|
|
1153
|
+
meta: block.meta,
|
|
1154
|
+
options: block.options,
|
|
1155
|
+
lineStart: block.lineStart,
|
|
1156
|
+
lineEnd: block.lineEnd,
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
return executable;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function splitHref(href: string): { path: string; query: string; fragment: string } {
|
|
1163
|
+
const hashIndex = href.indexOf('#');
|
|
1164
|
+
const pathAndQuery = hashIndex >= 0 ? href.slice(0, hashIndex) : href;
|
|
1165
|
+
const fragment = hashIndex >= 0 ? href.slice(hashIndex) : '';
|
|
1166
|
+
const queryIndex = pathAndQuery.indexOf('?');
|
|
1167
|
+
return {
|
|
1168
|
+
path: queryIndex >= 0 ? pathAndQuery.slice(0, queryIndex) : pathAndQuery,
|
|
1169
|
+
query: queryIndex >= 0 ? pathAndQuery.slice(queryIndex) : '',
|
|
1170
|
+
fragment
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function resolveWorkspacePath(workspaceRoot: string, hrefPath: string): string | undefined {
|
|
1175
|
+
const relative = hrefPath.slice('workspace:/'.length).replace(/^\/+/, '');
|
|
1176
|
+
const resolved = path.resolve(workspaceRoot, relative);
|
|
1177
|
+
const relativeToWorkspace = path.relative(workspaceRoot, resolved);
|
|
1178
|
+
if (relativeToWorkspace.startsWith('..') || path.isAbsolute(relativeToWorkspace)) {
|
|
1179
|
+
return undefined;
|
|
1180
|
+
}
|
|
1181
|
+
return resolved;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function toRelativeWebPath(fromDir: string, toFile: string): string {
|
|
1185
|
+
const relative = path.relative(fromDir, toFile).split(path.sep).join('/');
|
|
1186
|
+
if (!relative || relative === '.') {
|
|
1187
|
+
return '.';
|
|
1188
|
+
}
|
|
1189
|
+
return relative.startsWith('.') ? relative : `./${relative}`;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function isImagePath(filePath: string): boolean {
|
|
1193
|
+
return /\.(png|jpe?g|gif|svg|webp|bmp|ico|avif)$/i.test(filePath);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function resolveAssetTargetPath(
|
|
1197
|
+
resolvedFile: string,
|
|
1198
|
+
context: {
|
|
1199
|
+
workspaceRoot: string;
|
|
1200
|
+
inputRoot: string;
|
|
1201
|
+
outputRoot: string;
|
|
1202
|
+
}
|
|
1203
|
+
): string {
|
|
1204
|
+
const workspaceRelative = path.relative(context.workspaceRoot, resolvedFile);
|
|
1205
|
+
const markdownRelative = path.relative(context.inputRoot, resolvedFile);
|
|
1206
|
+
const isInsideMarkdownRoot = !markdownRelative.startsWith('..') && !path.isAbsolute(markdownRelative);
|
|
1207
|
+
if (isImagePath(resolvedFile)) {
|
|
1208
|
+
const imageRelative = (isInsideMarkdownRoot ? markdownRelative : workspaceRelative).split(path.sep).join('/');
|
|
1209
|
+
return path.join(context.outputRoot, 'assets', 'images', imageRelative);
|
|
1210
|
+
}
|
|
1211
|
+
const outputRelative = isInsideMarkdownRoot ? markdownRelative : workspaceRelative;
|
|
1212
|
+
return path.join(context.outputRoot, outputRelative);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function rewriteHref(
|
|
1216
|
+
href: string,
|
|
1217
|
+
context: {
|
|
1218
|
+
workspaceRoot: string;
|
|
1219
|
+
inputRoot: string;
|
|
1220
|
+
inputFile: string;
|
|
1221
|
+
outputRoot: string;
|
|
1222
|
+
outputFile: string;
|
|
1223
|
+
},
|
|
1224
|
+
workspaceAssets: Set<string>
|
|
1225
|
+
): string {
|
|
1226
|
+
const parts = splitHref(href);
|
|
1227
|
+
if (!parts.path) {
|
|
1228
|
+
return href;
|
|
1229
|
+
}
|
|
1230
|
+
const trimmedPath = parts.path.trim();
|
|
1231
|
+
if (!trimmedPath || trimmedPath.startsWith('#') || trimmedPath.startsWith('//')) {
|
|
1232
|
+
return href;
|
|
1233
|
+
}
|
|
1234
|
+
if (trimmedPath.startsWith('workspace:/')) {
|
|
1235
|
+
const resolved = resolveWorkspacePath(context.workspaceRoot, trimmedPath);
|
|
1236
|
+
if (!resolved) {
|
|
1237
|
+
return href;
|
|
1238
|
+
}
|
|
1239
|
+
if (!resolved.toLowerCase().endsWith('.md')) {
|
|
1240
|
+
workspaceAssets.add(resolved);
|
|
1241
|
+
}
|
|
1242
|
+
const targetInOutput = resolved.toLowerCase().endsWith('.md')
|
|
1243
|
+
? path.join(
|
|
1244
|
+
context.outputRoot,
|
|
1245
|
+
path.relative(context.inputRoot, resolved).replace(/\.md$/i, '.html')
|
|
1246
|
+
)
|
|
1247
|
+
: resolveAssetTargetPath(resolved, context);
|
|
1248
|
+
const rewrittenPath = toRelativeWebPath(path.dirname(context.outputFile), targetInOutput);
|
|
1249
|
+
return `${rewrittenPath}${parts.query}${parts.fragment}`;
|
|
1250
|
+
}
|
|
1251
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmedPath)) {
|
|
1252
|
+
return href;
|
|
1253
|
+
}
|
|
1254
|
+
if (!trimmedPath.startsWith('/')) {
|
|
1255
|
+
const resolved = path.resolve(path.dirname(context.inputFile), trimmedPath);
|
|
1256
|
+
const workspaceRelative = path.relative(context.workspaceRoot, resolved);
|
|
1257
|
+
if (!workspaceRelative.startsWith('..') && !path.isAbsolute(workspaceRelative)) {
|
|
1258
|
+
if (!resolved.toLowerCase().endsWith('.md')) {
|
|
1259
|
+
workspaceAssets.add(resolved);
|
|
1260
|
+
}
|
|
1261
|
+
const targetInOutput = resolved.toLowerCase().endsWith('.md')
|
|
1262
|
+
? path.join(
|
|
1263
|
+
context.outputRoot,
|
|
1264
|
+
path.relative(context.inputRoot, resolved).replace(/\.md$/i, '.html')
|
|
1265
|
+
)
|
|
1266
|
+
: resolveAssetTargetPath(resolved, context);
|
|
1267
|
+
const rewrittenPath = toRelativeWebPath(path.dirname(context.outputFile), targetInOutput);
|
|
1268
|
+
return `${rewrittenPath}${parts.query}${parts.fragment}`;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const rewrittenPath = trimmedPath.toLowerCase().endsWith('.md')
|
|
1272
|
+
? trimmedPath.slice(0, -3) + '.html'
|
|
1273
|
+
: trimmedPath;
|
|
1274
|
+
return `${rewrittenPath}${parts.query}${parts.fragment}`;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function rewriteRenderedLinks(
|
|
1278
|
+
content: string,
|
|
1279
|
+
context: {
|
|
1280
|
+
workspaceRoot: string;
|
|
1281
|
+
inputRoot: string;
|
|
1282
|
+
inputFile: string;
|
|
1283
|
+
outputRoot: string;
|
|
1284
|
+
outputFile: string;
|
|
1285
|
+
}
|
|
1286
|
+
): {
|
|
1287
|
+
html: string;
|
|
1288
|
+
workspaceAssets: Set<string>;
|
|
1289
|
+
} {
|
|
1290
|
+
const workspaceAssets = new Set<string>();
|
|
1291
|
+
const html = content.replace(/(\b(?:href|src)\s*=\s*)(["'])([^"']+)\2/gi, (_match, prefix: string, quote: string, rawHref: string) => {
|
|
1292
|
+
const rewritten = rewriteHref(rawHref, context, workspaceAssets);
|
|
1293
|
+
return `${prefix}${quote}${rewritten}${quote}`;
|
|
1294
|
+
});
|
|
1295
|
+
return { html, workspaceAssets };
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function rewriteAssetReference(
|
|
1299
|
+
rawValue: string,
|
|
1300
|
+
context: {
|
|
1301
|
+
workspaceRoot: string;
|
|
1302
|
+
inputRoot: string;
|
|
1303
|
+
sourceFile: string;
|
|
1304
|
+
outputRoot: string;
|
|
1305
|
+
outputFile: string;
|
|
1306
|
+
}
|
|
1307
|
+
): { rewritten: string; sourceAsset?: string } {
|
|
1308
|
+
const trimmed = rawValue.trim();
|
|
1309
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('data:') || trimmed.startsWith('//')) {
|
|
1310
|
+
return { rewritten: rawValue };
|
|
1311
|
+
}
|
|
1312
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !trimmed.startsWith('workspace:/')) {
|
|
1313
|
+
return { rewritten: rawValue };
|
|
1314
|
+
}
|
|
1315
|
+
let resolved: string | undefined;
|
|
1316
|
+
if (trimmed.startsWith('workspace:/')) {
|
|
1317
|
+
resolved = resolveWorkspacePath(context.workspaceRoot, trimmed);
|
|
1318
|
+
} else if (trimmed.startsWith('/')) {
|
|
1319
|
+
const candidate = path.resolve(context.workspaceRoot, trimmed.replace(/^\/+/, ''));
|
|
1320
|
+
const relative = path.relative(context.workspaceRoot, candidate);
|
|
1321
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
1322
|
+
resolved = candidate;
|
|
1323
|
+
}
|
|
1324
|
+
} else {
|
|
1325
|
+
const candidate = path.resolve(path.dirname(context.sourceFile), trimmed);
|
|
1326
|
+
const relative = path.relative(context.workspaceRoot, candidate);
|
|
1327
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
1328
|
+
resolved = candidate;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (!resolved) {
|
|
1332
|
+
return { rewritten: rawValue };
|
|
1333
|
+
}
|
|
1334
|
+
const target = resolveAssetTargetPath(resolved, {
|
|
1335
|
+
workspaceRoot: context.workspaceRoot,
|
|
1336
|
+
inputRoot: context.inputRoot,
|
|
1337
|
+
outputRoot: context.outputRoot,
|
|
1338
|
+
});
|
|
1339
|
+
return {
|
|
1340
|
+
rewritten: toRelativeWebPath(path.dirname(context.outputFile), target),
|
|
1341
|
+
sourceAsset: resolved,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function rewriteBlockResultAssetPaths(
|
|
1346
|
+
result: RenderedBlockResult,
|
|
1347
|
+
context: {
|
|
1348
|
+
workspaceRoot: string;
|
|
1349
|
+
inputRoot: string;
|
|
1350
|
+
sourceFile: string;
|
|
1351
|
+
outputRoot: string;
|
|
1352
|
+
outputFile: string;
|
|
1353
|
+
},
|
|
1354
|
+
workspaceAssets: Set<string>,
|
|
1355
|
+
): RenderedBlockResult {
|
|
1356
|
+
if (!result.options || result.kind !== 'diagram') {
|
|
1357
|
+
return result;
|
|
1358
|
+
}
|
|
1359
|
+
const stylesheet = result.options.stylesheet;
|
|
1360
|
+
if (!Array.isArray(stylesheet)) {
|
|
1361
|
+
return result;
|
|
1362
|
+
}
|
|
1363
|
+
let changed = false;
|
|
1364
|
+
const rewrittenStylesheet = stylesheet.map((entry) => {
|
|
1365
|
+
if (!isRecord(entry) || !isRecord(entry.style) || !isRecord(entry.style.icon)) {
|
|
1366
|
+
return entry;
|
|
1367
|
+
}
|
|
1368
|
+
let entryChanged = false;
|
|
1369
|
+
const icon = { ...entry.style.icon } as Record<string, unknown>;
|
|
1370
|
+
for (const key of ['href', 'xlinkHref', 'xlink:href']) {
|
|
1371
|
+
const value = icon[key];
|
|
1372
|
+
if (typeof value !== 'string') {
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
const rewritten = rewriteAssetReference(value, context);
|
|
1376
|
+
if (rewritten.sourceAsset) {
|
|
1377
|
+
workspaceAssets.add(rewritten.sourceAsset);
|
|
1378
|
+
}
|
|
1379
|
+
if (rewritten.rewritten !== value) {
|
|
1380
|
+
icon[key] = rewritten.rewritten;
|
|
1381
|
+
changed = true;
|
|
1382
|
+
entryChanged = true;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (!entryChanged) {
|
|
1386
|
+
return entry;
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
...entry,
|
|
1390
|
+
style: {
|
|
1391
|
+
...entry.style,
|
|
1392
|
+
icon,
|
|
1393
|
+
},
|
|
1394
|
+
};
|
|
1395
|
+
});
|
|
1396
|
+
if (!changed) {
|
|
1397
|
+
return result;
|
|
1398
|
+
}
|
|
1399
|
+
return {
|
|
1400
|
+
...result,
|
|
1401
|
+
options: {
|
|
1402
|
+
...result.options,
|
|
1403
|
+
stylesheet: rewrittenStylesheet,
|
|
1404
|
+
},
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function sanitizeBlockId(value: string): string {
|
|
1409
|
+
const trimmed = value.trim();
|
|
1410
|
+
if (!trimmed) {
|
|
1411
|
+
return 'block';
|
|
1412
|
+
}
|
|
1413
|
+
return trimmed.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
async function writeBlockArtifacts(outputFile: string, results: ReadonlyArray<RenderedBlockResult>): Promise<{
|
|
1417
|
+
count: number;
|
|
1418
|
+
manifest: Array<{ blockId: string; path: string }>;
|
|
1419
|
+
}> {
|
|
1420
|
+
if (results.length === 0) {
|
|
1421
|
+
return { count: 0, manifest: [] };
|
|
1422
|
+
}
|
|
1423
|
+
const blockDirName = `${path.basename(outputFile, '.html')}.blocks`;
|
|
1424
|
+
const blockDir = path.join(path.dirname(outputFile), blockDirName);
|
|
1425
|
+
await fs.mkdir(blockDir, { recursive: true });
|
|
1426
|
+
const manifest: Array<{ blockId: string; path: string }> = [];
|
|
1427
|
+
for (const result of results) {
|
|
1428
|
+
const safeId = sanitizeBlockId(result.blockId);
|
|
1429
|
+
const fileName = `${safeId}.json`;
|
|
1430
|
+
const filePath = path.join(blockDir, fileName);
|
|
1431
|
+
await fs.writeFile(filePath, JSON.stringify(result, null, 2), 'utf-8');
|
|
1432
|
+
manifest.push({
|
|
1433
|
+
blockId: result.blockId,
|
|
1434
|
+
path: `./${blockDirName}/${fileName}`
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
return { count: results.length, manifest };
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
async function loadStaticRuntimeBundle(): Promise<string> {
|
|
1441
|
+
const require = createRequire(import.meta.url);
|
|
1442
|
+
const staticEntry = require.resolve('@oml/markdown/static');
|
|
1443
|
+
const bundlePath = path.join(path.dirname(staticEntry), STATIC_MARKDOWN_RUNTIME_BUNDLE_FILE);
|
|
1444
|
+
try {
|
|
1445
|
+
return await fs.readFile(bundlePath, 'utf-8');
|
|
1446
|
+
} catch {
|
|
1447
|
+
throw new Error(`Unable to load markdown static runtime bundle at '${bundlePath}'.`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function loadCodeBlockStylesheet(): Promise<string> {
|
|
1452
|
+
const require = createRequire(import.meta.url);
|
|
1453
|
+
const staticEntry = require.resolve('@oml/markdown/static');
|
|
1454
|
+
const stylesheetPath = path.resolve(path.dirname(staticEntry), '..', '..', 'src', 'static', 'markdown-webview.css');
|
|
1455
|
+
try {
|
|
1456
|
+
return await fs.readFile(stylesheetPath, 'utf-8');
|
|
1457
|
+
} catch {
|
|
1458
|
+
throw new Error(`Unable to load markdown webview stylesheet at '${stylesheetPath}'.`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
async function writeStaticAssets(outputRoot: string): Promise<{
|
|
1463
|
+
runtimeScriptFile: string;
|
|
1464
|
+
stylesheetFile: string;
|
|
1465
|
+
}> {
|
|
1466
|
+
const runtimeBundle = await loadStaticRuntimeBundle();
|
|
1467
|
+
const sourceStylesheet = await loadCodeBlockStylesheet();
|
|
1468
|
+
const mergedStylesheet = `${sourceStylesheet}\n\n${STATIC_MARKDOWN_RUNTIME_CSS}\n`;
|
|
1469
|
+
const runtimeVersion = createHash('sha1').update(runtimeBundle).digest('hex').slice(0, 12);
|
|
1470
|
+
const stylesheetVersion = createHash('sha1').update(mergedStylesheet).digest('hex').slice(0, 12);
|
|
1471
|
+
const runtimeFile = path.join(outputRoot, 'assets', `markdown-static-${runtimeVersion}.js`);
|
|
1472
|
+
const stylesheetFile = path.join(outputRoot, 'assets', `markdown-webview-${stylesheetVersion}.css`);
|
|
1473
|
+
await fs.mkdir(path.dirname(runtimeFile), { recursive: true });
|
|
1474
|
+
await fs.writeFile(runtimeFile, runtimeBundle, 'utf-8');
|
|
1475
|
+
await fs.writeFile(stylesheetFile, mergedStylesheet, 'utf-8');
|
|
1476
|
+
return { runtimeScriptFile: runtimeFile, stylesheetFile };
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function escapeAttribute(value: string): string {
|
|
1480
|
+
return value
|
|
1481
|
+
.replace(/&/g, '&')
|
|
1482
|
+
.replace(/</g, '<')
|
|
1483
|
+
.replace(/>/g, '>')
|
|
1484
|
+
.replace(/"/g, '"')
|
|
1485
|
+
.replace(/'/g, ''');
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function escapeJsonForScript(value: string): string {
|
|
1489
|
+
return value.replace(/</g, '\\u003c');
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function normalizeWikiPathKey(raw: string): string {
|
|
1493
|
+
const normalized = raw.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/');
|
|
1494
|
+
if (!normalized) {
|
|
1495
|
+
return '';
|
|
1496
|
+
}
|
|
1497
|
+
const withoutHtml = normalized.replace(/\.html$/i, '');
|
|
1498
|
+
return withoutHtml.replace(/^\.\//, '').toLowerCase();
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function buildWikiPageIndex(markdownFiles: ReadonlyArray<string>, inputRoot: string, outputRoot: string): Map<string, string> {
|
|
1502
|
+
const index = new Map<string, string>();
|
|
1503
|
+
for (const markdownFile of markdownFiles) {
|
|
1504
|
+
const relativeInput = path.relative(inputRoot, markdownFile).split(path.sep).join('/');
|
|
1505
|
+
const normalizedRelative = normalizeWikiPathKey(relativeInput.replace(/\.md$/i, ''));
|
|
1506
|
+
if (!normalizedRelative) {
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
const outputFile = path.join(outputRoot, relativeInput.replace(/\.md$/i, '.html'));
|
|
1510
|
+
index.set(normalizedRelative, outputFile);
|
|
1511
|
+
}
|
|
1512
|
+
return index;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function buildWikiLinkHrefMapForPage(
|
|
1516
|
+
wikiPageIndex: ReadonlyMap<string, string>,
|
|
1517
|
+
outputFile: string
|
|
1518
|
+
): Record<string, string> {
|
|
1519
|
+
const map: Record<string, string> = {};
|
|
1520
|
+
for (const [key, targetFile] of wikiPageIndex.entries()) {
|
|
1521
|
+
map[key] = toRelativeWebPath(path.dirname(outputFile), targetFile);
|
|
1522
|
+
}
|
|
1523
|
+
return map;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function normalizeIri(value: string): string {
|
|
1527
|
+
return value.trim().replace(/^<|>$/g, '');
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function inferOntologyIriFromMemberIri(memberIri: string): string | undefined {
|
|
1531
|
+
const normalized = normalizeIri(memberIri);
|
|
1532
|
+
if (!normalized) {
|
|
1533
|
+
return undefined;
|
|
1534
|
+
}
|
|
1535
|
+
const hashIndex = normalized.indexOf('#');
|
|
1536
|
+
if (hashIndex >= 0) {
|
|
1537
|
+
return normalized.slice(0, hashIndex).replace(/[\/#]+$/, '');
|
|
1538
|
+
}
|
|
1539
|
+
const schemeIndex = normalized.indexOf('://');
|
|
1540
|
+
const slashIndex = normalized.lastIndexOf('/');
|
|
1541
|
+
if (slashIndex > schemeIndex + 2) {
|
|
1542
|
+
return normalized.slice(0, slashIndex).replace(/[\/#]+$/, '');
|
|
1543
|
+
}
|
|
1544
|
+
return normalized.replace(/[\/#]+$/, '');
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function memberIriToOutputFile(outputRoot: string, memberIri: string): string | undefined {
|
|
1548
|
+
const normalized = normalizeIri(memberIri);
|
|
1549
|
+
let parsed: URL;
|
|
1550
|
+
try {
|
|
1551
|
+
parsed = new URL(normalized);
|
|
1552
|
+
} catch {
|
|
1553
|
+
return undefined;
|
|
1554
|
+
}
|
|
1555
|
+
const host = (parsed.hostname || '').trim().toLowerCase();
|
|
1556
|
+
if (!host) {
|
|
1557
|
+
return undefined;
|
|
1558
|
+
}
|
|
1559
|
+
const pathname = decodeURIComponent(parsed.pathname || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
1560
|
+
const fragment = decodeURIComponent(parsed.hash.replace(/^#/, '')).trim();
|
|
1561
|
+
const tailSegment = pathname.split('/').filter((segment) => segment.length > 0).pop() ?? '';
|
|
1562
|
+
const memberSegment = fragment || tailSegment;
|
|
1563
|
+
if (!memberSegment) {
|
|
1564
|
+
return undefined;
|
|
1565
|
+
}
|
|
1566
|
+
const safeMember = memberSegment.replace(/[^\w.-]/g, '_');
|
|
1567
|
+
const basePath = fragment
|
|
1568
|
+
? pathname
|
|
1569
|
+
: pathname.split('/').slice(0, -1).join('/');
|
|
1570
|
+
return path.join(outputRoot, host, basePath, `${safeMember}.html`);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
async function queryMemberTypes(
|
|
1574
|
+
reasoningService: any,
|
|
1575
|
+
modelUri: string,
|
|
1576
|
+
): Promise<Map<string, string[]>> {
|
|
1577
|
+
const queryResult = await reasoningService.getSparqlService().query(modelUri, `
|
|
1578
|
+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
|
1579
|
+
SELECT DISTINCT ?member ?type
|
|
1580
|
+
WHERE {
|
|
1581
|
+
GRAPH ?g {
|
|
1582
|
+
?member rdf:type ?type .
|
|
1583
|
+
FILTER(isIRI(?member) && isIRI(?type))
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
`);
|
|
1587
|
+
if (!queryResult?.success || !Array.isArray(queryResult.rows)) {
|
|
1588
|
+
return new Map<string, string[]>();
|
|
1589
|
+
}
|
|
1590
|
+
const byMember = new Map<string, Set<string>>();
|
|
1591
|
+
for (const row of queryResult.rows as Array<Map<string, { value: string } | undefined>>) {
|
|
1592
|
+
const member = normalizeIri(row.get('member')?.value ?? '');
|
|
1593
|
+
const type = normalizeIri(row.get('type')?.value ?? '');
|
|
1594
|
+
if (!member || !type) {
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
const existing = byMember.get(member) ?? new Set<string>();
|
|
1598
|
+
existing.add(type);
|
|
1599
|
+
byMember.set(member, existing);
|
|
1600
|
+
}
|
|
1601
|
+
return new Map<string, string[]>(
|
|
1602
|
+
Array.from(byMember.entries()).map(([member, types]) => [member, Array.from(types)])
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function buildIriAliasMapForPage(
|
|
1607
|
+
aliasesByIri: ReadonlyMap<string, string>,
|
|
1608
|
+
outputFile: string
|
|
1609
|
+
): Record<string, string> {
|
|
1610
|
+
const aliases: Record<string, string> = {};
|
|
1611
|
+
for (const [iri, absoluteTarget] of aliasesByIri.entries()) {
|
|
1612
|
+
aliases[iri] = toRelativeWebPath(path.dirname(outputFile), absoluteTarget);
|
|
1613
|
+
}
|
|
1614
|
+
return aliases;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function wrapHtml(
|
|
1618
|
+
content: string,
|
|
1619
|
+
runtimeScriptPath: string,
|
|
1620
|
+
stylesheetPath: string,
|
|
1621
|
+
blockManifest: Array<{ blockId: string; path: string }>,
|
|
1622
|
+
blockResults: ReadonlyArray<RenderedBlockResult>,
|
|
1623
|
+
wikiLinkHrefByKey: Record<string, string>,
|
|
1624
|
+
iriAliasByIri: Record<string, string>,
|
|
1625
|
+
): string {
|
|
1626
|
+
const escapedManifest = escapeJsonForScript(JSON.stringify(blockManifest));
|
|
1627
|
+
const inlineResults = Object.fromEntries(blockResults.map((result) => [result.blockId, result]));
|
|
1628
|
+
const escapedInlineResults = escapeJsonForScript(JSON.stringify(inlineResults));
|
|
1629
|
+
const escapedWikiIndex = escapeJsonForScript(JSON.stringify(wikiLinkHrefByKey));
|
|
1630
|
+
const escapedIriAliasIndex = escapeJsonForScript(JSON.stringify(iriAliasByIri));
|
|
1631
|
+
const escapedLinkingConfig = escapeJsonForScript(JSON.stringify({ linkingEnabled: true }));
|
|
1632
|
+
return `<!doctype html>
|
|
1633
|
+
<html lang="en">
|
|
1634
|
+
<head>
|
|
1635
|
+
<meta charset="UTF-8">
|
|
1636
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1637
|
+
<title>OML Markdown</title>
|
|
1638
|
+
<link rel="stylesheet" href="${escapeAttribute(stylesheetPath)}">
|
|
1639
|
+
</head>
|
|
1640
|
+
<body>
|
|
1641
|
+
${content}
|
|
1642
|
+
<script id="oml-md-block-manifest" type="application/json">${escapedManifest}</script>
|
|
1643
|
+
<script id="oml-md-block-inline-results" type="application/json">${escapedInlineResults}</script>
|
|
1644
|
+
<script id="oml-md-wikilink-index" type="application/json">${escapedWikiIndex}</script>
|
|
1645
|
+
<script id="oml-md-wikilink-iri-aliases" type="application/json">${escapedIriAliasIndex}</script>
|
|
1646
|
+
<script id="oml-md-wikilink-config" type="application/json">${escapedLinkingConfig}</script>
|
|
1647
|
+
<script id="oml-md-member-labels" type="application/json">{}</script>
|
|
1648
|
+
<script src="${escapeAttribute(runtimeScriptPath)}"></script>
|
|
1649
|
+
</body>
|
|
1650
|
+
</html>
|
|
1651
|
+
`;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
class InMemoryJsonRpcLspClient {
|
|
1655
|
+
private readonly workspaceRoot: string;
|
|
1656
|
+
private readonly requestTimeoutMs: number;
|
|
1657
|
+
private readonly clientConnection?: Connection;
|
|
1658
|
+
private readonly serverConnection?: Connection;
|
|
1659
|
+
private readonly clientToServer?: PassThrough;
|
|
1660
|
+
private readonly serverToClient?: PassThrough;
|
|
1661
|
+
private initPromise?: Promise<void>;
|
|
1662
|
+
private readonly runtime: OmlLanguageServerRuntime;
|
|
1663
|
+
private readonly useExternalRuntime: boolean;
|
|
1664
|
+
private readonly watchWorkspace: boolean;
|
|
1665
|
+
private readonly watchers: Set<fsSync.FSWatcher> = new Set();
|
|
1666
|
+
private watcherFlushTimer: NodeJS.Timeout | undefined;
|
|
1667
|
+
private watcherActive = false;
|
|
1668
|
+
private refreshInFlight = false;
|
|
1669
|
+
private refreshQueued = false;
|
|
1670
|
+
private initialWorkspaceSyncCompleted = false;
|
|
1671
|
+
|
|
1672
|
+
constructor(workspaceRoot: string, requestTimeoutMs: number, watchWorkspace: boolean, runtime?: OmlLanguageServerRuntime) {
|
|
1673
|
+
this.workspaceRoot = workspaceRoot;
|
|
1674
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
1675
|
+
this.useExternalRuntime = runtime !== undefined;
|
|
1676
|
+
this.watchWorkspace = runtime ? false : watchWorkspace;
|
|
1677
|
+
if (runtime) {
|
|
1678
|
+
this.runtime = runtime;
|
|
1679
|
+
} else {
|
|
1680
|
+
this.clientToServer = new PassThrough();
|
|
1681
|
+
this.serverToClient = new PassThrough();
|
|
1682
|
+
this.serverConnection = createConnection(this.clientToServer, this.serverToClient);
|
|
1683
|
+
this.runtime = startOmlLanguageServer(this.serverConnection, {
|
|
1684
|
+
fileSystem: NodeFileSystem,
|
|
1685
|
+
registerDiagramHandlers: true,
|
|
1686
|
+
suppressTransientDiagnostics: true,
|
|
1687
|
+
installNodeProcessHandlers: true,
|
|
1688
|
+
});
|
|
1689
|
+
this.clientConnection = createConnection(this.serverToClient, this.clientToServer);
|
|
1690
|
+
this.clientConnection.listen();
|
|
1691
|
+
}
|
|
1692
|
+
if (this.watchWorkspace) {
|
|
1693
|
+
this.startWorkspaceWatcher();
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
private getWorkspaceOmlDocuments(): any[] {
|
|
1698
|
+
const documents = this.runtime.shared.workspace.LangiumDocuments;
|
|
1699
|
+
const allDocs = documents.all ?? [];
|
|
1700
|
+
const iterable: any[] = Array.isArray(allDocs)
|
|
1701
|
+
? allDocs
|
|
1702
|
+
: (typeof allDocs?.toArray === 'function' ? allDocs.toArray() : Array.from(allDocs as Iterable<any>));
|
|
1703
|
+
return iterable
|
|
1704
|
+
.filter((doc) => String(doc?.uri ?? '').trim().toLowerCase().endsWith('.oml'))
|
|
1705
|
+
.sort((left, right) => String(left?.uri ?? '').localeCompare(String(right?.uri ?? '')));
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
async lintWorkspace(params: Record<string, unknown> = {}): Promise<RestLintResult> {
|
|
1709
|
+
const context: RestValidationContext = {
|
|
1710
|
+
workspaceRoot: this.workspaceRoot,
|
|
1711
|
+
runtime: this.runtime,
|
|
1712
|
+
ensureInitialized: () => this.ensureInitialized(),
|
|
1713
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceCurrent(),
|
|
1714
|
+
getWorkspaceOmlDocuments: () => this.getWorkspaceOmlDocuments(),
|
|
1715
|
+
writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
|
|
1716
|
+
};
|
|
1717
|
+
return await lintWorkspace(context, params);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
async queryWorkspace(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1721
|
+
await this.ensureInitialized();
|
|
1722
|
+
await this.ensureWorkspaceCurrent();
|
|
1723
|
+
const modelUri = typeof params.modelUri === 'string' ? params.modelUri.trim() : '';
|
|
1724
|
+
const sparql = typeof params.sparql === 'string' ? params.sparql : '';
|
|
1725
|
+
if (!modelUri) {
|
|
1726
|
+
return {
|
|
1727
|
+
success: false,
|
|
1728
|
+
kind: detectSparqlKind(sparql),
|
|
1729
|
+
warnings: [],
|
|
1730
|
+
error: "Missing 'modelUri'.",
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
try {
|
|
1734
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService as any;
|
|
1735
|
+
const kind = detectSparqlKind(sparql);
|
|
1736
|
+
await reasoningService.ensureQueryContext(modelUri);
|
|
1737
|
+
const sparqlService = reasoningService.getSparqlService();
|
|
1738
|
+
if (kind === 'select') {
|
|
1739
|
+
const result = await sparqlService.query(modelUri, sparql);
|
|
1740
|
+
return {
|
|
1741
|
+
success: result.success,
|
|
1742
|
+
kind,
|
|
1743
|
+
warnings: result.warnings ?? [],
|
|
1744
|
+
rows: result.rows.map((row: Map<string, any>) => toSparqlRowDto(row)),
|
|
1745
|
+
error: result.error,
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
if (kind === 'ask') {
|
|
1749
|
+
const result = await sparqlService.ask(modelUri, sparql);
|
|
1750
|
+
return {
|
|
1751
|
+
success: result.success,
|
|
1752
|
+
kind,
|
|
1753
|
+
warnings: result.warnings ?? [],
|
|
1754
|
+
result: result.result,
|
|
1755
|
+
error: result.error,
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
if (kind === 'construct' || kind === 'describe') {
|
|
1759
|
+
const result = await sparqlService.construct(modelUri, sparql);
|
|
1760
|
+
return {
|
|
1761
|
+
success: result.success,
|
|
1762
|
+
kind,
|
|
1763
|
+
warnings: result.warnings ?? [],
|
|
1764
|
+
quads: result.quads.map((quad: any) => ({
|
|
1765
|
+
subject: quad.subject.value,
|
|
1766
|
+
predicate: quad.predicate.value,
|
|
1767
|
+
object: quad.object.value,
|
|
1768
|
+
graph: quad.graph.value,
|
|
1769
|
+
})),
|
|
1770
|
+
error: result.error,
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
return {
|
|
1774
|
+
success: false,
|
|
1775
|
+
kind: 'unknown',
|
|
1776
|
+
warnings: [],
|
|
1777
|
+
error: 'Unsupported or unknown SPARQL query kind.',
|
|
1778
|
+
};
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
return {
|
|
1781
|
+
success: false,
|
|
1782
|
+
kind: detectSparqlKind(sparql),
|
|
1783
|
+
warnings: [],
|
|
1784
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
async updateWorkspace(params: Record<string, unknown>): Promise<OmlEditResponse> {
|
|
1790
|
+
await this.ensureInitialized();
|
|
1791
|
+
await this.ensureWorkspaceCurrent();
|
|
1792
|
+
return await applyOmlUpdate(
|
|
1793
|
+
this.runtime.shared,
|
|
1794
|
+
params as OmlEditRequest,
|
|
1795
|
+
(message) => this.logError(message),
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
async fuzzySearchWorkspace(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1800
|
+
await this.ensureInitialized();
|
|
1801
|
+
await this.ensureWorkspaceCurrent();
|
|
1802
|
+
try {
|
|
1803
|
+
const text = (typeof params.text === 'string' ? params.text : '').trim();
|
|
1804
|
+
if (!text) {
|
|
1805
|
+
return { success: true, candidates: [] };
|
|
1806
|
+
}
|
|
1807
|
+
const limit = Math.max(1, Math.min(50, typeof params.limit === 'number' ? params.limit : 12));
|
|
1808
|
+
const ontologyIndex = getOntologyModelIndex(this.runtime.shared as any);
|
|
1809
|
+
const langiumDocuments: any = this.runtime.shared.workspace.LangiumDocuments;
|
|
1810
|
+
const allDocs = langiumDocuments.all ?? [];
|
|
1811
|
+
const iterable: any[] = Array.isArray(allDocs)
|
|
1812
|
+
? allDocs
|
|
1813
|
+
: (typeof allDocs?.toArray === 'function' ? allDocs.toArray() : Array.from(allDocs as Iterable<any>));
|
|
1814
|
+
const modelUris: string[] = [];
|
|
1815
|
+
const candidateByIri = new Map<string, { iri: string; label?: string }>();
|
|
1816
|
+
for (const doc of iterable) {
|
|
1817
|
+
const root = doc?.parseResult?.value;
|
|
1818
|
+
if (!root || !isOntology(root) || (!isVocabulary(root) && !isDescription(root))) {
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
modelUris.push(doc.uri.toString());
|
|
1822
|
+
for (const member of collectOntologyMembers(root)) {
|
|
1823
|
+
const iri = getIriForNode(member);
|
|
1824
|
+
if (!iri) {
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
candidateByIri.set(iri, { iri });
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const labelSnapshot = ontologyIndex.getMemberLabelSnapshot(modelUris);
|
|
1831
|
+
for (const [iri, label] of Object.entries(labelSnapshot)) {
|
|
1832
|
+
const entry = candidateByIri.get(iri) ?? { iri };
|
|
1833
|
+
entry.label = label;
|
|
1834
|
+
candidateByIri.set(iri, entry);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
const indexedEntries: OmlFuzzyIndexedEntry[] = [...candidateByIri.values()].map((entry) => ({
|
|
1838
|
+
iri: entry.iri,
|
|
1839
|
+
label: entry.label,
|
|
1840
|
+
fragment: iriFragment(entry.iri),
|
|
1841
|
+
fragmentTokens: tokenizeForFuzzy(iriFragment(entry.iri)),
|
|
1842
|
+
labelTokens: tokenizeForFuzzy(entry.label ?? ''),
|
|
1843
|
+
}));
|
|
1844
|
+
const haystack = indexedEntries.map((entry) => `${entry.fragment}\n${entry.label ?? ''}\n${entry.iri}`);
|
|
1845
|
+
const uf = new uFuzzy({});
|
|
1846
|
+
const filtered = uf.filter(haystack, text);
|
|
1847
|
+
if (!filtered || filtered.length === 0) {
|
|
1848
|
+
return { success: true, candidates: [] };
|
|
1849
|
+
}
|
|
1850
|
+
const info = uf.info(filtered, haystack, text);
|
|
1851
|
+
const order = info ? uf.sort(info, haystack, text) : null;
|
|
1852
|
+
const ranked = (order ?? filtered.map((_, index) => index))
|
|
1853
|
+
.map((index) => filtered[index]!)
|
|
1854
|
+
.slice(0, limit);
|
|
1855
|
+
const queryTokens = tokenizeForFuzzy(text).slice(0, 6);
|
|
1856
|
+
const candidates = ranked.map((entryIndex, index) => {
|
|
1857
|
+
const entry = indexedEntries[entryIndex]!;
|
|
1858
|
+
const fragment = entry.fragment.toLowerCase();
|
|
1859
|
+
const lowerLabel = (entry.label ?? '').toLowerCase();
|
|
1860
|
+
const lowerInput = text.toLowerCase();
|
|
1861
|
+
const fragmentTokenSet = new Set(entry.fragmentTokens);
|
|
1862
|
+
const labelTokenSet = new Set(entry.labelTokens);
|
|
1863
|
+
let fragmentTokenHits = 0;
|
|
1864
|
+
let labelTokenHits = 0;
|
|
1865
|
+
for (const token of queryTokens) {
|
|
1866
|
+
if (fragmentTokenSet.has(token)) {
|
|
1867
|
+
fragmentTokenHits += 1;
|
|
1868
|
+
}
|
|
1869
|
+
if (labelTokenSet.has(token)) {
|
|
1870
|
+
labelTokenHits += 1;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return {
|
|
1874
|
+
iri: entry.iri,
|
|
1875
|
+
label: entry.label,
|
|
1876
|
+
score: ranked.length - index,
|
|
1877
|
+
diagnostics: {
|
|
1878
|
+
fragment,
|
|
1879
|
+
label: lowerLabel,
|
|
1880
|
+
exactFragment: fragment === lowerInput,
|
|
1881
|
+
exactLabel: lowerLabel === lowerInput,
|
|
1882
|
+
containsInputInFragment: fragment.includes(lowerInput),
|
|
1883
|
+
containsInputInLabel: lowerLabel.includes(lowerInput),
|
|
1884
|
+
fragmentTokenHits,
|
|
1885
|
+
labelTokenHits,
|
|
1886
|
+
},
|
|
1887
|
+
};
|
|
1888
|
+
});
|
|
1889
|
+
return { success: true, candidates };
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
return {
|
|
1892
|
+
success: false,
|
|
1893
|
+
candidates: [],
|
|
1894
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
private async writeWorkspaceAssertedOwl(
|
|
1900
|
+
outputDir: string,
|
|
1901
|
+
format: 'ttl' | 'trig' | 'nt' | 'nq' | 'n3',
|
|
1902
|
+
pretty: boolean,
|
|
1903
|
+
): Promise<Array<{ modelUri: string; ontologyIri: string; owlPath: string }>> {
|
|
1904
|
+
const entries: Array<{ modelUri: string; ontologyIri: string; owlPath: string }> = [];
|
|
1905
|
+
const docs = this.getWorkspaceOmlDocuments();
|
|
1906
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService as any;
|
|
1907
|
+
const store = reasoningService.getStore().getStore();
|
|
1908
|
+
for (const doc of docs) {
|
|
1909
|
+
const modelUri = String(doc?.uri ?? '').trim();
|
|
1910
|
+
const root = doc?.parseResult?.value as { namespace?: string } | undefined;
|
|
1911
|
+
const ontologyIri = normalizeOntologyNamespace(root?.namespace)?.replace(/[\/#]+$/, '');
|
|
1912
|
+
if (!ontologyIri || BUILT_IN_ONTOLOGIES.has(ontologyIri)) {
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
await reasoningService.ensureQueryContext(modelUri);
|
|
1916
|
+
const quads = store.getQuads(null, null, null, DataFactory.namedNode(modelUri))
|
|
1917
|
+
.map((quad: any) => DataFactory.quad(quad.subject, quad.predicate, quad.object));
|
|
1918
|
+
const owlPath = path.join(outputDir, resolveOutputPathFromOntologyIriString(ontologyIri, format));
|
|
1919
|
+
await fs.mkdir(path.dirname(owlPath), { recursive: true });
|
|
1920
|
+
await fs.writeFile(owlPath, await serializeQuads(quads, format, pretty), 'utf-8');
|
|
1921
|
+
entries.push({ modelUri, ontologyIri, owlPath });
|
|
1922
|
+
}
|
|
1923
|
+
entries.sort((left, right) => left.ontologyIri.localeCompare(right.ontologyIri));
|
|
1924
|
+
return entries;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
async reasonWorkspace(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1928
|
+
const context: RestValidationContext = {
|
|
1929
|
+
workspaceRoot: this.workspaceRoot,
|
|
1930
|
+
runtime: this.runtime,
|
|
1931
|
+
ensureInitialized: () => this.ensureInitialized(),
|
|
1932
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceCurrent(),
|
|
1933
|
+
getWorkspaceOmlDocuments: () => this.getWorkspaceOmlDocuments(),
|
|
1934
|
+
writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
|
|
1935
|
+
};
|
|
1936
|
+
return await reasonWorkspace(context, params);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
async validateWorkspace(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1940
|
+
const context: RestValidationContext = {
|
|
1941
|
+
workspaceRoot: this.workspaceRoot,
|
|
1942
|
+
runtime: this.runtime,
|
|
1943
|
+
ensureInitialized: () => this.ensureInitialized(),
|
|
1944
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceCurrent(),
|
|
1945
|
+
getWorkspaceOmlDocuments: () => this.getWorkspaceOmlDocuments(),
|
|
1946
|
+
writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
|
|
1947
|
+
};
|
|
1948
|
+
return await validateWorkspace(context, params);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
async exportWorkspace(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1952
|
+
const context: RestExportContext = {
|
|
1953
|
+
workspaceRoot: this.workspaceRoot,
|
|
1954
|
+
ensureInitialized: () => this.ensureInitialized(),
|
|
1955
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceCurrent(),
|
|
1956
|
+
writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
|
|
1957
|
+
exportAssertedWorkspace: (options) => exportAssertedWorkspace(context, options),
|
|
1958
|
+
};
|
|
1959
|
+
return await exportWorkspace(context, params);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
async close(): Promise<void> {
|
|
1963
|
+
this.stopWorkspaceWatcher();
|
|
1964
|
+
if (!this.useExternalRuntime) {
|
|
1965
|
+
try {
|
|
1966
|
+
this.clientConnection?.sendNotification('exit');
|
|
1967
|
+
} catch {
|
|
1968
|
+
// Ignore close-time notification failures.
|
|
1969
|
+
}
|
|
1970
|
+
this.clientConnection?.dispose();
|
|
1971
|
+
this.serverConnection?.dispose();
|
|
1972
|
+
this.clientToServer?.destroy();
|
|
1973
|
+
this.serverToClient?.destroy();
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
private logError(message: string): void {
|
|
1978
|
+
if (this.clientConnection?.console?.error) {
|
|
1979
|
+
this.clientConnection.console.error(message);
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
console.error(`[oml-rest] ${message}`);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
private startWorkspaceWatcher(): void {
|
|
1986
|
+
if (this.watcherActive) {
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
this.watcherActive = true;
|
|
1990
|
+
this.watchDirectory(this.workspaceRoot);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
private stopWorkspaceWatcher(): void {
|
|
1994
|
+
this.watcherActive = false;
|
|
1995
|
+
if (this.watcherFlushTimer) {
|
|
1996
|
+
clearTimeout(this.watcherFlushTimer);
|
|
1997
|
+
this.watcherFlushTimer = undefined;
|
|
1998
|
+
}
|
|
1999
|
+
for (const watcher of this.watchers) {
|
|
2000
|
+
watcher.close();
|
|
2001
|
+
}
|
|
2002
|
+
this.watchers.clear();
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
private watchDirectory(dirPath: string): void {
|
|
2006
|
+
const basename = path.basename(dirPath);
|
|
2007
|
+
if (basename.startsWith('.') || basename === 'node_modules' || basename === 'out' || basename === 'build') {
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
const watcher = fsSync.watch(dirPath, { recursive: false }, (_eventType, filename) => {
|
|
2012
|
+
if (!this.watcherActive || !filename) {
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const fullPath = path.join(dirPath, filename.toString());
|
|
2016
|
+
if (!fullPath.toLowerCase().endsWith('.oml')) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
this.scheduleWorkspaceRefresh();
|
|
2020
|
+
});
|
|
2021
|
+
this.watchers.add(watcher);
|
|
2022
|
+
} catch {
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
try {
|
|
2027
|
+
const entries = fsSync.readdirSync(dirPath, { withFileTypes: true });
|
|
2028
|
+
for (const entry of entries) {
|
|
2029
|
+
if (entry.isDirectory()) {
|
|
2030
|
+
this.watchDirectory(path.join(dirPath, entry.name));
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
} catch {
|
|
2034
|
+
// Ignore unreadable directories.
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
private scheduleWorkspaceRefresh(): void {
|
|
2039
|
+
if (this.watcherFlushTimer) {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
this.watcherFlushTimer = setTimeout(() => {
|
|
2043
|
+
this.watcherFlushTimer = undefined;
|
|
2044
|
+
void this.refreshWorkspaceOmlDocumentsQueued().catch(() => {});
|
|
2045
|
+
}, 100);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
private async refreshWorkspaceOmlDocumentsQueued(): Promise<void> {
|
|
2049
|
+
if (this.refreshInFlight) {
|
|
2050
|
+
this.refreshQueued = true;
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
this.refreshInFlight = true;
|
|
2054
|
+
try {
|
|
2055
|
+
await this.refreshWorkspaceOmlDocuments();
|
|
2056
|
+
} finally {
|
|
2057
|
+
this.refreshInFlight = false;
|
|
2058
|
+
}
|
|
2059
|
+
if (this.refreshQueued) {
|
|
2060
|
+
this.refreshQueued = false;
|
|
2061
|
+
await this.refreshWorkspaceOmlDocumentsQueued();
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
async exportAssertedWorkspace(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
2066
|
+
const context: RestExportContext = {
|
|
2067
|
+
workspaceRoot: this.workspaceRoot,
|
|
2068
|
+
ensureInitialized: () => this.ensureInitialized(),
|
|
2069
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceCurrent(),
|
|
2070
|
+
writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
|
|
2071
|
+
exportAssertedWorkspace: (options) => exportAssertedWorkspace(context, options),
|
|
2072
|
+
};
|
|
2073
|
+
return await exportAssertedWorkspace(context, params);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
async renderWorkspace(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
2077
|
+
await this.ensureInitialized();
|
|
2078
|
+
await this.ensureWorkspaceCurrent();
|
|
2079
|
+
if (params.only !== true) {
|
|
2080
|
+
const lint = await this.lintWorkspace();
|
|
2081
|
+
if (lint.errors > 0 || lint.warnings > 0) {
|
|
2082
|
+
return {
|
|
2083
|
+
success: false,
|
|
2084
|
+
filesRendered: 0,
|
|
2085
|
+
outputDir: '',
|
|
2086
|
+
error: `lint failed with ${lint.errors} error(s) and ${lint.warnings} warning(s).`,
|
|
2087
|
+
lint,
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
const workspaceRoot = path.resolve(this.workspaceRoot);
|
|
2092
|
+
const inputFromOptions = typeof params.inputDir === 'string' && params.inputDir.trim().length > 0
|
|
2093
|
+
? params.inputDir.trim()
|
|
2094
|
+
: (typeof params.md === 'string' && params.md.trim().length > 0 ? params.md.trim() : 'src/md');
|
|
2095
|
+
const outputFromOptions = typeof params.outputDir === 'string' && params.outputDir.trim().length > 0
|
|
2096
|
+
? params.outputDir.trim()
|
|
2097
|
+
: (typeof params.web === 'string' && params.web.trim().length > 0 ? params.web.trim() : 'build/web');
|
|
2098
|
+
const inputDir = path.resolve(
|
|
2099
|
+
workspaceRoot,
|
|
2100
|
+
inputFromOptions,
|
|
2101
|
+
);
|
|
2102
|
+
const outputDir = path.resolve(
|
|
2103
|
+
workspaceRoot,
|
|
2104
|
+
outputFromOptions,
|
|
2105
|
+
);
|
|
2106
|
+
const contextOption = typeof params.contextOntologyIri === 'string' && params.contextOntologyIri.trim().length > 0
|
|
2107
|
+
? params.contextOntologyIri.trim()
|
|
2108
|
+
: (typeof params.context === 'string' && params.context.trim().length > 0 ? params.context.trim() : undefined);
|
|
2109
|
+
const defaultContextOntologyIri = contextOption
|
|
2110
|
+
? normalizeContextOntologyIri(contextOption)
|
|
2111
|
+
: undefined;
|
|
2112
|
+
const runtime = new MarkdownPreviewRuntime(new MarkdownHandlerRegistry());
|
|
2113
|
+
const templateCatalog = await buildTemplateCatalog(workspaceRoot);
|
|
2114
|
+
const navigationTemplateCatalog = buildNavigationTemplateCatalog(
|
|
2115
|
+
Array.from(templateCatalog.values()).flatMap((entries) => entries.map((entry) => entry.definition))
|
|
2116
|
+
);
|
|
2117
|
+
const staticAssets = await writeStaticAssets(outputDir);
|
|
2118
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService as any;
|
|
2119
|
+
const ontologyIndex = getOntologyModelIndex(this.runtime.shared as any);
|
|
2120
|
+
const executor = new MarkdownExecutor({
|
|
2121
|
+
ensureContext: (modelUri) => reasoningService.ensureQueryContext(modelUri),
|
|
2122
|
+
resolveContextIri: (modelUri) => reasoningService.getContextIri(modelUri),
|
|
2123
|
+
countContextQuads: (modelUri) => reasoningService.countContextDatasetQuads(modelUri),
|
|
2124
|
+
query: (modelUri, sparql) => reasoningService.getSparqlService().query(modelUri, sparql),
|
|
2125
|
+
construct: (modelUri, sparql) => reasoningService.getSparqlService().construct(modelUri, sparql),
|
|
2126
|
+
});
|
|
2127
|
+
const markdownFiles = await findFilesByExtension(inputDir, '.md');
|
|
2128
|
+
const wikiPageIndex = buildWikiPageIndex(markdownFiles, inputDir, outputDir);
|
|
2129
|
+
const workspaceAssetFiles = new Set<string>();
|
|
2130
|
+
const aliasesByIri = new Map<string, string>();
|
|
2131
|
+
const renderJobs: Array<{
|
|
2132
|
+
htmlPath: string;
|
|
2133
|
+
renderedHtml: string;
|
|
2134
|
+
blockManifest: Array<{ blockId: string; path: string }>;
|
|
2135
|
+
blockResults: ReadonlyArray<RenderedBlockResult>;
|
|
2136
|
+
wikiLinkHrefByKey: Record<string, string>;
|
|
2137
|
+
}> = [];
|
|
2138
|
+
const contextModelUris = new Set<string>();
|
|
2139
|
+
let filesRendered = 0;
|
|
2140
|
+
let blockArtifactFiles = 0;
|
|
2141
|
+
for (const markdownPath of markdownFiles) {
|
|
2142
|
+
const markdown = await fs.readFile(markdownPath, 'utf-8');
|
|
2143
|
+
if (isTemplateMarkdownFile(markdown)) {
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
const frontMatter = extractLeadingFrontMatter(markdown);
|
|
2147
|
+
const contextOntologyIri = normalizeContextOntologyIri(
|
|
2148
|
+
frontMatter?.contextOntologyIri
|
|
2149
|
+
?? frontMatterString(frontMatter?.data, 'ontologyIri')
|
|
2150
|
+
?? frontMatterString(frontMatter?.data, 'ontology')
|
|
2151
|
+
);
|
|
2152
|
+
const contextMemberIri = frontMatterString(frontMatter?.data, 'memberIri')
|
|
2153
|
+
?? frontMatterString(frontMatter?.data, 'contextMemberIri');
|
|
2154
|
+
const referencingUri = pathToFileURL(markdownPath).toString();
|
|
2155
|
+
const effectiveOntologyIri = contextOntologyIri ?? defaultContextOntologyIri;
|
|
2156
|
+
const resolvedModelUri = effectiveOntologyIri
|
|
2157
|
+
? ontologyIndex.resolveModelUri(effectiveOntologyIri, referencingUri)
|
|
2158
|
+
: undefined;
|
|
2159
|
+
const contextModelUri = resolvedModelUri;
|
|
2160
|
+
if (contextModelUri) {
|
|
2161
|
+
contextModelUris.add(contextModelUri);
|
|
2162
|
+
}
|
|
2163
|
+
const expandedMarkdown = await expandTemplateComposeBlocks(markdown, templateCatalog, {
|
|
2164
|
+
workspaceRoot,
|
|
2165
|
+
sourceMarkdownPath: markdownPath,
|
|
2166
|
+
contextOntologyIri: effectiveOntologyIri,
|
|
2167
|
+
contextModelUri,
|
|
2168
|
+
contextMemberIri,
|
|
2169
|
+
});
|
|
2170
|
+
const prepared = runtime.prepare(expandedMarkdown);
|
|
2171
|
+
const relative = path.relative(inputDir, markdownPath).replace(/\\/g, '/');
|
|
2172
|
+
const htmlPath = path.join(outputDir, relative.replace(/\.md$/i, '.html'));
|
|
2173
|
+
const rewriteResult = rewriteRenderedLinks(prepared.renderedHtml, {
|
|
2174
|
+
workspaceRoot,
|
|
2175
|
+
inputRoot: inputDir,
|
|
2176
|
+
inputFile: markdownPath,
|
|
2177
|
+
outputRoot: outputDir,
|
|
2178
|
+
outputFile: htmlPath,
|
|
2179
|
+
});
|
|
2180
|
+
for (const asset of rewriteResult.workspaceAssets) {
|
|
2181
|
+
workspaceAssetFiles.add(asset);
|
|
2182
|
+
}
|
|
2183
|
+
const executableBlocks = toExecutableBlocks(prepared.codeBlocks);
|
|
2184
|
+
const optionsByBlockId = new Map(prepared.codeBlocks.map((block) => [block.id, block.options] as const));
|
|
2185
|
+
const blockResults = executableBlocks.length === 0 || !contextModelUri
|
|
2186
|
+
? []
|
|
2187
|
+
: (await executor.executeBlocks({
|
|
2188
|
+
markdownUri: pathToFileURL(path.resolve(markdownPath)).toString(),
|
|
2189
|
+
modelUri: contextModelUri,
|
|
2190
|
+
blocks: executableBlocks,
|
|
2191
|
+
})).results.map((result) => ({
|
|
2192
|
+
...result,
|
|
2193
|
+
options: optionsByBlockId.get(result.blockId),
|
|
2194
|
+
} as RenderedBlockResult));
|
|
2195
|
+
const rewrittenBlockResults = blockResults.map((result) => rewriteBlockResultAssetPaths(result, {
|
|
2196
|
+
workspaceRoot,
|
|
2197
|
+
inputRoot: inputDir,
|
|
2198
|
+
sourceFile: markdownPath,
|
|
2199
|
+
outputRoot: outputDir,
|
|
2200
|
+
outputFile: htmlPath,
|
|
2201
|
+
}, workspaceAssetFiles));
|
|
2202
|
+
const blockArtifacts = await writeBlockArtifacts(htmlPath, rewrittenBlockResults);
|
|
2203
|
+
blockArtifactFiles += blockArtifacts.count;
|
|
2204
|
+
const wikiLinkHrefByKey = buildWikiLinkHrefMapForPage(wikiPageIndex, htmlPath);
|
|
2205
|
+
renderJobs.push({
|
|
2206
|
+
htmlPath,
|
|
2207
|
+
renderedHtml: rewriteResult.html,
|
|
2208
|
+
blockManifest: blockArtifacts.manifest,
|
|
2209
|
+
blockResults: rewrittenBlockResults,
|
|
2210
|
+
wikiLinkHrefByKey,
|
|
2211
|
+
});
|
|
2212
|
+
filesRendered += 1;
|
|
2213
|
+
}
|
|
2214
|
+
const renderedNavigationPages = new Set<string>();
|
|
2215
|
+
for (const modelUri of contextModelUris) {
|
|
2216
|
+
await reasoningService.ensureQueryContext(modelUri);
|
|
2217
|
+
const membersToTypes = await queryMemberTypes(reasoningService, modelUri);
|
|
2218
|
+
for (const [memberIri, rdfTypes] of membersToTypes.entries()) {
|
|
2219
|
+
const resolution = resolveTemplateForNavigation(navigationTemplateCatalog, rdfTypes);
|
|
2220
|
+
if (!resolution.template) {
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
const targetFile = memberIriToOutputFile(outputDir, memberIri);
|
|
2224
|
+
if (!targetFile) {
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
aliasesByIri.set(memberIri, targetFile);
|
|
2228
|
+
if (renderedNavigationPages.has(targetFile)) {
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
renderedNavigationPages.add(targetFile);
|
|
2232
|
+
const contextOntologyIri = inferOntologyIriFromMemberIri(memberIri);
|
|
2233
|
+
const sourceDocumentUri = resolution.template.sourceUri?.trim() || pathToFileURL(path.join(workspaceRoot, 'index.md')).toString();
|
|
2234
|
+
const invocation: TemplateInvocation = {
|
|
2235
|
+
templateId: resolution.template.id,
|
|
2236
|
+
mode: 'navigation',
|
|
2237
|
+
context: {
|
|
2238
|
+
invocation: {
|
|
2239
|
+
mode: 'navigation',
|
|
2240
|
+
sourceDocumentUri,
|
|
2241
|
+
referenceDocumentUri: sourceDocumentUri,
|
|
2242
|
+
},
|
|
2243
|
+
model: {
|
|
2244
|
+
ontologyIri: contextOntologyIri,
|
|
2245
|
+
modelUri,
|
|
2246
|
+
},
|
|
2247
|
+
focus: {
|
|
2248
|
+
memberIri,
|
|
2249
|
+
},
|
|
2250
|
+
selection: {
|
|
2251
|
+
iris: [memberIri],
|
|
2252
|
+
},
|
|
2253
|
+
},
|
|
2254
|
+
args: {},
|
|
2255
|
+
};
|
|
2256
|
+
const rendered = renderTemplate(resolution.template, invocation);
|
|
2257
|
+
if (rendered.missingRequired.length > 0) {
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
const syntheticSourcePath = resolution.template.sourceUri?.startsWith('file:')
|
|
2261
|
+
? decodeURIComponent(new URL(resolution.template.sourceUri).pathname)
|
|
2262
|
+
: path.join(workspaceRoot, 'index.md');
|
|
2263
|
+
const expanded = await expandTemplateComposeBlocks(rendered.output, templateCatalog, {
|
|
2264
|
+
workspaceRoot,
|
|
2265
|
+
sourceMarkdownPath: syntheticSourcePath,
|
|
2266
|
+
contextOntologyIri,
|
|
2267
|
+
contextModelUri: modelUri,
|
|
2268
|
+
contextMemberIri: memberIri,
|
|
2269
|
+
});
|
|
2270
|
+
const prepared = runtime.prepare(expanded);
|
|
2271
|
+
const rewriteResult = rewriteRenderedLinks(prepared.renderedHtml, {
|
|
2272
|
+
workspaceRoot,
|
|
2273
|
+
inputRoot: inputDir,
|
|
2274
|
+
inputFile: syntheticSourcePath,
|
|
2275
|
+
outputRoot: outputDir,
|
|
2276
|
+
outputFile: targetFile,
|
|
2277
|
+
});
|
|
2278
|
+
for (const asset of rewriteResult.workspaceAssets) {
|
|
2279
|
+
workspaceAssetFiles.add(asset);
|
|
2280
|
+
}
|
|
2281
|
+
const executableBlocks = toExecutableBlocks(prepared.codeBlocks);
|
|
2282
|
+
const optionsByBlockId = new Map(prepared.codeBlocks.map((block) => [block.id, block.options] as const));
|
|
2283
|
+
const blockResults = executableBlocks.length === 0
|
|
2284
|
+
? []
|
|
2285
|
+
: (await executor.executeBlocks({
|
|
2286
|
+
markdownUri: sourceDocumentUri,
|
|
2287
|
+
modelUri,
|
|
2288
|
+
blocks: executableBlocks,
|
|
2289
|
+
})).results.map((result) => ({
|
|
2290
|
+
...result,
|
|
2291
|
+
options: optionsByBlockId.get(result.blockId),
|
|
2292
|
+
} as RenderedBlockResult));
|
|
2293
|
+
const rewrittenBlockResults = blockResults.map((result) => rewriteBlockResultAssetPaths(result, {
|
|
2294
|
+
workspaceRoot,
|
|
2295
|
+
inputRoot: inputDir,
|
|
2296
|
+
sourceFile: syntheticSourcePath,
|
|
2297
|
+
outputRoot: outputDir,
|
|
2298
|
+
outputFile: targetFile,
|
|
2299
|
+
}, workspaceAssetFiles));
|
|
2300
|
+
const blockArtifacts = await writeBlockArtifacts(targetFile, rewrittenBlockResults);
|
|
2301
|
+
blockArtifactFiles += blockArtifacts.count;
|
|
2302
|
+
renderJobs.push({
|
|
2303
|
+
htmlPath: targetFile,
|
|
2304
|
+
renderedHtml: rewriteResult.html,
|
|
2305
|
+
blockManifest: blockArtifacts.manifest,
|
|
2306
|
+
blockResults: rewrittenBlockResults,
|
|
2307
|
+
wikiLinkHrefByKey: buildWikiLinkHrefMapForPage(wikiPageIndex, targetFile),
|
|
2308
|
+
});
|
|
2309
|
+
filesRendered += 1;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
for (const job of renderJobs) {
|
|
2313
|
+
const runtimeScriptRelative = toRelativeWebPath(path.dirname(job.htmlPath), staticAssets.runtimeScriptFile);
|
|
2314
|
+
const stylesheetRelative = toRelativeWebPath(path.dirname(job.htmlPath), staticAssets.stylesheetFile);
|
|
2315
|
+
const html = wrapHtml(
|
|
2316
|
+
job.renderedHtml,
|
|
2317
|
+
runtimeScriptRelative,
|
|
2318
|
+
stylesheetRelative,
|
|
2319
|
+
job.blockManifest,
|
|
2320
|
+
job.blockResults,
|
|
2321
|
+
job.wikiLinkHrefByKey,
|
|
2322
|
+
buildIriAliasMapForPage(aliasesByIri, job.htmlPath),
|
|
2323
|
+
);
|
|
2324
|
+
await fs.mkdir(path.dirname(job.htmlPath), { recursive: true });
|
|
2325
|
+
await fs.writeFile(job.htmlPath, html, 'utf-8');
|
|
2326
|
+
}
|
|
2327
|
+
for (const file of workspaceAssetFiles) {
|
|
2328
|
+
const workspaceRelative = path.relative(workspaceRoot, file);
|
|
2329
|
+
if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)) {
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (workspaceRelative.toLowerCase().endsWith('.md')) {
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
const target = resolveAssetTargetPath(file, {
|
|
2336
|
+
workspaceRoot,
|
|
2337
|
+
inputRoot: inputDir,
|
|
2338
|
+
outputRoot: outputDir,
|
|
2339
|
+
});
|
|
2340
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
2341
|
+
try {
|
|
2342
|
+
await fs.copyFile(file, target);
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
2345
|
+
if (code !== 'ENOENT') {
|
|
2346
|
+
throw error;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
return { success: true, filesRendered, outputDir, blockArtifactFiles };
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
private async ensureWorkspaceCurrent(): Promise<void> {
|
|
2354
|
+
if (this.useExternalRuntime) {
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
if (!this.watchWorkspace) {
|
|
2358
|
+
await this.refreshWorkspaceOmlDocuments();
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
if (!this.initialWorkspaceSyncCompleted) {
|
|
2362
|
+
await this.refreshWorkspaceOmlDocuments();
|
|
2363
|
+
this.initialWorkspaceSyncCompleted = true;
|
|
2364
|
+
}
|
|
2365
|
+
await this.waitForWatcherRefreshIdle();
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
private async waitForWatcherRefreshIdle(): Promise<void> {
|
|
2369
|
+
while (this.watcherFlushTimer !== undefined || this.refreshInFlight || this.refreshQueued) {
|
|
2370
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
private async refreshWorkspaceOmlDocuments(): Promise<void> {
|
|
2375
|
+
const workspaceRoot = path.resolve(this.workspaceRoot);
|
|
2376
|
+
const omlFiles = await findFilesByExtension(workspaceRoot, '.oml');
|
|
2377
|
+
const changedUris = omlFiles.map((filePath) => URI.parse(pathToFileURL(filePath).toString()));
|
|
2378
|
+
const documents = this.runtime.shared.workspace.LangiumDocuments;
|
|
2379
|
+
const allDocs = documents.all ?? [];
|
|
2380
|
+
const iterable: any[] = Array.isArray(allDocs)
|
|
2381
|
+
? allDocs
|
|
2382
|
+
: (typeof allDocs?.toArray === 'function' ? allDocs.toArray() : Array.from(allDocs as Iterable<any>));
|
|
2383
|
+
const currentOmlUris = new Set<string>();
|
|
2384
|
+
for (const document of iterable) {
|
|
2385
|
+
const uri = String(document?.uri ?? '').trim();
|
|
2386
|
+
if (!uri.toLowerCase().endsWith('.oml')) {
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
currentOmlUris.add(uri);
|
|
2390
|
+
}
|
|
2391
|
+
const changedUriStrings = new Set(changedUris.map((uri) => uri.toString()));
|
|
2392
|
+
const deletedUris = [...currentOmlUris]
|
|
2393
|
+
.filter((uri) => !changedUriStrings.has(uri))
|
|
2394
|
+
.map((uri) => URI.parse(uri));
|
|
2395
|
+
await Promise.all(changedUris.map((uri) => documents.getOrCreateDocument(uri)));
|
|
2396
|
+
const builder = this.runtime.shared.workspace.DocumentBuilder;
|
|
2397
|
+
await builder.update(changedUris, deletedUris);
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
private async ensureInitialized(): Promise<void> {
|
|
2401
|
+
if (this.initPromise) {
|
|
2402
|
+
await this.initPromise;
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
if (this.useExternalRuntime) {
|
|
2407
|
+
this.initPromise = (async () => {
|
|
2408
|
+
const workspace = this.runtime.shared.workspace.WorkspaceManager;
|
|
2409
|
+
await workspace.ready;
|
|
2410
|
+
})();
|
|
2411
|
+
} else {
|
|
2412
|
+
const rootPath = path.resolve(this.workspaceRoot);
|
|
2413
|
+
const rootUri = pathToFileURL(rootPath).toString();
|
|
2414
|
+
this.initPromise = (async () => {
|
|
2415
|
+
await withTimeout(this.clientConnection!.sendRequest('initialize', {
|
|
2416
|
+
processId: process.pid,
|
|
2417
|
+
rootUri,
|
|
2418
|
+
workspaceFolders: [{ uri: rootUri, name: path.basename(rootPath) }],
|
|
2419
|
+
capabilities: {},
|
|
2420
|
+
clientInfo: { name: 'oml-rest-server', version: '1.0' },
|
|
2421
|
+
initializationOptions: {},
|
|
2422
|
+
}), this.requestTimeoutMs, 'initialize');
|
|
2423
|
+
this.clientConnection!.sendNotification('initialized', {});
|
|
2424
|
+
})();
|
|
2425
|
+
}
|
|
2426
|
+
await this.initPromise;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
export async function startOmlRestServer(options: OmlRestServerOptions): Promise<{ server: http.Server; updateToken: (token: string) => void }> {
|
|
2431
|
+
const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
|
|
2432
|
+
const client = new InMemoryJsonRpcLspClient(
|
|
2433
|
+
workspaceRoot,
|
|
2434
|
+
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
2435
|
+
options.watchWorkspace === true,
|
|
2436
|
+
options.runtime,
|
|
2437
|
+
);
|
|
2438
|
+
const openApiSpec = createOpenApiSpec(options.host, options.port);
|
|
2439
|
+
let currentAccessToken: string | undefined = options.authToken;
|
|
2440
|
+
const getAccessToken = (): string | undefined => currentAccessToken;
|
|
2441
|
+
|
|
2442
|
+
const server = http.createServer(async (req, res) => {
|
|
2443
|
+
const requestId = randomUUID();
|
|
2444
|
+
try {
|
|
2445
|
+
const method = req.method ?? 'GET';
|
|
2446
|
+
const rawUrl = req.url ?? '/';
|
|
2447
|
+
const parsed = new URL(rawUrl, `http://${req.headers.host ?? 'localhost'}`);
|
|
2448
|
+
const pathname = parsed.pathname;
|
|
2449
|
+
|
|
2450
|
+
if (method === 'GET' && pathname === '/health') {
|
|
2451
|
+
jsonResponse(res, 200, {
|
|
2452
|
+
status: 'ok',
|
|
2453
|
+
service: 'oml-server',
|
|
2454
|
+
mode: 'rest',
|
|
2455
|
+
workspaceRoot,
|
|
2456
|
+
authenticated: getAccessToken() !== undefined,
|
|
2457
|
+
requestId,
|
|
2458
|
+
});
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
if (method === 'GET' && pathname === '/') {
|
|
2463
|
+
htmlResponse(res, 200, createSparqlWorkbenchPage(workspaceRoot));
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
if (method === 'GET' && pathname === '/openapi.json') {
|
|
2468
|
+
jsonResponse(res, 200, openApiSpec);
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
if (method === 'GET' && pathname === '/v0/models') {
|
|
2473
|
+
const workspaceModelFiles = await listWorkspaceModelFiles(workspaceRoot);
|
|
2474
|
+
jsonResponse(res, 200, {
|
|
2475
|
+
files: workspaceModelFiles,
|
|
2476
|
+
requestId,
|
|
2477
|
+
});
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
if (method === 'POST') {
|
|
2482
|
+
const body = await readJsonBody(req);
|
|
2483
|
+
const route = await dispatchRestRoute(method, pathname, body, client);
|
|
2484
|
+
if (route) {
|
|
2485
|
+
jsonResponse(res, 200, {
|
|
2486
|
+
ok: true,
|
|
2487
|
+
method: `oml/rest/${route.operationId}`,
|
|
2488
|
+
result: route.result,
|
|
2489
|
+
requestId
|
|
2490
|
+
});
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
jsonResponse(res, 404, { error: `No route for ${method} ${pathname}.`, requestId });
|
|
2496
|
+
} catch (error) {
|
|
2497
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2498
|
+
const status = error instanceof SyntaxError || message.includes('Request body exceeds') ? 400 : 500;
|
|
2499
|
+
jsonResponse(res, status, { error: message, requestId });
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
2502
|
+
|
|
2503
|
+
server.on('close', () => {
|
|
2504
|
+
void client.close();
|
|
2505
|
+
currentAccessToken = undefined;
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
await new Promise<void>((resolve, reject) => {
|
|
2509
|
+
server.once('error', reject);
|
|
2510
|
+
server.listen(options.port, options.host, () => resolve());
|
|
2511
|
+
});
|
|
2512
|
+
|
|
2513
|
+
return {
|
|
2514
|
+
server,
|
|
2515
|
+
updateToken: (token: string) => { currentAccessToken = token; },
|
|
2516
|
+
};
|
|
2517
|
+
}
|