@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/cli.js
CHANGED
|
@@ -1,16 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import { constants } from "fs";
|
|
6
|
+
import { access, mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
7
|
+
import { tmpdir } from "os";
|
|
8
|
+
import { basename, delimiter, extname, join } from "path";
|
|
6
9
|
import { parseArgs } from "util";
|
|
7
10
|
|
|
11
|
+
// src/inheritance.ts
|
|
12
|
+
function inheritance(classes) {
|
|
13
|
+
const declared = /* @__PURE__ */ new Map();
|
|
14
|
+
for (const cls of classes) {
|
|
15
|
+
if (!declared.has(cls.name.text)) declared.set(cls.name.text, cls);
|
|
16
|
+
}
|
|
17
|
+
const circles = /* @__PURE__ */ new Map();
|
|
18
|
+
const parentOf = (cls) => {
|
|
19
|
+
if (cls.parent === void 0 || circles.has(cls)) return void 0;
|
|
20
|
+
return declared.get(cls.parent.text);
|
|
21
|
+
};
|
|
22
|
+
for (const cls of declared.values()) {
|
|
23
|
+
const path = [cls];
|
|
24
|
+
const seen = new Set(path);
|
|
25
|
+
for (let next = parentOf(cls); next !== void 0; next = parentOf(next)) {
|
|
26
|
+
if (seen.has(next)) {
|
|
27
|
+
const circle = path.slice(path.indexOf(next));
|
|
28
|
+
let closer = circle[0];
|
|
29
|
+
for (const member2 of circle) {
|
|
30
|
+
if (member2.line > closer.line) closer = member2;
|
|
31
|
+
}
|
|
32
|
+
const at = circle.indexOf(closer);
|
|
33
|
+
circles.set(closer, [...circle.slice(at), ...circle.slice(0, at)]);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
path.push(next);
|
|
37
|
+
seen.add(next);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const ancestors = (cls) => {
|
|
41
|
+
const out = [];
|
|
42
|
+
const seen = /* @__PURE__ */ new Set([cls]);
|
|
43
|
+
for (let next = parentOf(cls); next !== void 0; next = parentOf(next)) {
|
|
44
|
+
if (seen.has(next)) break;
|
|
45
|
+
out.push(next);
|
|
46
|
+
seen.add(next);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
50
|
+
const find = (cls, matches) => {
|
|
51
|
+
for (const from of ancestors(cls)) {
|
|
52
|
+
const field = from.fields.find(matches);
|
|
53
|
+
if (field !== void 0) return { field, from };
|
|
54
|
+
}
|
|
55
|
+
return void 0;
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
ancestors,
|
|
59
|
+
inherited: (cls, fieldName) => find(cls, (field) => field.name.text === fieldName),
|
|
60
|
+
inheritedIdentifier: (cls) => find(cls, (field) => field.identifier),
|
|
61
|
+
circle: (cls) => circles.get(cls)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function sameField(child, inheritedField) {
|
|
65
|
+
return meaning(child) === meaning(inheritedField);
|
|
66
|
+
}
|
|
67
|
+
var meaning = (field) => JSON.stringify([
|
|
68
|
+
field.name.text,
|
|
69
|
+
field.identifier,
|
|
70
|
+
typeMeaning(field),
|
|
71
|
+
field.system?.text,
|
|
72
|
+
field.joinsTo === void 0 ? void 0 : [field.joinsTo.className.text, field.joinsTo.fieldName.text],
|
|
73
|
+
field.description,
|
|
74
|
+
field.note
|
|
75
|
+
]);
|
|
76
|
+
function typeMeaning(field) {
|
|
77
|
+
const type = field.type;
|
|
78
|
+
if (type === void 0) return void 0;
|
|
79
|
+
switch (type.kind) {
|
|
80
|
+
case "primitive":
|
|
81
|
+
return [type.kind, type.name, type.many];
|
|
82
|
+
case "class":
|
|
83
|
+
case "unknown":
|
|
84
|
+
return [type.kind, type.name.text, type.many];
|
|
85
|
+
case "enum":
|
|
86
|
+
return [type.kind, type.values.map((v) => v.text), type.many];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
8
90
|
// src/parse.ts
|
|
9
91
|
var isWs = (c) => c === " " || c === " ";
|
|
10
92
|
var trimWs = (s) => s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
|
|
11
93
|
var CLASS_NAME = /^[A-Z][A-Za-z0-9]*$/;
|
|
12
94
|
var FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
|
|
13
|
-
var
|
|
95
|
+
var SYSTEM_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
96
|
+
var ENUM_VALUE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
14
97
|
var PRIMITIVES = {
|
|
15
98
|
string: "string",
|
|
16
99
|
int: "int",
|
|
@@ -27,7 +110,7 @@ function primitiveFor(word) {
|
|
|
27
110
|
return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
|
|
28
111
|
}
|
|
29
112
|
var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
|
|
30
|
-
var CLASS_ORDER = "name, `@System`, `~ Class`, then `#` and `?`";
|
|
113
|
+
var CLASS_ORDER = "name, `< Parent`, `@System`, `~ Class`, then `#` and `?`";
|
|
31
114
|
function parse(source) {
|
|
32
115
|
const classes = [];
|
|
33
116
|
const diagnostics = [];
|
|
@@ -114,7 +197,7 @@ function splitTrailer(text) {
|
|
|
114
197
|
return trailer;
|
|
115
198
|
}
|
|
116
199
|
var WORD = /[\p{L}\p{N}_-]+/uy;
|
|
117
|
-
var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "["];
|
|
200
|
+
var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "<", ",", "["];
|
|
118
201
|
function tokenize(head) {
|
|
119
202
|
const tokens = [];
|
|
120
203
|
let i = 0;
|
|
@@ -176,6 +259,13 @@ function parseClassLine(text, line) {
|
|
|
176
259
|
);
|
|
177
260
|
}
|
|
178
261
|
const node = { name: name(head, line), line };
|
|
262
|
+
const lt = peek(c);
|
|
263
|
+
if (lt?.kind === "<") {
|
|
264
|
+
take(c);
|
|
265
|
+
const parent = parseParent(c, lt);
|
|
266
|
+
if (!parent.ok) return parent;
|
|
267
|
+
node.parent = parent.value;
|
|
268
|
+
}
|
|
179
269
|
const at = peek(c);
|
|
180
270
|
if (at?.kind === "@") {
|
|
181
271
|
take(c);
|
|
@@ -305,6 +395,31 @@ function parseFieldLine(text, line) {
|
|
|
305
395
|
if (trailer.note !== void 0) node.note = trailer.note;
|
|
306
396
|
return { ok: true, value: node };
|
|
307
397
|
}
|
|
398
|
+
function parseParent(c, lt) {
|
|
399
|
+
const parent = take(c);
|
|
400
|
+
if (parent === void 0 || parent.kind !== "word") {
|
|
401
|
+
return fail(
|
|
402
|
+
"E_UNPARSABLE",
|
|
403
|
+
"Expected a class name after `<`, e.g. `Jedi < Character`",
|
|
404
|
+
c.line,
|
|
405
|
+
parent ?? afterSpan(lt)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (!CLASS_NAME.test(parent.text)) {
|
|
409
|
+
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}\``;
|
|
410
|
+
return fail("E_BAD_NAME", message, c.line, parent);
|
|
411
|
+
}
|
|
412
|
+
const next = peek(c);
|
|
413
|
+
if (next?.kind === "," || next?.kind === "<") {
|
|
414
|
+
return fail(
|
|
415
|
+
"E_UNPARSABLE",
|
|
416
|
+
"A class has one parent; `<` takes a single class name",
|
|
417
|
+
c.line,
|
|
418
|
+
next
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return { ok: true, value: name(parent, c.line) };
|
|
422
|
+
}
|
|
308
423
|
function parseSystem(c, at) {
|
|
309
424
|
const system = take(c);
|
|
310
425
|
if (system === void 0 || system.kind !== "word") {
|
|
@@ -315,7 +430,7 @@ function parseSystem(c, at) {
|
|
|
315
430
|
system ?? afterSpan(at)
|
|
316
431
|
);
|
|
317
432
|
}
|
|
318
|
-
if (!
|
|
433
|
+
if (!SYSTEM_NAME.test(system.text)) {
|
|
319
434
|
return fail(
|
|
320
435
|
"E_UNPARSABLE",
|
|
321
436
|
`A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
|
|
@@ -373,10 +488,10 @@ function parseType(c, colon) {
|
|
|
373
488
|
}
|
|
374
489
|
if (words.length > 1) {
|
|
375
490
|
for (const w of words) {
|
|
376
|
-
if (!
|
|
491
|
+
if (!ENUM_VALUE.test(w.text)) {
|
|
377
492
|
return fail(
|
|
378
493
|
"E_UNPARSABLE",
|
|
379
|
-
`An enum value is letters, digits, \`-\` and \`_\`,
|
|
494
|
+
`An enum value is letters, digits, \`-\` and \`_\`, and does not start with \`-\` or \`_\`: \`${w.text}\``,
|
|
380
495
|
c.line,
|
|
381
496
|
w
|
|
382
497
|
);
|
|
@@ -414,7 +529,7 @@ function parseType(c, colon) {
|
|
|
414
529
|
);
|
|
415
530
|
}
|
|
416
531
|
function unexpected(token, line, kind, order) {
|
|
417
|
-
const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["@", "~"];
|
|
532
|
+
const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["<", "@", "~"];
|
|
418
533
|
if (modifiers.includes(token.kind)) {
|
|
419
534
|
return fail(
|
|
420
535
|
"E_UNPARSABLE",
|
|
@@ -423,10 +538,10 @@ function unexpected(token, line, kind, order) {
|
|
|
423
538
|
token
|
|
424
539
|
);
|
|
425
540
|
}
|
|
426
|
-
if (token.
|
|
541
|
+
if (token.kind === "<") {
|
|
427
542
|
return fail(
|
|
428
543
|
"E_UNPARSABLE",
|
|
429
|
-
"`<`
|
|
544
|
+
"`<` marks inheritance and belongs on the class line: `Child < Parent`",
|
|
430
545
|
line,
|
|
431
546
|
token
|
|
432
547
|
);
|
|
@@ -455,10 +570,14 @@ function resolve(doc) {
|
|
|
455
570
|
};
|
|
456
571
|
const classes = out.classes;
|
|
457
572
|
const declared = new Set(classes.map((c) => c.name.text));
|
|
458
|
-
const
|
|
573
|
+
const found = [];
|
|
459
574
|
const undeclared = /* @__PURE__ */ new Map();
|
|
575
|
+
const tree = inheritance(classes);
|
|
460
576
|
const warn = (code, message, at) => {
|
|
461
|
-
|
|
577
|
+
found.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
|
|
578
|
+
};
|
|
579
|
+
const error = (code, message, at) => {
|
|
580
|
+
found.push({ severity: "error", code, message, line: at.line, col: at.col, end: at.end });
|
|
462
581
|
};
|
|
463
582
|
const reference = (name2) => {
|
|
464
583
|
if (declared.has(name2.text)) return true;
|
|
@@ -479,9 +598,25 @@ function resolve(doc) {
|
|
|
479
598
|
cls.name
|
|
480
599
|
);
|
|
481
600
|
}
|
|
601
|
+
const parent = cls.parent;
|
|
602
|
+
if (parent !== void 0) {
|
|
603
|
+
reference(parent);
|
|
604
|
+
const circle = tree.circle(cls);
|
|
605
|
+
if (circle !== void 0) {
|
|
606
|
+
const through = circle.slice(1).map((member2) => `\`${member2.name.text}\``);
|
|
607
|
+
const path = through.length === 0 ? "" : ` through ${through.join(", ")}`;
|
|
608
|
+
error(
|
|
609
|
+
"E_INHERITANCE_CYCLE",
|
|
610
|
+
`class \`${cls.name.text}\` inherits from itself${path}; this \`<\` is not carried`,
|
|
611
|
+
parent
|
|
612
|
+
);
|
|
613
|
+
cls.parent = void 0;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
482
616
|
if (cls.similarTo !== void 0) reference(cls.similarTo);
|
|
483
617
|
const firstField = /* @__PURE__ */ new Map();
|
|
484
|
-
|
|
618
|
+
const inheritedIdentifier = tree.inheritedIdentifier(cls);
|
|
619
|
+
let identifier = inheritedIdentifier?.field;
|
|
485
620
|
for (const field of cls.fields) {
|
|
486
621
|
const earlierField = firstField.get(field.name.text);
|
|
487
622
|
if (earlierField === void 0) firstField.set(field.name.text, field);
|
|
@@ -495,14 +630,23 @@ function resolve(doc) {
|
|
|
495
630
|
if (field.identifier) {
|
|
496
631
|
if (identifier === void 0) identifier = field;
|
|
497
632
|
else {
|
|
633
|
+
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`;
|
|
498
634
|
warn(
|
|
499
635
|
"W_MULTIPLE_IDENTIFIERS",
|
|
500
|
-
`\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}
|
|
636
|
+
`\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\`${from} ${identifier.line}`,
|
|
501
637
|
field.identifierAt ?? field.name
|
|
502
638
|
);
|
|
503
639
|
field.identifier = false;
|
|
504
640
|
}
|
|
505
641
|
}
|
|
642
|
+
const replaced = tree.inherited(cls, field.name.text);
|
|
643
|
+
if (replaced !== void 0 && sameField(field, replaced.field)) {
|
|
644
|
+
warn(
|
|
645
|
+
"W_REDUNDANT_OVERRIDE",
|
|
646
|
+
`field \`${field.name.text}\` is identical to the one inherited from class \`${replaced.from.name.text}\` on line ${replaced.field.line}`,
|
|
647
|
+
field.name
|
|
648
|
+
);
|
|
649
|
+
}
|
|
506
650
|
if (field.type?.kind === "unknown") {
|
|
507
651
|
const word = field.type.name.text;
|
|
508
652
|
const suggestion = closestTypeWord(word);
|
|
@@ -513,6 +657,18 @@ function resolve(doc) {
|
|
|
513
657
|
);
|
|
514
658
|
} else if (field.type?.kind === "class") {
|
|
515
659
|
reference(field.type.name);
|
|
660
|
+
} else if (field.type?.kind === "enum") {
|
|
661
|
+
const seen = /* @__PURE__ */ new Set();
|
|
662
|
+
for (const value of field.type.values) {
|
|
663
|
+
if (seen.has(value.text)) {
|
|
664
|
+
warn(
|
|
665
|
+
"W_DUPLICATE_ENUM_VALUE",
|
|
666
|
+
`value \`${value.text}\` is already in this enum; it is carried once`,
|
|
667
|
+
value
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
seen.add(value.text);
|
|
671
|
+
}
|
|
516
672
|
}
|
|
517
673
|
if (field.joinsTo !== void 0) {
|
|
518
674
|
const { className, fieldName } = field.joinsTo;
|
|
@@ -528,8 +684,8 @@ function resolve(doc) {
|
|
|
528
684
|
}
|
|
529
685
|
}
|
|
530
686
|
const present2 = new Set(out.diagnostics.map(key));
|
|
531
|
-
for (const
|
|
532
|
-
if (!present2.has(key(
|
|
687
|
+
for (const d of found) {
|
|
688
|
+
if (!present2.has(key(d))) out.diagnostics.push(d);
|
|
533
689
|
}
|
|
534
690
|
out.diagnostics.sort((a, b) => a.line - b.line);
|
|
535
691
|
out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
|
|
@@ -560,6 +716,7 @@ function copyField(f) {
|
|
|
560
716
|
}
|
|
561
717
|
function copyClass(c) {
|
|
562
718
|
const out = { ...c, name: copyName(c.name), fields: c.fields.map(copyField) };
|
|
719
|
+
if (c.parent !== void 0) out.parent = copyName(c.parent);
|
|
563
720
|
if (c.system !== void 0) out.system = copyName(c.system);
|
|
564
721
|
if (c.similarTo !== void 0) out.similarTo = copyName(c.similarTo);
|
|
565
722
|
return out;
|
|
@@ -626,7 +783,8 @@ function toLinkML(doc, opts) {
|
|
|
626
783
|
const classNames = new Set(classes.map((cls) => cls.node.name.text));
|
|
627
784
|
for (const name2 of resolved.undeclared) classNames.add(name2.text);
|
|
628
785
|
const enums = nameEnums(classes, classNames);
|
|
629
|
-
const prefixes =
|
|
786
|
+
const prefixes = nameKeyed();
|
|
787
|
+
prefixes.linkml = "https://w3id.org/linkml/";
|
|
630
788
|
if (!Object.hasOwn(prefixes, schemaName)) prefixes[schemaName] = `${BASE_URI}/${schemaName}/`;
|
|
631
789
|
const systemPrefix = /* @__PURE__ */ new Map();
|
|
632
790
|
const addSystem = (system) => {
|
|
@@ -657,12 +815,12 @@ function toLinkML(doc, opts) {
|
|
|
657
815
|
prefixes,
|
|
658
816
|
imports: ["linkml:types"]
|
|
659
817
|
};
|
|
660
|
-
const out =
|
|
818
|
+
const out = nameKeyed();
|
|
661
819
|
for (const cls of classes) {
|
|
662
820
|
out[cls.node.name.text] = buildClass(cls, mappingPrefix, enums);
|
|
663
821
|
}
|
|
664
822
|
for (const name2 of resolved.undeclared) {
|
|
665
|
-
if (!(name2.text
|
|
823
|
+
if (!Object.hasOwn(out, name2.text)) out[name2.text] = { annotations: { undeclared: true } };
|
|
666
824
|
}
|
|
667
825
|
if (Object.keys(out).length > 0) schema.classes = out;
|
|
668
826
|
const enumBlock = buildEnums(classes, enums);
|
|
@@ -720,13 +878,13 @@ function nameEnums(classes, classNames) {
|
|
|
720
878
|
return names;
|
|
721
879
|
}
|
|
722
880
|
function buildEnums(classes, enums) {
|
|
723
|
-
const out =
|
|
881
|
+
const out = nameKeyed();
|
|
724
882
|
for (const { node, fields } of classes) {
|
|
725
883
|
for (const field of fields) {
|
|
726
884
|
if (field.type?.kind !== "enum") continue;
|
|
727
885
|
const name2 = enums.get(useKey(node.name.text, field.name.text));
|
|
728
|
-
if (name2 === void 0 || name2
|
|
729
|
-
const values =
|
|
886
|
+
if (name2 === void 0 || Object.hasOwn(out, name2)) continue;
|
|
887
|
+
const values = nameKeyed();
|
|
730
888
|
for (const value of field.type.values) values[value.text] = null;
|
|
731
889
|
out[name2] = { permissible_values: values };
|
|
732
890
|
}
|
|
@@ -737,6 +895,7 @@ function buildClass(cls, mappingPrefix, enums) {
|
|
|
737
895
|
const { node, fields } = cls;
|
|
738
896
|
const out = {};
|
|
739
897
|
if (node.description !== void 0) out.description = node.description;
|
|
898
|
+
if (node.parent !== void 0) out.is_a = node.parent.text;
|
|
740
899
|
const annotations = {};
|
|
741
900
|
if (node.system !== void 0) annotations.system = node.system.text;
|
|
742
901
|
if (node.note !== void 0) annotations.note = node.note;
|
|
@@ -745,7 +904,7 @@ function buildClass(cls, mappingPrefix, enums) {
|
|
|
745
904
|
const target = node.similarTo.text;
|
|
746
905
|
out.close_mappings = [`${mappingPrefix(target)}:${target}`];
|
|
747
906
|
}
|
|
748
|
-
const attributes =
|
|
907
|
+
const attributes = nameKeyed();
|
|
749
908
|
for (const field of fields) {
|
|
750
909
|
attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
|
|
751
910
|
}
|
|
@@ -783,10 +942,12 @@ function rangeOf(field, className, enums) {
|
|
|
783
942
|
}
|
|
784
943
|
}
|
|
785
944
|
var RESERVED_PREFIXES = /* @__PURE__ */ new Set(["linkml", "xsd", "shex", "schema"]);
|
|
945
|
+
var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
946
|
+
var nameKeyed = () => /* @__PURE__ */ Object.create(null);
|
|
786
947
|
var linkmlName = (text) => {
|
|
787
948
|
const name2 = text.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
788
949
|
if (!/[a-z0-9]/.test(name2)) return "sketch";
|
|
789
|
-
if (RESERVED_PREFIXES.has(name2)) return `${name2}_`;
|
|
950
|
+
if (RESERVED_PREFIXES.has(name2) || PROTOTYPE_KEYS.has(name2)) return `${name2}_`;
|
|
790
951
|
return /^[0-9]/.test(name2) ? `_${name2}` : name2;
|
|
791
952
|
};
|
|
792
953
|
var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
@@ -817,6 +978,7 @@ function toMermaid(doc, opts) {
|
|
|
817
978
|
}
|
|
818
979
|
for (const cls of resolved.classes) {
|
|
819
980
|
const from = cls.name.text;
|
|
981
|
+
if (cls.parent !== void 0) lines.push(` ${cls.parent.text} <|-- ${from}`);
|
|
820
982
|
if (cls.similarTo !== void 0) lines.push(` ${from} ..> ${cls.similarTo.text} : similar`);
|
|
821
983
|
for (const field of cls.fields) {
|
|
822
984
|
const type = field.type;
|
|
@@ -923,7 +1085,7 @@ function toSkiss(doc) {
|
|
|
923
1085
|
`;
|
|
924
1086
|
}
|
|
925
1087
|
function classBlock(cls) {
|
|
926
|
-
const head = cls.name.text + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
|
|
1088
|
+
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}`);
|
|
927
1089
|
return [withTrailer(head, cls), ...cls.fields.map(fieldLine)].join("\n");
|
|
928
1090
|
}
|
|
929
1091
|
function fieldLine(field) {
|
|
@@ -949,10 +1111,15 @@ function typeText2(type) {
|
|
|
949
1111
|
}
|
|
950
1112
|
return type.many ? `${text}[]` : text;
|
|
951
1113
|
}
|
|
1114
|
+
function writeDescription(text) {
|
|
1115
|
+
const attached = text.replace(/[ \t]+\?(?=[ \t]|$)/g, "?");
|
|
1116
|
+
return /^\?([ \t]|$)/.test(attached) ? `#${attached}` : `# ${attached}`;
|
|
1117
|
+
}
|
|
1118
|
+
var rewordsDescription = (text) => writeDescription(text) !== `# ${text}`;
|
|
952
1119
|
function withTrailer(head, node) {
|
|
953
1120
|
const parts2 = [];
|
|
954
1121
|
if (node.description !== void 0 && node.description !== "")
|
|
955
|
-
parts2.push(
|
|
1122
|
+
parts2.push(writeDescription(node.description));
|
|
956
1123
|
if (node.note !== void 0 && node.note !== "") parts2.push(`? ${node.note}`);
|
|
957
1124
|
if (parts2.length === 0) return head;
|
|
958
1125
|
const gap = head.length < TRAILER_COLUMN - 1 ? " ".repeat(TRAILER_COLUMN - head.length) : " ";
|
|
@@ -972,7 +1139,14 @@ var PRIMITIVE_RANGE = {
|
|
|
972
1139
|
uri: "uri",
|
|
973
1140
|
uriorcurie: "uri"
|
|
974
1141
|
};
|
|
975
|
-
var CLASS_KEYS = /* @__PURE__ */ new Set([
|
|
1142
|
+
var CLASS_KEYS = /* @__PURE__ */ new Set([
|
|
1143
|
+
"description",
|
|
1144
|
+
"is_a",
|
|
1145
|
+
"annotations",
|
|
1146
|
+
"close_mappings",
|
|
1147
|
+
"attributes",
|
|
1148
|
+
"slots"
|
|
1149
|
+
]);
|
|
976
1150
|
var SLOT_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "range", "multivalued", "identifier"]);
|
|
977
1151
|
var CLASS_TAGS = /* @__PURE__ */ new Set(["system", "note", "undeclared"]);
|
|
978
1152
|
var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
|
|
@@ -990,6 +1164,7 @@ var QUIET_SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
|
990
1164
|
"slots",
|
|
991
1165
|
"enums"
|
|
992
1166
|
]);
|
|
1167
|
+
var REWORDED = "a standalone `?` is attached to the word before it";
|
|
993
1168
|
var UNKNOWN_TYPE = /^[a-z][a-z0-9_-]*$/;
|
|
994
1169
|
function fromLinkML(schema) {
|
|
995
1170
|
const root = asRecord(schema);
|
|
@@ -1014,7 +1189,7 @@ function fromLinkML(schema) {
|
|
|
1014
1189
|
for (const key2 of Object.keys(classDefs)) {
|
|
1015
1190
|
const names = /* @__PURE__ */ new Map();
|
|
1016
1191
|
const taken = /* @__PURE__ */ new Set();
|
|
1017
|
-
for (const slot of
|
|
1192
|
+
for (const slot of fieldKeysOf(key2)) {
|
|
1018
1193
|
names.set(slot, unique(toFieldName(slot), taken));
|
|
1019
1194
|
}
|
|
1020
1195
|
fieldNames.set(key2, names);
|
|
@@ -1033,7 +1208,7 @@ function fromLinkML(schema) {
|
|
|
1033
1208
|
const rawValues = get(asRecord(get(enumDefs, key2)) ?? {}, "permissible_values");
|
|
1034
1209
|
const values = asRecord(rawValues);
|
|
1035
1210
|
const keys = Object.keys(values ?? {});
|
|
1036
|
-
const bad = keys.find((value) => !
|
|
1211
|
+
const bad = keys.find((value) => !ENUM_VALUE.test(value));
|
|
1037
1212
|
if (values === void 0 && present(rawValues)) {
|
|
1038
1213
|
unwritable.set(key2, `${shown(rawValues)} is not a map of permissible values`);
|
|
1039
1214
|
} else if (keys.length < 2) {
|
|
@@ -1064,14 +1239,32 @@ function fromLinkML(schema) {
|
|
|
1064
1239
|
}
|
|
1065
1240
|
continue;
|
|
1066
1241
|
}
|
|
1242
|
+
const undeclared = annotations.get("undeclared");
|
|
1243
|
+
if (undeclared !== void 0 && typeof undeclared !== "boolean") {
|
|
1244
|
+
report("annotation", name2, notText("undeclared", undeclared));
|
|
1245
|
+
}
|
|
1067
1246
|
const node = { name: at0(name2), fields: [], line: 0 };
|
|
1068
1247
|
const description = oneLine(stringOf(get(def, "description")));
|
|
1069
|
-
if (description !== void 0)
|
|
1248
|
+
if (description !== void 0) {
|
|
1249
|
+
node.description = description;
|
|
1250
|
+
if (rewordsDescription(description)) report("reworded", name2, REWORDED);
|
|
1251
|
+
}
|
|
1070
1252
|
const system = annotations.get("system");
|
|
1071
|
-
if (typeof system === "string"
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
if (
|
|
1253
|
+
if (typeof system === "string") {
|
|
1254
|
+
if (SYSTEM_NAME.test(system)) node.system = at0(system);
|
|
1255
|
+
else report("annotation", name2, "`system` is not a system name");
|
|
1256
|
+
} else if (system !== void 0) report("annotation", name2, notText("system", system));
|
|
1257
|
+
const rawNote = annotations.get("note");
|
|
1258
|
+
if (typeof rawNote === "string") {
|
|
1259
|
+
const note = oneLine(rawNote);
|
|
1260
|
+
if (note !== void 0) node.note = note;
|
|
1261
|
+
} else if (rawNote !== void 0) report("annotation", name2, notText("note", rawNote));
|
|
1262
|
+
const parent = stringOf(get(def, "is_a"));
|
|
1263
|
+
if (parent !== void 0) {
|
|
1264
|
+
const parentName = Object.hasOwn(classDefs, parent) ? classNameOf(parent) : toClassName(parent);
|
|
1265
|
+
node.parent = at0(parentName);
|
|
1266
|
+
reference(name2, parent, parentName);
|
|
1267
|
+
}
|
|
1075
1268
|
const mappings = stringList(get(def, "close_mappings"));
|
|
1076
1269
|
const first = mappings[0];
|
|
1077
1270
|
const target = first === void 0 ? "" : localName(first);
|
|
@@ -1094,6 +1287,9 @@ function fromLinkML(schema) {
|
|
|
1094
1287
|
}
|
|
1095
1288
|
}
|
|
1096
1289
|
const block = get(def, other);
|
|
1290
|
+
if (other === "is_a" && present(block) && stringOf(block) === void 0) {
|
|
1291
|
+
report("is_a", name2, `${shown(block)} is not a class name`);
|
|
1292
|
+
}
|
|
1097
1293
|
if (other === "attributes" && present(block) && asRecord(block) === void 0) {
|
|
1098
1294
|
report("attributes", name2, `${shown(block)} is not a map`);
|
|
1099
1295
|
}
|
|
@@ -1108,10 +1304,12 @@ function fromLinkML(schema) {
|
|
|
1108
1304
|
report("slot_usage", name2);
|
|
1109
1305
|
continue;
|
|
1110
1306
|
}
|
|
1307
|
+
const written = slotKeysOf(def);
|
|
1308
|
+
const inherited = inheritedSlots(key2);
|
|
1111
1309
|
for (const slot of Object.keys(usage)) {
|
|
1112
|
-
if (
|
|
1310
|
+
if (written.includes(slot)) {
|
|
1113
1311
|
report("slot_usage", `${name2}.${fieldNameOf(key2, slot)}`);
|
|
1114
|
-
} else {
|
|
1312
|
+
} else if (!inherited.has(slot)) {
|
|
1115
1313
|
report("slot_usage", name2, `\`${slot}\` is inherited and not in the sketch`);
|
|
1116
1314
|
}
|
|
1117
1315
|
}
|
|
@@ -1119,9 +1317,10 @@ function fromLinkML(schema) {
|
|
|
1119
1317
|
}
|
|
1120
1318
|
report(other, name2);
|
|
1121
1319
|
}
|
|
1122
|
-
|
|
1320
|
+
const own = slotKeysOf(def);
|
|
1321
|
+
for (const slot of fieldKeysOf(key2)) {
|
|
1123
1322
|
const attributes = asRecord(get(def, "attributes")) ?? {};
|
|
1124
|
-
const rawSlot = Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
|
|
1323
|
+
const rawSlot = !own.includes(slot) ? get(asRecord(get(def, "slot_usage")) ?? {}, slot) : Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
|
|
1125
1324
|
const slotDef = asRecord(rawSlot);
|
|
1126
1325
|
if (slotDef === void 0 && present(rawSlot)) {
|
|
1127
1326
|
report("slot", `${name2}.${fieldNameOf(key2, slot)}`, "is not a slot definition");
|
|
@@ -1176,13 +1375,18 @@ function fromLinkML(schema) {
|
|
|
1176
1375
|
line: 0
|
|
1177
1376
|
};
|
|
1178
1377
|
const description = oneLine(stringOf(get(def, "description")));
|
|
1179
|
-
if (description !== void 0)
|
|
1378
|
+
if (description !== void 0) {
|
|
1379
|
+
field.description = description;
|
|
1380
|
+
if (rewordsDescription(description)) report("reworded", element, REWORDED);
|
|
1381
|
+
}
|
|
1180
1382
|
const type = typeFor(element, name2, def);
|
|
1181
1383
|
if (type !== void 0) field.type = type;
|
|
1182
1384
|
const annotations = annotationsOf(def);
|
|
1183
1385
|
const system = annotations.get("system");
|
|
1184
|
-
if (typeof system === "string"
|
|
1185
|
-
|
|
1386
|
+
if (typeof system === "string") {
|
|
1387
|
+
if (SYSTEM_NAME.test(system)) field.system = at0(system);
|
|
1388
|
+
else report("annotation", element, "`system` is not a system name");
|
|
1389
|
+
} else if (system !== void 0) report("annotation", element, notText("system", system));
|
|
1186
1390
|
const joins = annotations.get("joins_to");
|
|
1187
1391
|
if (typeof joins === "string") {
|
|
1188
1392
|
const dot = joins.indexOf(".");
|
|
@@ -1195,9 +1399,12 @@ function fromLinkML(schema) {
|
|
|
1195
1399
|
reference(element, target, targetClass);
|
|
1196
1400
|
reference(element, targetField, targetName);
|
|
1197
1401
|
} else report("annotation", element, "`joins_to` is not `Class.field`");
|
|
1198
|
-
} else if (joins !== void 0) report("annotation", element, "
|
|
1199
|
-
const
|
|
1200
|
-
if (
|
|
1402
|
+
} else if (joins !== void 0) report("annotation", element, notText("joins_to", joins));
|
|
1403
|
+
const rawNote = annotations.get("note");
|
|
1404
|
+
if (typeof rawNote === "string") {
|
|
1405
|
+
const note = oneLine(rawNote);
|
|
1406
|
+
if (note !== void 0) field.note = note;
|
|
1407
|
+
} else if (rawNote !== void 0) report("annotation", element, notText("note", rawNote));
|
|
1201
1408
|
for (const other of Object.keys(def)) {
|
|
1202
1409
|
if (!SLOT_KEYS.has(other)) {
|
|
1203
1410
|
report(other, element);
|
|
@@ -1250,6 +1457,34 @@ function fromLinkML(schema) {
|
|
|
1250
1457
|
report("narrowed", element, `\`${range}\` is not a Skiss type; it falls back to string`);
|
|
1251
1458
|
return { kind: "unknown", name: at0(word), many };
|
|
1252
1459
|
}
|
|
1460
|
+
function parentKeyOf(classKey) {
|
|
1461
|
+
const def = asRecord(get(classDefs, classKey)) ?? {};
|
|
1462
|
+
const parent = stringOf(get(def, "is_a"));
|
|
1463
|
+
if (parent === void 0 || !Object.hasOwn(classDefs, parent)) return void 0;
|
|
1464
|
+
return parent;
|
|
1465
|
+
}
|
|
1466
|
+
function inheritedSlots(classKey) {
|
|
1467
|
+
const out = /* @__PURE__ */ new Set();
|
|
1468
|
+
const seen = /* @__PURE__ */ new Set([classKey]);
|
|
1469
|
+
let key2 = parentKeyOf(classKey);
|
|
1470
|
+
while (key2 !== void 0 && !seen.has(key2)) {
|
|
1471
|
+
seen.add(key2);
|
|
1472
|
+
for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) out.add(slot);
|
|
1473
|
+
key2 = parentKeyOf(key2);
|
|
1474
|
+
}
|
|
1475
|
+
return out;
|
|
1476
|
+
}
|
|
1477
|
+
function fieldKeysOf(classKey) {
|
|
1478
|
+
const def = asRecord(get(classDefs, classKey)) ?? {};
|
|
1479
|
+
const keys = slotKeysOf(def);
|
|
1480
|
+
const usage = asRecord(get(def, "slot_usage"));
|
|
1481
|
+
if (usage === void 0) return keys;
|
|
1482
|
+
const inherited = inheritedSlots(classKey);
|
|
1483
|
+
for (const slot of Object.keys(usage)) {
|
|
1484
|
+
if (inherited.has(slot) && !keys.includes(slot)) keys.push(slot);
|
|
1485
|
+
}
|
|
1486
|
+
return keys;
|
|
1487
|
+
}
|
|
1253
1488
|
function listedSomewhere(slot) {
|
|
1254
1489
|
for (const key2 of Object.keys(classDefs)) {
|
|
1255
1490
|
const def = asRecord(get(classDefs, key2)) ?? {};
|
|
@@ -1272,13 +1507,17 @@ var COUNTED = {
|
|
|
1272
1507
|
class: ["class", "classes"],
|
|
1273
1508
|
slot: ["slot", "slots"],
|
|
1274
1509
|
schema: ["schema key", "schema keys"],
|
|
1510
|
+
narrowed: ["narrowed range", "narrowed ranges"],
|
|
1511
|
+
enum_detail: ["enum detail", "enum details"],
|
|
1512
|
+
annotation: ["unreadable annotation", "unreadable annotations"],
|
|
1513
|
+
reworded: ["description reworded", "descriptions reworded"],
|
|
1275
1514
|
// `mixin: true` marks the class; `mixins: [M]` points at another one.
|
|
1276
1515
|
mixin: ["mixin class", "mixin classes"],
|
|
1277
1516
|
mixins: ["mixin", "mixins"],
|
|
1278
1517
|
pattern: ["pattern", "patterns"],
|
|
1279
1518
|
unit: ["unit", "units"]
|
|
1280
1519
|
};
|
|
1281
|
-
var
|
|
1520
|
+
var UNREADABLE = "unreadable";
|
|
1282
1521
|
function formatDropped(dropped) {
|
|
1283
1522
|
const groups = /* @__PURE__ */ new Map();
|
|
1284
1523
|
for (const entry of dropped) {
|
|
@@ -1287,15 +1526,21 @@ function formatDropped(dropped) {
|
|
|
1287
1526
|
else group.push(entry);
|
|
1288
1527
|
}
|
|
1289
1528
|
const drops = [];
|
|
1529
|
+
const notRead = [];
|
|
1290
1530
|
let renamed = "";
|
|
1291
1531
|
let inlined = "";
|
|
1292
1532
|
for (const [kind, entries] of groups) {
|
|
1533
|
+
if (kind === UNREADABLE) {
|
|
1534
|
+
for (const entry of entries) notRead.push(entry.detail ?? "the schema could not be read");
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1293
1537
|
const phrase = phraseFor(kind, entries);
|
|
1294
1538
|
if (kind === "renamed") renamed = phrase;
|
|
1295
1539
|
else if (kind === "inlined") inlined = phrase;
|
|
1296
1540
|
else drops.push(phrase);
|
|
1297
1541
|
}
|
|
1298
1542
|
const sentences = [];
|
|
1543
|
+
if (notRead.length > 0) sentences.push(`Not read: ${notRead.join("; ")}.`);
|
|
1299
1544
|
if (drops.length > 0) sentences.push(`Dropped: ${drops.join(", ")}.`);
|
|
1300
1545
|
if (renamed !== "") sentences.push(`Renamed: ${renamed}.`);
|
|
1301
1546
|
if (inlined !== "") sentences.push(`Inlined: ${inlined}.`);
|
|
@@ -1305,11 +1550,10 @@ function phraseFor(kind, entries) {
|
|
|
1305
1550
|
const n = entries.length;
|
|
1306
1551
|
const counted = Object.hasOwn(COUNTED, kind) ? COUNTED[kind] : void 0;
|
|
1307
1552
|
if (counted !== void 0) return `${n} ${n === 1 ? counted[0] : counted[1]}`;
|
|
1308
|
-
return `${kind} on ${n} ${nounFor(
|
|
1553
|
+
return `${kind} on ${n} ${nounFor(entries)}`;
|
|
1309
1554
|
}
|
|
1310
|
-
function nounFor(
|
|
1555
|
+
function nounFor(entries) {
|
|
1311
1556
|
const one = entries.length === 1;
|
|
1312
|
-
if (ENUM_KINDS.has(kind)) return one ? "enum" : "enums";
|
|
1313
1557
|
const slots = entries.filter((entry) => entry.element.includes(".")).length;
|
|
1314
1558
|
if (slots === entries.length) return one ? "slot" : "slots";
|
|
1315
1559
|
if (slots === 0) return one ? "class" : "classes";
|
|
@@ -1360,12 +1604,18 @@ function annotationsOf(def) {
|
|
|
1360
1604
|
if (annotations === void 0) return out;
|
|
1361
1605
|
for (const tag of Object.keys(annotations)) {
|
|
1362
1606
|
const raw = get(annotations, tag);
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1607
|
+
const long = asRecord(raw);
|
|
1608
|
+
const value = long === void 0 ? raw : get(long, "value");
|
|
1609
|
+
out.set(tag, value ?? null);
|
|
1366
1610
|
}
|
|
1367
1611
|
return out;
|
|
1368
1612
|
}
|
|
1613
|
+
function notText(tag, value) {
|
|
1614
|
+
if (value === null) return `\`${tag}\` was written with no value`;
|
|
1615
|
+
if (Array.isArray(value)) return `\`${tag}\` is a list, not text`;
|
|
1616
|
+
if (typeof value === "object") return `\`${tag}\` is a map, not text`;
|
|
1617
|
+
return `\`${tag}\` is a ${typeof value}, not text`;
|
|
1618
|
+
}
|
|
1369
1619
|
function slotKeysOf(def) {
|
|
1370
1620
|
const keys = Object.keys(asRecord(get(def, "attributes")) ?? {});
|
|
1371
1621
|
for (const slot of stringList(get(def, "slots"))) {
|
|
@@ -1390,7 +1640,7 @@ var schemaElement = (root) => stringOf(get(root, "name")) ?? "schema";
|
|
|
1390
1640
|
var nothing = (element, detail) => ({
|
|
1391
1641
|
document: parse(""),
|
|
1392
1642
|
source: "",
|
|
1393
|
-
dropped: [{ kind:
|
|
1643
|
+
dropped: [{ kind: UNREADABLE, element, detail }]
|
|
1394
1644
|
});
|
|
1395
1645
|
|
|
1396
1646
|
// src/import.ts
|
|
@@ -1417,7 +1667,7 @@ function notYAML(error) {
|
|
|
1417
1667
|
}
|
|
1418
1668
|
|
|
1419
1669
|
// src/index.ts
|
|
1420
|
-
var VERSION = "0.
|
|
1670
|
+
var VERSION = "0.5.0";
|
|
1421
1671
|
|
|
1422
1672
|
// src/cli.ts
|
|
1423
1673
|
var HELP = `Usage: skiss <command> [options]
|
|
@@ -1426,15 +1676,22 @@ Commands:
|
|
|
1426
1676
|
diagram <file> Write a Mermaid class diagram for a .skiss file.
|
|
1427
1677
|
compile <file> Write a LinkML schema for a .skiss file.
|
|
1428
1678
|
import <file> Write Skiss for a LinkML schema file, YAML or JSON.
|
|
1679
|
+
render <file> Write an SVG or a PNG of the class diagram, through
|
|
1680
|
+
the Mermaid CLI (mmdc), which must be installed.
|
|
1429
1681
|
<file> may be - to read standard input.
|
|
1430
1682
|
|
|
1431
1683
|
Options:
|
|
1432
1684
|
-o, --output <path> Write the output to <path> instead of standard output
|
|
1433
|
-
--notes diagram: emit \`? text\` doubts as Mermaid notes
|
|
1685
|
+
--notes diagram, render: emit \`? text\` doubts as Mermaid notes
|
|
1434
1686
|
--json compile: write the schema as JSON instead of YAML
|
|
1435
1687
|
--name <name> compile: the schema name. Defaults to the file's
|
|
1436
1688
|
basename without its extension, or \`sketch\` for
|
|
1437
1689
|
standard input.
|
|
1690
|
+
--format <fmt> render: svg or png. Defaults to the extension of
|
|
1691
|
+
-o, and to svg when there is no -o. png needs an -o:
|
|
1692
|
+
only the SVG can go to standard output.
|
|
1693
|
+
--scale <n> render: the scale mmdc rasterises at. Defaults to 2
|
|
1694
|
+
for png, and to mmdc's own default for svg.
|
|
1438
1695
|
--strict Exit 1 when the input has any diagnostic; for import,
|
|
1439
1696
|
also when the projection dropped anything
|
|
1440
1697
|
-h, --help Show this help and exit
|
|
@@ -1450,8 +1707,12 @@ wrote has, so their line numbers are lines of that sketch.
|
|
|
1450
1707
|
Exit codes:
|
|
1451
1708
|
0 the output was produced
|
|
1452
1709
|
1 --strict and the input has diagnostics (the output is still written)
|
|
1453
|
-
2 usage error, missing file or unreadable input
|
|
1710
|
+
2 usage error, missing file, or unreadable input: for import, a text no
|
|
1711
|
+
schema could be read out of, which produces no sketch at all; for
|
|
1712
|
+
render, the Mermaid CLI not being installed
|
|
1454
1713
|
70 internal error, a bug in skiss
|
|
1714
|
+
|
|
1715
|
+
render exits with mmdc's own status when mmdc ran and failed.
|
|
1455
1716
|
`;
|
|
1456
1717
|
var EXIT_OK = 0;
|
|
1457
1718
|
var EXIT_STRICT = 1;
|
|
@@ -1460,7 +1721,8 @@ var EXIT_INTERNAL = 70;
|
|
|
1460
1721
|
var COMMANDS = {
|
|
1461
1722
|
diagram: ["output", "notes", "strict"],
|
|
1462
1723
|
compile: ["output", "json", "name", "strict"],
|
|
1463
|
-
import: ["output", "strict"]
|
|
1724
|
+
import: ["output", "strict"],
|
|
1725
|
+
render: ["output", "notes", "format", "scale", "strict"]
|
|
1464
1726
|
};
|
|
1465
1727
|
var isCommand = (text) => Object.hasOwn(COMMANDS, text);
|
|
1466
1728
|
var UsageError = class extends Error {
|
|
@@ -1482,6 +1744,8 @@ async function main(argv) {
|
|
|
1482
1744
|
notes: { type: "boolean" },
|
|
1483
1745
|
json: { type: "boolean" },
|
|
1484
1746
|
name: { type: "string" },
|
|
1747
|
+
format: { type: "string" },
|
|
1748
|
+
scale: { type: "string" },
|
|
1485
1749
|
strict: { type: "boolean" },
|
|
1486
1750
|
help: { type: "boolean", short: "h" },
|
|
1487
1751
|
version: { type: "boolean" }
|
|
@@ -1511,8 +1775,16 @@ async function main(argv) {
|
|
|
1511
1775
|
}
|
|
1512
1776
|
}
|
|
1513
1777
|
const source = file === "-" ? await readStdin() : await readSource(file);
|
|
1514
|
-
const { output, diagnostics, dropped } = runCommand(command, source, file, values);
|
|
1515
1778
|
const label = file === "-" ? "<stdin>" : file;
|
|
1779
|
+
if (command === "render") {
|
|
1780
|
+
const picture = compile(source, { target: "mermaid", notes: values.notes });
|
|
1781
|
+
for (const d of picture.diagnostics) process.stderr.write(`${formatDiagnostic(d, label)}
|
|
1782
|
+
`);
|
|
1783
|
+
const status = await render(picture.output, values);
|
|
1784
|
+
if (status !== EXIT_OK) return status;
|
|
1785
|
+
return values.strict === true && picture.diagnostics.length > 0 ? EXIT_STRICT : EXIT_OK;
|
|
1786
|
+
}
|
|
1787
|
+
const { output, diagnostics, dropped } = runCommand(command, source, file, values);
|
|
1516
1788
|
const report = formatDropped(dropped);
|
|
1517
1789
|
if (report !== "") process.stderr.write(`${report}
|
|
1518
1790
|
`);
|
|
@@ -1520,9 +1792,15 @@ async function main(argv) {
|
|
|
1520
1792
|
`);
|
|
1521
1793
|
if (values.output === void 0) process.stdout.write(output);
|
|
1522
1794
|
else await writeOutput(values.output, output);
|
|
1795
|
+
if (unreadableInput(command, diagnostics, dropped)) return EXIT_USAGE;
|
|
1523
1796
|
const strict = values.strict === true && (diagnostics.length > 0 || dropped.length > 0);
|
|
1524
1797
|
return strict ? EXIT_STRICT : EXIT_OK;
|
|
1525
1798
|
}
|
|
1799
|
+
function unreadableInput(command, diagnostics, dropped) {
|
|
1800
|
+
if (command !== "import") return false;
|
|
1801
|
+
if (dropped.some((entry) => entry.kind === "unreadable")) return true;
|
|
1802
|
+
return diagnostics.some((d) => d.code === "E_NOT_YAML");
|
|
1803
|
+
}
|
|
1526
1804
|
function runCommand(command, source, file, values) {
|
|
1527
1805
|
if (command === "import") return importLinkML(source);
|
|
1528
1806
|
if (command === "compile") {
|
|
@@ -1561,6 +1839,76 @@ async function writeOutput(path, text) {
|
|
|
1561
1839
|
throw new UsageError(`cannot write \`${path}\`: ${reason(error)}`);
|
|
1562
1840
|
}
|
|
1563
1841
|
}
|
|
1842
|
+
var MMDC_MISSING = "skiss render needs the Mermaid CLI: npm install -g @mermaid-js/mermaid-cli";
|
|
1843
|
+
var isFormat = (text) => text === "svg" || text === "png";
|
|
1844
|
+
async function render(mermaid, values) {
|
|
1845
|
+
const format = renderFormat(values);
|
|
1846
|
+
const scale = renderScale(values.scale, format);
|
|
1847
|
+
const mmdc = await findMmdc();
|
|
1848
|
+
if (mmdc === void 0) {
|
|
1849
|
+
process.stderr.write(`${MMDC_MISSING}
|
|
1850
|
+
`);
|
|
1851
|
+
return EXIT_USAGE;
|
|
1852
|
+
}
|
|
1853
|
+
const dir = await mkdtemp(join(tmpdir(), "skiss-render-"));
|
|
1854
|
+
try {
|
|
1855
|
+
const input = join(dir, "diagram.mmd");
|
|
1856
|
+
await writeFile(input, mermaid, "utf8");
|
|
1857
|
+
const target = values.output ?? join(dir, `diagram.${format}`);
|
|
1858
|
+
const args = ["-i", input, "-o", target, "-e", format, "-b", "transparent"];
|
|
1859
|
+
if (scale !== void 0) args.push("--scale", scale);
|
|
1860
|
+
const run = spawnSync(mmdc, args, {
|
|
1861
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
1862
|
+
encoding: "utf8"
|
|
1863
|
+
});
|
|
1864
|
+
if (run.error !== void 0)
|
|
1865
|
+
throw new UsageError(`cannot run \`${mmdc}\`: ${reason(run.error)}`);
|
|
1866
|
+
if (run.stdout !== "") process.stderr.write(run.stdout);
|
|
1867
|
+
if (run.status !== EXIT_OK) return run.status ?? EXIT_INTERNAL;
|
|
1868
|
+
if (values.output === void 0) process.stdout.write(await readFile(target, "utf8"));
|
|
1869
|
+
return EXIT_OK;
|
|
1870
|
+
} finally {
|
|
1871
|
+
await rm(dir, { recursive: true, force: true });
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
function renderFormat(values) {
|
|
1875
|
+
if (values.format !== void 0) {
|
|
1876
|
+
if (!isFormat(values.format)) {
|
|
1877
|
+
throw new UsageError(`unknown \`--format ${values.format}\`: svg or png`);
|
|
1878
|
+
}
|
|
1879
|
+
if (values.format === "png" && values.output === void 0) {
|
|
1880
|
+
throw new UsageError(
|
|
1881
|
+
"render --format png needs -o <path>: a PNG cannot go to standard output"
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
return values.format;
|
|
1885
|
+
}
|
|
1886
|
+
if (values.output === void 0) return "svg";
|
|
1887
|
+
const extension = extname(values.output).slice(1).toLowerCase();
|
|
1888
|
+
if (isFormat(extension)) return extension;
|
|
1889
|
+
throw new UsageError(
|
|
1890
|
+
`cannot tell the format of \`${values.output}\`: give --format svg or --format png`
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
function renderScale(scale, format) {
|
|
1894
|
+
if (scale === void 0) return format === "png" ? "2" : void 0;
|
|
1895
|
+
const value = Number(scale);
|
|
1896
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1897
|
+
throw new UsageError(`\`--scale ${scale}\` is not a positive number`);
|
|
1898
|
+
}
|
|
1899
|
+
return scale;
|
|
1900
|
+
}
|
|
1901
|
+
async function findMmdc() {
|
|
1902
|
+
const onPath = (process.env.PATH ?? "").split(delimiter).filter((dir) => dir !== "").map((dir) => join(dir, "mmdc"));
|
|
1903
|
+
for (const candidate of [...onPath, join(process.cwd(), "node_modules", ".bin", "mmdc")]) {
|
|
1904
|
+
try {
|
|
1905
|
+
await access(candidate, constants.X_OK);
|
|
1906
|
+
return candidate;
|
|
1907
|
+
} catch {
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
return void 0;
|
|
1911
|
+
}
|
|
1564
1912
|
function reason(error) {
|
|
1565
1913
|
if (error instanceof Error) {
|
|
1566
1914
|
const code = error.code;
|
|
@@ -1568,6 +1916,12 @@ function reason(error) {
|
|
|
1568
1916
|
}
|
|
1569
1917
|
return String(error);
|
|
1570
1918
|
}
|
|
1919
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
1920
|
+
stream.on("error", (error) => {
|
|
1921
|
+
if (error.code === "EPIPE") process.exit(EXIT_OK);
|
|
1922
|
+
throw error;
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1571
1925
|
main(process.argv.slice(2)).then(
|
|
1572
1926
|
(code) => {
|
|
1573
1927
|
process.exitCode = code;
|