@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/index.js CHANGED
@@ -1,424 +1,3 @@
1
- // src/resolve.ts
2
- function resolve(doc) {
3
- const out = {
4
- ...doc,
5
- classes: doc.classes.map(copyClass),
6
- diagnostics: doc.diagnostics.map((d) => ({ ...d })),
7
- undeclared: []
8
- };
9
- const classes = out.classes;
10
- const declared = new Set(classes.map((c) => c.name.text));
11
- const warnings = [];
12
- const undeclared = /* @__PURE__ */ new Map();
13
- const warn = (code, message, at) => {
14
- warnings.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
15
- };
16
- const reference = (name2) => {
17
- if (declared.has(name2.text)) return true;
18
- if (!undeclared.has(name2.text)) undeclared.set(name2.text, name2);
19
- warn("W_UNDECLARED_CLASS", `class \`${name2.text}\` is not declared`, name2);
20
- return false;
21
- };
22
- const firstClass = /* @__PURE__ */ new Map();
23
- for (const cls of classes) {
24
- const earlier = firstClass.get(cls.name.text);
25
- if (earlier === void 0) firstClass.set(cls.name.text, cls);
26
- else {
27
- warn(
28
- "W_DUPLICATE_CLASS",
29
- `class \`${cls.name.text}\` is already declared on line ${earlier.line}`,
30
- cls.name
31
- );
32
- }
33
- if (cls.similarTo !== void 0) reference(cls.similarTo);
34
- const firstField = /* @__PURE__ */ new Map();
35
- let identifier;
36
- for (const field of cls.fields) {
37
- const earlierField = firstField.get(field.name.text);
38
- if (earlierField === void 0) firstField.set(field.name.text, field);
39
- else {
40
- warn(
41
- "W_DUPLICATE_FIELD",
42
- `field \`${field.name.text}\` is already declared on line ${earlierField.line} in class \`${cls.name.text}\``,
43
- field.name
44
- );
45
- }
46
- if (field.identifier) {
47
- if (identifier === void 0) identifier = field;
48
- else {
49
- warn(
50
- "W_MULTIPLE_IDENTIFIERS",
51
- `\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\` already has the identifier \`${identifier.name.text}\` on line ${identifier.line}`,
52
- field.name
53
- );
54
- field.identifier = false;
55
- }
56
- }
57
- if (field.type?.kind === "unknown") {
58
- const word = field.type.name.text;
59
- const suggestion = closestTypeWord(word);
60
- warn(
61
- "W_UNKNOWN_TYPE",
62
- suggestion === void 0 ? `unknown type \`${word}\`, treated as \`string\`` : `unknown type \`${word}\`, did you mean \`${suggestion}\`?`,
63
- field.type.name
64
- );
65
- } else if (field.type?.kind === "class") {
66
- reference(field.type.name);
67
- }
68
- if (field.joinsTo !== void 0) {
69
- const { className, fieldName } = field.joinsTo;
70
- const hasField = (c) => c.name.text === className.text && c.fields.some((f) => f.name.text === fieldName.text);
71
- if (reference(className) && !classes.some(hasField)) {
72
- warn(
73
- "W_UNDECLARED_FIELD",
74
- `class \`${className.text}\` has no field \`${fieldName.text}\``,
75
- fieldName
76
- );
77
- }
78
- }
79
- }
80
- }
81
- const present = new Set(out.diagnostics.map(key));
82
- for (const w of warnings) {
83
- if (!present.has(key(w))) out.diagnostics.push(w);
84
- }
85
- out.diagnostics.sort((a, b) => a.line - b.line);
86
- out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
87
- return out;
88
- }
89
- var copyName = (n) => ({ ...n });
90
- function copyType(t) {
91
- switch (t.kind) {
92
- case "primitive":
93
- return { ...t, written: copyName(t.written) };
94
- case "class":
95
- case "unknown":
96
- return { ...t, name: copyName(t.name) };
97
- case "enum":
98
- return { ...t, values: t.values.map(copyName) };
99
- }
100
- }
101
- var copyJoin = (j) => ({
102
- className: copyName(j.className),
103
- fieldName: copyName(j.fieldName)
104
- });
105
- function copyField(f) {
106
- const out = { ...f, name: copyName(f.name) };
107
- if (f.type !== void 0) out.type = copyType(f.type);
108
- if (f.joinsTo !== void 0) out.joinsTo = copyJoin(f.joinsTo);
109
- return out;
110
- }
111
- function copyClass(c) {
112
- const out = { ...c, name: copyName(c.name), fields: c.fields.map(copyField) };
113
- if (c.system !== void 0) out.system = copyName(c.system);
114
- if (c.similarTo !== void 0) out.similarTo = copyName(c.similarTo);
115
- return out;
116
- }
117
- var key = (d) => [d.severity, d.code, d.line, d.col, d.end, d.message].join(" ");
118
- var TYPE_WORDS = [
119
- "string",
120
- "int",
121
- "float",
122
- "bool",
123
- "date",
124
- "datetime",
125
- "uri",
126
- "text",
127
- "integer",
128
- "boolean"
129
- ];
130
- var MAX_DISTANCE = 2;
131
- function closestTypeWord(word) {
132
- let best;
133
- let bestDistance = MAX_DISTANCE + 1;
134
- let bestPrefix = -1;
135
- for (const candidate of TYPE_WORDS) {
136
- const distance = levenshtein(word, candidate);
137
- if (distance > MAX_DISTANCE || distance > bestDistance) continue;
138
- const prefix = commonPrefixLength(word, candidate);
139
- if (distance < bestDistance || prefix > bestPrefix) {
140
- best = candidate;
141
- bestDistance = distance;
142
- bestPrefix = prefix;
143
- }
144
- }
145
- return best;
146
- }
147
- function commonPrefixLength(a, b) {
148
- let i = 0;
149
- while (i < a.length && i < b.length && a[i] === b[i]) i++;
150
- return i;
151
- }
152
- function levenshtein(a, b) {
153
- if (a === b) return 0;
154
- if (a.length === 0) return b.length;
155
- if (b.length === 0) return a.length;
156
- let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
157
- for (let i = 1; i <= a.length; i++) {
158
- const current = [i];
159
- for (let j = 1; j <= b.length; j++) {
160
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
161
- current[j] = Math.min(
162
- (previous[j] ?? 0) + 1,
163
- (current[j - 1] ?? 0) + 1,
164
- (previous[j - 1] ?? 0) + cost
165
- );
166
- }
167
- previous = current;
168
- }
169
- return previous[b.length] ?? 0;
170
- }
171
-
172
- // src/generators/linkml.ts
173
- var BASE_URI = "https://example.org";
174
- var RANGE = {
175
- string: "string",
176
- int: "integer",
177
- float: "float",
178
- bool: "boolean",
179
- date: "date",
180
- datetime: "datetime",
181
- uri: "uri"
182
- };
183
- function toLinkML(doc, opts) {
184
- const resolved = resolve(doc);
185
- const schemaName = linkmlName(opts.schemaName);
186
- const classes = emittedClasses(resolved.classes);
187
- const classNames = new Set(classes.map((cls) => cls.node.name.text));
188
- for (const name2 of resolved.undeclared) classNames.add(name2.text);
189
- const enums = nameEnums(classes, classNames);
190
- const prefixes = { linkml: "https://w3id.org/linkml/" };
191
- if (!Object.hasOwn(prefixes, schemaName)) prefixes[schemaName] = `${BASE_URI}/${schemaName}/`;
192
- const systemPrefix = /* @__PURE__ */ new Map();
193
- const addSystem = (system) => {
194
- const prefix = linkmlName(system);
195
- if (!systemPrefix.has(system)) systemPrefix.set(system, prefix);
196
- if (!Object.hasOwn(prefixes, prefix)) prefixes[prefix] = `${BASE_URI}/system/${prefix}/`;
197
- };
198
- for (const { node, fields } of classes) {
199
- if (node.system !== void 0) addSystem(node.system.text);
200
- for (const field of fields) {
201
- if (field.system !== void 0) addSystem(field.system.text);
202
- }
203
- }
204
- const declaredSystem = /* @__PURE__ */ new Map();
205
- for (const { node } of classes) {
206
- if (!declaredSystem.has(node.name.text)) declaredSystem.set(node.name.text, node.system?.text);
207
- }
208
- const mappingPrefix = (target) => {
209
- const system = declaredSystem.get(target);
210
- if (system === void 0) return schemaName;
211
- return systemPrefix.get(system) ?? schemaName;
212
- };
213
- const schema = {
214
- id: `${BASE_URI}/${schemaName}`,
215
- name: schemaName,
216
- default_prefix: schemaName,
217
- default_range: "string",
218
- prefixes,
219
- imports: ["linkml:types"]
220
- };
221
- const out = {};
222
- for (const cls of classes) {
223
- out[cls.node.name.text] = buildClass(cls, mappingPrefix, enums);
224
- }
225
- for (const name2 of resolved.undeclared) {
226
- if (!(name2.text in out)) out[name2.text] = { annotations: { undeclared: true } };
227
- }
228
- if (Object.keys(out).length > 0) schema.classes = out;
229
- const enumBlock = buildEnums(classes, enums);
230
- if (Object.keys(enumBlock).length > 0) schema.enums = enumBlock;
231
- return schema;
232
- }
233
- function emittedClasses(classes) {
234
- const out = [];
235
- const seen = /* @__PURE__ */ new Set();
236
- for (const node of classes) {
237
- if (seen.has(node.name.text)) continue;
238
- seen.add(node.name.text);
239
- const fields = [];
240
- const seenField = /* @__PURE__ */ new Set();
241
- for (const field of node.fields) {
242
- if (seenField.has(field.name.text)) continue;
243
- seenField.add(field.name.text);
244
- fields.push(field);
245
- }
246
- out.push({ node, fields });
247
- }
248
- return out;
249
- }
250
- var useKey = (className, fieldName) => `${className}\0${fieldName}`;
251
- function nameEnums(classes, classNames) {
252
- const uses = [];
253
- for (const { node, fields } of classes) {
254
- for (const field of fields) {
255
- if (field.type?.kind !== "enum") continue;
256
- uses.push({
257
- className: node.name.text,
258
- fieldName: field.name.text,
259
- values: field.type.values.map((v) => v.text).join("|")
260
- });
261
- }
262
- }
263
- const valueSets = /* @__PURE__ */ new Map();
264
- for (const use of uses) {
265
- const seen = valueSets.get(use.fieldName) ?? /* @__PURE__ */ new Set();
266
- seen.add(use.values);
267
- valueSets.set(use.fieldName, seen);
268
- }
269
- const taken = /* @__PURE__ */ new Map();
270
- const names = /* @__PURE__ */ new Map();
271
- for (const use of uses) {
272
- const shared = (valueSets.get(use.fieldName)?.size ?? 0) <= 1;
273
- const base = shared ? `${capitalise(use.fieldName)}Enum` : `${capitalise(use.className)}${capitalise(use.fieldName)}Enum`;
274
- let name2 = base;
275
- for (let n = 2; classNames.has(name2) || taken.has(name2) && taken.get(name2) !== use.values; n++) {
276
- name2 = `${base}${n}`;
277
- }
278
- taken.set(name2, use.values);
279
- names.set(useKey(use.className, use.fieldName), name2);
280
- }
281
- return names;
282
- }
283
- function buildEnums(classes, enums) {
284
- const out = {};
285
- for (const { node, fields } of classes) {
286
- for (const field of fields) {
287
- if (field.type?.kind !== "enum") continue;
288
- const name2 = enums.get(useKey(node.name.text, field.name.text));
289
- if (name2 === void 0 || name2 in out) continue;
290
- const values = {};
291
- for (const value of field.type.values) values[value.text] = null;
292
- out[name2] = { permissible_values: values };
293
- }
294
- }
295
- return out;
296
- }
297
- function buildClass(cls, mappingPrefix, enums) {
298
- const { node, fields } = cls;
299
- const out = {};
300
- if (node.description !== void 0) out.description = node.description;
301
- const annotations = {};
302
- if (node.system !== void 0) annotations.system = node.system.text;
303
- if (node.note !== void 0) annotations.note = node.note;
304
- if (Object.keys(annotations).length > 0) out.annotations = annotations;
305
- if (node.similarTo !== void 0) {
306
- const target = node.similarTo.text;
307
- out.close_mappings = [`${mappingPrefix(target)}:${target}`];
308
- }
309
- const attributes = {};
310
- for (const field of fields) {
311
- attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
312
- }
313
- if (Object.keys(attributes).length > 0) out.attributes = attributes;
314
- return out;
315
- }
316
- function buildAttribute(field, className, enums) {
317
- const out = {};
318
- if (field.description !== void 0) out.description = field.description;
319
- const range = rangeOf(field, className, enums);
320
- if (range !== void 0) out.range = range;
321
- if (field.type?.many === true) out.multivalued = true;
322
- if (field.identifier) out.identifier = true;
323
- const annotations = {};
324
- if (field.system !== void 0) annotations.system = field.system.text;
325
- if (field.joinsTo !== void 0) {
326
- annotations.joins_to = `${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}`;
327
- }
328
- if (field.note !== void 0) annotations.note = field.note;
329
- if (Object.keys(annotations).length > 0) out.annotations = annotations;
330
- return out;
331
- }
332
- function rangeOf(field, className, enums) {
333
- const type = field.type;
334
- if (type === void 0) return void 0;
335
- switch (type.kind) {
336
- case "primitive":
337
- return RANGE[type.name];
338
- case "class":
339
- return type.name.text;
340
- case "unknown":
341
- return "string";
342
- case "enum":
343
- return enums.get(useKey(className, field.name.text));
344
- }
345
- }
346
- var RESERVED_PREFIXES = /* @__PURE__ */ new Set(["linkml", "xsd", "shex", "schema"]);
347
- var linkmlName = (text) => {
348
- const name2 = text.toLowerCase().replace(/[^a-z0-9]/g, "_");
349
- if (!/[a-z0-9]/.test(name2)) return "sketch";
350
- if (RESERVED_PREFIXES.has(name2)) return `${name2}_`;
351
- return /^[0-9]/.test(name2) ? `_${name2}` : name2;
352
- };
353
- var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
354
-
355
- // src/generators/mermaid.ts
356
- function toMermaid(doc, opts) {
357
- const resolved = resolve(doc);
358
- const notes = opts?.notes === true;
359
- const lines = ["classDiagram"];
360
- for (const cls of resolved.classes) {
361
- lines.push(` class ${cls.name.text} {`);
362
- if (cls.system !== void 0) lines.push(` <<${cls.system.text}>>`);
363
- for (const field of cls.fields) lines.push(` ${member(field)}`);
364
- lines.push(" }");
365
- }
366
- for (const name2 of resolved.undeclared) {
367
- lines.push(` class ${name2.text} {`, " <<undeclared>>", " }");
368
- }
369
- if (notes) {
370
- for (const cls of resolved.classes) {
371
- if (cls.note !== void 0) lines.push(` note for ${cls.name.text} ${quote(cls.note)}`);
372
- for (const field of cls.fields) {
373
- if (field.note !== void 0) {
374
- lines.push(` note for ${cls.name.text} ${quote(`${field.name.text}: ${field.note}`)}`);
375
- }
376
- }
377
- }
378
- }
379
- for (const cls of resolved.classes) {
380
- const from = cls.name.text;
381
- if (cls.similarTo !== void 0) lines.push(` ${from} ..> ${cls.similarTo.text} : similar`);
382
- for (const field of cls.fields) {
383
- const type = field.type;
384
- if (type?.kind === "class") {
385
- lines.push(
386
- type.many ? ` ${from} "1" --> "*" ${type.name.text} : ${field.name.text}` : ` ${from} --> ${type.name.text} : ${field.name.text}`
387
- );
388
- }
389
- if (field.joinsTo !== void 0) {
390
- const { className, fieldName } = field.joinsTo;
391
- lines.push(` ${from} ..> ${className.text} : ${field.name.text} = ${fieldName.text}`);
392
- }
393
- }
394
- }
395
- return `${lines.join("\n")}
396
- `;
397
- }
398
- function member(field) {
399
- const marker = field.identifier ? "*" : "+";
400
- const type = field.type === void 0 ? "" : `${typeText(field.type)} `;
401
- const system = field.system === void 0 ? "" : ` @${field.system.text}`;
402
- return `${marker}${type}${field.name.text}${system}`;
403
- }
404
- function typeText(type) {
405
- let text;
406
- switch (type.kind) {
407
- case "primitive":
408
- text = type.written.text;
409
- break;
410
- case "class":
411
- case "unknown":
412
- text = type.name.text;
413
- break;
414
- case "enum":
415
- text = type.values.map((v) => v.text).join("|");
416
- break;
417
- }
418
- return type.many ? `${text}[]` : text;
419
- }
420
- var quote = (text) => `"${text.replaceAll('"', "'")}"`;
421
-
422
1
  // src/parse.ts
423
2
  var isWs = (c) => c === " " || c === " ";
424
3
  var trimWs = (s) => s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
@@ -437,6 +16,9 @@ var PRIMITIVES = {
437
16
  integer: "int",
438
17
  boolean: "bool"
439
18
  };
19
+ function primitiveFor(word) {
20
+ return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
21
+ }
440
22
  var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
441
23
  var CLASS_ORDER = "name, `@System`, `~ Class`, then `#` and `?`";
442
24
  function parse(source) {
@@ -502,357 +84,772 @@ function splitTrailer(text) {
502
84
  break;
503
85
  }
504
86
  }
505
- if (start < 0) return { head: text };
506
- const trailer = { head: text.slice(0, start).replace(/[ \t]+$/, "") };
507
- if (text[start] === "?") {
508
- const note = trimWs(text.slice(start + 1));
509
- if (note !== "") trailer.note = note;
510
- return trailer;
87
+ if (start < 0) return { head: text };
88
+ const trailer = { head: text.slice(0, start).replace(/[ \t]+$/, "") };
89
+ if (text[start] === "?") {
90
+ const note = trimWs(text.slice(start + 1));
91
+ if (note !== "") trailer.note = note;
92
+ return trailer;
93
+ }
94
+ let doubt = -1;
95
+ for (let i = start + 1; i < text.length; i++) {
96
+ if (isDoubtMarker(text, i)) {
97
+ doubt = i;
98
+ break;
99
+ }
100
+ }
101
+ const description = trimWs(text.slice(start + 1, doubt < 0 ? text.length : doubt));
102
+ if (description !== "") trailer.description = description;
103
+ if (doubt >= 0) {
104
+ const note = trimWs(text.slice(doubt + 1));
105
+ if (note !== "") trailer.note = note;
106
+ }
107
+ return trailer;
108
+ }
109
+ var WORD = /[\p{L}\p{N}_-]+/uy;
110
+ var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "["];
111
+ function tokenize(head) {
112
+ const tokens = [];
113
+ let i = 0;
114
+ while (i < head.length) {
115
+ const c = head[i] ?? "";
116
+ if (isWs(c)) {
117
+ i++;
118
+ continue;
119
+ }
120
+ WORD.lastIndex = i;
121
+ const word = WORD.exec(head);
122
+ if (word !== null) {
123
+ tokens.push({ kind: "word", text: word[0], col: i, end: i + word[0].length });
124
+ i += word[0].length;
125
+ continue;
126
+ }
127
+ if (c === "[" && head[i + 1] === "]") {
128
+ tokens.push({ kind: "[]", text: "[]", col: i, end: i + 2 });
129
+ i += 2;
130
+ continue;
131
+ }
132
+ const ch = String.fromCodePoint(head.codePointAt(i) ?? 0);
133
+ const kind = OPERATORS.find((op) => op === ch) ?? "other";
134
+ tokens.push({ kind, text: ch, col: i, end: i + ch.length });
135
+ i += ch.length;
136
+ }
137
+ return tokens;
138
+ }
139
+ var peek = (c) => c.tokens[c.pos];
140
+ function take(c) {
141
+ const t = c.tokens[c.pos];
142
+ if (t !== void 0) c.pos++;
143
+ return t;
144
+ }
145
+ var name = (t, line) => ({ text: t.text, line, col: t.col, end: t.end });
146
+ function fail(code, message, line, span) {
147
+ return {
148
+ ok: false,
149
+ diagnostic: { severity: "error", code, message, line, col: span.col, end: span.end }
150
+ };
151
+ }
152
+ var afterSpan = (after) => ({ col: after.end, end: after.end });
153
+ function parseClassLine(text, line) {
154
+ const trailer = splitTrailer(text);
155
+ const c = { tokens: tokenize(trailer.head), pos: 0, line };
156
+ const head = take(c);
157
+ if (head === void 0 || head.kind !== "word") {
158
+ return fail("E_UNPARSABLE", "A class line must start with a class name", line, {
159
+ col: 0,
160
+ end: Math.max(1, trailer.head.length)
161
+ });
162
+ }
163
+ if (!CLASS_NAME.test(head.text)) {
164
+ return fail(
165
+ "E_BAD_NAME",
166
+ `Class names are UpperCamelCase (letters and digits, uppercase first): \`${head.text}\``,
167
+ line,
168
+ head
169
+ );
170
+ }
171
+ const node = { name: name(head, line), line };
172
+ const at = peek(c);
173
+ if (at?.kind === "@") {
174
+ take(c);
175
+ const system = parseSystem(c, at);
176
+ if (!system.ok) return system;
177
+ node.system = system.value;
178
+ }
179
+ const tilde = peek(c);
180
+ if (tilde?.kind === "~") {
181
+ take(c);
182
+ const target = take(c);
183
+ if (target === void 0 || target.kind !== "word") {
184
+ return fail(
185
+ "E_UNPARSABLE",
186
+ "Expected a class name after `~`, e.g. `CharacterPage @Community ~ Character`",
187
+ line,
188
+ target ?? afterSpan(tilde)
189
+ );
190
+ }
191
+ if (!CLASS_NAME.test(target.text)) {
192
+ return fail(
193
+ "E_BAD_NAME",
194
+ `The class after \`~\` must be UpperCamelCase: \`${target.text}\``,
195
+ line,
196
+ target
197
+ );
198
+ }
199
+ node.similarTo = name(target, line);
200
+ }
201
+ const rest = peek(c);
202
+ if (rest !== void 0) return unexpected(rest, line, "class", CLASS_ORDER);
203
+ if (trailer.description !== void 0) node.description = trailer.description;
204
+ if (trailer.note !== void 0) node.note = trailer.note;
205
+ return { ok: true, value: { ...node, fields: [] } };
206
+ }
207
+ function parseFieldLine(text, line) {
208
+ const trailer = splitTrailer(text);
209
+ const c = { tokens: tokenize(trailer.head), pos: 0, line };
210
+ const head = take(c);
211
+ if (head === void 0) {
212
+ const col = text.search(/[^ \t]/);
213
+ 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";
214
+ return fail("E_UNPARSABLE", message, line, { col, end: Math.max(col + 1, text.length) });
215
+ }
216
+ if (head.kind !== "word") return unexpected(head, line, "field", FIELD_ORDER);
217
+ if (!FIELD_NAME.test(head.text)) {
218
+ return fail(
219
+ "E_BAD_NAME",
220
+ `Field names are lowerCamelCase (letters and digits, lowercase first): \`${head.text}\``,
221
+ line,
222
+ head
223
+ );
224
+ }
225
+ const node = { name: name(head, line), line, identifier: false };
226
+ const star = peek(c);
227
+ if (star?.kind === "*") {
228
+ take(c);
229
+ node.identifier = true;
230
+ node.identifierAt = name(star, line);
231
+ }
232
+ const colon = peek(c);
233
+ if (colon?.kind === ":") {
234
+ take(c);
235
+ const type = parseType(c, colon);
236
+ if (!type.ok) return type;
237
+ node.type = type.value;
238
+ }
239
+ const at = peek(c);
240
+ if (at?.kind === "@") {
241
+ take(c);
242
+ const system = parseSystem(c, at);
243
+ if (!system.ok) return system;
244
+ node.system = system.value;
245
+ }
246
+ const equals = peek(c);
247
+ if (equals?.kind === "=") {
248
+ take(c);
249
+ const example = "e.g. `characterId = Character.id`";
250
+ const className = take(c);
251
+ if (className === void 0 || className.kind !== "word") {
252
+ return fail(
253
+ "E_UNPARSABLE",
254
+ `Expected \`Class.field\` after \`=\`, ${example}`,
255
+ line,
256
+ className ?? afterSpan(equals)
257
+ );
258
+ }
259
+ if (!CLASS_NAME.test(className.text)) {
260
+ return fail(
261
+ "E_BAD_NAME",
262
+ `The class after \`=\` must be UpperCamelCase: \`${className.text}\``,
263
+ line,
264
+ className
265
+ );
266
+ }
267
+ const dot = take(c);
268
+ if (dot === void 0 || dot.kind !== ".") {
269
+ return fail(
270
+ "E_UNPARSABLE",
271
+ `Expected \`.\` and a field name after \`= ${className.text}\`, ${example}`,
272
+ line,
273
+ dot ?? afterSpan(className)
274
+ );
275
+ }
276
+ const fieldName = take(c);
277
+ if (fieldName === void 0 || fieldName.kind !== "word") {
278
+ return fail(
279
+ "E_UNPARSABLE",
280
+ `Expected a field name after \`= ${className.text}.\`, ${example}`,
281
+ line,
282
+ fieldName ?? afterSpan(dot)
283
+ );
284
+ }
285
+ if (!FIELD_NAME.test(fieldName.text)) {
286
+ return fail(
287
+ "E_BAD_NAME",
288
+ `The field after \`= ${className.text}.\` must be lowerCamelCase: \`${fieldName.text}\``,
289
+ line,
290
+ fieldName
291
+ );
292
+ }
293
+ node.joinsTo = { className: name(className, line), fieldName: name(fieldName, line) };
294
+ }
295
+ const rest = peek(c);
296
+ if (rest !== void 0) return unexpected(rest, line, "field", FIELD_ORDER);
297
+ if (trailer.description !== void 0) node.description = trailer.description;
298
+ if (trailer.note !== void 0) node.note = trailer.note;
299
+ return { ok: true, value: node };
300
+ }
301
+ function parseSystem(c, at) {
302
+ const system = take(c);
303
+ if (system === void 0 || system.kind !== "word") {
304
+ return fail(
305
+ "E_UNPARSABLE",
306
+ "Expected a system name after `@`, e.g. `@Catalog`",
307
+ c.line,
308
+ system ?? afterSpan(at)
309
+ );
310
+ }
311
+ if (!SYSTEM_OR_VALUE.test(system.text)) {
312
+ return fail(
313
+ "E_UNPARSABLE",
314
+ `A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
315
+ c.line,
316
+ system
317
+ );
318
+ }
319
+ return { ok: true, value: name(system, c.line) };
320
+ }
321
+ function parseType(c, colon) {
322
+ const first = peek(c);
323
+ if (first === void 0 || first.kind === "*" || first.kind === "@" || first.kind === "=") {
324
+ return fail(
325
+ "E_MISSING_TYPE",
326
+ "A colon needs a type after it, e.g. `crewSize: int`",
327
+ c.line,
328
+ colon
329
+ );
330
+ }
331
+ if (first.kind !== "word") {
332
+ return fail(
333
+ "E_UNPARSABLE",
334
+ `Expected a type after \`:\`, found \`${first.text}\``,
335
+ c.line,
336
+ first
337
+ );
338
+ }
339
+ take(c);
340
+ const words = [first];
341
+ for (let pipe = peek(c); pipe?.kind === "|"; pipe = peek(c)) {
342
+ take(c);
343
+ const value = take(c);
344
+ if (value === void 0 || value.kind !== "word") {
345
+ return fail(
346
+ "E_UNPARSABLE",
347
+ "Expected an enum value after `|`, e.g. `climate: arid|temperate|frozen`",
348
+ c.line,
349
+ value ?? afterSpan(pipe)
350
+ );
351
+ }
352
+ words.push(value);
353
+ }
354
+ let many = false;
355
+ const bracket = peek(c);
356
+ if (bracket?.kind === "[]") {
357
+ take(c);
358
+ many = true;
359
+ } else if (bracket?.kind === "[") {
360
+ return fail(
361
+ "E_UNCLOSED_MANY",
362
+ "`[` must be closed: `[]` marks a many-valued field",
363
+ c.line,
364
+ bracket
365
+ );
511
366
  }
512
- let doubt = -1;
513
- for (let i = start + 1; i < text.length; i++) {
514
- if (isDoubtMarker(text, i)) {
515
- doubt = i;
516
- break;
367
+ if (words.length > 1) {
368
+ for (const w of words) {
369
+ if (!SYSTEM_OR_VALUE.test(w.text)) {
370
+ return fail(
371
+ "E_UNPARSABLE",
372
+ `An enum value is letters, digits, \`-\` and \`_\`, starting with a letter: \`${w.text}\``,
373
+ c.line,
374
+ w
375
+ );
376
+ }
517
377
  }
378
+ return { ok: true, value: { kind: "enum", values: words.map((w) => name(w, c.line)), many } };
518
379
  }
519
- const description = trimWs(text.slice(start + 1, doubt < 0 ? text.length : doubt));
520
- if (description !== "") trailer.description = description;
521
- if (doubt >= 0) {
522
- const note = trimWs(text.slice(doubt + 1));
523
- if (note !== "") trailer.note = note;
380
+ const word = first.text;
381
+ const primitive = primitiveFor(word);
382
+ if (primitive !== void 0) {
383
+ return {
384
+ ok: true,
385
+ value: { kind: "primitive", name: primitive, written: name(first, c.line), many }
386
+ };
524
387
  }
525
- return trailer;
526
- }
527
- var WORD = /[\p{L}\p{N}_-]+/uy;
528
- var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "["];
529
- function tokenize(head) {
530
- const tokens = [];
531
- let i = 0;
532
- while (i < head.length) {
533
- const c = head[i] ?? "";
534
- if (isWs(c)) {
535
- i++;
536
- continue;
537
- }
538
- WORD.lastIndex = i;
539
- const word = WORD.exec(head);
540
- if (word !== null) {
541
- tokens.push({ kind: "word", text: word[0], col: i, end: i + word[0].length });
542
- i += word[0].length;
543
- continue;
544
- }
545
- if (c === "[" && head[i + 1] === "]") {
546
- tokens.push({ kind: "[]", text: "[]", col: i, end: i + 2 });
547
- i += 2;
548
- continue;
388
+ if (/^[A-Z]/.test(word)) {
389
+ if (!CLASS_NAME.test(word)) {
390
+ return fail(
391
+ "E_BAD_NAME",
392
+ `A referenced class must be UpperCamelCase: \`${word}\``,
393
+ c.line,
394
+ first
395
+ );
549
396
  }
550
- const ch = String.fromCodePoint(head.codePointAt(i) ?? 0);
551
- const kind = OPERATORS.find((op) => op === ch) ?? "other";
552
- tokens.push({ kind, text: ch, col: i, end: i + ch.length });
553
- i += ch.length;
397
+ return { ok: true, value: { kind: "class", name: name(first, c.line), many } };
554
398
  }
555
- return tokens;
556
- }
557
- var peek = (c) => c.tokens[c.pos];
558
- function take(c) {
559
- const t = c.tokens[c.pos];
560
- if (t !== void 0) c.pos++;
561
- return t;
562
- }
563
- var name = (t, line) => ({ text: t.text, line, col: t.col, end: t.end });
564
- function fail(code, message, line, span) {
565
- return {
566
- ok: false,
567
- diagnostic: { severity: "error", code, message, line, col: span.col, end: span.end }
568
- };
399
+ if (/^[a-z]/.test(word)) {
400
+ return { ok: true, value: { kind: "unknown", name: name(first, c.line), many } };
401
+ }
402
+ return fail(
403
+ "E_UNPARSABLE",
404
+ `A type is a primitive, a class name or an enum, not \`${word}\``,
405
+ c.line,
406
+ first
407
+ );
569
408
  }
570
- var afterSpan = (after) => ({ col: after.end, end: after.end });
571
- function parseClassLine(text, line) {
572
- const trailer = splitTrailer(text);
573
- const c = { tokens: tokenize(trailer.head), pos: 0, line };
574
- const head = take(c);
575
- if (head === void 0 || head.kind !== "word") {
576
- return fail("E_UNPARSABLE", "A class line must start with a class name", line, {
577
- col: 0,
578
- end: Math.max(1, trailer.head.length)
579
- });
409
+ function unexpected(token, line, kind, order) {
410
+ const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["@", "~"];
411
+ if (modifiers.includes(token.kind)) {
412
+ return fail(
413
+ "E_UNPARSABLE",
414
+ `\`${token.text}\` is out of order; a ${kind} line reads: ${order}`,
415
+ line,
416
+ token
417
+ );
580
418
  }
581
- if (!CLASS_NAME.test(head.text)) {
419
+ if (token.text === "<") {
582
420
  return fail(
583
- "E_BAD_NAME",
584
- `Class names are UpperCamelCase (letters and digits, uppercase first): \`${head.text}\``,
421
+ "E_UNPARSABLE",
422
+ "`<` is reserved for inheritance and not yet part of the language",
585
423
  line,
586
- head
424
+ token
587
425
  );
588
426
  }
589
- const node = { name: name(head, line), line };
590
- const at = peek(c);
591
- if (at?.kind === "@") {
592
- take(c);
593
- const system = parseSystem(c, at);
594
- if (!system.ok) return system;
595
- node.system = system.value;
427
+ if (token.text === "?") {
428
+ return fail(
429
+ "E_UNPARSABLE",
430
+ "`?` attached to a word is not a marker; a doubt needs whitespace before `?` and after it",
431
+ line,
432
+ token
433
+ );
434
+ }
435
+ if (token.text === "#") {
436
+ return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line, token);
437
+ }
438
+ return fail("E_UNPARSABLE", `Unexpected \`${token.text}\` on a ${kind} line`, line, token);
439
+ }
440
+
441
+ // src/resolve.ts
442
+ function resolve(doc) {
443
+ const out = {
444
+ ...doc,
445
+ classes: doc.classes.map(copyClass),
446
+ diagnostics: doc.diagnostics.map((d) => ({ ...d })),
447
+ undeclared: []
448
+ };
449
+ const classes = out.classes;
450
+ const declared = new Set(classes.map((c) => c.name.text));
451
+ const warnings = [];
452
+ const undeclared = /* @__PURE__ */ new Map();
453
+ const warn = (code, message, at) => {
454
+ warnings.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
455
+ };
456
+ const reference = (name2) => {
457
+ if (declared.has(name2.text)) return true;
458
+ if (!undeclared.has(name2.text)) undeclared.set(name2.text, name2);
459
+ const lower = name2.text.toLowerCase();
460
+ const hint = Object.hasOwn(PRIMITIVES, lower) ? `. Did you mean \`${lower}\`?` : "";
461
+ warn("W_UNDECLARED_CLASS", `class \`${name2.text}\` is not declared${hint}`, name2);
462
+ return false;
463
+ };
464
+ const firstClass = /* @__PURE__ */ new Map();
465
+ for (const cls of classes) {
466
+ const earlier = firstClass.get(cls.name.text);
467
+ if (earlier === void 0) firstClass.set(cls.name.text, cls);
468
+ else {
469
+ warn(
470
+ "W_DUPLICATE_CLASS",
471
+ `class \`${cls.name.text}\` is already declared on line ${earlier.line}`,
472
+ cls.name
473
+ );
474
+ }
475
+ if (cls.similarTo !== void 0) reference(cls.similarTo);
476
+ const firstField = /* @__PURE__ */ new Map();
477
+ let identifier;
478
+ for (const field of cls.fields) {
479
+ const earlierField = firstField.get(field.name.text);
480
+ if (earlierField === void 0) firstField.set(field.name.text, field);
481
+ else {
482
+ warn(
483
+ "W_DUPLICATE_FIELD",
484
+ `field \`${field.name.text}\` is already declared on line ${earlierField.line} in class \`${cls.name.text}\``,
485
+ field.name
486
+ );
487
+ }
488
+ if (field.identifier) {
489
+ if (identifier === void 0) identifier = field;
490
+ else {
491
+ warn(
492
+ "W_MULTIPLE_IDENTIFIERS",
493
+ `\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\` already has the identifier \`${identifier.name.text}\` on line ${identifier.line}`,
494
+ field.identifierAt ?? field.name
495
+ );
496
+ field.identifier = false;
497
+ }
498
+ }
499
+ if (field.type?.kind === "unknown") {
500
+ const word = field.type.name.text;
501
+ const suggestion = closestTypeWord(word);
502
+ warn(
503
+ "W_UNKNOWN_TYPE",
504
+ suggestion === void 0 ? `unknown type \`${word}\`, treated as \`string\`` : `unknown type \`${word}\`, did you mean \`${suggestion}\`?`,
505
+ field.type.name
506
+ );
507
+ } else if (field.type?.kind === "class") {
508
+ reference(field.type.name);
509
+ }
510
+ if (field.joinsTo !== void 0) {
511
+ const { className, fieldName } = field.joinsTo;
512
+ const hasField = (c) => c.name.text === className.text && c.fields.some((f) => f.name.text === fieldName.text);
513
+ if (reference(className) && !classes.some(hasField)) {
514
+ warn(
515
+ "W_UNDECLARED_FIELD",
516
+ `class \`${className.text}\` has no field \`${fieldName.text}\``,
517
+ fieldName
518
+ );
519
+ }
520
+ }
521
+ }
522
+ }
523
+ const present = new Set(out.diagnostics.map(key));
524
+ for (const w of warnings) {
525
+ if (!present.has(key(w))) out.diagnostics.push(w);
526
+ }
527
+ out.diagnostics.sort((a, b) => a.line - b.line);
528
+ out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
529
+ return out;
530
+ }
531
+ var copyName = (n) => ({ ...n });
532
+ function copyType(t) {
533
+ switch (t.kind) {
534
+ case "primitive":
535
+ return { ...t, written: copyName(t.written) };
536
+ case "class":
537
+ case "unknown":
538
+ return { ...t, name: copyName(t.name) };
539
+ case "enum":
540
+ return { ...t, values: t.values.map(copyName) };
596
541
  }
597
- const tilde = peek(c);
598
- if (tilde?.kind === "~") {
599
- take(c);
600
- const target = take(c);
601
- if (target === void 0 || target.kind !== "word") {
602
- return fail(
603
- "E_UNPARSABLE",
604
- "Expected a class name after `~`, e.g. `CharacterPage @Community ~ Character`",
605
- line,
606
- target ?? afterSpan(tilde)
607
- );
542
+ }
543
+ var copyJoin = (j) => ({
544
+ className: copyName(j.className),
545
+ fieldName: copyName(j.fieldName)
546
+ });
547
+ function copyField(f) {
548
+ const out = { ...f, name: copyName(f.name) };
549
+ if (f.identifierAt !== void 0) out.identifierAt = copyName(f.identifierAt);
550
+ if (f.type !== void 0) out.type = copyType(f.type);
551
+ if (f.joinsTo !== void 0) out.joinsTo = copyJoin(f.joinsTo);
552
+ return out;
553
+ }
554
+ function copyClass(c) {
555
+ const out = { ...c, name: copyName(c.name), fields: c.fields.map(copyField) };
556
+ if (c.system !== void 0) out.system = copyName(c.system);
557
+ if (c.similarTo !== void 0) out.similarTo = copyName(c.similarTo);
558
+ return out;
559
+ }
560
+ var key = (d) => [d.severity, d.code, d.line, d.col, d.end, d.message].join(" ");
561
+ var TYPE_WORDS = Object.keys(PRIMITIVES);
562
+ var MAX_DISTANCE = 2;
563
+ function closestTypeWord(word) {
564
+ let best;
565
+ let bestDistance = MAX_DISTANCE + 1;
566
+ let bestPrefix = -1;
567
+ for (const candidate of TYPE_WORDS) {
568
+ const distance = levenshtein(word, candidate);
569
+ if (distance > MAX_DISTANCE || distance > bestDistance) continue;
570
+ const prefix = commonPrefixLength(word, candidate);
571
+ if (distance < bestDistance || prefix > bestPrefix) {
572
+ best = candidate;
573
+ bestDistance = distance;
574
+ bestPrefix = prefix;
608
575
  }
609
- if (!CLASS_NAME.test(target.text)) {
610
- return fail(
611
- "E_BAD_NAME",
612
- `The class after \`~\` must be UpperCamelCase: \`${target.text}\``,
613
- line,
614
- target
576
+ }
577
+ return best;
578
+ }
579
+ function commonPrefixLength(a, b) {
580
+ let i = 0;
581
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
582
+ return i;
583
+ }
584
+ function levenshtein(a, b) {
585
+ if (a === b) return 0;
586
+ if (a.length === 0) return b.length;
587
+ if (b.length === 0) return a.length;
588
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
589
+ for (let i = 1; i <= a.length; i++) {
590
+ const current = [i];
591
+ for (let j = 1; j <= b.length; j++) {
592
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
593
+ current[j] = Math.min(
594
+ (previous[j] ?? 0) + 1,
595
+ (current[j - 1] ?? 0) + 1,
596
+ (previous[j - 1] ?? 0) + cost
615
597
  );
616
598
  }
617
- node.similarTo = name(target, line);
599
+ previous = current;
618
600
  }
619
- const rest = peek(c);
620
- if (rest !== void 0) return unexpected(rest, line, "class", CLASS_ORDER);
621
- if (trailer.description !== void 0) node.description = trailer.description;
622
- if (trailer.note !== void 0) node.note = trailer.note;
623
- return { ok: true, value: { ...node, fields: [] } };
601
+ return previous[b.length] ?? 0;
624
602
  }
625
- function parseFieldLine(text, line) {
626
- const trailer = splitTrailer(text);
627
- const c = { tokens: tokenize(trailer.head), pos: 0, line };
628
- const head = take(c);
629
- if (head === void 0) {
630
- const col = text.search(/[^ \t]/);
631
- 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";
632
- return fail("E_UNPARSABLE", message, line, { col, end: Math.max(col + 1, text.length) });
633
- }
634
- if (head.kind !== "word") return unexpected(head, line, "field", FIELD_ORDER);
635
- if (!FIELD_NAME.test(head.text)) {
636
- return fail(
637
- "E_BAD_NAME",
638
- `Field names are lowerCamelCase (letters and digits, lowercase first): \`${head.text}\``,
639
- line,
640
- head
641
- );
603
+
604
+ // src/generators/linkml.ts
605
+ var BASE_URI = "https://example.org";
606
+ var RANGE = {
607
+ string: "string",
608
+ int: "integer",
609
+ float: "float",
610
+ bool: "boolean",
611
+ date: "date",
612
+ datetime: "datetime",
613
+ uri: "uri"
614
+ };
615
+ function toLinkML(doc, opts) {
616
+ const resolved = resolve(doc);
617
+ const schemaName = linkmlName(opts.schemaName);
618
+ const classes = emittedClasses(resolved.classes);
619
+ const classNames = new Set(classes.map((cls) => cls.node.name.text));
620
+ for (const name2 of resolved.undeclared) classNames.add(name2.text);
621
+ const enums = nameEnums(classes, classNames);
622
+ const prefixes = { linkml: "https://w3id.org/linkml/" };
623
+ if (!Object.hasOwn(prefixes, schemaName)) prefixes[schemaName] = `${BASE_URI}/${schemaName}/`;
624
+ const systemPrefix = /* @__PURE__ */ new Map();
625
+ const addSystem = (system) => {
626
+ const prefix = linkmlName(system);
627
+ if (!systemPrefix.has(system)) systemPrefix.set(system, prefix);
628
+ if (!Object.hasOwn(prefixes, prefix)) prefixes[prefix] = `${BASE_URI}/system/${prefix}/`;
629
+ };
630
+ for (const { node, fields } of classes) {
631
+ if (node.system !== void 0) addSystem(node.system.text);
632
+ for (const field of fields) {
633
+ if (field.system !== void 0) addSystem(field.system.text);
634
+ }
642
635
  }
643
- const node = { name: name(head, line), line, identifier: false };
644
- if (peek(c)?.kind === "*") {
645
- take(c);
646
- node.identifier = true;
636
+ const declaredSystem = /* @__PURE__ */ new Map();
637
+ for (const { node } of classes) {
638
+ if (!declaredSystem.has(node.name.text)) declaredSystem.set(node.name.text, node.system?.text);
647
639
  }
648
- const colon = peek(c);
649
- if (colon?.kind === ":") {
650
- take(c);
651
- const type = parseType(c, colon);
652
- if (!type.ok) return type;
653
- node.type = type.value;
640
+ const mappingPrefix = (target) => {
641
+ const system = declaredSystem.get(target);
642
+ if (system === void 0) return schemaName;
643
+ return systemPrefix.get(system) ?? schemaName;
644
+ };
645
+ const schema = {
646
+ id: `${BASE_URI}/${schemaName}`,
647
+ name: schemaName,
648
+ default_prefix: schemaName,
649
+ default_range: "string",
650
+ prefixes,
651
+ imports: ["linkml:types"]
652
+ };
653
+ const out = {};
654
+ for (const cls of classes) {
655
+ out[cls.node.name.text] = buildClass(cls, mappingPrefix, enums);
654
656
  }
655
- const at = peek(c);
656
- if (at?.kind === "@") {
657
- take(c);
658
- const system = parseSystem(c, at);
659
- if (!system.ok) return system;
660
- node.system = system.value;
657
+ for (const name2 of resolved.undeclared) {
658
+ if (!(name2.text in out)) out[name2.text] = { annotations: { undeclared: true } };
661
659
  }
662
- const equals = peek(c);
663
- if (equals?.kind === "=") {
664
- take(c);
665
- const example = "e.g. `characterId = Character.id`";
666
- const className = take(c);
667
- if (className === void 0 || className.kind !== "word") {
668
- return fail(
669
- "E_UNPARSABLE",
670
- `Expected \`Class.field\` after \`=\`, ${example}`,
671
- line,
672
- className ?? afterSpan(equals)
673
- );
674
- }
675
- if (!CLASS_NAME.test(className.text)) {
676
- return fail(
677
- "E_BAD_NAME",
678
- `The class after \`=\` must be UpperCamelCase: \`${className.text}\``,
679
- line,
680
- className
681
- );
682
- }
683
- const dot = take(c);
684
- if (dot === void 0 || dot.kind !== ".") {
685
- return fail(
686
- "E_UNPARSABLE",
687
- `Expected \`.\` and a field name after \`= ${className.text}\`, ${example}`,
688
- line,
689
- dot ?? afterSpan(className)
690
- );
660
+ if (Object.keys(out).length > 0) schema.classes = out;
661
+ const enumBlock = buildEnums(classes, enums);
662
+ if (Object.keys(enumBlock).length > 0) schema.enums = enumBlock;
663
+ return schema;
664
+ }
665
+ function emittedClasses(classes) {
666
+ const out = [];
667
+ const seen = /* @__PURE__ */ new Set();
668
+ for (const node of classes) {
669
+ if (seen.has(node.name.text)) continue;
670
+ seen.add(node.name.text);
671
+ const fields = [];
672
+ const seenField = /* @__PURE__ */ new Set();
673
+ for (const field of node.fields) {
674
+ if (seenField.has(field.name.text)) continue;
675
+ seenField.add(field.name.text);
676
+ fields.push(field);
691
677
  }
692
- const fieldName = take(c);
693
- if (fieldName === void 0 || fieldName.kind !== "word") {
694
- return fail(
695
- "E_UNPARSABLE",
696
- `Expected a field name after \`= ${className.text}.\`, ${example}`,
697
- line,
698
- fieldName ?? afterSpan(dot)
699
- );
678
+ out.push({ node, fields });
679
+ }
680
+ return out;
681
+ }
682
+ var useKey = (className, fieldName) => `${className}\0${fieldName}`;
683
+ function nameEnums(classes, classNames) {
684
+ const uses = [];
685
+ for (const { node, fields } of classes) {
686
+ for (const field of fields) {
687
+ if (field.type?.kind !== "enum") continue;
688
+ uses.push({
689
+ className: node.name.text,
690
+ fieldName: field.name.text,
691
+ values: field.type.values.map((v) => v.text).join("|")
692
+ });
700
693
  }
701
- if (!FIELD_NAME.test(fieldName.text)) {
702
- return fail(
703
- "E_BAD_NAME",
704
- `The field after \`= ${className.text}.\` must be lowerCamelCase: \`${fieldName.text}\``,
705
- line,
706
- fieldName
707
- );
694
+ }
695
+ const valueSets = /* @__PURE__ */ new Map();
696
+ for (const use of uses) {
697
+ const seen = valueSets.get(use.fieldName) ?? /* @__PURE__ */ new Set();
698
+ seen.add(use.values);
699
+ valueSets.set(use.fieldName, seen);
700
+ }
701
+ const taken = /* @__PURE__ */ new Map();
702
+ const names = /* @__PURE__ */ new Map();
703
+ for (const use of uses) {
704
+ const shared = (valueSets.get(use.fieldName)?.size ?? 0) <= 1;
705
+ const base = shared ? `${capitalise(use.fieldName)}Enum` : `${capitalise(use.className)}${capitalise(use.fieldName)}Enum`;
706
+ let name2 = base;
707
+ for (let n = 2; classNames.has(name2) || taken.has(name2) && taken.get(name2) !== use.values; n++) {
708
+ name2 = `${base}${n}`;
708
709
  }
709
- node.joinsTo = { className: name(className, line), fieldName: name(fieldName, line) };
710
+ taken.set(name2, use.values);
711
+ names.set(useKey(use.className, use.fieldName), name2);
710
712
  }
711
- const rest = peek(c);
712
- if (rest !== void 0) return unexpected(rest, line, "field", FIELD_ORDER);
713
- if (trailer.description !== void 0) node.description = trailer.description;
714
- if (trailer.note !== void 0) node.note = trailer.note;
715
- return { ok: true, value: node };
713
+ return names;
716
714
  }
717
- function parseSystem(c, at) {
718
- const system = take(c);
719
- if (system === void 0 || system.kind !== "word") {
720
- return fail(
721
- "E_UNPARSABLE",
722
- "Expected a system name after `@`, e.g. `@Catalog`",
723
- c.line,
724
- system ?? afterSpan(at)
725
- );
715
+ function buildEnums(classes, enums) {
716
+ const out = {};
717
+ for (const { node, fields } of classes) {
718
+ for (const field of fields) {
719
+ if (field.type?.kind !== "enum") continue;
720
+ const name2 = enums.get(useKey(node.name.text, field.name.text));
721
+ if (name2 === void 0 || name2 in out) continue;
722
+ const values = {};
723
+ for (const value of field.type.values) values[value.text] = null;
724
+ out[name2] = { permissible_values: values };
725
+ }
726
726
  }
727
- if (!SYSTEM_OR_VALUE.test(system.text)) {
728
- return fail(
729
- "E_UNPARSABLE",
730
- `A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
731
- c.line,
732
- system
733
- );
727
+ return out;
728
+ }
729
+ function buildClass(cls, mappingPrefix, enums) {
730
+ const { node, fields } = cls;
731
+ const out = {};
732
+ if (node.description !== void 0) out.description = node.description;
733
+ const annotations = {};
734
+ if (node.system !== void 0) annotations.system = node.system.text;
735
+ if (node.note !== void 0) annotations.note = node.note;
736
+ if (Object.keys(annotations).length > 0) out.annotations = annotations;
737
+ if (node.similarTo !== void 0) {
738
+ const target = node.similarTo.text;
739
+ out.close_mappings = [`${mappingPrefix(target)}:${target}`];
734
740
  }
735
- return { ok: true, value: name(system, c.line) };
741
+ const attributes = {};
742
+ for (const field of fields) {
743
+ attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
744
+ }
745
+ if (Object.keys(attributes).length > 0) out.attributes = attributes;
746
+ return out;
736
747
  }
737
- function parseType(c, colon) {
738
- const first = peek(c);
739
- if (first === void 0 || first.kind === "*" || first.kind === "@" || first.kind === "=") {
740
- return fail(
741
- "E_MISSING_TYPE",
742
- "A colon needs a type after it, e.g. `crewSize: int`",
743
- c.line,
744
- colon
745
- );
748
+ function buildAttribute(field, className, enums) {
749
+ const out = {};
750
+ if (field.description !== void 0) out.description = field.description;
751
+ const range = rangeOf(field, className, enums);
752
+ if (range !== void 0) out.range = range;
753
+ if (field.type?.many === true) out.multivalued = true;
754
+ if (field.identifier) out.identifier = true;
755
+ const annotations = {};
756
+ if (field.system !== void 0) annotations.system = field.system.text;
757
+ if (field.joinsTo !== void 0) {
758
+ annotations.joins_to = `${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}`;
746
759
  }
747
- if (first.kind !== "word") {
748
- return fail(
749
- "E_UNPARSABLE",
750
- `Expected a type after \`:\`, found \`${first.text}\``,
751
- c.line,
752
- first
753
- );
760
+ if (field.note !== void 0) annotations.note = field.note;
761
+ if (Object.keys(annotations).length > 0) out.annotations = annotations;
762
+ return out;
763
+ }
764
+ function rangeOf(field, className, enums) {
765
+ const type = field.type;
766
+ if (type === void 0) return void 0;
767
+ switch (type.kind) {
768
+ case "primitive":
769
+ return RANGE[type.name];
770
+ case "class":
771
+ return type.name.text;
772
+ case "unknown":
773
+ return "string";
774
+ case "enum":
775
+ return enums.get(useKey(className, field.name.text));
754
776
  }
755
- take(c);
756
- const words = [first];
757
- for (let pipe = peek(c); pipe?.kind === "|"; pipe = peek(c)) {
758
- take(c);
759
- const value = take(c);
760
- if (value === void 0 || value.kind !== "word") {
761
- return fail(
762
- "E_UNPARSABLE",
763
- "Expected an enum value after `|`, e.g. `climate: arid|temperate|frozen`",
764
- c.line,
765
- value ?? afterSpan(pipe)
766
- );
767
- }
768
- words.push(value);
777
+ }
778
+ var RESERVED_PREFIXES = /* @__PURE__ */ new Set(["linkml", "xsd", "shex", "schema"]);
779
+ var linkmlName = (text) => {
780
+ const name2 = text.toLowerCase().replace(/[^a-z0-9]/g, "_");
781
+ if (!/[a-z0-9]/.test(name2)) return "sketch";
782
+ if (RESERVED_PREFIXES.has(name2)) return `${name2}_`;
783
+ return /^[0-9]/.test(name2) ? `_${name2}` : name2;
784
+ };
785
+ var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
786
+
787
+ // src/generators/mermaid.ts
788
+ function toMermaid(doc, opts) {
789
+ const resolved = resolve(doc);
790
+ const notes = opts?.notes === true;
791
+ const lines = ["classDiagram"];
792
+ for (const cls of resolved.classes) {
793
+ lines.push(` class ${cls.name.text} {`);
794
+ if (cls.system !== void 0) lines.push(` <<${cls.system.text}>>`);
795
+ for (const field of cls.fields) lines.push(` ${member(field)}`);
796
+ lines.push(" }");
769
797
  }
770
- let many = false;
771
- const bracket = peek(c);
772
- if (bracket?.kind === "[]") {
773
- take(c);
774
- many = true;
775
- } else if (bracket?.kind === "[") {
776
- return fail(
777
- "E_UNCLOSED_MANY",
778
- "`[` must be closed: `[]` marks a many-valued field",
779
- c.line,
780
- bracket
781
- );
798
+ for (const name2 of resolved.undeclared) {
799
+ lines.push(` class ${name2.text} {`, " <<undeclared>>", " }");
782
800
  }
783
- if (words.length > 1) {
784
- for (const w of words) {
785
- if (!SYSTEM_OR_VALUE.test(w.text)) {
786
- return fail(
787
- "E_UNPARSABLE",
788
- `An enum value is letters, digits, \`-\` and \`_\`, starting with a letter: \`${w.text}\``,
789
- c.line,
790
- w
791
- );
801
+ if (notes) {
802
+ for (const cls of resolved.classes) {
803
+ if (cls.note !== void 0) lines.push(` note for ${cls.name.text} ${quote(cls.note)}`);
804
+ for (const field of cls.fields) {
805
+ if (field.note !== void 0) {
806
+ lines.push(` note for ${cls.name.text} ${quote(`${field.name.text}: ${field.note}`)}`);
807
+ }
792
808
  }
793
809
  }
794
- return { ok: true, value: { kind: "enum", values: words.map((w) => name(w, c.line)), many } };
795
- }
796
- const word = first.text;
797
- const primitive = PRIMITIVES[word];
798
- if (primitive !== void 0) {
799
- return {
800
- ok: true,
801
- value: { kind: "primitive", name: primitive, written: name(first, c.line), many }
802
- };
803
810
  }
804
- if (/^[A-Z]/.test(word)) {
805
- if (!CLASS_NAME.test(word)) {
806
- return fail(
807
- "E_BAD_NAME",
808
- `A referenced class must be UpperCamelCase: \`${word}\``,
809
- c.line,
810
- first
811
- );
811
+ for (const cls of resolved.classes) {
812
+ const from = cls.name.text;
813
+ if (cls.similarTo !== void 0) lines.push(` ${from} ..> ${cls.similarTo.text} : similar`);
814
+ for (const field of cls.fields) {
815
+ const type = field.type;
816
+ if (type?.kind === "class") {
817
+ lines.push(
818
+ type.many ? ` ${from} "1" --> "*" ${type.name.text} : ${field.name.text}` : ` ${from} --> ${type.name.text} : ${field.name.text}`
819
+ );
820
+ }
821
+ if (field.joinsTo !== void 0) {
822
+ const { className, fieldName } = field.joinsTo;
823
+ lines.push(` ${from} ..> ${className.text} : ${field.name.text} = ${fieldName.text}`);
824
+ }
812
825
  }
813
- return { ok: true, value: { kind: "class", name: name(first, c.line), many } };
814
- }
815
- if (/^[a-z]/.test(word)) {
816
- return { ok: true, value: { kind: "unknown", name: name(first, c.line), many } };
817
826
  }
818
- return fail(
819
- "E_UNPARSABLE",
820
- `A type is a primitive, a class name or an enum, not \`${word}\``,
821
- c.line,
822
- first
823
- );
827
+ return `${lines.join("\n")}
828
+ `;
824
829
  }
825
- function unexpected(token, line, kind, order) {
826
- const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["@", "~"];
827
- if (modifiers.includes(token.kind)) {
828
- return fail(
829
- "E_UNPARSABLE",
830
- `\`${token.text}\` is out of order; a ${kind} line reads: ${order}`,
831
- line,
832
- token
833
- );
834
- }
835
- if (token.text === "<") {
836
- return fail(
837
- "E_UNPARSABLE",
838
- "`<` is reserved for inheritance and not yet part of the language",
839
- line,
840
- token
841
- );
842
- }
843
- if (token.text === "?") {
844
- return fail(
845
- "E_UNPARSABLE",
846
- "`?` attached to a word is not a marker; a doubt needs whitespace before `?` and after it",
847
- line,
848
- token
849
- );
850
- }
851
- if (token.text === "#") {
852
- return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line, token);
830
+ function member(field) {
831
+ const marker = field.identifier ? "*" : "+";
832
+ const type = field.type === void 0 ? "" : `${typeText(field.type)} `;
833
+ const system = field.system === void 0 ? "" : ` @${field.system.text}`;
834
+ return `${marker}${type}${field.name.text}${system}`;
835
+ }
836
+ function typeText(type) {
837
+ let text;
838
+ switch (type.kind) {
839
+ case "primitive":
840
+ text = type.written.text;
841
+ break;
842
+ case "class":
843
+ case "unknown":
844
+ text = type.name.text;
845
+ break;
846
+ case "enum":
847
+ text = type.values.map((v) => v.text).join("|");
848
+ break;
853
849
  }
854
- return fail("E_UNPARSABLE", `Unexpected \`${token.text}\` on a ${kind} line`, line, token);
850
+ return type.many ? `${text}[]` : text;
855
851
  }
852
+ var quote = (text) => `"${text.replaceAll('"', "'")}"`;
856
853
 
857
854
  // src/serialize.ts
858
855
  import { isMap, isScalar, Scalar, visit, Document as YAMLDocument } from "yaml";
@@ -912,7 +909,7 @@ function formatDiagnostic(d, file) {
912
909
  }
913
910
 
914
911
  // src/index.ts
915
- var VERSION = "0.2.0";
912
+ var VERSION = "0.2.1";
916
913
  export {
917
914
  VERSION,
918
915
  compile,