@eriknaslund/skiss 0.2.0 → 0.2.1
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/dist/cli.js +728 -731
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +728 -731
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,427 +5,6 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
5
5
|
import { basename, extname } from "path";
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
|
|
8
|
-
// src/resolve.ts
|
|
9
|
-
function resolve(doc) {
|
|
10
|
-
const out = {
|
|
11
|
-
...doc,
|
|
12
|
-
classes: doc.classes.map(copyClass),
|
|
13
|
-
diagnostics: doc.diagnostics.map((d) => ({ ...d })),
|
|
14
|
-
undeclared: []
|
|
15
|
-
};
|
|
16
|
-
const classes = out.classes;
|
|
17
|
-
const declared = new Set(classes.map((c) => c.name.text));
|
|
18
|
-
const warnings = [];
|
|
19
|
-
const undeclared = /* @__PURE__ */ new Map();
|
|
20
|
-
const warn = (code, message, at) => {
|
|
21
|
-
warnings.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
|
|
22
|
-
};
|
|
23
|
-
const reference = (name2) => {
|
|
24
|
-
if (declared.has(name2.text)) return true;
|
|
25
|
-
if (!undeclared.has(name2.text)) undeclared.set(name2.text, name2);
|
|
26
|
-
warn("W_UNDECLARED_CLASS", `class \`${name2.text}\` is not declared`, name2);
|
|
27
|
-
return false;
|
|
28
|
-
};
|
|
29
|
-
const firstClass = /* @__PURE__ */ new Map();
|
|
30
|
-
for (const cls of classes) {
|
|
31
|
-
const earlier = firstClass.get(cls.name.text);
|
|
32
|
-
if (earlier === void 0) firstClass.set(cls.name.text, cls);
|
|
33
|
-
else {
|
|
34
|
-
warn(
|
|
35
|
-
"W_DUPLICATE_CLASS",
|
|
36
|
-
`class \`${cls.name.text}\` is already declared on line ${earlier.line}`,
|
|
37
|
-
cls.name
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
if (cls.similarTo !== void 0) reference(cls.similarTo);
|
|
41
|
-
const firstField = /* @__PURE__ */ new Map();
|
|
42
|
-
let identifier;
|
|
43
|
-
for (const field of cls.fields) {
|
|
44
|
-
const earlierField = firstField.get(field.name.text);
|
|
45
|
-
if (earlierField === void 0) firstField.set(field.name.text, field);
|
|
46
|
-
else {
|
|
47
|
-
warn(
|
|
48
|
-
"W_DUPLICATE_FIELD",
|
|
49
|
-
`field \`${field.name.text}\` is already declared on line ${earlierField.line} in class \`${cls.name.text}\``,
|
|
50
|
-
field.name
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
if (field.identifier) {
|
|
54
|
-
if (identifier === void 0) identifier = field;
|
|
55
|
-
else {
|
|
56
|
-
warn(
|
|
57
|
-
"W_MULTIPLE_IDENTIFIERS",
|
|
58
|
-
`\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\` already has the identifier \`${identifier.name.text}\` on line ${identifier.line}`,
|
|
59
|
-
field.name
|
|
60
|
-
);
|
|
61
|
-
field.identifier = false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (field.type?.kind === "unknown") {
|
|
65
|
-
const word = field.type.name.text;
|
|
66
|
-
const suggestion = closestTypeWord(word);
|
|
67
|
-
warn(
|
|
68
|
-
"W_UNKNOWN_TYPE",
|
|
69
|
-
suggestion === void 0 ? `unknown type \`${word}\`, treated as \`string\`` : `unknown type \`${word}\`, did you mean \`${suggestion}\`?`,
|
|
70
|
-
field.type.name
|
|
71
|
-
);
|
|
72
|
-
} else if (field.type?.kind === "class") {
|
|
73
|
-
reference(field.type.name);
|
|
74
|
-
}
|
|
75
|
-
if (field.joinsTo !== void 0) {
|
|
76
|
-
const { className, fieldName } = field.joinsTo;
|
|
77
|
-
const hasField = (c) => c.name.text === className.text && c.fields.some((f) => f.name.text === fieldName.text);
|
|
78
|
-
if (reference(className) && !classes.some(hasField)) {
|
|
79
|
-
warn(
|
|
80
|
-
"W_UNDECLARED_FIELD",
|
|
81
|
-
`class \`${className.text}\` has no field \`${fieldName.text}\``,
|
|
82
|
-
fieldName
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const present = new Set(out.diagnostics.map(key));
|
|
89
|
-
for (const w of warnings) {
|
|
90
|
-
if (!present.has(key(w))) out.diagnostics.push(w);
|
|
91
|
-
}
|
|
92
|
-
out.diagnostics.sort((a, b) => a.line - b.line);
|
|
93
|
-
out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
var copyName = (n) => ({ ...n });
|
|
97
|
-
function copyType(t) {
|
|
98
|
-
switch (t.kind) {
|
|
99
|
-
case "primitive":
|
|
100
|
-
return { ...t, written: copyName(t.written) };
|
|
101
|
-
case "class":
|
|
102
|
-
case "unknown":
|
|
103
|
-
return { ...t, name: copyName(t.name) };
|
|
104
|
-
case "enum":
|
|
105
|
-
return { ...t, values: t.values.map(copyName) };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
var copyJoin = (j) => ({
|
|
109
|
-
className: copyName(j.className),
|
|
110
|
-
fieldName: copyName(j.fieldName)
|
|
111
|
-
});
|
|
112
|
-
function copyField(f) {
|
|
113
|
-
const out = { ...f, name: copyName(f.name) };
|
|
114
|
-
if (f.type !== void 0) out.type = copyType(f.type);
|
|
115
|
-
if (f.joinsTo !== void 0) out.joinsTo = copyJoin(f.joinsTo);
|
|
116
|
-
return out;
|
|
117
|
-
}
|
|
118
|
-
function copyClass(c) {
|
|
119
|
-
const out = { ...c, name: copyName(c.name), fields: c.fields.map(copyField) };
|
|
120
|
-
if (c.system !== void 0) out.system = copyName(c.system);
|
|
121
|
-
if (c.similarTo !== void 0) out.similarTo = copyName(c.similarTo);
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
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
|
-
];
|
|
137
|
-
var MAX_DISTANCE = 2;
|
|
138
|
-
function closestTypeWord(word) {
|
|
139
|
-
let best;
|
|
140
|
-
let bestDistance = MAX_DISTANCE + 1;
|
|
141
|
-
let bestPrefix = -1;
|
|
142
|
-
for (const candidate of TYPE_WORDS) {
|
|
143
|
-
const distance = levenshtein(word, candidate);
|
|
144
|
-
if (distance > MAX_DISTANCE || distance > bestDistance) continue;
|
|
145
|
-
const prefix = commonPrefixLength(word, candidate);
|
|
146
|
-
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
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return `${lines.join("\n")}
|
|
403
|
-
`;
|
|
404
|
-
}
|
|
405
|
-
function member(field) {
|
|
406
|
-
const marker = field.identifier ? "*" : "+";
|
|
407
|
-
const type = field.type === void 0 ? "" : `${typeText(field.type)} `;
|
|
408
|
-
const system = field.system === void 0 ? "" : ` @${field.system.text}`;
|
|
409
|
-
return `${marker}${type}${field.name.text}${system}`;
|
|
410
|
-
}
|
|
411
|
-
function typeText(type) {
|
|
412
|
-
let text;
|
|
413
|
-
switch (type.kind) {
|
|
414
|
-
case "primitive":
|
|
415
|
-
text = type.written.text;
|
|
416
|
-
break;
|
|
417
|
-
case "class":
|
|
418
|
-
case "unknown":
|
|
419
|
-
text = type.name.text;
|
|
420
|
-
break;
|
|
421
|
-
case "enum":
|
|
422
|
-
text = type.values.map((v) => v.text).join("|");
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
return type.many ? `${text}[]` : text;
|
|
426
|
-
}
|
|
427
|
-
var quote = (text) => `"${text.replaceAll('"', "'")}"`;
|
|
428
|
-
|
|
429
8
|
// src/parse.ts
|
|
430
9
|
var isWs = (c) => c === " " || c === " ";
|
|
431
10
|
var trimWs = (s) => s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
|
|
@@ -444,6 +23,9 @@ var PRIMITIVES = {
|
|
|
444
23
|
integer: "int",
|
|
445
24
|
boolean: "bool"
|
|
446
25
|
};
|
|
26
|
+
function primitiveFor(word) {
|
|
27
|
+
return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
|
|
28
|
+
}
|
|
447
29
|
var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
|
|
448
30
|
var CLASS_ORDER = "name, `@System`, `~ Class`, then `#` and `?`";
|
|
449
31
|
function parse(source) {
|
|
@@ -509,357 +91,772 @@ function splitTrailer(text) {
|
|
|
509
91
|
break;
|
|
510
92
|
}
|
|
511
93
|
}
|
|
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;
|
|
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
|
+
);
|
|
518
373
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
+
}
|
|
524
384
|
}
|
|
385
|
+
return { ok: true, value: { kind: "enum", values: words.map((w) => name(w, c.line)), many } };
|
|
525
386
|
}
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
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
|
+
};
|
|
531
394
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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;
|
|
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
|
+
);
|
|
556
403
|
}
|
|
557
|
-
|
|
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;
|
|
404
|
+
return { ok: true, value: { kind: "class", name: name(first, c.line), many } };
|
|
561
405
|
}
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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 }
|
|
575
|
-
};
|
|
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
|
+
);
|
|
576
415
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
});
|
|
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
|
+
);
|
|
587
425
|
}
|
|
588
|
-
if (
|
|
426
|
+
if (token.text === "<") {
|
|
589
427
|
return fail(
|
|
590
|
-
"
|
|
591
|
-
|
|
428
|
+
"E_UNPARSABLE",
|
|
429
|
+
"`<` is reserved for inheritance and not yet part of the language",
|
|
592
430
|
line,
|
|
593
|
-
|
|
431
|
+
token
|
|
594
432
|
);
|
|
595
433
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
|
|
448
|
+
// src/resolve.ts
|
|
449
|
+
function resolve(doc) {
|
|
450
|
+
const out = {
|
|
451
|
+
...doc,
|
|
452
|
+
classes: doc.classes.map(copyClass),
|
|
453
|
+
diagnostics: doc.diagnostics.map((d) => ({ ...d })),
|
|
454
|
+
undeclared: []
|
|
455
|
+
};
|
|
456
|
+
const classes = out.classes;
|
|
457
|
+
const declared = new Set(classes.map((c) => c.name.text));
|
|
458
|
+
const warnings = [];
|
|
459
|
+
const undeclared = /* @__PURE__ */ new Map();
|
|
460
|
+
const warn = (code, message, at) => {
|
|
461
|
+
warnings.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
|
|
462
|
+
};
|
|
463
|
+
const reference = (name2) => {
|
|
464
|
+
if (declared.has(name2.text)) return true;
|
|
465
|
+
if (!undeclared.has(name2.text)) undeclared.set(name2.text, name2);
|
|
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);
|
|
469
|
+
return false;
|
|
470
|
+
};
|
|
471
|
+
const firstClass = /* @__PURE__ */ new Map();
|
|
472
|
+
for (const cls of classes) {
|
|
473
|
+
const earlier = firstClass.get(cls.name.text);
|
|
474
|
+
if (earlier === void 0) firstClass.set(cls.name.text, cls);
|
|
475
|
+
else {
|
|
476
|
+
warn(
|
|
477
|
+
"W_DUPLICATE_CLASS",
|
|
478
|
+
`class \`${cls.name.text}\` is already declared on line ${earlier.line}`,
|
|
479
|
+
cls.name
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
if (cls.similarTo !== void 0) reference(cls.similarTo);
|
|
483
|
+
const firstField = /* @__PURE__ */ new Map();
|
|
484
|
+
let identifier;
|
|
485
|
+
for (const field of cls.fields) {
|
|
486
|
+
const earlierField = firstField.get(field.name.text);
|
|
487
|
+
if (earlierField === void 0) firstField.set(field.name.text, field);
|
|
488
|
+
else {
|
|
489
|
+
warn(
|
|
490
|
+
"W_DUPLICATE_FIELD",
|
|
491
|
+
`field \`${field.name.text}\` is already declared on line ${earlierField.line} in class \`${cls.name.text}\``,
|
|
492
|
+
field.name
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (field.identifier) {
|
|
496
|
+
if (identifier === void 0) identifier = field;
|
|
497
|
+
else {
|
|
498
|
+
warn(
|
|
499
|
+
"W_MULTIPLE_IDENTIFIERS",
|
|
500
|
+
`\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\` already has the identifier \`${identifier.name.text}\` on line ${identifier.line}`,
|
|
501
|
+
field.identifierAt ?? field.name
|
|
502
|
+
);
|
|
503
|
+
field.identifier = false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (field.type?.kind === "unknown") {
|
|
507
|
+
const word = field.type.name.text;
|
|
508
|
+
const suggestion = closestTypeWord(word);
|
|
509
|
+
warn(
|
|
510
|
+
"W_UNKNOWN_TYPE",
|
|
511
|
+
suggestion === void 0 ? `unknown type \`${word}\`, treated as \`string\`` : `unknown type \`${word}\`, did you mean \`${suggestion}\`?`,
|
|
512
|
+
field.type.name
|
|
513
|
+
);
|
|
514
|
+
} else if (field.type?.kind === "class") {
|
|
515
|
+
reference(field.type.name);
|
|
516
|
+
}
|
|
517
|
+
if (field.joinsTo !== void 0) {
|
|
518
|
+
const { className, fieldName } = field.joinsTo;
|
|
519
|
+
const hasField = (c) => c.name.text === className.text && c.fields.some((f) => f.name.text === fieldName.text);
|
|
520
|
+
if (reference(className) && !classes.some(hasField)) {
|
|
521
|
+
warn(
|
|
522
|
+
"W_UNDECLARED_FIELD",
|
|
523
|
+
`class \`${className.text}\` has no field \`${fieldName.text}\``,
|
|
524
|
+
fieldName
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const present = new Set(out.diagnostics.map(key));
|
|
531
|
+
for (const w of warnings) {
|
|
532
|
+
if (!present.has(key(w))) out.diagnostics.push(w);
|
|
533
|
+
}
|
|
534
|
+
out.diagnostics.sort((a, b) => a.line - b.line);
|
|
535
|
+
out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
|
|
536
|
+
return out;
|
|
537
|
+
}
|
|
538
|
+
var copyName = (n) => ({ ...n });
|
|
539
|
+
function copyType(t) {
|
|
540
|
+
switch (t.kind) {
|
|
541
|
+
case "primitive":
|
|
542
|
+
return { ...t, written: copyName(t.written) };
|
|
543
|
+
case "class":
|
|
544
|
+
case "unknown":
|
|
545
|
+
return { ...t, name: copyName(t.name) };
|
|
546
|
+
case "enum":
|
|
547
|
+
return { ...t, values: t.values.map(copyName) };
|
|
603
548
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
549
|
+
}
|
|
550
|
+
var copyJoin = (j) => ({
|
|
551
|
+
className: copyName(j.className),
|
|
552
|
+
fieldName: copyName(j.fieldName)
|
|
553
|
+
});
|
|
554
|
+
function copyField(f) {
|
|
555
|
+
const out = { ...f, name: copyName(f.name) };
|
|
556
|
+
if (f.identifierAt !== void 0) out.identifierAt = copyName(f.identifierAt);
|
|
557
|
+
if (f.type !== void 0) out.type = copyType(f.type);
|
|
558
|
+
if (f.joinsTo !== void 0) out.joinsTo = copyJoin(f.joinsTo);
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
function copyClass(c) {
|
|
562
|
+
const out = { ...c, name: copyName(c.name), fields: c.fields.map(copyField) };
|
|
563
|
+
if (c.system !== void 0) out.system = copyName(c.system);
|
|
564
|
+
if (c.similarTo !== void 0) out.similarTo = copyName(c.similarTo);
|
|
565
|
+
return out;
|
|
566
|
+
}
|
|
567
|
+
var key = (d) => [d.severity, d.code, d.line, d.col, d.end, d.message].join(" ");
|
|
568
|
+
var TYPE_WORDS = Object.keys(PRIMITIVES);
|
|
569
|
+
var MAX_DISTANCE = 2;
|
|
570
|
+
function closestTypeWord(word) {
|
|
571
|
+
let best;
|
|
572
|
+
let bestDistance = MAX_DISTANCE + 1;
|
|
573
|
+
let bestPrefix = -1;
|
|
574
|
+
for (const candidate of TYPE_WORDS) {
|
|
575
|
+
const distance = levenshtein(word, candidate);
|
|
576
|
+
if (distance > MAX_DISTANCE || distance > bestDistance) continue;
|
|
577
|
+
const prefix = commonPrefixLength(word, candidate);
|
|
578
|
+
if (distance < bestDistance || prefix > bestPrefix) {
|
|
579
|
+
best = candidate;
|
|
580
|
+
bestDistance = distance;
|
|
581
|
+
bestPrefix = prefix;
|
|
615
582
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
583
|
+
}
|
|
584
|
+
return best;
|
|
585
|
+
}
|
|
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;
|
|
590
|
+
}
|
|
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
|
|
622
604
|
);
|
|
623
605
|
}
|
|
624
|
-
|
|
606
|
+
previous = current;
|
|
625
607
|
}
|
|
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: [] } };
|
|
608
|
+
return previous[b.length] ?? 0;
|
|
631
609
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
610
|
+
|
|
611
|
+
// src/generators/linkml.ts
|
|
612
|
+
var BASE_URI = "https://example.org";
|
|
613
|
+
var RANGE = {
|
|
614
|
+
string: "string",
|
|
615
|
+
int: "integer",
|
|
616
|
+
float: "float",
|
|
617
|
+
bool: "boolean",
|
|
618
|
+
date: "date",
|
|
619
|
+
datetime: "datetime",
|
|
620
|
+
uri: "uri"
|
|
621
|
+
};
|
|
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);
|
|
641
|
+
}
|
|
649
642
|
}
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
node.identifier = true;
|
|
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);
|
|
654
646
|
}
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
647
|
+
const mappingPrefix = (target) => {
|
|
648
|
+
const system = declaredSystem.get(target);
|
|
649
|
+
if (system === void 0) return schemaName;
|
|
650
|
+
return systemPrefix.get(system) ?? schemaName;
|
|
651
|
+
};
|
|
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);
|
|
661
663
|
}
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
take(c);
|
|
665
|
-
const system = parseSystem(c, at);
|
|
666
|
-
if (!system.ok) return system;
|
|
667
|
-
node.system = system.value;
|
|
664
|
+
for (const name2 of resolved.undeclared) {
|
|
665
|
+
if (!(name2.text in out)) out[name2.text] = { annotations: { undeclared: true } };
|
|
668
666
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
);
|
|
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);
|
|
698
684
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
685
|
+
out.push({ node, fields });
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
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
|
+
});
|
|
707
700
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
701
|
+
}
|
|
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);
|
|
707
|
+
}
|
|
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}`;
|
|
715
716
|
}
|
|
716
|
-
|
|
717
|
+
taken.set(name2, use.values);
|
|
718
|
+
names.set(useKey(use.className, use.fieldName), name2);
|
|
717
719
|
}
|
|
718
|
-
|
|
719
|
-
if (rest !== void 0) return unexpected(rest, line, "field", FIELD_ORDER);
|
|
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 };
|
|
720
|
+
return names;
|
|
723
721
|
}
|
|
724
|
-
function
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
"
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
+
}
|
|
733
733
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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}`];
|
|
741
747
|
}
|
|
742
|
-
|
|
748
|
+
const attributes = {};
|
|
749
|
+
for (const field of fields) {
|
|
750
|
+
attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
|
|
751
|
+
}
|
|
752
|
+
if (Object.keys(attributes).length > 0) out.attributes = attributes;
|
|
753
|
+
return out;
|
|
743
754
|
}
|
|
744
|
-
function
|
|
745
|
-
const
|
|
746
|
-
if (
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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}`;
|
|
753
766
|
}
|
|
754
|
-
if (
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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));
|
|
761
783
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
784
|
+
}
|
|
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(" }");
|
|
776
804
|
}
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
);
|
|
805
|
+
for (const name2 of resolved.undeclared) {
|
|
806
|
+
lines.push(` class ${name2.text} {`, " <<undeclared>>", " }");
|
|
789
807
|
}
|
|
790
|
-
if (
|
|
791
|
-
for (const
|
|
792
|
-
if (
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
`
|
|
796
|
-
|
|
797
|
-
w
|
|
798
|
-
);
|
|
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
|
+
}
|
|
799
815
|
}
|
|
800
816
|
}
|
|
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
817
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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}`
|
|
826
|
+
);
|
|
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
|
+
}
|
|
819
832
|
}
|
|
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";
|
|
@@ -919,7 +916,7 @@ function formatDiagnostic(d, file) {
|
|
|
919
916
|
}
|
|
920
917
|
|
|
921
918
|
// src/index.ts
|
|
922
|
-
var VERSION = "0.2.
|
|
919
|
+
var VERSION = "0.2.1";
|
|
923
920
|
|
|
924
921
|
// src/cli.ts
|
|
925
922
|
var HELP = `Usage: skiss <command> [options]
|