@eriknaslund/skiss 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +8 -2
- package/dist/cli.js +412 -58
- package/dist/index.d.ts +15 -7
- package/dist/index.js +299 -52
- package/package.json +6 -3
- package/dist/cli.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,89 @@
|
|
|
1
|
+
// src/inheritance.ts
|
|
2
|
+
function inheritance(classes) {
|
|
3
|
+
const declared = /* @__PURE__ */ new Map();
|
|
4
|
+
for (const cls of classes) {
|
|
5
|
+
if (!declared.has(cls.name.text)) declared.set(cls.name.text, cls);
|
|
6
|
+
}
|
|
7
|
+
const circles = /* @__PURE__ */ new Map();
|
|
8
|
+
const parentOf = (cls) => {
|
|
9
|
+
if (cls.parent === void 0 || circles.has(cls)) return void 0;
|
|
10
|
+
return declared.get(cls.parent.text);
|
|
11
|
+
};
|
|
12
|
+
for (const cls of declared.values()) {
|
|
13
|
+
const path = [cls];
|
|
14
|
+
const seen = new Set(path);
|
|
15
|
+
for (let next = parentOf(cls); next !== void 0; next = parentOf(next)) {
|
|
16
|
+
if (seen.has(next)) {
|
|
17
|
+
const circle = path.slice(path.indexOf(next));
|
|
18
|
+
let closer = circle[0];
|
|
19
|
+
for (const member2 of circle) {
|
|
20
|
+
if (member2.line > closer.line) closer = member2;
|
|
21
|
+
}
|
|
22
|
+
const at = circle.indexOf(closer);
|
|
23
|
+
circles.set(closer, [...circle.slice(at), ...circle.slice(0, at)]);
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
path.push(next);
|
|
27
|
+
seen.add(next);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const ancestors = (cls) => {
|
|
31
|
+
const out = [];
|
|
32
|
+
const seen = /* @__PURE__ */ new Set([cls]);
|
|
33
|
+
for (let next = parentOf(cls); next !== void 0; next = parentOf(next)) {
|
|
34
|
+
if (seen.has(next)) break;
|
|
35
|
+
out.push(next);
|
|
36
|
+
seen.add(next);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
};
|
|
40
|
+
const find = (cls, matches) => {
|
|
41
|
+
for (const from of ancestors(cls)) {
|
|
42
|
+
const field = from.fields.find(matches);
|
|
43
|
+
if (field !== void 0) return { field, from };
|
|
44
|
+
}
|
|
45
|
+
return void 0;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
ancestors,
|
|
49
|
+
inherited: (cls, fieldName) => find(cls, (field) => field.name.text === fieldName),
|
|
50
|
+
inheritedIdentifier: (cls) => find(cls, (field) => field.identifier),
|
|
51
|
+
circle: (cls) => circles.get(cls)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function sameField(child, inheritedField) {
|
|
55
|
+
return meaning(child) === meaning(inheritedField);
|
|
56
|
+
}
|
|
57
|
+
var meaning = (field) => JSON.stringify([
|
|
58
|
+
field.name.text,
|
|
59
|
+
field.identifier,
|
|
60
|
+
typeMeaning(field),
|
|
61
|
+
field.system?.text,
|
|
62
|
+
field.joinsTo === void 0 ? void 0 : [field.joinsTo.className.text, field.joinsTo.fieldName.text],
|
|
63
|
+
field.description,
|
|
64
|
+
field.note
|
|
65
|
+
]);
|
|
66
|
+
function typeMeaning(field) {
|
|
67
|
+
const type = field.type;
|
|
68
|
+
if (type === void 0) return void 0;
|
|
69
|
+
switch (type.kind) {
|
|
70
|
+
case "primitive":
|
|
71
|
+
return [type.kind, type.name, type.many];
|
|
72
|
+
case "class":
|
|
73
|
+
case "unknown":
|
|
74
|
+
return [type.kind, type.name.text, type.many];
|
|
75
|
+
case "enum":
|
|
76
|
+
return [type.kind, type.values.map((v) => v.text), type.many];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
1
80
|
// src/parse.ts
|
|
2
81
|
var isWs = (c) => c === " " || c === " ";
|
|
3
82
|
var trimWs = (s) => s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
|
|
4
83
|
var CLASS_NAME = /^[A-Z][A-Za-z0-9]*$/;
|
|
5
84
|
var FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
|
|
6
|
-
var
|
|
85
|
+
var SYSTEM_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
86
|
+
var ENUM_VALUE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
7
87
|
var PRIMITIVES = {
|
|
8
88
|
string: "string",
|
|
9
89
|
int: "int",
|
|
@@ -20,7 +100,7 @@ function primitiveFor(word) {
|
|
|
20
100
|
return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
|
|
21
101
|
}
|
|
22
102
|
var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
|
|
23
|
-
var CLASS_ORDER = "name, `@System`, `~ Class`, then `#` and `?`";
|
|
103
|
+
var CLASS_ORDER = "name, `< Parent`, `@System`, `~ Class`, then `#` and `?`";
|
|
24
104
|
function parse(source) {
|
|
25
105
|
const classes = [];
|
|
26
106
|
const diagnostics = [];
|
|
@@ -107,7 +187,7 @@ function splitTrailer(text) {
|
|
|
107
187
|
return trailer;
|
|
108
188
|
}
|
|
109
189
|
var WORD = /[\p{L}\p{N}_-]+/uy;
|
|
110
|
-
var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "["];
|
|
190
|
+
var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "<", ",", "["];
|
|
111
191
|
function tokenize(head) {
|
|
112
192
|
const tokens = [];
|
|
113
193
|
let i = 0;
|
|
@@ -169,6 +249,13 @@ function parseClassLine(text, line) {
|
|
|
169
249
|
);
|
|
170
250
|
}
|
|
171
251
|
const node = { name: name(head, line), line };
|
|
252
|
+
const lt = peek(c);
|
|
253
|
+
if (lt?.kind === "<") {
|
|
254
|
+
take(c);
|
|
255
|
+
const parent = parseParent(c, lt);
|
|
256
|
+
if (!parent.ok) return parent;
|
|
257
|
+
node.parent = parent.value;
|
|
258
|
+
}
|
|
172
259
|
const at = peek(c);
|
|
173
260
|
if (at?.kind === "@") {
|
|
174
261
|
take(c);
|
|
@@ -298,6 +385,31 @@ function parseFieldLine(text, line) {
|
|
|
298
385
|
if (trailer.note !== void 0) node.note = trailer.note;
|
|
299
386
|
return { ok: true, value: node };
|
|
300
387
|
}
|
|
388
|
+
function parseParent(c, lt) {
|
|
389
|
+
const parent = take(c);
|
|
390
|
+
if (parent === void 0 || parent.kind !== "word") {
|
|
391
|
+
return fail(
|
|
392
|
+
"E_UNPARSABLE",
|
|
393
|
+
"Expected a class name after `<`, e.g. `Jedi < Character`",
|
|
394
|
+
c.line,
|
|
395
|
+
parent ?? afterSpan(lt)
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (!CLASS_NAME.test(parent.text)) {
|
|
399
|
+
const message = Object.hasOwn(PRIMITIVES, parent.text) ? `\`${parent.text}\` is a primitive, not a class; a parent is a class` : `The class after \`<\` must be UpperCamelCase: \`${parent.text}\``;
|
|
400
|
+
return fail("E_BAD_NAME", message, c.line, parent);
|
|
401
|
+
}
|
|
402
|
+
const next = peek(c);
|
|
403
|
+
if (next?.kind === "," || next?.kind === "<") {
|
|
404
|
+
return fail(
|
|
405
|
+
"E_UNPARSABLE",
|
|
406
|
+
"A class has one parent; `<` takes a single class name",
|
|
407
|
+
c.line,
|
|
408
|
+
next
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return { ok: true, value: name(parent, c.line) };
|
|
412
|
+
}
|
|
301
413
|
function parseSystem(c, at) {
|
|
302
414
|
const system = take(c);
|
|
303
415
|
if (system === void 0 || system.kind !== "word") {
|
|
@@ -308,7 +420,7 @@ function parseSystem(c, at) {
|
|
|
308
420
|
system ?? afterSpan(at)
|
|
309
421
|
);
|
|
310
422
|
}
|
|
311
|
-
if (!
|
|
423
|
+
if (!SYSTEM_NAME.test(system.text)) {
|
|
312
424
|
return fail(
|
|
313
425
|
"E_UNPARSABLE",
|
|
314
426
|
`A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
|
|
@@ -366,10 +478,10 @@ function parseType(c, colon) {
|
|
|
366
478
|
}
|
|
367
479
|
if (words.length > 1) {
|
|
368
480
|
for (const w of words) {
|
|
369
|
-
if (!
|
|
481
|
+
if (!ENUM_VALUE.test(w.text)) {
|
|
370
482
|
return fail(
|
|
371
483
|
"E_UNPARSABLE",
|
|
372
|
-
`An enum value is letters, digits, \`-\` and \`_\`,
|
|
484
|
+
`An enum value is letters, digits, \`-\` and \`_\`, and does not start with \`-\` or \`_\`: \`${w.text}\``,
|
|
373
485
|
c.line,
|
|
374
486
|
w
|
|
375
487
|
);
|
|
@@ -407,7 +519,7 @@ function parseType(c, colon) {
|
|
|
407
519
|
);
|
|
408
520
|
}
|
|
409
521
|
function unexpected(token, line, kind, order) {
|
|
410
|
-
const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["@", "~"];
|
|
522
|
+
const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["<", "@", "~"];
|
|
411
523
|
if (modifiers.includes(token.kind)) {
|
|
412
524
|
return fail(
|
|
413
525
|
"E_UNPARSABLE",
|
|
@@ -416,10 +528,10 @@ function unexpected(token, line, kind, order) {
|
|
|
416
528
|
token
|
|
417
529
|
);
|
|
418
530
|
}
|
|
419
|
-
if (token.
|
|
531
|
+
if (token.kind === "<") {
|
|
420
532
|
return fail(
|
|
421
533
|
"E_UNPARSABLE",
|
|
422
|
-
"`<`
|
|
534
|
+
"`<` marks inheritance and belongs on the class line: `Child < Parent`",
|
|
423
535
|
line,
|
|
424
536
|
token
|
|
425
537
|
);
|
|
@@ -448,10 +560,14 @@ function resolve(doc) {
|
|
|
448
560
|
};
|
|
449
561
|
const classes = out.classes;
|
|
450
562
|
const declared = new Set(classes.map((c) => c.name.text));
|
|
451
|
-
const
|
|
563
|
+
const found = [];
|
|
452
564
|
const undeclared = /* @__PURE__ */ new Map();
|
|
565
|
+
const tree = inheritance(classes);
|
|
453
566
|
const warn = (code, message, at) => {
|
|
454
|
-
|
|
567
|
+
found.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
|
|
568
|
+
};
|
|
569
|
+
const error = (code, message, at) => {
|
|
570
|
+
found.push({ severity: "error", code, message, line: at.line, col: at.col, end: at.end });
|
|
455
571
|
};
|
|
456
572
|
const reference = (name2) => {
|
|
457
573
|
if (declared.has(name2.text)) return true;
|
|
@@ -472,9 +588,25 @@ function resolve(doc) {
|
|
|
472
588
|
cls.name
|
|
473
589
|
);
|
|
474
590
|
}
|
|
591
|
+
const parent = cls.parent;
|
|
592
|
+
if (parent !== void 0) {
|
|
593
|
+
reference(parent);
|
|
594
|
+
const circle = tree.circle(cls);
|
|
595
|
+
if (circle !== void 0) {
|
|
596
|
+
const through = circle.slice(1).map((member2) => `\`${member2.name.text}\``);
|
|
597
|
+
const path = through.length === 0 ? "" : ` through ${through.join(", ")}`;
|
|
598
|
+
error(
|
|
599
|
+
"E_INHERITANCE_CYCLE",
|
|
600
|
+
`class \`${cls.name.text}\` inherits from itself${path}; this \`<\` is not carried`,
|
|
601
|
+
parent
|
|
602
|
+
);
|
|
603
|
+
cls.parent = void 0;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
475
606
|
if (cls.similarTo !== void 0) reference(cls.similarTo);
|
|
476
607
|
const firstField = /* @__PURE__ */ new Map();
|
|
477
|
-
|
|
608
|
+
const inheritedIdentifier = tree.inheritedIdentifier(cls);
|
|
609
|
+
let identifier = inheritedIdentifier?.field;
|
|
478
610
|
for (const field of cls.fields) {
|
|
479
611
|
const earlierField = firstField.get(field.name.text);
|
|
480
612
|
if (earlierField === void 0) firstField.set(field.name.text, field);
|
|
@@ -488,14 +620,23 @@ function resolve(doc) {
|
|
|
488
620
|
if (field.identifier) {
|
|
489
621
|
if (identifier === void 0) identifier = field;
|
|
490
622
|
else {
|
|
623
|
+
const from = identifier === inheritedIdentifier?.field ? ` inherits the identifier \`${identifier.name.text}\` from class \`${inheritedIdentifier.from.name.text}\`, line` : ` already has the identifier \`${identifier.name.text}\` on line`;
|
|
491
624
|
warn(
|
|
492
625
|
"W_MULTIPLE_IDENTIFIERS",
|
|
493
|
-
`\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}
|
|
626
|
+
`\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\`${from} ${identifier.line}`,
|
|
494
627
|
field.identifierAt ?? field.name
|
|
495
628
|
);
|
|
496
629
|
field.identifier = false;
|
|
497
630
|
}
|
|
498
631
|
}
|
|
632
|
+
const replaced = tree.inherited(cls, field.name.text);
|
|
633
|
+
if (replaced !== void 0 && sameField(field, replaced.field)) {
|
|
634
|
+
warn(
|
|
635
|
+
"W_REDUNDANT_OVERRIDE",
|
|
636
|
+
`field \`${field.name.text}\` is identical to the one inherited from class \`${replaced.from.name.text}\` on line ${replaced.field.line}`,
|
|
637
|
+
field.name
|
|
638
|
+
);
|
|
639
|
+
}
|
|
499
640
|
if (field.type?.kind === "unknown") {
|
|
500
641
|
const word = field.type.name.text;
|
|
501
642
|
const suggestion = closestTypeWord(word);
|
|
@@ -506,6 +647,18 @@ function resolve(doc) {
|
|
|
506
647
|
);
|
|
507
648
|
} else if (field.type?.kind === "class") {
|
|
508
649
|
reference(field.type.name);
|
|
650
|
+
} else if (field.type?.kind === "enum") {
|
|
651
|
+
const seen = /* @__PURE__ */ new Set();
|
|
652
|
+
for (const value of field.type.values) {
|
|
653
|
+
if (seen.has(value.text)) {
|
|
654
|
+
warn(
|
|
655
|
+
"W_DUPLICATE_ENUM_VALUE",
|
|
656
|
+
`value \`${value.text}\` is already in this enum; it is carried once`,
|
|
657
|
+
value
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
seen.add(value.text);
|
|
661
|
+
}
|
|
509
662
|
}
|
|
510
663
|
if (field.joinsTo !== void 0) {
|
|
511
664
|
const { className, fieldName } = field.joinsTo;
|
|
@@ -521,8 +674,8 @@ function resolve(doc) {
|
|
|
521
674
|
}
|
|
522
675
|
}
|
|
523
676
|
const present2 = new Set(out.diagnostics.map(key));
|
|
524
|
-
for (const
|
|
525
|
-
if (!present2.has(key(
|
|
677
|
+
for (const d of found) {
|
|
678
|
+
if (!present2.has(key(d))) out.diagnostics.push(d);
|
|
526
679
|
}
|
|
527
680
|
out.diagnostics.sort((a, b) => a.line - b.line);
|
|
528
681
|
out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
|
|
@@ -553,6 +706,7 @@ function copyField(f) {
|
|
|
553
706
|
}
|
|
554
707
|
function copyClass(c) {
|
|
555
708
|
const out = { ...c, name: copyName(c.name), fields: c.fields.map(copyField) };
|
|
709
|
+
if (c.parent !== void 0) out.parent = copyName(c.parent);
|
|
556
710
|
if (c.system !== void 0) out.system = copyName(c.system);
|
|
557
711
|
if (c.similarTo !== void 0) out.similarTo = copyName(c.similarTo);
|
|
558
712
|
return out;
|
|
@@ -619,7 +773,8 @@ function toLinkML(doc, opts) {
|
|
|
619
773
|
const classNames = new Set(classes.map((cls) => cls.node.name.text));
|
|
620
774
|
for (const name2 of resolved.undeclared) classNames.add(name2.text);
|
|
621
775
|
const enums = nameEnums(classes, classNames);
|
|
622
|
-
const prefixes =
|
|
776
|
+
const prefixes = nameKeyed();
|
|
777
|
+
prefixes.linkml = "https://w3id.org/linkml/";
|
|
623
778
|
if (!Object.hasOwn(prefixes, schemaName)) prefixes[schemaName] = `${BASE_URI}/${schemaName}/`;
|
|
624
779
|
const systemPrefix = /* @__PURE__ */ new Map();
|
|
625
780
|
const addSystem = (system) => {
|
|
@@ -650,12 +805,12 @@ function toLinkML(doc, opts) {
|
|
|
650
805
|
prefixes,
|
|
651
806
|
imports: ["linkml:types"]
|
|
652
807
|
};
|
|
653
|
-
const out =
|
|
808
|
+
const out = nameKeyed();
|
|
654
809
|
for (const cls of classes) {
|
|
655
810
|
out[cls.node.name.text] = buildClass(cls, mappingPrefix, enums);
|
|
656
811
|
}
|
|
657
812
|
for (const name2 of resolved.undeclared) {
|
|
658
|
-
if (!(name2.text
|
|
813
|
+
if (!Object.hasOwn(out, name2.text)) out[name2.text] = { annotations: { undeclared: true } };
|
|
659
814
|
}
|
|
660
815
|
if (Object.keys(out).length > 0) schema.classes = out;
|
|
661
816
|
const enumBlock = buildEnums(classes, enums);
|
|
@@ -713,13 +868,13 @@ function nameEnums(classes, classNames) {
|
|
|
713
868
|
return names;
|
|
714
869
|
}
|
|
715
870
|
function buildEnums(classes, enums) {
|
|
716
|
-
const out =
|
|
871
|
+
const out = nameKeyed();
|
|
717
872
|
for (const { node, fields } of classes) {
|
|
718
873
|
for (const field of fields) {
|
|
719
874
|
if (field.type?.kind !== "enum") continue;
|
|
720
875
|
const name2 = enums.get(useKey(node.name.text, field.name.text));
|
|
721
|
-
if (name2 === void 0 || name2
|
|
722
|
-
const values =
|
|
876
|
+
if (name2 === void 0 || Object.hasOwn(out, name2)) continue;
|
|
877
|
+
const values = nameKeyed();
|
|
723
878
|
for (const value of field.type.values) values[value.text] = null;
|
|
724
879
|
out[name2] = { permissible_values: values };
|
|
725
880
|
}
|
|
@@ -730,6 +885,7 @@ function buildClass(cls, mappingPrefix, enums) {
|
|
|
730
885
|
const { node, fields } = cls;
|
|
731
886
|
const out = {};
|
|
732
887
|
if (node.description !== void 0) out.description = node.description;
|
|
888
|
+
if (node.parent !== void 0) out.is_a = node.parent.text;
|
|
733
889
|
const annotations = {};
|
|
734
890
|
if (node.system !== void 0) annotations.system = node.system.text;
|
|
735
891
|
if (node.note !== void 0) annotations.note = node.note;
|
|
@@ -738,7 +894,7 @@ function buildClass(cls, mappingPrefix, enums) {
|
|
|
738
894
|
const target = node.similarTo.text;
|
|
739
895
|
out.close_mappings = [`${mappingPrefix(target)}:${target}`];
|
|
740
896
|
}
|
|
741
|
-
const attributes =
|
|
897
|
+
const attributes = nameKeyed();
|
|
742
898
|
for (const field of fields) {
|
|
743
899
|
attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
|
|
744
900
|
}
|
|
@@ -776,10 +932,12 @@ function rangeOf(field, className, enums) {
|
|
|
776
932
|
}
|
|
777
933
|
}
|
|
778
934
|
var RESERVED_PREFIXES = /* @__PURE__ */ new Set(["linkml", "xsd", "shex", "schema"]);
|
|
935
|
+
var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
936
|
+
var nameKeyed = () => /* @__PURE__ */ Object.create(null);
|
|
779
937
|
var linkmlName = (text) => {
|
|
780
938
|
const name2 = text.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
781
939
|
if (!/[a-z0-9]/.test(name2)) return "sketch";
|
|
782
|
-
if (RESERVED_PREFIXES.has(name2)) return `${name2}_`;
|
|
940
|
+
if (RESERVED_PREFIXES.has(name2) || PROTOTYPE_KEYS.has(name2)) return `${name2}_`;
|
|
783
941
|
return /^[0-9]/.test(name2) ? `_${name2}` : name2;
|
|
784
942
|
};
|
|
785
943
|
var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
@@ -810,6 +968,7 @@ function toMermaid(doc, opts) {
|
|
|
810
968
|
}
|
|
811
969
|
for (const cls of resolved.classes) {
|
|
812
970
|
const from = cls.name.text;
|
|
971
|
+
if (cls.parent !== void 0) lines.push(` ${cls.parent.text} <|-- ${from}`);
|
|
813
972
|
if (cls.similarTo !== void 0) lines.push(` ${from} ..> ${cls.similarTo.text} : similar`);
|
|
814
973
|
for (const field of cls.fields) {
|
|
815
974
|
const type = field.type;
|
|
@@ -916,7 +1075,7 @@ function toSkiss(doc) {
|
|
|
916
1075
|
`;
|
|
917
1076
|
}
|
|
918
1077
|
function classBlock(cls) {
|
|
919
|
-
const head = cls.name.text + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
|
|
1078
|
+
const head = cls.name.text + (cls.parent === void 0 ? "" : ` < ${cls.parent.text}`) + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
|
|
920
1079
|
return [withTrailer(head, cls), ...cls.fields.map(fieldLine)].join("\n");
|
|
921
1080
|
}
|
|
922
1081
|
function fieldLine(field) {
|
|
@@ -942,10 +1101,15 @@ function typeText2(type) {
|
|
|
942
1101
|
}
|
|
943
1102
|
return type.many ? `${text}[]` : text;
|
|
944
1103
|
}
|
|
1104
|
+
function writeDescription(text) {
|
|
1105
|
+
const attached = text.replace(/[ \t]+\?(?=[ \t]|$)/g, "?");
|
|
1106
|
+
return /^\?([ \t]|$)/.test(attached) ? `#${attached}` : `# ${attached}`;
|
|
1107
|
+
}
|
|
1108
|
+
var rewordsDescription = (text) => writeDescription(text) !== `# ${text}`;
|
|
945
1109
|
function withTrailer(head, node) {
|
|
946
1110
|
const parts2 = [];
|
|
947
1111
|
if (node.description !== void 0 && node.description !== "")
|
|
948
|
-
parts2.push(
|
|
1112
|
+
parts2.push(writeDescription(node.description));
|
|
949
1113
|
if (node.note !== void 0 && node.note !== "") parts2.push(`? ${node.note}`);
|
|
950
1114
|
if (parts2.length === 0) return head;
|
|
951
1115
|
const gap = head.length < TRAILER_COLUMN - 1 ? " ".repeat(TRAILER_COLUMN - head.length) : " ";
|
|
@@ -965,7 +1129,14 @@ var PRIMITIVE_RANGE = {
|
|
|
965
1129
|
uri: "uri",
|
|
966
1130
|
uriorcurie: "uri"
|
|
967
1131
|
};
|
|
968
|
-
var CLASS_KEYS = /* @__PURE__ */ new Set([
|
|
1132
|
+
var CLASS_KEYS = /* @__PURE__ */ new Set([
|
|
1133
|
+
"description",
|
|
1134
|
+
"is_a",
|
|
1135
|
+
"annotations",
|
|
1136
|
+
"close_mappings",
|
|
1137
|
+
"attributes",
|
|
1138
|
+
"slots"
|
|
1139
|
+
]);
|
|
969
1140
|
var SLOT_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "range", "multivalued", "identifier"]);
|
|
970
1141
|
var CLASS_TAGS = /* @__PURE__ */ new Set(["system", "note", "undeclared"]);
|
|
971
1142
|
var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
|
|
@@ -983,6 +1154,7 @@ var QUIET_SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
|
983
1154
|
"slots",
|
|
984
1155
|
"enums"
|
|
985
1156
|
]);
|
|
1157
|
+
var REWORDED = "a standalone `?` is attached to the word before it";
|
|
986
1158
|
var UNKNOWN_TYPE = /^[a-z][a-z0-9_-]*$/;
|
|
987
1159
|
function fromLinkML(schema) {
|
|
988
1160
|
const root = asRecord(schema);
|
|
@@ -1007,7 +1179,7 @@ function fromLinkML(schema) {
|
|
|
1007
1179
|
for (const key2 of Object.keys(classDefs)) {
|
|
1008
1180
|
const names = /* @__PURE__ */ new Map();
|
|
1009
1181
|
const taken = /* @__PURE__ */ new Set();
|
|
1010
|
-
for (const slot of
|
|
1182
|
+
for (const slot of fieldKeysOf(key2)) {
|
|
1011
1183
|
names.set(slot, unique(toFieldName(slot), taken));
|
|
1012
1184
|
}
|
|
1013
1185
|
fieldNames.set(key2, names);
|
|
@@ -1026,7 +1198,7 @@ function fromLinkML(schema) {
|
|
|
1026
1198
|
const rawValues = get(asRecord(get(enumDefs, key2)) ?? {}, "permissible_values");
|
|
1027
1199
|
const values = asRecord(rawValues);
|
|
1028
1200
|
const keys = Object.keys(values ?? {});
|
|
1029
|
-
const bad = keys.find((value) => !
|
|
1201
|
+
const bad = keys.find((value) => !ENUM_VALUE.test(value));
|
|
1030
1202
|
if (values === void 0 && present(rawValues)) {
|
|
1031
1203
|
unwritable.set(key2, `${shown(rawValues)} is not a map of permissible values`);
|
|
1032
1204
|
} else if (keys.length < 2) {
|
|
@@ -1057,14 +1229,32 @@ function fromLinkML(schema) {
|
|
|
1057
1229
|
}
|
|
1058
1230
|
continue;
|
|
1059
1231
|
}
|
|
1232
|
+
const undeclared = annotations.get("undeclared");
|
|
1233
|
+
if (undeclared !== void 0 && typeof undeclared !== "boolean") {
|
|
1234
|
+
report("annotation", name2, notText("undeclared", undeclared));
|
|
1235
|
+
}
|
|
1060
1236
|
const node = { name: at0(name2), fields: [], line: 0 };
|
|
1061
1237
|
const description = oneLine(stringOf(get(def, "description")));
|
|
1062
|
-
if (description !== void 0)
|
|
1238
|
+
if (description !== void 0) {
|
|
1239
|
+
node.description = description;
|
|
1240
|
+
if (rewordsDescription(description)) report("reworded", name2, REWORDED);
|
|
1241
|
+
}
|
|
1063
1242
|
const system = annotations.get("system");
|
|
1064
|
-
if (typeof system === "string"
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
if (
|
|
1243
|
+
if (typeof system === "string") {
|
|
1244
|
+
if (SYSTEM_NAME.test(system)) node.system = at0(system);
|
|
1245
|
+
else report("annotation", name2, "`system` is not a system name");
|
|
1246
|
+
} else if (system !== void 0) report("annotation", name2, notText("system", system));
|
|
1247
|
+
const rawNote = annotations.get("note");
|
|
1248
|
+
if (typeof rawNote === "string") {
|
|
1249
|
+
const note = oneLine(rawNote);
|
|
1250
|
+
if (note !== void 0) node.note = note;
|
|
1251
|
+
} else if (rawNote !== void 0) report("annotation", name2, notText("note", rawNote));
|
|
1252
|
+
const parent = stringOf(get(def, "is_a"));
|
|
1253
|
+
if (parent !== void 0) {
|
|
1254
|
+
const parentName = Object.hasOwn(classDefs, parent) ? classNameOf(parent) : toClassName(parent);
|
|
1255
|
+
node.parent = at0(parentName);
|
|
1256
|
+
reference(name2, parent, parentName);
|
|
1257
|
+
}
|
|
1068
1258
|
const mappings = stringList(get(def, "close_mappings"));
|
|
1069
1259
|
const first = mappings[0];
|
|
1070
1260
|
const target = first === void 0 ? "" : localName(first);
|
|
@@ -1087,6 +1277,9 @@ function fromLinkML(schema) {
|
|
|
1087
1277
|
}
|
|
1088
1278
|
}
|
|
1089
1279
|
const block = get(def, other);
|
|
1280
|
+
if (other === "is_a" && present(block) && stringOf(block) === void 0) {
|
|
1281
|
+
report("is_a", name2, `${shown(block)} is not a class name`);
|
|
1282
|
+
}
|
|
1090
1283
|
if (other === "attributes" && present(block) && asRecord(block) === void 0) {
|
|
1091
1284
|
report("attributes", name2, `${shown(block)} is not a map`);
|
|
1092
1285
|
}
|
|
@@ -1101,10 +1294,12 @@ function fromLinkML(schema) {
|
|
|
1101
1294
|
report("slot_usage", name2);
|
|
1102
1295
|
continue;
|
|
1103
1296
|
}
|
|
1297
|
+
const written = slotKeysOf(def);
|
|
1298
|
+
const inherited = inheritedSlots(key2);
|
|
1104
1299
|
for (const slot of Object.keys(usage)) {
|
|
1105
|
-
if (
|
|
1300
|
+
if (written.includes(slot)) {
|
|
1106
1301
|
report("slot_usage", `${name2}.${fieldNameOf(key2, slot)}`);
|
|
1107
|
-
} else {
|
|
1302
|
+
} else if (!inherited.has(slot)) {
|
|
1108
1303
|
report("slot_usage", name2, `\`${slot}\` is inherited and not in the sketch`);
|
|
1109
1304
|
}
|
|
1110
1305
|
}
|
|
@@ -1112,9 +1307,10 @@ function fromLinkML(schema) {
|
|
|
1112
1307
|
}
|
|
1113
1308
|
report(other, name2);
|
|
1114
1309
|
}
|
|
1115
|
-
|
|
1310
|
+
const own = slotKeysOf(def);
|
|
1311
|
+
for (const slot of fieldKeysOf(key2)) {
|
|
1116
1312
|
const attributes = asRecord(get(def, "attributes")) ?? {};
|
|
1117
|
-
const rawSlot = Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
|
|
1313
|
+
const rawSlot = !own.includes(slot) ? get(asRecord(get(def, "slot_usage")) ?? {}, slot) : Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
|
|
1118
1314
|
const slotDef = asRecord(rawSlot);
|
|
1119
1315
|
if (slotDef === void 0 && present(rawSlot)) {
|
|
1120
1316
|
report("slot", `${name2}.${fieldNameOf(key2, slot)}`, "is not a slot definition");
|
|
@@ -1169,13 +1365,18 @@ function fromLinkML(schema) {
|
|
|
1169
1365
|
line: 0
|
|
1170
1366
|
};
|
|
1171
1367
|
const description = oneLine(stringOf(get(def, "description")));
|
|
1172
|
-
if (description !== void 0)
|
|
1368
|
+
if (description !== void 0) {
|
|
1369
|
+
field.description = description;
|
|
1370
|
+
if (rewordsDescription(description)) report("reworded", element, REWORDED);
|
|
1371
|
+
}
|
|
1173
1372
|
const type = typeFor(element, name2, def);
|
|
1174
1373
|
if (type !== void 0) field.type = type;
|
|
1175
1374
|
const annotations = annotationsOf(def);
|
|
1176
1375
|
const system = annotations.get("system");
|
|
1177
|
-
if (typeof system === "string"
|
|
1178
|
-
|
|
1376
|
+
if (typeof system === "string") {
|
|
1377
|
+
if (SYSTEM_NAME.test(system)) field.system = at0(system);
|
|
1378
|
+
else report("annotation", element, "`system` is not a system name");
|
|
1379
|
+
} else if (system !== void 0) report("annotation", element, notText("system", system));
|
|
1179
1380
|
const joins = annotations.get("joins_to");
|
|
1180
1381
|
if (typeof joins === "string") {
|
|
1181
1382
|
const dot = joins.indexOf(".");
|
|
@@ -1188,9 +1389,12 @@ function fromLinkML(schema) {
|
|
|
1188
1389
|
reference(element, target, targetClass);
|
|
1189
1390
|
reference(element, targetField, targetName);
|
|
1190
1391
|
} else report("annotation", element, "`joins_to` is not `Class.field`");
|
|
1191
|
-
} else if (joins !== void 0) report("annotation", element, "
|
|
1192
|
-
const
|
|
1193
|
-
if (
|
|
1392
|
+
} else if (joins !== void 0) report("annotation", element, notText("joins_to", joins));
|
|
1393
|
+
const rawNote = annotations.get("note");
|
|
1394
|
+
if (typeof rawNote === "string") {
|
|
1395
|
+
const note = oneLine(rawNote);
|
|
1396
|
+
if (note !== void 0) field.note = note;
|
|
1397
|
+
} else if (rawNote !== void 0) report("annotation", element, notText("note", rawNote));
|
|
1194
1398
|
for (const other of Object.keys(def)) {
|
|
1195
1399
|
if (!SLOT_KEYS.has(other)) {
|
|
1196
1400
|
report(other, element);
|
|
@@ -1243,6 +1447,34 @@ function fromLinkML(schema) {
|
|
|
1243
1447
|
report("narrowed", element, `\`${range}\` is not a Skiss type; it falls back to string`);
|
|
1244
1448
|
return { kind: "unknown", name: at0(word), many };
|
|
1245
1449
|
}
|
|
1450
|
+
function parentKeyOf(classKey) {
|
|
1451
|
+
const def = asRecord(get(classDefs, classKey)) ?? {};
|
|
1452
|
+
const parent = stringOf(get(def, "is_a"));
|
|
1453
|
+
if (parent === void 0 || !Object.hasOwn(classDefs, parent)) return void 0;
|
|
1454
|
+
return parent;
|
|
1455
|
+
}
|
|
1456
|
+
function inheritedSlots(classKey) {
|
|
1457
|
+
const out = /* @__PURE__ */ new Set();
|
|
1458
|
+
const seen = /* @__PURE__ */ new Set([classKey]);
|
|
1459
|
+
let key2 = parentKeyOf(classKey);
|
|
1460
|
+
while (key2 !== void 0 && !seen.has(key2)) {
|
|
1461
|
+
seen.add(key2);
|
|
1462
|
+
for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) out.add(slot);
|
|
1463
|
+
key2 = parentKeyOf(key2);
|
|
1464
|
+
}
|
|
1465
|
+
return out;
|
|
1466
|
+
}
|
|
1467
|
+
function fieldKeysOf(classKey) {
|
|
1468
|
+
const def = asRecord(get(classDefs, classKey)) ?? {};
|
|
1469
|
+
const keys = slotKeysOf(def);
|
|
1470
|
+
const usage = asRecord(get(def, "slot_usage"));
|
|
1471
|
+
if (usage === void 0) return keys;
|
|
1472
|
+
const inherited = inheritedSlots(classKey);
|
|
1473
|
+
for (const slot of Object.keys(usage)) {
|
|
1474
|
+
if (inherited.has(slot) && !keys.includes(slot)) keys.push(slot);
|
|
1475
|
+
}
|
|
1476
|
+
return keys;
|
|
1477
|
+
}
|
|
1246
1478
|
function listedSomewhere(slot) {
|
|
1247
1479
|
for (const key2 of Object.keys(classDefs)) {
|
|
1248
1480
|
const def = asRecord(get(classDefs, key2)) ?? {};
|
|
@@ -1265,13 +1497,17 @@ var COUNTED = {
|
|
|
1265
1497
|
class: ["class", "classes"],
|
|
1266
1498
|
slot: ["slot", "slots"],
|
|
1267
1499
|
schema: ["schema key", "schema keys"],
|
|
1500
|
+
narrowed: ["narrowed range", "narrowed ranges"],
|
|
1501
|
+
enum_detail: ["enum detail", "enum details"],
|
|
1502
|
+
annotation: ["unreadable annotation", "unreadable annotations"],
|
|
1503
|
+
reworded: ["description reworded", "descriptions reworded"],
|
|
1268
1504
|
// `mixin: true` marks the class; `mixins: [M]` points at another one.
|
|
1269
1505
|
mixin: ["mixin class", "mixin classes"],
|
|
1270
1506
|
mixins: ["mixin", "mixins"],
|
|
1271
1507
|
pattern: ["pattern", "patterns"],
|
|
1272
1508
|
unit: ["unit", "units"]
|
|
1273
1509
|
};
|
|
1274
|
-
var
|
|
1510
|
+
var UNREADABLE = "unreadable";
|
|
1275
1511
|
function formatDropped(dropped) {
|
|
1276
1512
|
const groups = /* @__PURE__ */ new Map();
|
|
1277
1513
|
for (const entry of dropped) {
|
|
@@ -1280,15 +1516,21 @@ function formatDropped(dropped) {
|
|
|
1280
1516
|
else group.push(entry);
|
|
1281
1517
|
}
|
|
1282
1518
|
const drops = [];
|
|
1519
|
+
const notRead = [];
|
|
1283
1520
|
let renamed = "";
|
|
1284
1521
|
let inlined = "";
|
|
1285
1522
|
for (const [kind, entries] of groups) {
|
|
1523
|
+
if (kind === UNREADABLE) {
|
|
1524
|
+
for (const entry of entries) notRead.push(entry.detail ?? "the schema could not be read");
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1286
1527
|
const phrase = phraseFor(kind, entries);
|
|
1287
1528
|
if (kind === "renamed") renamed = phrase;
|
|
1288
1529
|
else if (kind === "inlined") inlined = phrase;
|
|
1289
1530
|
else drops.push(phrase);
|
|
1290
1531
|
}
|
|
1291
1532
|
const sentences = [];
|
|
1533
|
+
if (notRead.length > 0) sentences.push(`Not read: ${notRead.join("; ")}.`);
|
|
1292
1534
|
if (drops.length > 0) sentences.push(`Dropped: ${drops.join(", ")}.`);
|
|
1293
1535
|
if (renamed !== "") sentences.push(`Renamed: ${renamed}.`);
|
|
1294
1536
|
if (inlined !== "") sentences.push(`Inlined: ${inlined}.`);
|
|
@@ -1298,11 +1540,10 @@ function phraseFor(kind, entries) {
|
|
|
1298
1540
|
const n = entries.length;
|
|
1299
1541
|
const counted = Object.hasOwn(COUNTED, kind) ? COUNTED[kind] : void 0;
|
|
1300
1542
|
if (counted !== void 0) return `${n} ${n === 1 ? counted[0] : counted[1]}`;
|
|
1301
|
-
return `${kind} on ${n} ${nounFor(
|
|
1543
|
+
return `${kind} on ${n} ${nounFor(entries)}`;
|
|
1302
1544
|
}
|
|
1303
|
-
function nounFor(
|
|
1545
|
+
function nounFor(entries) {
|
|
1304
1546
|
const one = entries.length === 1;
|
|
1305
|
-
if (ENUM_KINDS.has(kind)) return one ? "enum" : "enums";
|
|
1306
1547
|
const slots = entries.filter((entry) => entry.element.includes(".")).length;
|
|
1307
1548
|
if (slots === entries.length) return one ? "slot" : "slots";
|
|
1308
1549
|
if (slots === 0) return one ? "class" : "classes";
|
|
@@ -1353,12 +1594,18 @@ function annotationsOf(def) {
|
|
|
1353
1594
|
if (annotations === void 0) return out;
|
|
1354
1595
|
for (const tag of Object.keys(annotations)) {
|
|
1355
1596
|
const raw = get(annotations, tag);
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1597
|
+
const long = asRecord(raw);
|
|
1598
|
+
const value = long === void 0 ? raw : get(long, "value");
|
|
1599
|
+
out.set(tag, value ?? null);
|
|
1359
1600
|
}
|
|
1360
1601
|
return out;
|
|
1361
1602
|
}
|
|
1603
|
+
function notText(tag, value) {
|
|
1604
|
+
if (value === null) return `\`${tag}\` was written with no value`;
|
|
1605
|
+
if (Array.isArray(value)) return `\`${tag}\` is a list, not text`;
|
|
1606
|
+
if (typeof value === "object") return `\`${tag}\` is a map, not text`;
|
|
1607
|
+
return `\`${tag}\` is a ${typeof value}, not text`;
|
|
1608
|
+
}
|
|
1362
1609
|
function slotKeysOf(def) {
|
|
1363
1610
|
const keys = Object.keys(asRecord(get(def, "attributes")) ?? {});
|
|
1364
1611
|
for (const slot of stringList(get(def, "slots"))) {
|
|
@@ -1383,7 +1630,7 @@ var schemaElement = (root) => stringOf(get(root, "name")) ?? "schema";
|
|
|
1383
1630
|
var nothing = (element, detail) => ({
|
|
1384
1631
|
document: parse(""),
|
|
1385
1632
|
source: "",
|
|
1386
|
-
dropped: [{ kind:
|
|
1633
|
+
dropped: [{ kind: UNREADABLE, element, detail }]
|
|
1387
1634
|
});
|
|
1388
1635
|
|
|
1389
1636
|
// src/import.ts
|
|
@@ -1410,7 +1657,7 @@ function notYAML(error) {
|
|
|
1410
1657
|
}
|
|
1411
1658
|
|
|
1412
1659
|
// src/index.ts
|
|
1413
|
-
var VERSION = "0.
|
|
1660
|
+
var VERSION = "0.5.0";
|
|
1414
1661
|
export {
|
|
1415
1662
|
VERSION,
|
|
1416
1663
|
compile,
|