@eriknaslund/skiss 0.2.0 → 0.3.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 +3 -1
- package/dist/cli.js +1218 -699
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +50 -4
- package/dist/index.js +1195 -693
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,446 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
5
5
|
import { basename, extname } from "path";
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
|
|
8
|
+
// src/parse.ts
|
|
9
|
+
var isWs = (c) => c === " " || c === " ";
|
|
10
|
+
var trimWs = (s) => s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
|
|
11
|
+
var CLASS_NAME = /^[A-Z][A-Za-z0-9]*$/;
|
|
12
|
+
var FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
|
|
13
|
+
var SYSTEM_OR_VALUE = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
14
|
+
var PRIMITIVES = {
|
|
15
|
+
string: "string",
|
|
16
|
+
int: "int",
|
|
17
|
+
float: "float",
|
|
18
|
+
bool: "bool",
|
|
19
|
+
date: "date",
|
|
20
|
+
datetime: "datetime",
|
|
21
|
+
uri: "uri",
|
|
22
|
+
text: "string",
|
|
23
|
+
integer: "int",
|
|
24
|
+
boolean: "bool"
|
|
25
|
+
};
|
|
26
|
+
function primitiveFor(word) {
|
|
27
|
+
return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
|
|
28
|
+
}
|
|
29
|
+
var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
|
|
30
|
+
var CLASS_ORDER = "name, `@System`, `~ Class`, then `#` and `?`";
|
|
31
|
+
function parse(source) {
|
|
32
|
+
const classes = [];
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
let current;
|
|
35
|
+
const text0 = source.startsWith("\uFEFF") ? source.slice(1) : source;
|
|
36
|
+
const lines = text0.split(/\r\n|\n|\r/);
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = i + 1;
|
|
39
|
+
const text = (lines[i] ?? "").replace(/[ \t]+$/, "");
|
|
40
|
+
const first = text[0];
|
|
41
|
+
if (first === void 0 || first === "#") continue;
|
|
42
|
+
if (isWs(first)) {
|
|
43
|
+
const start = text.search(/[^ \t]/);
|
|
44
|
+
if (current === void 0) {
|
|
45
|
+
diagnostics.push({
|
|
46
|
+
severity: "error",
|
|
47
|
+
code: "E_FIELD_WITHOUT_CLASS",
|
|
48
|
+
message: "An indented line is a field, but there is no valid class line above it",
|
|
49
|
+
line,
|
|
50
|
+
col: start,
|
|
51
|
+
end: text.length
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const result = parseFieldLine(text, line);
|
|
56
|
+
if (result.ok) current.fields.push(result.value);
|
|
57
|
+
else diagnostics.push(result.diagnostic);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (/\p{L}/u.test(first)) {
|
|
61
|
+
const result = parseClassLine(text, line);
|
|
62
|
+
if (result.ok) {
|
|
63
|
+
classes.push(result.value);
|
|
64
|
+
current = result.value;
|
|
65
|
+
} else {
|
|
66
|
+
diagnostics.push(result.diagnostic);
|
|
67
|
+
current = void 0;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
diagnostics.push({
|
|
72
|
+
severity: "error",
|
|
73
|
+
code: "E_UNPARSABLE",
|
|
74
|
+
message: "A line at column 0 must start with a class name or `#` for a comment",
|
|
75
|
+
line,
|
|
76
|
+
col: 0,
|
|
77
|
+
end: text.length
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return { classes, diagnostics };
|
|
81
|
+
}
|
|
82
|
+
function isDoubtMarker(text, i) {
|
|
83
|
+
return text[i] === "?" && isWs(text[i - 1]) && (i + 1 === text.length || isWs(text[i + 1]));
|
|
84
|
+
}
|
|
85
|
+
function splitTrailer(text) {
|
|
86
|
+
let start = -1;
|
|
87
|
+
for (let i = 1; i < text.length; i++) {
|
|
88
|
+
if (!isWs(text[i - 1])) continue;
|
|
89
|
+
if (text[i] === "#" || isDoubtMarker(text, i)) {
|
|
90
|
+
start = i;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (start < 0) return { head: text };
|
|
95
|
+
const trailer = { head: text.slice(0, start).replace(/[ \t]+$/, "") };
|
|
96
|
+
if (text[start] === "?") {
|
|
97
|
+
const note = trimWs(text.slice(start + 1));
|
|
98
|
+
if (note !== "") trailer.note = note;
|
|
99
|
+
return trailer;
|
|
100
|
+
}
|
|
101
|
+
let doubt = -1;
|
|
102
|
+
for (let i = start + 1; i < text.length; i++) {
|
|
103
|
+
if (isDoubtMarker(text, i)) {
|
|
104
|
+
doubt = i;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const description = trimWs(text.slice(start + 1, doubt < 0 ? text.length : doubt));
|
|
109
|
+
if (description !== "") trailer.description = description;
|
|
110
|
+
if (doubt >= 0) {
|
|
111
|
+
const note = trimWs(text.slice(doubt + 1));
|
|
112
|
+
if (note !== "") trailer.note = note;
|
|
113
|
+
}
|
|
114
|
+
return trailer;
|
|
115
|
+
}
|
|
116
|
+
var WORD = /[\p{L}\p{N}_-]+/uy;
|
|
117
|
+
var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "["];
|
|
118
|
+
function tokenize(head) {
|
|
119
|
+
const tokens = [];
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (i < head.length) {
|
|
122
|
+
const c = head[i] ?? "";
|
|
123
|
+
if (isWs(c)) {
|
|
124
|
+
i++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
WORD.lastIndex = i;
|
|
128
|
+
const word = WORD.exec(head);
|
|
129
|
+
if (word !== null) {
|
|
130
|
+
tokens.push({ kind: "word", text: word[0], col: i, end: i + word[0].length });
|
|
131
|
+
i += word[0].length;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (c === "[" && head[i + 1] === "]") {
|
|
135
|
+
tokens.push({ kind: "[]", text: "[]", col: i, end: i + 2 });
|
|
136
|
+
i += 2;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const ch = String.fromCodePoint(head.codePointAt(i) ?? 0);
|
|
140
|
+
const kind = OPERATORS.find((op) => op === ch) ?? "other";
|
|
141
|
+
tokens.push({ kind, text: ch, col: i, end: i + ch.length });
|
|
142
|
+
i += ch.length;
|
|
143
|
+
}
|
|
144
|
+
return tokens;
|
|
145
|
+
}
|
|
146
|
+
var peek = (c) => c.tokens[c.pos];
|
|
147
|
+
function take(c) {
|
|
148
|
+
const t = c.tokens[c.pos];
|
|
149
|
+
if (t !== void 0) c.pos++;
|
|
150
|
+
return t;
|
|
151
|
+
}
|
|
152
|
+
var name = (t, line) => ({ text: t.text, line, col: t.col, end: t.end });
|
|
153
|
+
function fail(code, message, line, span) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
diagnostic: { severity: "error", code, message, line, col: span.col, end: span.end }
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
var afterSpan = (after) => ({ col: after.end, end: after.end });
|
|
160
|
+
function parseClassLine(text, line) {
|
|
161
|
+
const trailer = splitTrailer(text);
|
|
162
|
+
const c = { tokens: tokenize(trailer.head), pos: 0, line };
|
|
163
|
+
const head = take(c);
|
|
164
|
+
if (head === void 0 || head.kind !== "word") {
|
|
165
|
+
return fail("E_UNPARSABLE", "A class line must start with a class name", line, {
|
|
166
|
+
col: 0,
|
|
167
|
+
end: Math.max(1, trailer.head.length)
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!CLASS_NAME.test(head.text)) {
|
|
171
|
+
return fail(
|
|
172
|
+
"E_BAD_NAME",
|
|
173
|
+
`Class names are UpperCamelCase (letters and digits, uppercase first): \`${head.text}\``,
|
|
174
|
+
line,
|
|
175
|
+
head
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const node = { name: name(head, line), line };
|
|
179
|
+
const at = peek(c);
|
|
180
|
+
if (at?.kind === "@") {
|
|
181
|
+
take(c);
|
|
182
|
+
const system = parseSystem(c, at);
|
|
183
|
+
if (!system.ok) return system;
|
|
184
|
+
node.system = system.value;
|
|
185
|
+
}
|
|
186
|
+
const tilde = peek(c);
|
|
187
|
+
if (tilde?.kind === "~") {
|
|
188
|
+
take(c);
|
|
189
|
+
const target = take(c);
|
|
190
|
+
if (target === void 0 || target.kind !== "word") {
|
|
191
|
+
return fail(
|
|
192
|
+
"E_UNPARSABLE",
|
|
193
|
+
"Expected a class name after `~`, e.g. `CharacterPage @Community ~ Character`",
|
|
194
|
+
line,
|
|
195
|
+
target ?? afterSpan(tilde)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (!CLASS_NAME.test(target.text)) {
|
|
199
|
+
return fail(
|
|
200
|
+
"E_BAD_NAME",
|
|
201
|
+
`The class after \`~\` must be UpperCamelCase: \`${target.text}\``,
|
|
202
|
+
line,
|
|
203
|
+
target
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
node.similarTo = name(target, line);
|
|
207
|
+
}
|
|
208
|
+
const rest = peek(c);
|
|
209
|
+
if (rest !== void 0) return unexpected(rest, line, "class", CLASS_ORDER);
|
|
210
|
+
if (trailer.description !== void 0) node.description = trailer.description;
|
|
211
|
+
if (trailer.note !== void 0) node.note = trailer.note;
|
|
212
|
+
return { ok: true, value: { ...node, fields: [] } };
|
|
213
|
+
}
|
|
214
|
+
function parseFieldLine(text, line) {
|
|
215
|
+
const trailer = splitTrailer(text);
|
|
216
|
+
const c = { tokens: tokenize(trailer.head), pos: 0, line };
|
|
217
|
+
const head = take(c);
|
|
218
|
+
if (head === void 0) {
|
|
219
|
+
const col = text.search(/[^ \t]/);
|
|
220
|
+
const message = text[col] === "#" ? "A `#` comment must start at column 0; an indented line is a field and needs a name" : "A field line needs a field name before its trailer";
|
|
221
|
+
return fail("E_UNPARSABLE", message, line, { col, end: Math.max(col + 1, text.length) });
|
|
222
|
+
}
|
|
223
|
+
if (head.kind !== "word") return unexpected(head, line, "field", FIELD_ORDER);
|
|
224
|
+
if (!FIELD_NAME.test(head.text)) {
|
|
225
|
+
return fail(
|
|
226
|
+
"E_BAD_NAME",
|
|
227
|
+
`Field names are lowerCamelCase (letters and digits, lowercase first): \`${head.text}\``,
|
|
228
|
+
line,
|
|
229
|
+
head
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const node = { name: name(head, line), line, identifier: false };
|
|
233
|
+
const star = peek(c);
|
|
234
|
+
if (star?.kind === "*") {
|
|
235
|
+
take(c);
|
|
236
|
+
node.identifier = true;
|
|
237
|
+
node.identifierAt = name(star, line);
|
|
238
|
+
}
|
|
239
|
+
const colon = peek(c);
|
|
240
|
+
if (colon?.kind === ":") {
|
|
241
|
+
take(c);
|
|
242
|
+
const type = parseType(c, colon);
|
|
243
|
+
if (!type.ok) return type;
|
|
244
|
+
node.type = type.value;
|
|
245
|
+
}
|
|
246
|
+
const at = peek(c);
|
|
247
|
+
if (at?.kind === "@") {
|
|
248
|
+
take(c);
|
|
249
|
+
const system = parseSystem(c, at);
|
|
250
|
+
if (!system.ok) return system;
|
|
251
|
+
node.system = system.value;
|
|
252
|
+
}
|
|
253
|
+
const equals = peek(c);
|
|
254
|
+
if (equals?.kind === "=") {
|
|
255
|
+
take(c);
|
|
256
|
+
const example = "e.g. `characterId = Character.id`";
|
|
257
|
+
const className = take(c);
|
|
258
|
+
if (className === void 0 || className.kind !== "word") {
|
|
259
|
+
return fail(
|
|
260
|
+
"E_UNPARSABLE",
|
|
261
|
+
`Expected \`Class.field\` after \`=\`, ${example}`,
|
|
262
|
+
line,
|
|
263
|
+
className ?? afterSpan(equals)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (!CLASS_NAME.test(className.text)) {
|
|
267
|
+
return fail(
|
|
268
|
+
"E_BAD_NAME",
|
|
269
|
+
`The class after \`=\` must be UpperCamelCase: \`${className.text}\``,
|
|
270
|
+
line,
|
|
271
|
+
className
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const dot = take(c);
|
|
275
|
+
if (dot === void 0 || dot.kind !== ".") {
|
|
276
|
+
return fail(
|
|
277
|
+
"E_UNPARSABLE",
|
|
278
|
+
`Expected \`.\` and a field name after \`= ${className.text}\`, ${example}`,
|
|
279
|
+
line,
|
|
280
|
+
dot ?? afterSpan(className)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const fieldName = take(c);
|
|
284
|
+
if (fieldName === void 0 || fieldName.kind !== "word") {
|
|
285
|
+
return fail(
|
|
286
|
+
"E_UNPARSABLE",
|
|
287
|
+
`Expected a field name after \`= ${className.text}.\`, ${example}`,
|
|
288
|
+
line,
|
|
289
|
+
fieldName ?? afterSpan(dot)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (!FIELD_NAME.test(fieldName.text)) {
|
|
293
|
+
return fail(
|
|
294
|
+
"E_BAD_NAME",
|
|
295
|
+
`The field after \`= ${className.text}.\` must be lowerCamelCase: \`${fieldName.text}\``,
|
|
296
|
+
line,
|
|
297
|
+
fieldName
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
node.joinsTo = { className: name(className, line), fieldName: name(fieldName, line) };
|
|
301
|
+
}
|
|
302
|
+
const rest = peek(c);
|
|
303
|
+
if (rest !== void 0) return unexpected(rest, line, "field", FIELD_ORDER);
|
|
304
|
+
if (trailer.description !== void 0) node.description = trailer.description;
|
|
305
|
+
if (trailer.note !== void 0) node.note = trailer.note;
|
|
306
|
+
return { ok: true, value: node };
|
|
307
|
+
}
|
|
308
|
+
function parseSystem(c, at) {
|
|
309
|
+
const system = take(c);
|
|
310
|
+
if (system === void 0 || system.kind !== "word") {
|
|
311
|
+
return fail(
|
|
312
|
+
"E_UNPARSABLE",
|
|
313
|
+
"Expected a system name after `@`, e.g. `@Catalog`",
|
|
314
|
+
c.line,
|
|
315
|
+
system ?? afterSpan(at)
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (!SYSTEM_OR_VALUE.test(system.text)) {
|
|
319
|
+
return fail(
|
|
320
|
+
"E_UNPARSABLE",
|
|
321
|
+
`A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
|
|
322
|
+
c.line,
|
|
323
|
+
system
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return { ok: true, value: name(system, c.line) };
|
|
327
|
+
}
|
|
328
|
+
function parseType(c, colon) {
|
|
329
|
+
const first = peek(c);
|
|
330
|
+
if (first === void 0 || first.kind === "*" || first.kind === "@" || first.kind === "=") {
|
|
331
|
+
return fail(
|
|
332
|
+
"E_MISSING_TYPE",
|
|
333
|
+
"A colon needs a type after it, e.g. `crewSize: int`",
|
|
334
|
+
c.line,
|
|
335
|
+
colon
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (first.kind !== "word") {
|
|
339
|
+
return fail(
|
|
340
|
+
"E_UNPARSABLE",
|
|
341
|
+
`Expected a type after \`:\`, found \`${first.text}\``,
|
|
342
|
+
c.line,
|
|
343
|
+
first
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
take(c);
|
|
347
|
+
const words = [first];
|
|
348
|
+
for (let pipe = peek(c); pipe?.kind === "|"; pipe = peek(c)) {
|
|
349
|
+
take(c);
|
|
350
|
+
const value = take(c);
|
|
351
|
+
if (value === void 0 || value.kind !== "word") {
|
|
352
|
+
return fail(
|
|
353
|
+
"E_UNPARSABLE",
|
|
354
|
+
"Expected an enum value after `|`, e.g. `climate: arid|temperate|frozen`",
|
|
355
|
+
c.line,
|
|
356
|
+
value ?? afterSpan(pipe)
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
words.push(value);
|
|
360
|
+
}
|
|
361
|
+
let many = false;
|
|
362
|
+
const bracket = peek(c);
|
|
363
|
+
if (bracket?.kind === "[]") {
|
|
364
|
+
take(c);
|
|
365
|
+
many = true;
|
|
366
|
+
} else if (bracket?.kind === "[") {
|
|
367
|
+
return fail(
|
|
368
|
+
"E_UNCLOSED_MANY",
|
|
369
|
+
"`[` must be closed: `[]` marks a many-valued field",
|
|
370
|
+
c.line,
|
|
371
|
+
bracket
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (words.length > 1) {
|
|
375
|
+
for (const w of words) {
|
|
376
|
+
if (!SYSTEM_OR_VALUE.test(w.text)) {
|
|
377
|
+
return fail(
|
|
378
|
+
"E_UNPARSABLE",
|
|
379
|
+
`An enum value is letters, digits, \`-\` and \`_\`, starting with a letter: \`${w.text}\``,
|
|
380
|
+
c.line,
|
|
381
|
+
w
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return { ok: true, value: { kind: "enum", values: words.map((w) => name(w, c.line)), many } };
|
|
386
|
+
}
|
|
387
|
+
const word = first.text;
|
|
388
|
+
const primitive = primitiveFor(word);
|
|
389
|
+
if (primitive !== void 0) {
|
|
390
|
+
return {
|
|
391
|
+
ok: true,
|
|
392
|
+
value: { kind: "primitive", name: primitive, written: name(first, c.line), many }
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (/^[A-Z]/.test(word)) {
|
|
396
|
+
if (!CLASS_NAME.test(word)) {
|
|
397
|
+
return fail(
|
|
398
|
+
"E_BAD_NAME",
|
|
399
|
+
`A referenced class must be UpperCamelCase: \`${word}\``,
|
|
400
|
+
c.line,
|
|
401
|
+
first
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return { ok: true, value: { kind: "class", name: name(first, c.line), many } };
|
|
405
|
+
}
|
|
406
|
+
if (/^[a-z]/.test(word)) {
|
|
407
|
+
return { ok: true, value: { kind: "unknown", name: name(first, c.line), many } };
|
|
408
|
+
}
|
|
409
|
+
return fail(
|
|
410
|
+
"E_UNPARSABLE",
|
|
411
|
+
`A type is a primitive, a class name or an enum, not \`${word}\``,
|
|
412
|
+
c.line,
|
|
413
|
+
first
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
function unexpected(token, line, kind, order) {
|
|
417
|
+
const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["@", "~"];
|
|
418
|
+
if (modifiers.includes(token.kind)) {
|
|
419
|
+
return fail(
|
|
420
|
+
"E_UNPARSABLE",
|
|
421
|
+
`\`${token.text}\` is out of order; a ${kind} line reads: ${order}`,
|
|
422
|
+
line,
|
|
423
|
+
token
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (token.text === "<") {
|
|
427
|
+
return fail(
|
|
428
|
+
"E_UNPARSABLE",
|
|
429
|
+
"`<` is reserved for inheritance and not yet part of the language",
|
|
430
|
+
line,
|
|
431
|
+
token
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (token.text === "?") {
|
|
435
|
+
return fail(
|
|
436
|
+
"E_UNPARSABLE",
|
|
437
|
+
"`?` attached to a word is not a marker; a doubt needs whitespace before `?` and after it",
|
|
438
|
+
line,
|
|
439
|
+
token
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
if (token.text === "#") {
|
|
443
|
+
return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line, token);
|
|
444
|
+
}
|
|
445
|
+
return fail("E_UNPARSABLE", `Unexpected \`${token.text}\` on a ${kind} line`, line, token);
|
|
446
|
+
}
|
|
447
|
+
|
|
8
448
|
// src/resolve.ts
|
|
9
449
|
function resolve(doc) {
|
|
10
450
|
const out = {
|
|
@@ -23,7 +463,9 @@ function resolve(doc) {
|
|
|
23
463
|
const reference = (name2) => {
|
|
24
464
|
if (declared.has(name2.text)) return true;
|
|
25
465
|
if (!undeclared.has(name2.text)) undeclared.set(name2.text, name2);
|
|
26
|
-
|
|
466
|
+
const lower = name2.text.toLowerCase();
|
|
467
|
+
const hint = Object.hasOwn(PRIMITIVES, lower) ? `. Did you mean \`${lower}\`?` : "";
|
|
468
|
+
warn("W_UNDECLARED_CLASS", `class \`${name2.text}\` is not declared${hint}`, name2);
|
|
27
469
|
return false;
|
|
28
470
|
};
|
|
29
471
|
const firstClass = /* @__PURE__ */ new Map();
|
|
@@ -56,7 +498,7 @@ function resolve(doc) {
|
|
|
56
498
|
warn(
|
|
57
499
|
"W_MULTIPLE_IDENTIFIERS",
|
|
58
500
|
`\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\` already has the identifier \`${identifier.name.text}\` on line ${identifier.line}`,
|
|
59
|
-
field.name
|
|
501
|
+
field.identifierAt ?? field.name
|
|
60
502
|
);
|
|
61
503
|
field.identifier = false;
|
|
62
504
|
}
|
|
@@ -85,9 +527,9 @@ function resolve(doc) {
|
|
|
85
527
|
}
|
|
86
528
|
}
|
|
87
529
|
}
|
|
88
|
-
const
|
|
530
|
+
const present2 = new Set(out.diagnostics.map(key));
|
|
89
531
|
for (const w of warnings) {
|
|
90
|
-
if (!
|
|
532
|
+
if (!present2.has(key(w))) out.diagnostics.push(w);
|
|
91
533
|
}
|
|
92
534
|
out.diagnostics.sort((a, b) => a.line - b.line);
|
|
93
535
|
out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
|
|
@@ -111,6 +553,7 @@ var copyJoin = (j) => ({
|
|
|
111
553
|
});
|
|
112
554
|
function copyField(f) {
|
|
113
555
|
const out = { ...f, name: copyName(f.name) };
|
|
556
|
+
if (f.identifierAt !== void 0) out.identifierAt = copyName(f.identifierAt);
|
|
114
557
|
if (f.type !== void 0) out.type = copyType(f.type);
|
|
115
558
|
if (f.joinsTo !== void 0) out.joinsTo = copyJoin(f.joinsTo);
|
|
116
559
|
return out;
|
|
@@ -122,18 +565,7 @@ function copyClass(c) {
|
|
|
122
565
|
return out;
|
|
123
566
|
}
|
|
124
567
|
var key = (d) => [d.severity, d.code, d.line, d.col, d.end, d.message].join(" ");
|
|
125
|
-
var TYPE_WORDS =
|
|
126
|
-
"string",
|
|
127
|
-
"int",
|
|
128
|
-
"float",
|
|
129
|
-
"bool",
|
|
130
|
-
"date",
|
|
131
|
-
"datetime",
|
|
132
|
-
"uri",
|
|
133
|
-
"text",
|
|
134
|
-
"integer",
|
|
135
|
-
"boolean"
|
|
136
|
-
];
|
|
568
|
+
var TYPE_WORDS = Object.keys(PRIMITIVES);
|
|
137
569
|
var MAX_DISTANCE = 2;
|
|
138
570
|
function closestTypeWord(word) {
|
|
139
571
|
let best;
|
|
@@ -144,722 +576,287 @@ function closestTypeWord(word) {
|
|
|
144
576
|
if (distance > MAX_DISTANCE || distance > bestDistance) continue;
|
|
145
577
|
const prefix = commonPrefixLength(word, candidate);
|
|
146
578
|
if (distance < bestDistance || prefix > bestPrefix) {
|
|
147
|
-
best = candidate;
|
|
148
|
-
bestDistance = distance;
|
|
149
|
-
bestPrefix = prefix;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return best;
|
|
153
|
-
}
|
|
154
|
-
function commonPrefixLength(a, b) {
|
|
155
|
-
let i = 0;
|
|
156
|
-
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
157
|
-
return i;
|
|
158
|
-
}
|
|
159
|
-
function levenshtein(a, b) {
|
|
160
|
-
if (a === b) return 0;
|
|
161
|
-
if (a.length === 0) return b.length;
|
|
162
|
-
if (b.length === 0) return a.length;
|
|
163
|
-
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
164
|
-
for (let i = 1; i <= a.length; i++) {
|
|
165
|
-
const current = [i];
|
|
166
|
-
for (let j = 1; j <= b.length; j++) {
|
|
167
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
168
|
-
current[j] = Math.min(
|
|
169
|
-
(previous[j] ?? 0) + 1,
|
|
170
|
-
(current[j - 1] ?? 0) + 1,
|
|
171
|
-
(previous[j - 1] ?? 0) + cost
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
previous = current;
|
|
175
|
-
}
|
|
176
|
-
return previous[b.length] ?? 0;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// src/generators/linkml.ts
|
|
180
|
-
var BASE_URI = "https://example.org";
|
|
181
|
-
var RANGE = {
|
|
182
|
-
string: "string",
|
|
183
|
-
int: "integer",
|
|
184
|
-
float: "float",
|
|
185
|
-
bool: "boolean",
|
|
186
|
-
date: "date",
|
|
187
|
-
datetime: "datetime",
|
|
188
|
-
uri: "uri"
|
|
189
|
-
};
|
|
190
|
-
function toLinkML(doc, opts) {
|
|
191
|
-
const resolved = resolve(doc);
|
|
192
|
-
const schemaName = linkmlName(opts.schemaName);
|
|
193
|
-
const classes = emittedClasses(resolved.classes);
|
|
194
|
-
const classNames = new Set(classes.map((cls) => cls.node.name.text));
|
|
195
|
-
for (const name2 of resolved.undeclared) classNames.add(name2.text);
|
|
196
|
-
const enums = nameEnums(classes, classNames);
|
|
197
|
-
const prefixes = { linkml: "https://w3id.org/linkml/" };
|
|
198
|
-
if (!Object.hasOwn(prefixes, schemaName)) prefixes[schemaName] = `${BASE_URI}/${schemaName}/`;
|
|
199
|
-
const systemPrefix = /* @__PURE__ */ new Map();
|
|
200
|
-
const addSystem = (system) => {
|
|
201
|
-
const prefix = linkmlName(system);
|
|
202
|
-
if (!systemPrefix.has(system)) systemPrefix.set(system, prefix);
|
|
203
|
-
if (!Object.hasOwn(prefixes, prefix)) prefixes[prefix] = `${BASE_URI}/system/${prefix}/`;
|
|
204
|
-
};
|
|
205
|
-
for (const { node, fields } of classes) {
|
|
206
|
-
if (node.system !== void 0) addSystem(node.system.text);
|
|
207
|
-
for (const field of fields) {
|
|
208
|
-
if (field.system !== void 0) addSystem(field.system.text);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const declaredSystem = /* @__PURE__ */ new Map();
|
|
212
|
-
for (const { node } of classes) {
|
|
213
|
-
if (!declaredSystem.has(node.name.text)) declaredSystem.set(node.name.text, node.system?.text);
|
|
214
|
-
}
|
|
215
|
-
const mappingPrefix = (target) => {
|
|
216
|
-
const system = declaredSystem.get(target);
|
|
217
|
-
if (system === void 0) return schemaName;
|
|
218
|
-
return systemPrefix.get(system) ?? schemaName;
|
|
219
|
-
};
|
|
220
|
-
const schema = {
|
|
221
|
-
id: `${BASE_URI}/${schemaName}`,
|
|
222
|
-
name: schemaName,
|
|
223
|
-
default_prefix: schemaName,
|
|
224
|
-
default_range: "string",
|
|
225
|
-
prefixes,
|
|
226
|
-
imports: ["linkml:types"]
|
|
227
|
-
};
|
|
228
|
-
const out = {};
|
|
229
|
-
for (const cls of classes) {
|
|
230
|
-
out[cls.node.name.text] = buildClass(cls, mappingPrefix, enums);
|
|
231
|
-
}
|
|
232
|
-
for (const name2 of resolved.undeclared) {
|
|
233
|
-
if (!(name2.text in out)) out[name2.text] = { annotations: { undeclared: true } };
|
|
234
|
-
}
|
|
235
|
-
if (Object.keys(out).length > 0) schema.classes = out;
|
|
236
|
-
const enumBlock = buildEnums(classes, enums);
|
|
237
|
-
if (Object.keys(enumBlock).length > 0) schema.enums = enumBlock;
|
|
238
|
-
return schema;
|
|
239
|
-
}
|
|
240
|
-
function emittedClasses(classes) {
|
|
241
|
-
const out = [];
|
|
242
|
-
const seen = /* @__PURE__ */ new Set();
|
|
243
|
-
for (const node of classes) {
|
|
244
|
-
if (seen.has(node.name.text)) continue;
|
|
245
|
-
seen.add(node.name.text);
|
|
246
|
-
const fields = [];
|
|
247
|
-
const seenField = /* @__PURE__ */ new Set();
|
|
248
|
-
for (const field of node.fields) {
|
|
249
|
-
if (seenField.has(field.name.text)) continue;
|
|
250
|
-
seenField.add(field.name.text);
|
|
251
|
-
fields.push(field);
|
|
252
|
-
}
|
|
253
|
-
out.push({ node, fields });
|
|
254
|
-
}
|
|
255
|
-
return out;
|
|
256
|
-
}
|
|
257
|
-
var useKey = (className, fieldName) => `${className}\0${fieldName}`;
|
|
258
|
-
function nameEnums(classes, classNames) {
|
|
259
|
-
const uses = [];
|
|
260
|
-
for (const { node, fields } of classes) {
|
|
261
|
-
for (const field of fields) {
|
|
262
|
-
if (field.type?.kind !== "enum") continue;
|
|
263
|
-
uses.push({
|
|
264
|
-
className: node.name.text,
|
|
265
|
-
fieldName: field.name.text,
|
|
266
|
-
values: field.type.values.map((v) => v.text).join("|")
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
const valueSets = /* @__PURE__ */ new Map();
|
|
271
|
-
for (const use of uses) {
|
|
272
|
-
const seen = valueSets.get(use.fieldName) ?? /* @__PURE__ */ new Set();
|
|
273
|
-
seen.add(use.values);
|
|
274
|
-
valueSets.set(use.fieldName, seen);
|
|
275
|
-
}
|
|
276
|
-
const taken = /* @__PURE__ */ new Map();
|
|
277
|
-
const names = /* @__PURE__ */ new Map();
|
|
278
|
-
for (const use of uses) {
|
|
279
|
-
const shared = (valueSets.get(use.fieldName)?.size ?? 0) <= 1;
|
|
280
|
-
const base = shared ? `${capitalise(use.fieldName)}Enum` : `${capitalise(use.className)}${capitalise(use.fieldName)}Enum`;
|
|
281
|
-
let name2 = base;
|
|
282
|
-
for (let n = 2; classNames.has(name2) || taken.has(name2) && taken.get(name2) !== use.values; n++) {
|
|
283
|
-
name2 = `${base}${n}`;
|
|
284
|
-
}
|
|
285
|
-
taken.set(name2, use.values);
|
|
286
|
-
names.set(useKey(use.className, use.fieldName), name2);
|
|
287
|
-
}
|
|
288
|
-
return names;
|
|
289
|
-
}
|
|
290
|
-
function buildEnums(classes, enums) {
|
|
291
|
-
const out = {};
|
|
292
|
-
for (const { node, fields } of classes) {
|
|
293
|
-
for (const field of fields) {
|
|
294
|
-
if (field.type?.kind !== "enum") continue;
|
|
295
|
-
const name2 = enums.get(useKey(node.name.text, field.name.text));
|
|
296
|
-
if (name2 === void 0 || name2 in out) continue;
|
|
297
|
-
const values = {};
|
|
298
|
-
for (const value of field.type.values) values[value.text] = null;
|
|
299
|
-
out[name2] = { permissible_values: values };
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return out;
|
|
303
|
-
}
|
|
304
|
-
function buildClass(cls, mappingPrefix, enums) {
|
|
305
|
-
const { node, fields } = cls;
|
|
306
|
-
const out = {};
|
|
307
|
-
if (node.description !== void 0) out.description = node.description;
|
|
308
|
-
const annotations = {};
|
|
309
|
-
if (node.system !== void 0) annotations.system = node.system.text;
|
|
310
|
-
if (node.note !== void 0) annotations.note = node.note;
|
|
311
|
-
if (Object.keys(annotations).length > 0) out.annotations = annotations;
|
|
312
|
-
if (node.similarTo !== void 0) {
|
|
313
|
-
const target = node.similarTo.text;
|
|
314
|
-
out.close_mappings = [`${mappingPrefix(target)}:${target}`];
|
|
315
|
-
}
|
|
316
|
-
const attributes = {};
|
|
317
|
-
for (const field of fields) {
|
|
318
|
-
attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
|
|
319
|
-
}
|
|
320
|
-
if (Object.keys(attributes).length > 0) out.attributes = attributes;
|
|
321
|
-
return out;
|
|
322
|
-
}
|
|
323
|
-
function buildAttribute(field, className, enums) {
|
|
324
|
-
const out = {};
|
|
325
|
-
if (field.description !== void 0) out.description = field.description;
|
|
326
|
-
const range = rangeOf(field, className, enums);
|
|
327
|
-
if (range !== void 0) out.range = range;
|
|
328
|
-
if (field.type?.many === true) out.multivalued = true;
|
|
329
|
-
if (field.identifier) out.identifier = true;
|
|
330
|
-
const annotations = {};
|
|
331
|
-
if (field.system !== void 0) annotations.system = field.system.text;
|
|
332
|
-
if (field.joinsTo !== void 0) {
|
|
333
|
-
annotations.joins_to = `${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}`;
|
|
334
|
-
}
|
|
335
|
-
if (field.note !== void 0) annotations.note = field.note;
|
|
336
|
-
if (Object.keys(annotations).length > 0) out.annotations = annotations;
|
|
337
|
-
return out;
|
|
338
|
-
}
|
|
339
|
-
function rangeOf(field, className, enums) {
|
|
340
|
-
const type = field.type;
|
|
341
|
-
if (type === void 0) return void 0;
|
|
342
|
-
switch (type.kind) {
|
|
343
|
-
case "primitive":
|
|
344
|
-
return RANGE[type.name];
|
|
345
|
-
case "class":
|
|
346
|
-
return type.name.text;
|
|
347
|
-
case "unknown":
|
|
348
|
-
return "string";
|
|
349
|
-
case "enum":
|
|
350
|
-
return enums.get(useKey(className, field.name.text));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
var RESERVED_PREFIXES = /* @__PURE__ */ new Set(["linkml", "xsd", "shex", "schema"]);
|
|
354
|
-
var linkmlName = (text) => {
|
|
355
|
-
const name2 = text.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
356
|
-
if (!/[a-z0-9]/.test(name2)) return "sketch";
|
|
357
|
-
if (RESERVED_PREFIXES.has(name2)) return `${name2}_`;
|
|
358
|
-
return /^[0-9]/.test(name2) ? `_${name2}` : name2;
|
|
359
|
-
};
|
|
360
|
-
var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
361
|
-
|
|
362
|
-
// src/generators/mermaid.ts
|
|
363
|
-
function toMermaid(doc, opts) {
|
|
364
|
-
const resolved = resolve(doc);
|
|
365
|
-
const notes = opts?.notes === true;
|
|
366
|
-
const lines = ["classDiagram"];
|
|
367
|
-
for (const cls of resolved.classes) {
|
|
368
|
-
lines.push(` class ${cls.name.text} {`);
|
|
369
|
-
if (cls.system !== void 0) lines.push(` <<${cls.system.text}>>`);
|
|
370
|
-
for (const field of cls.fields) lines.push(` ${member(field)}`);
|
|
371
|
-
lines.push(" }");
|
|
372
|
-
}
|
|
373
|
-
for (const name2 of resolved.undeclared) {
|
|
374
|
-
lines.push(` class ${name2.text} {`, " <<undeclared>>", " }");
|
|
375
|
-
}
|
|
376
|
-
if (notes) {
|
|
377
|
-
for (const cls of resolved.classes) {
|
|
378
|
-
if (cls.note !== void 0) lines.push(` note for ${cls.name.text} ${quote(cls.note)}`);
|
|
379
|
-
for (const field of cls.fields) {
|
|
380
|
-
if (field.note !== void 0) {
|
|
381
|
-
lines.push(` note for ${cls.name.text} ${quote(`${field.name.text}: ${field.note}`)}`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
for (const cls of resolved.classes) {
|
|
387
|
-
const from = cls.name.text;
|
|
388
|
-
if (cls.similarTo !== void 0) lines.push(` ${from} ..> ${cls.similarTo.text} : similar`);
|
|
389
|
-
for (const field of cls.fields) {
|
|
390
|
-
const type = field.type;
|
|
391
|
-
if (type?.kind === "class") {
|
|
392
|
-
lines.push(
|
|
393
|
-
type.many ? ` ${from} "1" --> "*" ${type.name.text} : ${field.name.text}` : ` ${from} --> ${type.name.text} : ${field.name.text}`
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
if (field.joinsTo !== void 0) {
|
|
397
|
-
const { className, fieldName } = field.joinsTo;
|
|
398
|
-
lines.push(` ${from} ..> ${className.text} : ${field.name.text} = ${fieldName.text}`);
|
|
399
|
-
}
|
|
579
|
+
best = candidate;
|
|
580
|
+
bestDistance = distance;
|
|
581
|
+
bestPrefix = prefix;
|
|
400
582
|
}
|
|
401
583
|
}
|
|
402
|
-
return
|
|
403
|
-
`;
|
|
584
|
+
return best;
|
|
404
585
|
}
|
|
405
|
-
function
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return `${marker}${type}${field.name.text}${system}`;
|
|
586
|
+
function commonPrefixLength(a, b) {
|
|
587
|
+
let i = 0;
|
|
588
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
589
|
+
return i;
|
|
410
590
|
}
|
|
411
|
-
function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
591
|
+
function levenshtein(a, b) {
|
|
592
|
+
if (a === b) return 0;
|
|
593
|
+
if (a.length === 0) return b.length;
|
|
594
|
+
if (b.length === 0) return a.length;
|
|
595
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
596
|
+
for (let i = 1; i <= a.length; i++) {
|
|
597
|
+
const current = [i];
|
|
598
|
+
for (let j = 1; j <= b.length; j++) {
|
|
599
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
600
|
+
current[j] = Math.min(
|
|
601
|
+
(previous[j] ?? 0) + 1,
|
|
602
|
+
(current[j - 1] ?? 0) + 1,
|
|
603
|
+
(previous[j - 1] ?? 0) + cost
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
previous = current;
|
|
424
607
|
}
|
|
425
|
-
return
|
|
608
|
+
return previous[b.length] ?? 0;
|
|
426
609
|
}
|
|
427
|
-
var quote = (text) => `"${text.replaceAll('"', "'")}"`;
|
|
428
610
|
|
|
429
|
-
// src/
|
|
430
|
-
var
|
|
431
|
-
var
|
|
432
|
-
var CLASS_NAME = /^[A-Z][A-Za-z0-9]*$/;
|
|
433
|
-
var FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
|
|
434
|
-
var SYSTEM_OR_VALUE = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
435
|
-
var PRIMITIVES = {
|
|
611
|
+
// src/generators/linkml.ts
|
|
612
|
+
var BASE_URI = "https://example.org";
|
|
613
|
+
var RANGE = {
|
|
436
614
|
string: "string",
|
|
437
|
-
int: "
|
|
615
|
+
int: "integer",
|
|
438
616
|
float: "float",
|
|
439
|
-
bool: "
|
|
617
|
+
bool: "boolean",
|
|
440
618
|
date: "date",
|
|
441
619
|
datetime: "datetime",
|
|
442
|
-
uri: "uri"
|
|
443
|
-
text: "string",
|
|
444
|
-
integer: "int",
|
|
445
|
-
boolean: "bool"
|
|
620
|
+
uri: "uri"
|
|
446
621
|
};
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const classes =
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
if (
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
message: "An indented line is a field, but there is no valid class line above it",
|
|
467
|
-
line,
|
|
468
|
-
col: start,
|
|
469
|
-
end: text.length
|
|
470
|
-
});
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
const result = parseFieldLine(text, line);
|
|
474
|
-
if (result.ok) current.fields.push(result.value);
|
|
475
|
-
else diagnostics.push(result.diagnostic);
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
if (/\p{L}/u.test(first)) {
|
|
479
|
-
const result = parseClassLine(text, line);
|
|
480
|
-
if (result.ok) {
|
|
481
|
-
classes.push(result.value);
|
|
482
|
-
current = result.value;
|
|
483
|
-
} else {
|
|
484
|
-
diagnostics.push(result.diagnostic);
|
|
485
|
-
current = void 0;
|
|
486
|
-
}
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
diagnostics.push({
|
|
490
|
-
severity: "error",
|
|
491
|
-
code: "E_UNPARSABLE",
|
|
492
|
-
message: "A line at column 0 must start with a class name or `#` for a comment",
|
|
493
|
-
line,
|
|
494
|
-
col: 0,
|
|
495
|
-
end: text.length
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
return { classes, diagnostics };
|
|
499
|
-
}
|
|
500
|
-
function isDoubtMarker(text, i) {
|
|
501
|
-
return text[i] === "?" && isWs(text[i - 1]) && (i + 1 === text.length || isWs(text[i + 1]));
|
|
502
|
-
}
|
|
503
|
-
function splitTrailer(text) {
|
|
504
|
-
let start = -1;
|
|
505
|
-
for (let i = 1; i < text.length; i++) {
|
|
506
|
-
if (!isWs(text[i - 1])) continue;
|
|
507
|
-
if (text[i] === "#" || isDoubtMarker(text, i)) {
|
|
508
|
-
start = i;
|
|
509
|
-
break;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
if (start < 0) return { head: text };
|
|
513
|
-
const trailer = { head: text.slice(0, start).replace(/[ \t]+$/, "") };
|
|
514
|
-
if (text[start] === "?") {
|
|
515
|
-
const note = trimWs(text.slice(start + 1));
|
|
516
|
-
if (note !== "") trailer.note = note;
|
|
517
|
-
return trailer;
|
|
518
|
-
}
|
|
519
|
-
let doubt = -1;
|
|
520
|
-
for (let i = start + 1; i < text.length; i++) {
|
|
521
|
-
if (isDoubtMarker(text, i)) {
|
|
522
|
-
doubt = i;
|
|
523
|
-
break;
|
|
622
|
+
function toLinkML(doc, opts) {
|
|
623
|
+
const resolved = resolve(doc);
|
|
624
|
+
const schemaName = linkmlName(opts.schemaName);
|
|
625
|
+
const classes = emittedClasses(resolved.classes);
|
|
626
|
+
const classNames = new Set(classes.map((cls) => cls.node.name.text));
|
|
627
|
+
for (const name2 of resolved.undeclared) classNames.add(name2.text);
|
|
628
|
+
const enums = nameEnums(classes, classNames);
|
|
629
|
+
const prefixes = { linkml: "https://w3id.org/linkml/" };
|
|
630
|
+
if (!Object.hasOwn(prefixes, schemaName)) prefixes[schemaName] = `${BASE_URI}/${schemaName}/`;
|
|
631
|
+
const systemPrefix = /* @__PURE__ */ new Map();
|
|
632
|
+
const addSystem = (system) => {
|
|
633
|
+
const prefix = linkmlName(system);
|
|
634
|
+
if (!systemPrefix.has(system)) systemPrefix.set(system, prefix);
|
|
635
|
+
if (!Object.hasOwn(prefixes, prefix)) prefixes[prefix] = `${BASE_URI}/system/${prefix}/`;
|
|
636
|
+
};
|
|
637
|
+
for (const { node, fields } of classes) {
|
|
638
|
+
if (node.system !== void 0) addSystem(node.system.text);
|
|
639
|
+
for (const field of fields) {
|
|
640
|
+
if (field.system !== void 0) addSystem(field.system.text);
|
|
524
641
|
}
|
|
525
642
|
}
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const note = trimWs(text.slice(doubt + 1));
|
|
530
|
-
if (note !== "") trailer.note = note;
|
|
531
|
-
}
|
|
532
|
-
return trailer;
|
|
533
|
-
}
|
|
534
|
-
var WORD = /[\p{L}\p{N}_-]+/uy;
|
|
535
|
-
var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "["];
|
|
536
|
-
function tokenize(head) {
|
|
537
|
-
const tokens = [];
|
|
538
|
-
let i = 0;
|
|
539
|
-
while (i < head.length) {
|
|
540
|
-
const c = head[i] ?? "";
|
|
541
|
-
if (isWs(c)) {
|
|
542
|
-
i++;
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
WORD.lastIndex = i;
|
|
546
|
-
const word = WORD.exec(head);
|
|
547
|
-
if (word !== null) {
|
|
548
|
-
tokens.push({ kind: "word", text: word[0], col: i, end: i + word[0].length });
|
|
549
|
-
i += word[0].length;
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
if (c === "[" && head[i + 1] === "]") {
|
|
553
|
-
tokens.push({ kind: "[]", text: "[]", col: i, end: i + 2 });
|
|
554
|
-
i += 2;
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
const ch = String.fromCodePoint(head.codePointAt(i) ?? 0);
|
|
558
|
-
const kind = OPERATORS.find((op) => op === ch) ?? "other";
|
|
559
|
-
tokens.push({ kind, text: ch, col: i, end: i + ch.length });
|
|
560
|
-
i += ch.length;
|
|
643
|
+
const declaredSystem = /* @__PURE__ */ new Map();
|
|
644
|
+
for (const { node } of classes) {
|
|
645
|
+
if (!declaredSystem.has(node.name.text)) declaredSystem.set(node.name.text, node.system?.text);
|
|
561
646
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const t = c.tokens[c.pos];
|
|
567
|
-
if (t !== void 0) c.pos++;
|
|
568
|
-
return t;
|
|
569
|
-
}
|
|
570
|
-
var name = (t, line) => ({ text: t.text, line, col: t.col, end: t.end });
|
|
571
|
-
function fail(code, message, line, span) {
|
|
572
|
-
return {
|
|
573
|
-
ok: false,
|
|
574
|
-
diagnostic: { severity: "error", code, message, line, col: span.col, end: span.end }
|
|
647
|
+
const mappingPrefix = (target) => {
|
|
648
|
+
const system = declaredSystem.get(target);
|
|
649
|
+
if (system === void 0) return schemaName;
|
|
650
|
+
return systemPrefix.get(system) ?? schemaName;
|
|
575
651
|
};
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
if (!CLASS_NAME.test(head.text)) {
|
|
589
|
-
return fail(
|
|
590
|
-
"E_BAD_NAME",
|
|
591
|
-
`Class names are UpperCamelCase (letters and digits, uppercase first): \`${head.text}\``,
|
|
592
|
-
line,
|
|
593
|
-
head
|
|
594
|
-
);
|
|
652
|
+
const schema = {
|
|
653
|
+
id: `${BASE_URI}/${schemaName}`,
|
|
654
|
+
name: schemaName,
|
|
655
|
+
default_prefix: schemaName,
|
|
656
|
+
default_range: "string",
|
|
657
|
+
prefixes,
|
|
658
|
+
imports: ["linkml:types"]
|
|
659
|
+
};
|
|
660
|
+
const out = {};
|
|
661
|
+
for (const cls of classes) {
|
|
662
|
+
out[cls.node.name.text] = buildClass(cls, mappingPrefix, enums);
|
|
595
663
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
if (at?.kind === "@") {
|
|
599
|
-
take(c);
|
|
600
|
-
const system = parseSystem(c, at);
|
|
601
|
-
if (!system.ok) return system;
|
|
602
|
-
node.system = system.value;
|
|
664
|
+
for (const name2 of resolved.undeclared) {
|
|
665
|
+
if (!(name2.text in out)) out[name2.text] = { annotations: { undeclared: true } };
|
|
603
666
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
target
|
|
622
|
-
);
|
|
667
|
+
if (Object.keys(out).length > 0) schema.classes = out;
|
|
668
|
+
const enumBlock = buildEnums(classes, enums);
|
|
669
|
+
if (Object.keys(enumBlock).length > 0) schema.enums = enumBlock;
|
|
670
|
+
return schema;
|
|
671
|
+
}
|
|
672
|
+
function emittedClasses(classes) {
|
|
673
|
+
const out = [];
|
|
674
|
+
const seen = /* @__PURE__ */ new Set();
|
|
675
|
+
for (const node of classes) {
|
|
676
|
+
if (seen.has(node.name.text)) continue;
|
|
677
|
+
seen.add(node.name.text);
|
|
678
|
+
const fields = [];
|
|
679
|
+
const seenField = /* @__PURE__ */ new Set();
|
|
680
|
+
for (const field of node.fields) {
|
|
681
|
+
if (seenField.has(field.name.text)) continue;
|
|
682
|
+
seenField.add(field.name.text);
|
|
683
|
+
fields.push(field);
|
|
623
684
|
}
|
|
624
|
-
|
|
685
|
+
out.push({ node, fields });
|
|
625
686
|
}
|
|
626
|
-
|
|
627
|
-
if (rest !== void 0) return unexpected(rest, line, "class", CLASS_ORDER);
|
|
628
|
-
if (trailer.description !== void 0) node.description = trailer.description;
|
|
629
|
-
if (trailer.note !== void 0) node.note = trailer.note;
|
|
630
|
-
return { ok: true, value: { ...node, fields: [] } };
|
|
687
|
+
return out;
|
|
631
688
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
689
|
+
var useKey = (className, fieldName) => `${className}\0${fieldName}`;
|
|
690
|
+
function nameEnums(classes, classNames) {
|
|
691
|
+
const uses = [];
|
|
692
|
+
for (const { node, fields } of classes) {
|
|
693
|
+
for (const field of fields) {
|
|
694
|
+
if (field.type?.kind !== "enum") continue;
|
|
695
|
+
uses.push({
|
|
696
|
+
className: node.name.text,
|
|
697
|
+
fieldName: field.name.text,
|
|
698
|
+
values: field.type.values.map((v) => v.text).join("|")
|
|
699
|
+
});
|
|
700
|
+
}
|
|
640
701
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
line,
|
|
647
|
-
head
|
|
648
|
-
);
|
|
702
|
+
const valueSets = /* @__PURE__ */ new Map();
|
|
703
|
+
for (const use of uses) {
|
|
704
|
+
const seen = valueSets.get(use.fieldName) ?? /* @__PURE__ */ new Set();
|
|
705
|
+
seen.add(use.values);
|
|
706
|
+
valueSets.set(use.fieldName, seen);
|
|
649
707
|
}
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
708
|
+
const taken = /* @__PURE__ */ new Map();
|
|
709
|
+
const names = /* @__PURE__ */ new Map();
|
|
710
|
+
for (const use of uses) {
|
|
711
|
+
const shared = (valueSets.get(use.fieldName)?.size ?? 0) <= 1;
|
|
712
|
+
const base = shared ? `${capitalise(use.fieldName)}Enum` : `${capitalise(use.className)}${capitalise(use.fieldName)}Enum`;
|
|
713
|
+
let name2 = base;
|
|
714
|
+
for (let n = 2; classNames.has(name2) || taken.has(name2) && taken.get(name2) !== use.values; n++) {
|
|
715
|
+
name2 = `${base}${n}`;
|
|
716
|
+
}
|
|
717
|
+
taken.set(name2, use.values);
|
|
718
|
+
names.set(useKey(use.className, use.fieldName), name2);
|
|
654
719
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
720
|
+
return names;
|
|
721
|
+
}
|
|
722
|
+
function buildEnums(classes, enums) {
|
|
723
|
+
const out = {};
|
|
724
|
+
for (const { node, fields } of classes) {
|
|
725
|
+
for (const field of fields) {
|
|
726
|
+
if (field.type?.kind !== "enum") continue;
|
|
727
|
+
const name2 = enums.get(useKey(node.name.text, field.name.text));
|
|
728
|
+
if (name2 === void 0 || name2 in out) continue;
|
|
729
|
+
const values = {};
|
|
730
|
+
for (const value of field.type.values) values[value.text] = null;
|
|
731
|
+
out[name2] = { permissible_values: values };
|
|
732
|
+
}
|
|
661
733
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
function buildClass(cls, mappingPrefix, enums) {
|
|
737
|
+
const { node, fields } = cls;
|
|
738
|
+
const out = {};
|
|
739
|
+
if (node.description !== void 0) out.description = node.description;
|
|
740
|
+
const annotations = {};
|
|
741
|
+
if (node.system !== void 0) annotations.system = node.system.text;
|
|
742
|
+
if (node.note !== void 0) annotations.note = node.note;
|
|
743
|
+
if (Object.keys(annotations).length > 0) out.annotations = annotations;
|
|
744
|
+
if (node.similarTo !== void 0) {
|
|
745
|
+
const target = node.similarTo.text;
|
|
746
|
+
out.close_mappings = [`${mappingPrefix(target)}:${target}`];
|
|
668
747
|
}
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const example = "e.g. `characterId = Character.id`";
|
|
673
|
-
const className = take(c);
|
|
674
|
-
if (className === void 0 || className.kind !== "word") {
|
|
675
|
-
return fail(
|
|
676
|
-
"E_UNPARSABLE",
|
|
677
|
-
`Expected \`Class.field\` after \`=\`, ${example}`,
|
|
678
|
-
line,
|
|
679
|
-
className ?? afterSpan(equals)
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
if (!CLASS_NAME.test(className.text)) {
|
|
683
|
-
return fail(
|
|
684
|
-
"E_BAD_NAME",
|
|
685
|
-
`The class after \`=\` must be UpperCamelCase: \`${className.text}\``,
|
|
686
|
-
line,
|
|
687
|
-
className
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
const dot = take(c);
|
|
691
|
-
if (dot === void 0 || dot.kind !== ".") {
|
|
692
|
-
return fail(
|
|
693
|
-
"E_UNPARSABLE",
|
|
694
|
-
`Expected \`.\` and a field name after \`= ${className.text}\`, ${example}`,
|
|
695
|
-
line,
|
|
696
|
-
dot ?? afterSpan(className)
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
const fieldName = take(c);
|
|
700
|
-
if (fieldName === void 0 || fieldName.kind !== "word") {
|
|
701
|
-
return fail(
|
|
702
|
-
"E_UNPARSABLE",
|
|
703
|
-
`Expected a field name after \`= ${className.text}.\`, ${example}`,
|
|
704
|
-
line,
|
|
705
|
-
fieldName ?? afterSpan(dot)
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
if (!FIELD_NAME.test(fieldName.text)) {
|
|
709
|
-
return fail(
|
|
710
|
-
"E_BAD_NAME",
|
|
711
|
-
`The field after \`= ${className.text}.\` must be lowerCamelCase: \`${fieldName.text}\``,
|
|
712
|
-
line,
|
|
713
|
-
fieldName
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
node.joinsTo = { className: name(className, line), fieldName: name(fieldName, line) };
|
|
748
|
+
const attributes = {};
|
|
749
|
+
for (const field of fields) {
|
|
750
|
+
attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
|
|
717
751
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (trailer.description !== void 0) node.description = trailer.description;
|
|
721
|
-
if (trailer.note !== void 0) node.note = trailer.note;
|
|
722
|
-
return { ok: true, value: node };
|
|
752
|
+
if (Object.keys(attributes).length > 0) out.attributes = attributes;
|
|
753
|
+
return out;
|
|
723
754
|
}
|
|
724
|
-
function
|
|
725
|
-
const
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
755
|
+
function buildAttribute(field, className, enums) {
|
|
756
|
+
const out = {};
|
|
757
|
+
if (field.description !== void 0) out.description = field.description;
|
|
758
|
+
const range = rangeOf(field, className, enums);
|
|
759
|
+
if (range !== void 0) out.range = range;
|
|
760
|
+
if (field.type?.many === true) out.multivalued = true;
|
|
761
|
+
if (field.identifier) out.identifier = true;
|
|
762
|
+
const annotations = {};
|
|
763
|
+
if (field.system !== void 0) annotations.system = field.system.text;
|
|
764
|
+
if (field.joinsTo !== void 0) {
|
|
765
|
+
annotations.joins_to = `${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}`;
|
|
733
766
|
}
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
767
|
+
if (field.note !== void 0) annotations.note = field.note;
|
|
768
|
+
if (Object.keys(annotations).length > 0) out.annotations = annotations;
|
|
769
|
+
return out;
|
|
770
|
+
}
|
|
771
|
+
function rangeOf(field, className, enums) {
|
|
772
|
+
const type = field.type;
|
|
773
|
+
if (type === void 0) return void 0;
|
|
774
|
+
switch (type.kind) {
|
|
775
|
+
case "primitive":
|
|
776
|
+
return RANGE[type.name];
|
|
777
|
+
case "class":
|
|
778
|
+
return type.name.text;
|
|
779
|
+
case "unknown":
|
|
780
|
+
return "string";
|
|
781
|
+
case "enum":
|
|
782
|
+
return enums.get(useKey(className, field.name.text));
|
|
741
783
|
}
|
|
742
|
-
return { ok: true, value: name(system, c.line) };
|
|
743
784
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
785
|
+
var RESERVED_PREFIXES = /* @__PURE__ */ new Set(["linkml", "xsd", "shex", "schema"]);
|
|
786
|
+
var linkmlName = (text) => {
|
|
787
|
+
const name2 = text.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
788
|
+
if (!/[a-z0-9]/.test(name2)) return "sketch";
|
|
789
|
+
if (RESERVED_PREFIXES.has(name2)) return `${name2}_`;
|
|
790
|
+
return /^[0-9]/.test(name2) ? `_${name2}` : name2;
|
|
791
|
+
};
|
|
792
|
+
var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
793
|
+
|
|
794
|
+
// src/generators/mermaid.ts
|
|
795
|
+
function toMermaid(doc, opts) {
|
|
796
|
+
const resolved = resolve(doc);
|
|
797
|
+
const notes = opts?.notes === true;
|
|
798
|
+
const lines = ["classDiagram"];
|
|
799
|
+
for (const cls of resolved.classes) {
|
|
800
|
+
lines.push(` class ${cls.name.text} {`);
|
|
801
|
+
if (cls.system !== void 0) lines.push(` <<${cls.system.text}>>`);
|
|
802
|
+
for (const field of cls.fields) lines.push(` ${member(field)}`);
|
|
803
|
+
lines.push(" }");
|
|
753
804
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
"E_UNPARSABLE",
|
|
757
|
-
`Expected a type after \`:\`, found \`${first.text}\``,
|
|
758
|
-
c.line,
|
|
759
|
-
first
|
|
760
|
-
);
|
|
805
|
+
for (const name2 of resolved.undeclared) {
|
|
806
|
+
lines.push(` class ${name2.text} {`, " <<undeclared>>", " }");
|
|
761
807
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
"Expected an enum value after `|`, e.g. `climate: arid|temperate|frozen`",
|
|
771
|
-
c.line,
|
|
772
|
-
value ?? afterSpan(pipe)
|
|
773
|
-
);
|
|
808
|
+
if (notes) {
|
|
809
|
+
for (const cls of resolved.classes) {
|
|
810
|
+
if (cls.note !== void 0) lines.push(` note for ${cls.name.text} ${quote(cls.note)}`);
|
|
811
|
+
for (const field of cls.fields) {
|
|
812
|
+
if (field.note !== void 0) {
|
|
813
|
+
lines.push(` note for ${cls.name.text} ${quote(`${field.name.text}: ${field.note}`)}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
774
816
|
}
|
|
775
|
-
words.push(value);
|
|
776
|
-
}
|
|
777
|
-
let many = false;
|
|
778
|
-
const bracket = peek(c);
|
|
779
|
-
if (bracket?.kind === "[]") {
|
|
780
|
-
take(c);
|
|
781
|
-
many = true;
|
|
782
|
-
} else if (bracket?.kind === "[") {
|
|
783
|
-
return fail(
|
|
784
|
-
"E_UNCLOSED_MANY",
|
|
785
|
-
"`[` must be closed: `[]` marks a many-valued field",
|
|
786
|
-
c.line,
|
|
787
|
-
bracket
|
|
788
|
-
);
|
|
789
817
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
818
|
+
for (const cls of resolved.classes) {
|
|
819
|
+
const from = cls.name.text;
|
|
820
|
+
if (cls.similarTo !== void 0) lines.push(` ${from} ..> ${cls.similarTo.text} : similar`);
|
|
821
|
+
for (const field of cls.fields) {
|
|
822
|
+
const type = field.type;
|
|
823
|
+
if (type?.kind === "class") {
|
|
824
|
+
lines.push(
|
|
825
|
+
type.many ? ` ${from} "1" --> "*" ${type.name.text} : ${field.name.text}` : ` ${from} --> ${type.name.text} : ${field.name.text}`
|
|
798
826
|
);
|
|
799
827
|
}
|
|
828
|
+
if (field.joinsTo !== void 0) {
|
|
829
|
+
const { className, fieldName } = field.joinsTo;
|
|
830
|
+
lines.push(` ${from} ..> ${className.text} : ${field.name.text} = ${fieldName.text}`);
|
|
831
|
+
}
|
|
800
832
|
}
|
|
801
|
-
return { ok: true, value: { kind: "enum", values: words.map((w) => name(w, c.line)), many } };
|
|
802
|
-
}
|
|
803
|
-
const word = first.text;
|
|
804
|
-
const primitive = PRIMITIVES[word];
|
|
805
|
-
if (primitive !== void 0) {
|
|
806
|
-
return {
|
|
807
|
-
ok: true,
|
|
808
|
-
value: { kind: "primitive", name: primitive, written: name(first, c.line), many }
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
if (/^[A-Z]/.test(word)) {
|
|
812
|
-
if (!CLASS_NAME.test(word)) {
|
|
813
|
-
return fail(
|
|
814
|
-
"E_BAD_NAME",
|
|
815
|
-
`A referenced class must be UpperCamelCase: \`${word}\``,
|
|
816
|
-
c.line,
|
|
817
|
-
first
|
|
818
|
-
);
|
|
819
|
-
}
|
|
820
|
-
return { ok: true, value: { kind: "class", name: name(first, c.line), many } };
|
|
821
|
-
}
|
|
822
|
-
if (/^[a-z]/.test(word)) {
|
|
823
|
-
return { ok: true, value: { kind: "unknown", name: name(first, c.line), many } };
|
|
824
833
|
}
|
|
825
|
-
return
|
|
826
|
-
|
|
827
|
-
`A type is a primitive, a class name or an enum, not \`${word}\``,
|
|
828
|
-
c.line,
|
|
829
|
-
first
|
|
830
|
-
);
|
|
834
|
+
return `${lines.join("\n")}
|
|
835
|
+
`;
|
|
831
836
|
}
|
|
832
|
-
function
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
return fail(
|
|
852
|
-
"E_UNPARSABLE",
|
|
853
|
-
"`?` attached to a word is not a marker; a doubt needs whitespace before `?` and after it",
|
|
854
|
-
line,
|
|
855
|
-
token
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
if (token.text === "#") {
|
|
859
|
-
return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line, token);
|
|
837
|
+
function member(field) {
|
|
838
|
+
const marker = field.identifier ? "*" : "+";
|
|
839
|
+
const type = field.type === void 0 ? "" : `${typeText(field.type)} `;
|
|
840
|
+
const system = field.system === void 0 ? "" : ` @${field.system.text}`;
|
|
841
|
+
return `${marker}${type}${field.name.text}${system}`;
|
|
842
|
+
}
|
|
843
|
+
function typeText(type) {
|
|
844
|
+
let text;
|
|
845
|
+
switch (type.kind) {
|
|
846
|
+
case "primitive":
|
|
847
|
+
text = type.written.text;
|
|
848
|
+
break;
|
|
849
|
+
case "class":
|
|
850
|
+
case "unknown":
|
|
851
|
+
text = type.name.text;
|
|
852
|
+
break;
|
|
853
|
+
case "enum":
|
|
854
|
+
text = type.values.map((v) => v.text).join("|");
|
|
855
|
+
break;
|
|
860
856
|
}
|
|
861
|
-
return
|
|
857
|
+
return type.many ? `${text}[]` : text;
|
|
862
858
|
}
|
|
859
|
+
var quote = (text) => `"${text.replaceAll('"', "'")}"`;
|
|
863
860
|
|
|
864
861
|
// src/serialize.ts
|
|
865
862
|
import { isMap, isScalar, Scalar, visit, Document as YAMLDocument } from "yaml";
|
|
@@ -918,8 +915,509 @@ function formatDiagnostic(d, file) {
|
|
|
918
915
|
return `${where}: ${d.severity} ${d.code} ${d.message}`;
|
|
919
916
|
}
|
|
920
917
|
|
|
918
|
+
// src/generators/skiss.ts
|
|
919
|
+
var TRAILER_COLUMN = 40;
|
|
920
|
+
function toSkiss(doc) {
|
|
921
|
+
if (doc.classes.length === 0) return "";
|
|
922
|
+
return `${doc.classes.map(classBlock).join("\n\n")}
|
|
923
|
+
`;
|
|
924
|
+
}
|
|
925
|
+
function classBlock(cls) {
|
|
926
|
+
const head = cls.name.text + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
|
|
927
|
+
return [withTrailer(head, cls), ...cls.fields.map(fieldLine)].join("\n");
|
|
928
|
+
}
|
|
929
|
+
function fieldLine(field) {
|
|
930
|
+
const type = typeText2(field.type);
|
|
931
|
+
const head = ` ${field.name.text}` + (field.identifier ? "*" : "") + (type === void 0 ? "" : `: ${type}`) + (field.system === void 0 ? "" : ` @${field.system.text}`) + (field.joinsTo === void 0 ? "" : ` = ${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}`);
|
|
932
|
+
return withTrailer(head, field);
|
|
933
|
+
}
|
|
934
|
+
function typeText2(type) {
|
|
935
|
+
if (type === void 0) return void 0;
|
|
936
|
+
let text;
|
|
937
|
+
switch (type.kind) {
|
|
938
|
+
case "primitive":
|
|
939
|
+
if (type.name === "string" && !type.many) return void 0;
|
|
940
|
+
text = type.name;
|
|
941
|
+
break;
|
|
942
|
+
case "class":
|
|
943
|
+
case "unknown":
|
|
944
|
+
text = type.name.text;
|
|
945
|
+
break;
|
|
946
|
+
case "enum":
|
|
947
|
+
text = type.values.map((v) => v.text).join("|");
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
return type.many ? `${text}[]` : text;
|
|
951
|
+
}
|
|
952
|
+
function withTrailer(head, node) {
|
|
953
|
+
const parts2 = [];
|
|
954
|
+
if (node.description !== void 0 && node.description !== "")
|
|
955
|
+
parts2.push(`# ${node.description}`);
|
|
956
|
+
if (node.note !== void 0 && node.note !== "") parts2.push(`? ${node.note}`);
|
|
957
|
+
if (parts2.length === 0) return head;
|
|
958
|
+
const gap = head.length < TRAILER_COLUMN - 1 ? " ".repeat(TRAILER_COLUMN - head.length) : " ";
|
|
959
|
+
return head + gap + parts2.join(" ");
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/generators/from-linkml.ts
|
|
963
|
+
var PRIMITIVE_RANGE = {
|
|
964
|
+
string: "string",
|
|
965
|
+
integer: "int",
|
|
966
|
+
float: "float",
|
|
967
|
+
double: "float",
|
|
968
|
+
decimal: "float",
|
|
969
|
+
boolean: "bool",
|
|
970
|
+
date: "date",
|
|
971
|
+
datetime: "datetime",
|
|
972
|
+
uri: "uri",
|
|
973
|
+
uriorcurie: "uri"
|
|
974
|
+
};
|
|
975
|
+
var CLASS_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "close_mappings", "attributes", "slots"]);
|
|
976
|
+
var SLOT_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "range", "multivalued", "identifier"]);
|
|
977
|
+
var CLASS_TAGS = /* @__PURE__ */ new Set(["system", "note", "undeclared"]);
|
|
978
|
+
var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
|
|
979
|
+
var QUIET_SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
980
|
+
"id",
|
|
981
|
+
"name",
|
|
982
|
+
"prefixes",
|
|
983
|
+
"imports",
|
|
984
|
+
"default_prefix",
|
|
985
|
+
"default_range",
|
|
986
|
+
"title",
|
|
987
|
+
"license",
|
|
988
|
+
"version",
|
|
989
|
+
"classes",
|
|
990
|
+
"slots",
|
|
991
|
+
"enums"
|
|
992
|
+
]);
|
|
993
|
+
var UNKNOWN_TYPE = /^[a-z][a-z0-9_-]*$/;
|
|
994
|
+
function fromLinkML(schema) {
|
|
995
|
+
const root = asRecord(schema);
|
|
996
|
+
if (root === void 0) return nothing("schema", "the schema is not an object");
|
|
997
|
+
const schemaName = schemaElement(root);
|
|
998
|
+
const classBlock2 = asRecord(get(root, "classes"));
|
|
999
|
+
if (classBlock2 === void 0) return nothing(schemaName, "the schema has no `classes`");
|
|
1000
|
+
const classDefs = classBlock2;
|
|
1001
|
+
const dropped = [];
|
|
1002
|
+
const report = (kind, element, detail) => {
|
|
1003
|
+
dropped.push(detail === void 0 ? { kind, element } : { kind, element, detail });
|
|
1004
|
+
};
|
|
1005
|
+
const slotDefs = asRecord(get(root, "slots")) ?? {};
|
|
1006
|
+
const enumDefs = asRecord(get(root, "enums")) ?? {};
|
|
1007
|
+
const defaultRange = stringOf(get(root, "default_range")) ?? "string";
|
|
1008
|
+
const classNames = /* @__PURE__ */ new Map();
|
|
1009
|
+
const takenClasses = /* @__PURE__ */ new Set();
|
|
1010
|
+
for (const key2 of Object.keys(classDefs)) {
|
|
1011
|
+
classNames.set(key2, unique(toClassName(key2), takenClasses));
|
|
1012
|
+
}
|
|
1013
|
+
const fieldNames = /* @__PURE__ */ new Map();
|
|
1014
|
+
for (const key2 of Object.keys(classDefs)) {
|
|
1015
|
+
const names = /* @__PURE__ */ new Map();
|
|
1016
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1017
|
+
for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) {
|
|
1018
|
+
names.set(slot, unique(toFieldName(slot), taken));
|
|
1019
|
+
}
|
|
1020
|
+
fieldNames.set(key2, names);
|
|
1021
|
+
}
|
|
1022
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1023
|
+
const reference = (element, from, to) => {
|
|
1024
|
+
if (from === to || referenced.has(`${element}\0${to}`)) return;
|
|
1025
|
+
referenced.add(`${element}\0${to}`);
|
|
1026
|
+
report("renamed", element, `\`${to}\` from \`${from}\``);
|
|
1027
|
+
};
|
|
1028
|
+
const classNameOf = (key2) => classNames.get(key2) ?? toClassName(key2);
|
|
1029
|
+
const fieldNameOf = (classKey, slot) => fieldNames.get(classKey)?.get(slot) ?? toFieldName(slot);
|
|
1030
|
+
const enumValues = /* @__PURE__ */ new Map();
|
|
1031
|
+
const unwritable = /* @__PURE__ */ new Map();
|
|
1032
|
+
for (const key2 of Object.keys(enumDefs)) {
|
|
1033
|
+
const rawValues = get(asRecord(get(enumDefs, key2)) ?? {}, "permissible_values");
|
|
1034
|
+
const values = asRecord(rawValues);
|
|
1035
|
+
const keys = Object.keys(values ?? {});
|
|
1036
|
+
const bad = keys.find((value) => !SYSTEM_OR_VALUE.test(value));
|
|
1037
|
+
if (values === void 0 && present(rawValues)) {
|
|
1038
|
+
unwritable.set(key2, `${shown(rawValues)} is not a map of permissible values`);
|
|
1039
|
+
} else if (keys.length < 2) {
|
|
1040
|
+
const count = `${keys.length} permissible value${keys.length === 1 ? "" : "s"}`;
|
|
1041
|
+
unwritable.set(key2, `${count} cannot be an inline enum (\xA73.4)`);
|
|
1042
|
+
} else if (bad !== void 0) {
|
|
1043
|
+
unwritable.set(key2, `\`${bad}\` is not a value Skiss can write (\xA74)`);
|
|
1044
|
+
} else {
|
|
1045
|
+
enumValues.set(key2, keys);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const enumUses = [];
|
|
1049
|
+
const classes = [];
|
|
1050
|
+
for (const key2 of Object.keys(classDefs)) {
|
|
1051
|
+
const raw = get(classDefs, key2);
|
|
1052
|
+
const body = asRecord(raw);
|
|
1053
|
+
const name2 = classNameOf(key2);
|
|
1054
|
+
if (body === void 0 && raw !== null && raw !== void 0) {
|
|
1055
|
+
report("class", name2, "is not a class definition");
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
const def = body ?? {};
|
|
1059
|
+
if (name2 !== key2) report("renamed", name2, `from \`${key2}\``);
|
|
1060
|
+
const annotations = annotationsOf(def);
|
|
1061
|
+
if (annotations.get("undeclared") === true) {
|
|
1062
|
+
for (const other of Object.keys(def)) {
|
|
1063
|
+
if (other !== "annotations") report(other, name2);
|
|
1064
|
+
}
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
const node = { name: at0(name2), fields: [], line: 0 };
|
|
1068
|
+
const description = oneLine(stringOf(get(def, "description")));
|
|
1069
|
+
if (description !== void 0) node.description = description;
|
|
1070
|
+
const system = annotations.get("system");
|
|
1071
|
+
if (typeof system === "string" && SYSTEM_OR_VALUE.test(system)) node.system = at0(system);
|
|
1072
|
+
else if (system !== void 0) report("annotation", name2, "`system` is not a system name");
|
|
1073
|
+
const note = oneLine(stringOf(annotations.get("note")));
|
|
1074
|
+
if (note !== void 0) node.note = note;
|
|
1075
|
+
const mappings = stringList(get(def, "close_mappings"));
|
|
1076
|
+
const first = mappings[0];
|
|
1077
|
+
const target = first === void 0 ? "" : localName(first);
|
|
1078
|
+
if (target !== "") {
|
|
1079
|
+
const similar = Object.hasOwn(classDefs, target) ? classNameOf(target) : toClassName(target);
|
|
1080
|
+
node.similarTo = at0(similar);
|
|
1081
|
+
reference(name2, target, similar);
|
|
1082
|
+
}
|
|
1083
|
+
for (const other of Object.keys(def)) {
|
|
1084
|
+
if (CLASS_KEYS.has(other)) {
|
|
1085
|
+
if (other === "annotations") {
|
|
1086
|
+
for (const tag of annotations.keys()) {
|
|
1087
|
+
if (!CLASS_TAGS.has(tag)) report("annotation", name2, `\`${tag}\``);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (other === "close_mappings") {
|
|
1091
|
+
if (target === "") report("close_mappings", name2, "no mapping this can read");
|
|
1092
|
+
else if (mappings.length > 1) {
|
|
1093
|
+
report("close_mappings", name2, `${mappings.length - 1} beyond the first`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const block = get(def, other);
|
|
1097
|
+
if (other === "attributes" && present(block) && asRecord(block) === void 0) {
|
|
1098
|
+
report("attributes", name2, `${shown(block)} is not a map`);
|
|
1099
|
+
}
|
|
1100
|
+
if (other === "slots" && present(block) && !Array.isArray(block)) {
|
|
1101
|
+
report("slots", name2, `${shown(block)} is not a list`);
|
|
1102
|
+
}
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
if (other === "slot_usage") {
|
|
1106
|
+
const usage = asRecord(get(def, other));
|
|
1107
|
+
if (usage === void 0) {
|
|
1108
|
+
report("slot_usage", name2);
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
for (const slot of Object.keys(usage)) {
|
|
1112
|
+
if (fieldNames.get(key2)?.has(slot) === true) {
|
|
1113
|
+
report("slot_usage", `${name2}.${fieldNameOf(key2, slot)}`);
|
|
1114
|
+
} else {
|
|
1115
|
+
report("slot_usage", name2, `\`${slot}\` is inherited and not in the sketch`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
report(other, name2);
|
|
1121
|
+
}
|
|
1122
|
+
for (const slot of slotKeysOf(def)) {
|
|
1123
|
+
const attributes = asRecord(get(def, "attributes")) ?? {};
|
|
1124
|
+
const rawSlot = Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
|
|
1125
|
+
const slotDef = asRecord(rawSlot);
|
|
1126
|
+
if (slotDef === void 0 && present(rawSlot)) {
|
|
1127
|
+
report("slot", `${name2}.${fieldNameOf(key2, slot)}`, "is not a slot definition");
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
node.fields.push(buildField(key2, name2, slot, slotDef ?? {}));
|
|
1131
|
+
}
|
|
1132
|
+
classes.push(node);
|
|
1133
|
+
}
|
|
1134
|
+
for (const key2 of Object.keys(enumDefs)) {
|
|
1135
|
+
const def = asRecord(get(enumDefs, key2)) ?? {};
|
|
1136
|
+
const uses = enumUses.filter((use) => use.enumName === key2);
|
|
1137
|
+
const why = unwritable.get(key2);
|
|
1138
|
+
if (why !== void 0) report("enum", key2, `${why}; those attributes keep no type`);
|
|
1139
|
+
else if (uses.length === 0) report("unused_enum", key2, "no attribute has it as its range");
|
|
1140
|
+
else if (uses.length > 1 && sharingLost(uses, enumUses)) {
|
|
1141
|
+
report("inlined", key2, `inlined at ${uses.length} attributes`);
|
|
1142
|
+
}
|
|
1143
|
+
const detail = [];
|
|
1144
|
+
const values = asRecord(get(def, "permissible_values")) ?? {};
|
|
1145
|
+
const detailed = Object.keys(values).filter((value) => {
|
|
1146
|
+
const valueBody = asRecord(get(values, value));
|
|
1147
|
+
return valueBody !== void 0 && Object.keys(valueBody).length > 0;
|
|
1148
|
+
});
|
|
1149
|
+
if (detailed.length > 0) {
|
|
1150
|
+
const bodies = detailed.length === 1 ? "carries a body of its own" : "carry bodies of their own";
|
|
1151
|
+
detail.push(`${list(detailed)} ${bodies}`);
|
|
1152
|
+
}
|
|
1153
|
+
const others = Object.keys(def).filter((other) => other !== "permissible_values");
|
|
1154
|
+
if (others.length > 0) detail.push(`the enum carries ${list(others)}`);
|
|
1155
|
+
if (detail.length > 0) report("enum_detail", key2, detail.join("; "));
|
|
1156
|
+
}
|
|
1157
|
+
for (const slot of Object.keys(slotDefs)) {
|
|
1158
|
+
if (!listedSomewhere(slot)) report("slot", slot, "no class lists it");
|
|
1159
|
+
}
|
|
1160
|
+
for (const key2 of Object.keys(root)) {
|
|
1161
|
+
if (!QUIET_SCHEMA_KEYS.has(key2)) report("schema", schemaName, `\`${key2}\``);
|
|
1162
|
+
}
|
|
1163
|
+
const source = toSkiss({ classes, diagnostics: [] });
|
|
1164
|
+
return { document: parse(source), source, dropped };
|
|
1165
|
+
function buildField(classKey, className, slot, def) {
|
|
1166
|
+
const name2 = fieldNameOf(classKey, slot);
|
|
1167
|
+
const element = `${className}.${name2}`;
|
|
1168
|
+
if (name2 !== slot) report("renamed", element, `from \`${slot}\``);
|
|
1169
|
+
const identifier = get(def, "identifier");
|
|
1170
|
+
if (present(identifier) && typeof identifier !== "boolean") {
|
|
1171
|
+
report("identifier", element, `${shown(identifier)} is not a boolean`);
|
|
1172
|
+
}
|
|
1173
|
+
const field = {
|
|
1174
|
+
name: at0(name2),
|
|
1175
|
+
identifier: identifier === true,
|
|
1176
|
+
line: 0
|
|
1177
|
+
};
|
|
1178
|
+
const description = oneLine(stringOf(get(def, "description")));
|
|
1179
|
+
if (description !== void 0) field.description = description;
|
|
1180
|
+
const type = typeFor(element, name2, def);
|
|
1181
|
+
if (type !== void 0) field.type = type;
|
|
1182
|
+
const annotations = annotationsOf(def);
|
|
1183
|
+
const system = annotations.get("system");
|
|
1184
|
+
if (typeof system === "string" && SYSTEM_OR_VALUE.test(system)) field.system = at0(system);
|
|
1185
|
+
else if (system !== void 0) report("annotation", element, "`system` is not a system name");
|
|
1186
|
+
const joins = annotations.get("joins_to");
|
|
1187
|
+
if (typeof joins === "string") {
|
|
1188
|
+
const dot = joins.indexOf(".");
|
|
1189
|
+
const target = joins.slice(0, dot);
|
|
1190
|
+
const targetField = joins.slice(dot + 1);
|
|
1191
|
+
if (dot > 0 && targetField !== "") {
|
|
1192
|
+
const targetClass = classNameOf(target);
|
|
1193
|
+
const targetName = fieldNameOf(target, targetField);
|
|
1194
|
+
field.joinsTo = { className: at0(targetClass), fieldName: at0(targetName) };
|
|
1195
|
+
reference(element, target, targetClass);
|
|
1196
|
+
reference(element, targetField, targetName);
|
|
1197
|
+
} else report("annotation", element, "`joins_to` is not `Class.field`");
|
|
1198
|
+
} else if (joins !== void 0) report("annotation", element, "`joins_to` is not text");
|
|
1199
|
+
const note = oneLine(stringOf(annotations.get("note")));
|
|
1200
|
+
if (note !== void 0) field.note = note;
|
|
1201
|
+
for (const other of Object.keys(def)) {
|
|
1202
|
+
if (!SLOT_KEYS.has(other)) {
|
|
1203
|
+
report(other, element);
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (other !== "annotations") continue;
|
|
1207
|
+
for (const tag of annotations.keys()) {
|
|
1208
|
+
if (!SLOT_TAGS.has(tag)) report("annotation", element, `\`${tag}\``);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return field;
|
|
1212
|
+
}
|
|
1213
|
+
function typeFor(element, field, def) {
|
|
1214
|
+
const rawMany = get(def, "multivalued");
|
|
1215
|
+
if (present(rawMany) && typeof rawMany !== "boolean") {
|
|
1216
|
+
report("multivalued", element, `${shown(rawMany)} is not a boolean`);
|
|
1217
|
+
}
|
|
1218
|
+
const many = rawMany === true;
|
|
1219
|
+
const rawRange = get(def, "range");
|
|
1220
|
+
if (present(rawRange) && typeof rawRange !== "string") {
|
|
1221
|
+
report("range", element, `${shown(rawRange)} is not a string`);
|
|
1222
|
+
} else if (rawRange === "") report("range", element, "the range is empty");
|
|
1223
|
+
const range = stringOf(rawRange) ?? defaultRange;
|
|
1224
|
+
if (Object.hasOwn(classDefs, range)) {
|
|
1225
|
+
const target = classNameOf(range);
|
|
1226
|
+
reference(element, range, target);
|
|
1227
|
+
return { kind: "class", name: at0(target), many };
|
|
1228
|
+
}
|
|
1229
|
+
if (Object.hasOwn(enumDefs, range)) {
|
|
1230
|
+
const values = enumValues.get(range);
|
|
1231
|
+
if (values === void 0) return many ? stringType(true) : void 0;
|
|
1232
|
+
enumUses.push({ enumName: range, field, values: values.join("|") });
|
|
1233
|
+
return { kind: "enum", values: values.map(at0), many };
|
|
1234
|
+
}
|
|
1235
|
+
if (Object.hasOwn(PRIMITIVE_RANGE, range)) {
|
|
1236
|
+
const primitive = PRIMITIVE_RANGE[range] ?? "string";
|
|
1237
|
+
if (primitive === "string") return many ? stringType(true) : void 0;
|
|
1238
|
+
return { kind: "primitive", name: primitive, written: at0(primitive), many };
|
|
1239
|
+
}
|
|
1240
|
+
if (/^[A-Z]/.test(range)) {
|
|
1241
|
+
const target = toClassName(range);
|
|
1242
|
+
reference(element, range, target);
|
|
1243
|
+
return { kind: "class", name: at0(target), many };
|
|
1244
|
+
}
|
|
1245
|
+
const word = range.toLowerCase();
|
|
1246
|
+
if (!UNKNOWN_TYPE.test(word)) {
|
|
1247
|
+
report("narrowed", element, `\`${range}\` is not a type Skiss can write; it reads as string`);
|
|
1248
|
+
return many ? stringType(true) : void 0;
|
|
1249
|
+
}
|
|
1250
|
+
report("narrowed", element, `\`${range}\` is not a Skiss type; it falls back to string`);
|
|
1251
|
+
return { kind: "unknown", name: at0(word), many };
|
|
1252
|
+
}
|
|
1253
|
+
function listedSomewhere(slot) {
|
|
1254
|
+
for (const key2 of Object.keys(classDefs)) {
|
|
1255
|
+
const def = asRecord(get(classDefs, key2)) ?? {};
|
|
1256
|
+
if (stringList(get(def, "slots")).includes(slot)) return true;
|
|
1257
|
+
}
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
function sharingLost(uses, all) {
|
|
1262
|
+
const first = uses[0];
|
|
1263
|
+
if (first === void 0) return false;
|
|
1264
|
+
if (uses.some((use) => use.field !== first.field)) return true;
|
|
1265
|
+
return all.some((use) => use.field === first.field && use.values !== first.values);
|
|
1266
|
+
}
|
|
1267
|
+
var COUNTED = {
|
|
1268
|
+
renamed: ["name", "names"],
|
|
1269
|
+
inlined: ["enum", "enums"],
|
|
1270
|
+
enum: ["unwritable enum", "unwritable enums"],
|
|
1271
|
+
unused_enum: ["unused enum", "unused enums"],
|
|
1272
|
+
class: ["class", "classes"],
|
|
1273
|
+
slot: ["slot", "slots"],
|
|
1274
|
+
schema: ["schema key", "schema keys"],
|
|
1275
|
+
// `mixin: true` marks the class; `mixins: [M]` points at another one.
|
|
1276
|
+
mixin: ["mixin class", "mixin classes"],
|
|
1277
|
+
mixins: ["mixin", "mixins"],
|
|
1278
|
+
pattern: ["pattern", "patterns"],
|
|
1279
|
+
unit: ["unit", "units"]
|
|
1280
|
+
};
|
|
1281
|
+
var ENUM_KINDS = /* @__PURE__ */ new Set(["enum_detail"]);
|
|
1282
|
+
function formatDropped(dropped) {
|
|
1283
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1284
|
+
for (const entry of dropped) {
|
|
1285
|
+
const group = groups.get(entry.kind);
|
|
1286
|
+
if (group === void 0) groups.set(entry.kind, [entry]);
|
|
1287
|
+
else group.push(entry);
|
|
1288
|
+
}
|
|
1289
|
+
const drops = [];
|
|
1290
|
+
let renamed = "";
|
|
1291
|
+
let inlined = "";
|
|
1292
|
+
for (const [kind, entries] of groups) {
|
|
1293
|
+
const phrase = phraseFor(kind, entries);
|
|
1294
|
+
if (kind === "renamed") renamed = phrase;
|
|
1295
|
+
else if (kind === "inlined") inlined = phrase;
|
|
1296
|
+
else drops.push(phrase);
|
|
1297
|
+
}
|
|
1298
|
+
const sentences = [];
|
|
1299
|
+
if (drops.length > 0) sentences.push(`Dropped: ${drops.join(", ")}.`);
|
|
1300
|
+
if (renamed !== "") sentences.push(`Renamed: ${renamed}.`);
|
|
1301
|
+
if (inlined !== "") sentences.push(`Inlined: ${inlined}.`);
|
|
1302
|
+
return sentences.join(" ");
|
|
1303
|
+
}
|
|
1304
|
+
function phraseFor(kind, entries) {
|
|
1305
|
+
const n = entries.length;
|
|
1306
|
+
const counted = Object.hasOwn(COUNTED, kind) ? COUNTED[kind] : void 0;
|
|
1307
|
+
if (counted !== void 0) return `${n} ${n === 1 ? counted[0] : counted[1]}`;
|
|
1308
|
+
return `${kind} on ${n} ${nounFor(kind, entries)}`;
|
|
1309
|
+
}
|
|
1310
|
+
function nounFor(kind, entries) {
|
|
1311
|
+
const one = entries.length === 1;
|
|
1312
|
+
if (ENUM_KINDS.has(kind)) return one ? "enum" : "enums";
|
|
1313
|
+
const slots = entries.filter((entry) => entry.element.includes(".")).length;
|
|
1314
|
+
if (slots === entries.length) return one ? "slot" : "slots";
|
|
1315
|
+
if (slots === 0) return one ? "class" : "classes";
|
|
1316
|
+
return one ? "element" : "elements";
|
|
1317
|
+
}
|
|
1318
|
+
var capitalise2 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
1319
|
+
var decapitalise = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
1320
|
+
var parts = (text) => text.split(/[^A-Za-z0-9]+/).filter((part) => part !== "");
|
|
1321
|
+
function toClassName(text) {
|
|
1322
|
+
if (CLASS_NAME.test(text)) return text;
|
|
1323
|
+
const name2 = parts(text).map(capitalise2).join("");
|
|
1324
|
+
if (name2 === "") return "Class";
|
|
1325
|
+
return CLASS_NAME.test(name2) ? name2 : `X${name2}`;
|
|
1326
|
+
}
|
|
1327
|
+
function toFieldName(text) {
|
|
1328
|
+
if (FIELD_NAME.test(text)) return text;
|
|
1329
|
+
const name2 = parts(text).map((part, i) => i === 0 ? decapitalise(part) : capitalise2(part)).join("");
|
|
1330
|
+
if (name2 === "") return "field";
|
|
1331
|
+
return FIELD_NAME.test(name2) ? name2 : `x${capitalise2(name2)}`;
|
|
1332
|
+
}
|
|
1333
|
+
function unique(base, taken) {
|
|
1334
|
+
let name2 = base;
|
|
1335
|
+
for (let n = 2; taken.has(name2); n++) name2 = `${base}${n}`;
|
|
1336
|
+
taken.add(name2);
|
|
1337
|
+
return name2;
|
|
1338
|
+
}
|
|
1339
|
+
var get = (record, key2) => Object.hasOwn(record, key2) ? record[key2] : void 0;
|
|
1340
|
+
function asRecord(value) {
|
|
1341
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1342
|
+
return value;
|
|
1343
|
+
}
|
|
1344
|
+
var present = (value) => value !== void 0 && value !== null;
|
|
1345
|
+
function shown(value) {
|
|
1346
|
+
if (Array.isArray(value)) return "a list";
|
|
1347
|
+
if (typeof value === "object" && value !== null) return "a map";
|
|
1348
|
+
return `\`${String(value)}\``;
|
|
1349
|
+
}
|
|
1350
|
+
function localName(mapping) {
|
|
1351
|
+
let cut = -1;
|
|
1352
|
+
for (const mark of [":", "/", "#"]) cut = Math.max(cut, mapping.lastIndexOf(mark));
|
|
1353
|
+
return mapping.slice(cut + 1);
|
|
1354
|
+
}
|
|
1355
|
+
var stringOf = (value) => typeof value === "string" && value !== "" ? value : void 0;
|
|
1356
|
+
var stringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1357
|
+
function annotationsOf(def) {
|
|
1358
|
+
const out = /* @__PURE__ */ new Map();
|
|
1359
|
+
const annotations = asRecord(get(def, "annotations"));
|
|
1360
|
+
if (annotations === void 0) return out;
|
|
1361
|
+
for (const tag of Object.keys(annotations)) {
|
|
1362
|
+
const raw = get(annotations, tag);
|
|
1363
|
+
const value = asRecord(raw) === void 0 ? raw : get(asRecord(raw) ?? {}, "value");
|
|
1364
|
+
if (typeof value === "string" || typeof value === "boolean") out.set(tag, value);
|
|
1365
|
+
else out.set(tag, String(value));
|
|
1366
|
+
}
|
|
1367
|
+
return out;
|
|
1368
|
+
}
|
|
1369
|
+
function slotKeysOf(def) {
|
|
1370
|
+
const keys = Object.keys(asRecord(get(def, "attributes")) ?? {});
|
|
1371
|
+
for (const slot of stringList(get(def, "slots"))) {
|
|
1372
|
+
if (!keys.includes(slot)) keys.push(slot);
|
|
1373
|
+
}
|
|
1374
|
+
return keys;
|
|
1375
|
+
}
|
|
1376
|
+
function oneLine(text) {
|
|
1377
|
+
if (text === void 0) return void 0;
|
|
1378
|
+
const line = text.replace(/\s*[\r\n]+\s*/g, " ").trim();
|
|
1379
|
+
return line === "" ? void 0 : line;
|
|
1380
|
+
}
|
|
1381
|
+
var list = (items) => items.map((item) => `\`${item}\``).join(", ");
|
|
1382
|
+
var at0 = (text) => ({ text, line: 0, col: 0, end: 0 });
|
|
1383
|
+
var stringType = (many) => ({
|
|
1384
|
+
kind: "primitive",
|
|
1385
|
+
name: "string",
|
|
1386
|
+
written: at0("string"),
|
|
1387
|
+
many
|
|
1388
|
+
});
|
|
1389
|
+
var schemaElement = (root) => stringOf(get(root, "name")) ?? "schema";
|
|
1390
|
+
var nothing = (element, detail) => ({
|
|
1391
|
+
document: parse(""),
|
|
1392
|
+
source: "",
|
|
1393
|
+
dropped: [{ kind: "schema", element, detail }]
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
// src/import.ts
|
|
1397
|
+
import { parse as parseYAML } from "yaml";
|
|
1398
|
+
function importLinkML(text) {
|
|
1399
|
+
let schema;
|
|
1400
|
+
try {
|
|
1401
|
+
schema = parseYAML(text);
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
return { output: "", dropped: [], diagnostics: [notYAML(error)] };
|
|
1404
|
+
}
|
|
1405
|
+
const { document, source, dropped } = fromLinkML(schema);
|
|
1406
|
+
return { output: source, dropped, diagnostics: resolve(document).diagnostics };
|
|
1407
|
+
}
|
|
1408
|
+
function notYAML(error) {
|
|
1409
|
+
const first = error instanceof Error ? error.message.split("\n")[0] ?? "" : String(error);
|
|
1410
|
+
const detail = first.replace(/:$/, "");
|
|
1411
|
+
return {
|
|
1412
|
+
severity: "error",
|
|
1413
|
+
code: "E_NOT_YAML",
|
|
1414
|
+
message: `the input is not YAML: ${detail}`,
|
|
1415
|
+
line: 1
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
921
1419
|
// src/index.ts
|
|
922
|
-
var VERSION = "0.
|
|
1420
|
+
var VERSION = "0.3.0";
|
|
923
1421
|
|
|
924
1422
|
// src/cli.ts
|
|
925
1423
|
var HELP = `Usage: skiss <command> [options]
|
|
@@ -927,6 +1425,7 @@ var HELP = `Usage: skiss <command> [options]
|
|
|
927
1425
|
Commands:
|
|
928
1426
|
diagram <file> Write a Mermaid class diagram for a .skiss file.
|
|
929
1427
|
compile <file> Write a LinkML schema for a .skiss file.
|
|
1428
|
+
import <file> Write Skiss for a LinkML schema file, YAML or JSON.
|
|
930
1429
|
<file> may be - to read standard input.
|
|
931
1430
|
|
|
932
1431
|
Options:
|
|
@@ -936,13 +1435,18 @@ Options:
|
|
|
936
1435
|
--name <name> compile: the schema name. Defaults to the file's
|
|
937
1436
|
basename without its extension, or \`sketch\` for
|
|
938
1437
|
standard input.
|
|
939
|
-
--strict Exit 1 when the input has any diagnostic
|
|
1438
|
+
--strict Exit 1 when the input has any diagnostic; for import,
|
|
1439
|
+
also when the projection dropped anything
|
|
940
1440
|
-h, --help Show this help and exit
|
|
941
1441
|
--version Print the version and exit
|
|
942
1442
|
|
|
943
1443
|
Diagnostics go to standard error, one per line:
|
|
944
1444
|
<file>:<line>:<col>: <severity> <CODE> <message>
|
|
945
1445
|
|
|
1446
|
+
import writes what the projection could not carry to standard error too, as
|
|
1447
|
+
one line before the diagnostics. Its diagnostics are the ones the sketch it
|
|
1448
|
+
wrote has, so their line numbers are lines of that sketch.
|
|
1449
|
+
|
|
946
1450
|
Exit codes:
|
|
947
1451
|
0 the output was produced
|
|
948
1452
|
1 --strict and the input has diagnostics (the output is still written)
|
|
@@ -955,7 +1459,8 @@ var EXIT_USAGE = 2;
|
|
|
955
1459
|
var EXIT_INTERNAL = 70;
|
|
956
1460
|
var COMMANDS = {
|
|
957
1461
|
diagram: ["output", "notes", "strict"],
|
|
958
|
-
compile: ["output", "json", "name", "strict"]
|
|
1462
|
+
compile: ["output", "json", "name", "strict"],
|
|
1463
|
+
import: ["output", "strict"]
|
|
959
1464
|
};
|
|
960
1465
|
var isCommand = (text) => Object.hasOwn(COMMANDS, text);
|
|
961
1466
|
var UsageError = class extends Error {
|
|
@@ -1006,17 +1511,31 @@ async function main(argv) {
|
|
|
1006
1511
|
}
|
|
1007
1512
|
}
|
|
1008
1513
|
const source = file === "-" ? await readStdin() : await readSource(file);
|
|
1009
|
-
const { output, diagnostics } = command
|
|
1010
|
-
target: "linkml",
|
|
1011
|
-
schemaName: values.name ?? defaultSchemaName(file),
|
|
1012
|
-
format: values.json === true ? "json" : "yaml"
|
|
1013
|
-
}) : compile(source, { target: "mermaid", notes: values.notes });
|
|
1514
|
+
const { output, diagnostics, dropped } = runCommand(command, source, file, values);
|
|
1014
1515
|
const label = file === "-" ? "<stdin>" : file;
|
|
1516
|
+
const report = formatDropped(dropped);
|
|
1517
|
+
if (report !== "") process.stderr.write(`${report}
|
|
1518
|
+
`);
|
|
1015
1519
|
for (const d of diagnostics) process.stderr.write(`${formatDiagnostic(d, label)}
|
|
1016
1520
|
`);
|
|
1017
1521
|
if (values.output === void 0) process.stdout.write(output);
|
|
1018
1522
|
else await writeOutput(values.output, output);
|
|
1019
|
-
|
|
1523
|
+
const strict = values.strict === true && (diagnostics.length > 0 || dropped.length > 0);
|
|
1524
|
+
return strict ? EXIT_STRICT : EXIT_OK;
|
|
1525
|
+
}
|
|
1526
|
+
function runCommand(command, source, file, values) {
|
|
1527
|
+
if (command === "import") return importLinkML(source);
|
|
1528
|
+
if (command === "compile") {
|
|
1529
|
+
return {
|
|
1530
|
+
...compile(source, {
|
|
1531
|
+
target: "linkml",
|
|
1532
|
+
schemaName: values.name ?? defaultSchemaName(file),
|
|
1533
|
+
format: values.json === true ? "json" : "yaml"
|
|
1534
|
+
}),
|
|
1535
|
+
dropped: []
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
return { ...compile(source, { target: "mermaid", notes: values.notes }), dropped: [] };
|
|
1020
1539
|
}
|
|
1021
1540
|
var defaultSchemaName = (file) => file === "-" ? "sketch" : basename(file, extname(file));
|
|
1022
1541
|
async function readSource(file) {
|