@eriknaslund/skiss 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/cli.js +317 -30
- package/dist/index.d.ts +58 -5
- package/dist/index.js +427 -35
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Skiss is text you can type while someone talks. A field is one line. A change is
|
|
|
41
41
|
- **What you leave out means "not yet".** Never "unknown", never "none". A sketch is silent about what you do not know.
|
|
42
42
|
- **`@Catalog` says which system owns a thing.** On a class, or on a single field when the box is really a view over two systems.
|
|
43
43
|
- **`~` and `=` are two different statements.** `~` says two classes are about the same thing. `=` says which field joins to which. The first is a domain conversation, the second an integration conversation.
|
|
44
|
+
- **`<` inherits, once.** `Jedi < Character` on the class line: the child has everything the parent has and adds its own. One parent, no mixins; it compiles to `is_a`.
|
|
44
45
|
- **`#` describes, `?` doubts.** A description ends up in generated documentation. A doubt never does.
|
|
45
46
|
- **Every line stands on its own.** A half-typed line breaks that line and nothing else, so the diagram keeps rendering while you type.
|
|
46
47
|
- **Everything you can write compiles to valid LinkML.** Skiss is a strict subset. Nothing is lost on the way up.
|
|
@@ -62,11 +63,15 @@ Or without installing: `npx @eriknaslund/skiss diagram model.skiss`.
|
|
|
62
63
|
|
|
63
64
|
`skiss diagram` writes a Mermaid class diagram to standard output; `-o diagram.mmd` writes it to a file, `--notes` includes the `?` doubts as notes, and `-` reads standard input. Anything the file gets wrong is reported on standard error, one line each, and the diagram is still drawn from the lines that parse. `--strict` makes that a non-zero exit. `skiss --help` lists the rest.
|
|
64
65
|
|
|
66
|
+
`skiss render model.skiss -o model.svg` writes a picture rather than the Mermaid text. A class diagram is laid out in a browser, so this one command needs a browser: it delegates to the official [Mermaid CLI](https://github.com/mermaid-js/mermaid-cli), which you install yourself with `npm install -g @mermaid-js/mermaid-cli`. Nothing in the package or the library depends on it, and `skiss render` says that line and exits 2 when it is not there. The format is the extension of `-o` — `.svg` or `.png` — and `--format svg|png` overrides it; with no `-o` the SVG goes to standard output, and a PNG always needs an `-o`. `--scale` is what the PNG is rasterised at and defaults to 2; `--notes`, `--strict` and the diagnostics work as they do for `diagram`. When `mmdc` itself fails, its exit status is the command's.
|
|
67
|
+
|
|
65
68
|
`skiss compile model.skiss` writes a LinkML schema instead, as YAML on standard output. `--json` writes the same schema as JSON, `--name` sets the schema name (it defaults to the file name without its extension, and to `sketch` when reading standard input), and `-o`, `-`, `--strict` and the diagnostics work exactly as they do for `diagram`.
|
|
66
69
|
|
|
67
|
-
`skiss import schema.linkml.yaml` goes the other way: it reads a LinkML schema, YAML or JSON, and writes Skiss. What the projection could not carry —
|
|
70
|
+
`skiss import schema.linkml.yaml` goes the other way: it reads a LinkML schema, YAML or JSON, and writes Skiss. What the projection could not carry — patterns, `required`, `mixins`, the names it had to convert — is one line on standard error before the diagnostics, because a sketch is a smaller language than LinkML and that line says by how much. `-o`, `-` and the diagnostics work as they do for `compile`; `--strict` exits 1 when the projected sketch has a diagnostic, and also when anything was dropped. An input that is not a schema at all — not YAML, or YAML no schema can be read out of — produces no sketch, says why on standard error, and exits 2, the code every command uses for unreadable input.
|
|
71
|
+
|
|
72
|
+
`npm install @eriknaslund/skiss` also gives you the library: `compile`, `formatDiagnostic`, `parse`, `resolve`, `toLinkML`, `serialize`, `toMermaid`, `toSkiss`, `fromLinkML`, `formatDropped`, `importLinkML`, `tokenizeLine` and `VERSION`, browser-safe.
|
|
68
73
|
|
|
69
|
-
`
|
|
74
|
+
`tokenizeLine` is there for editors: it says where the words of one line are — `{ kind, from, to }` for each run of characters, so a host can colour them. It is line-based and stateless, as the language is: one line in, its tokens out, nothing carried from the line before, and nothing said about a run it does not recognise, so a half-typed line keeps the colours it has earned. It is not a parser and reports nothing; diagnostics stay `parse` and `resolve`'s ([ADR 0010](docs/adr/0010-tokeniser-in-the-package.md)).
|
|
70
75
|
|
|
71
76
|
Editors that render Skiss live are separate projects built on this package. This repository is the language, the library and the command line.
|
|
72
77
|
|
|
@@ -84,9 +89,9 @@ Editors that render Skiss live are separate projects built on this package. This
|
|
|
84
89
|
|
|
85
90
|
## Status
|
|
86
91
|
|
|
87
|
-
0.
|
|
92
|
+
0.6.0, published on npm as [`@eriknaslund/skiss`](https://www.npmjs.com/package/@eriknaslund/skiss). The language parses, resolves and diagnoses; `skiss diagram` draws it, `skiss compile` writes LinkML that the real LinkML toolchain validates in CI, and `skiss import` reads a LinkML schema back into a sketch. The library exports the same in both directions and is browser-safe.
|
|
88
93
|
|
|
89
|
-
The [specification](docs/SPEC.md) is at 0.
|
|
94
|
+
The [specification](docs/SPEC.md) is at 0.3 and moves when the language does, which is not with every release. Not built: editing a projected schema so that what Skiss cannot express survives the edit (SPEC §8.1). [CHANGELOG.md](CHANGELOG.md) is what changed when.
|
|
90
95
|
|
|
91
96
|
## License
|
|
92
97
|
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,92 @@
|
|
|
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]+$/, "");
|
|
@@ -28,7 +110,7 @@ function primitiveFor(word) {
|
|
|
28
110
|
return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
|
|
29
111
|
}
|
|
30
112
|
var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
|
|
31
|
-
var CLASS_ORDER = "name, `@System`, `~ Class`, then `#` and `?`";
|
|
113
|
+
var CLASS_ORDER = "name, `< Parent`, `@System`, `~ Class`, then `#` and `?`";
|
|
32
114
|
function parse(source) {
|
|
33
115
|
const classes = [];
|
|
34
116
|
const diagnostics = [];
|
|
@@ -115,7 +197,11 @@ function splitTrailer(text) {
|
|
|
115
197
|
return trailer;
|
|
116
198
|
}
|
|
117
199
|
var WORD = /[\p{L}\p{N}_-]+/uy;
|
|
118
|
-
|
|
200
|
+
function wordAt(text, i) {
|
|
201
|
+
WORD.lastIndex = i;
|
|
202
|
+
return WORD.exec(text)?.[0];
|
|
203
|
+
}
|
|
204
|
+
var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "<", ",", "["];
|
|
119
205
|
function tokenize(head) {
|
|
120
206
|
const tokens = [];
|
|
121
207
|
let i = 0;
|
|
@@ -125,11 +211,10 @@ function tokenize(head) {
|
|
|
125
211
|
i++;
|
|
126
212
|
continue;
|
|
127
213
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
i += word[0].length;
|
|
214
|
+
const word = wordAt(head, i);
|
|
215
|
+
if (word !== void 0) {
|
|
216
|
+
tokens.push({ kind: "word", text: word, col: i, end: i + word.length });
|
|
217
|
+
i += word.length;
|
|
133
218
|
continue;
|
|
134
219
|
}
|
|
135
220
|
if (c === "[" && head[i + 1] === "]") {
|
|
@@ -177,6 +262,13 @@ function parseClassLine(text, line) {
|
|
|
177
262
|
);
|
|
178
263
|
}
|
|
179
264
|
const node = { name: name(head, line), line };
|
|
265
|
+
const lt = peek(c);
|
|
266
|
+
if (lt?.kind === "<") {
|
|
267
|
+
take(c);
|
|
268
|
+
const parent = parseParent(c, lt);
|
|
269
|
+
if (!parent.ok) return parent;
|
|
270
|
+
node.parent = parent.value;
|
|
271
|
+
}
|
|
180
272
|
const at = peek(c);
|
|
181
273
|
if (at?.kind === "@") {
|
|
182
274
|
take(c);
|
|
@@ -306,6 +398,31 @@ function parseFieldLine(text, line) {
|
|
|
306
398
|
if (trailer.note !== void 0) node.note = trailer.note;
|
|
307
399
|
return { ok: true, value: node };
|
|
308
400
|
}
|
|
401
|
+
function parseParent(c, lt) {
|
|
402
|
+
const parent = take(c);
|
|
403
|
+
if (parent === void 0 || parent.kind !== "word") {
|
|
404
|
+
return fail(
|
|
405
|
+
"E_UNPARSABLE",
|
|
406
|
+
"Expected a class name after `<`, e.g. `Jedi < Character`",
|
|
407
|
+
c.line,
|
|
408
|
+
parent ?? afterSpan(lt)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
if (!CLASS_NAME.test(parent.text)) {
|
|
412
|
+
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}\``;
|
|
413
|
+
return fail("E_BAD_NAME", message, c.line, parent);
|
|
414
|
+
}
|
|
415
|
+
const next = peek(c);
|
|
416
|
+
if (next?.kind === "," || next?.kind === "<") {
|
|
417
|
+
return fail(
|
|
418
|
+
"E_UNPARSABLE",
|
|
419
|
+
"A class has one parent; `<` takes a single class name",
|
|
420
|
+
c.line,
|
|
421
|
+
next
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
return { ok: true, value: name(parent, c.line) };
|
|
425
|
+
}
|
|
309
426
|
function parseSystem(c, at) {
|
|
310
427
|
const system = take(c);
|
|
311
428
|
if (system === void 0 || system.kind !== "word") {
|
|
@@ -415,7 +532,7 @@ function parseType(c, colon) {
|
|
|
415
532
|
);
|
|
416
533
|
}
|
|
417
534
|
function unexpected(token, line, kind, order) {
|
|
418
|
-
const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["@", "~"];
|
|
535
|
+
const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["<", "@", "~"];
|
|
419
536
|
if (modifiers.includes(token.kind)) {
|
|
420
537
|
return fail(
|
|
421
538
|
"E_UNPARSABLE",
|
|
@@ -424,10 +541,10 @@ function unexpected(token, line, kind, order) {
|
|
|
424
541
|
token
|
|
425
542
|
);
|
|
426
543
|
}
|
|
427
|
-
if (token.
|
|
544
|
+
if (token.kind === "<") {
|
|
428
545
|
return fail(
|
|
429
546
|
"E_UNPARSABLE",
|
|
430
|
-
"`<`
|
|
547
|
+
"`<` marks inheritance and belongs on the class line: `Child < Parent`",
|
|
431
548
|
line,
|
|
432
549
|
token
|
|
433
550
|
);
|
|
@@ -456,10 +573,14 @@ function resolve(doc) {
|
|
|
456
573
|
};
|
|
457
574
|
const classes = out.classes;
|
|
458
575
|
const declared = new Set(classes.map((c) => c.name.text));
|
|
459
|
-
const
|
|
576
|
+
const found = [];
|
|
460
577
|
const undeclared = /* @__PURE__ */ new Map();
|
|
578
|
+
const tree = inheritance(classes);
|
|
461
579
|
const warn = (code, message, at) => {
|
|
462
|
-
|
|
580
|
+
found.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
|
|
581
|
+
};
|
|
582
|
+
const error = (code, message, at) => {
|
|
583
|
+
found.push({ severity: "error", code, message, line: at.line, col: at.col, end: at.end });
|
|
463
584
|
};
|
|
464
585
|
const reference = (name2) => {
|
|
465
586
|
if (declared.has(name2.text)) return true;
|
|
@@ -480,9 +601,25 @@ function resolve(doc) {
|
|
|
480
601
|
cls.name
|
|
481
602
|
);
|
|
482
603
|
}
|
|
604
|
+
const parent = cls.parent;
|
|
605
|
+
if (parent !== void 0) {
|
|
606
|
+
reference(parent);
|
|
607
|
+
const circle = tree.circle(cls);
|
|
608
|
+
if (circle !== void 0) {
|
|
609
|
+
const through = circle.slice(1).map((member2) => `\`${member2.name.text}\``);
|
|
610
|
+
const path = through.length === 0 ? "" : ` through ${through.join(", ")}`;
|
|
611
|
+
error(
|
|
612
|
+
"E_INHERITANCE_CYCLE",
|
|
613
|
+
`class \`${cls.name.text}\` inherits from itself${path}; this \`<\` is not carried`,
|
|
614
|
+
parent
|
|
615
|
+
);
|
|
616
|
+
cls.parent = void 0;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
483
619
|
if (cls.similarTo !== void 0) reference(cls.similarTo);
|
|
484
620
|
const firstField = /* @__PURE__ */ new Map();
|
|
485
|
-
|
|
621
|
+
const inheritedIdentifier = tree.inheritedIdentifier(cls);
|
|
622
|
+
let identifier = inheritedIdentifier?.field;
|
|
486
623
|
for (const field of cls.fields) {
|
|
487
624
|
const earlierField = firstField.get(field.name.text);
|
|
488
625
|
if (earlierField === void 0) firstField.set(field.name.text, field);
|
|
@@ -496,14 +633,23 @@ function resolve(doc) {
|
|
|
496
633
|
if (field.identifier) {
|
|
497
634
|
if (identifier === void 0) identifier = field;
|
|
498
635
|
else {
|
|
636
|
+
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`;
|
|
499
637
|
warn(
|
|
500
638
|
"W_MULTIPLE_IDENTIFIERS",
|
|
501
|
-
`\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}
|
|
639
|
+
`\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\`${from} ${identifier.line}`,
|
|
502
640
|
field.identifierAt ?? field.name
|
|
503
641
|
);
|
|
504
642
|
field.identifier = false;
|
|
505
643
|
}
|
|
506
644
|
}
|
|
645
|
+
const replaced = tree.inherited(cls, field.name.text);
|
|
646
|
+
if (replaced !== void 0 && sameField(field, replaced.field)) {
|
|
647
|
+
warn(
|
|
648
|
+
"W_REDUNDANT_OVERRIDE",
|
|
649
|
+
`field \`${field.name.text}\` is identical to the one inherited from class \`${replaced.from.name.text}\` on line ${replaced.field.line}`,
|
|
650
|
+
field.name
|
|
651
|
+
);
|
|
652
|
+
}
|
|
507
653
|
if (field.type?.kind === "unknown") {
|
|
508
654
|
const word = field.type.name.text;
|
|
509
655
|
const suggestion = closestTypeWord(word);
|
|
@@ -541,8 +687,8 @@ function resolve(doc) {
|
|
|
541
687
|
}
|
|
542
688
|
}
|
|
543
689
|
const present2 = new Set(out.diagnostics.map(key));
|
|
544
|
-
for (const
|
|
545
|
-
if (!present2.has(key(
|
|
690
|
+
for (const d of found) {
|
|
691
|
+
if (!present2.has(key(d))) out.diagnostics.push(d);
|
|
546
692
|
}
|
|
547
693
|
out.diagnostics.sort((a, b) => a.line - b.line);
|
|
548
694
|
out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
|
|
@@ -573,6 +719,7 @@ function copyField(f) {
|
|
|
573
719
|
}
|
|
574
720
|
function copyClass(c) {
|
|
575
721
|
const out = { ...c, name: copyName(c.name), fields: c.fields.map(copyField) };
|
|
722
|
+
if (c.parent !== void 0) out.parent = copyName(c.parent);
|
|
576
723
|
if (c.system !== void 0) out.system = copyName(c.system);
|
|
577
724
|
if (c.similarTo !== void 0) out.similarTo = copyName(c.similarTo);
|
|
578
725
|
return out;
|
|
@@ -751,6 +898,7 @@ function buildClass(cls, mappingPrefix, enums) {
|
|
|
751
898
|
const { node, fields } = cls;
|
|
752
899
|
const out = {};
|
|
753
900
|
if (node.description !== void 0) out.description = node.description;
|
|
901
|
+
if (node.parent !== void 0) out.is_a = node.parent.text;
|
|
754
902
|
const annotations = {};
|
|
755
903
|
if (node.system !== void 0) annotations.system = node.system.text;
|
|
756
904
|
if (node.note !== void 0) annotations.note = node.note;
|
|
@@ -833,6 +981,7 @@ function toMermaid(doc, opts) {
|
|
|
833
981
|
}
|
|
834
982
|
for (const cls of resolved.classes) {
|
|
835
983
|
const from = cls.name.text;
|
|
984
|
+
if (cls.parent !== void 0) lines.push(` ${cls.parent.text} <|-- ${from}`);
|
|
836
985
|
if (cls.similarTo !== void 0) lines.push(` ${from} ..> ${cls.similarTo.text} : similar`);
|
|
837
986
|
for (const field of cls.fields) {
|
|
838
987
|
const type = field.type;
|
|
@@ -939,7 +1088,7 @@ function toSkiss(doc) {
|
|
|
939
1088
|
`;
|
|
940
1089
|
}
|
|
941
1090
|
function classBlock(cls) {
|
|
942
|
-
const head = cls.name.text + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
|
|
1091
|
+
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}`);
|
|
943
1092
|
return [withTrailer(head, cls), ...cls.fields.map(fieldLine)].join("\n");
|
|
944
1093
|
}
|
|
945
1094
|
function fieldLine(field) {
|
|
@@ -993,7 +1142,14 @@ var PRIMITIVE_RANGE = {
|
|
|
993
1142
|
uri: "uri",
|
|
994
1143
|
uriorcurie: "uri"
|
|
995
1144
|
};
|
|
996
|
-
var CLASS_KEYS = /* @__PURE__ */ new Set([
|
|
1145
|
+
var CLASS_KEYS = /* @__PURE__ */ new Set([
|
|
1146
|
+
"description",
|
|
1147
|
+
"is_a",
|
|
1148
|
+
"annotations",
|
|
1149
|
+
"close_mappings",
|
|
1150
|
+
"attributes",
|
|
1151
|
+
"slots"
|
|
1152
|
+
]);
|
|
997
1153
|
var SLOT_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "range", "multivalued", "identifier"]);
|
|
998
1154
|
var CLASS_TAGS = /* @__PURE__ */ new Set(["system", "note", "undeclared"]);
|
|
999
1155
|
var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
|
|
@@ -1036,7 +1192,7 @@ function fromLinkML(schema) {
|
|
|
1036
1192
|
for (const key2 of Object.keys(classDefs)) {
|
|
1037
1193
|
const names = /* @__PURE__ */ new Map();
|
|
1038
1194
|
const taken = /* @__PURE__ */ new Set();
|
|
1039
|
-
for (const slot of
|
|
1195
|
+
for (const slot of fieldKeysOf(key2)) {
|
|
1040
1196
|
names.set(slot, unique(toFieldName(slot), taken));
|
|
1041
1197
|
}
|
|
1042
1198
|
fieldNames.set(key2, names);
|
|
@@ -1106,6 +1262,12 @@ function fromLinkML(schema) {
|
|
|
1106
1262
|
const note = oneLine(rawNote);
|
|
1107
1263
|
if (note !== void 0) node.note = note;
|
|
1108
1264
|
} else if (rawNote !== void 0) report("annotation", name2, notText("note", rawNote));
|
|
1265
|
+
const parent = stringOf(get(def, "is_a"));
|
|
1266
|
+
if (parent !== void 0) {
|
|
1267
|
+
const parentName = Object.hasOwn(classDefs, parent) ? classNameOf(parent) : toClassName(parent);
|
|
1268
|
+
node.parent = at0(parentName);
|
|
1269
|
+
reference(name2, parent, parentName);
|
|
1270
|
+
}
|
|
1109
1271
|
const mappings = stringList(get(def, "close_mappings"));
|
|
1110
1272
|
const first = mappings[0];
|
|
1111
1273
|
const target = first === void 0 ? "" : localName(first);
|
|
@@ -1128,6 +1290,9 @@ function fromLinkML(schema) {
|
|
|
1128
1290
|
}
|
|
1129
1291
|
}
|
|
1130
1292
|
const block = get(def, other);
|
|
1293
|
+
if (other === "is_a" && present(block) && stringOf(block) === void 0) {
|
|
1294
|
+
report("is_a", name2, `${shown(block)} is not a class name`);
|
|
1295
|
+
}
|
|
1131
1296
|
if (other === "attributes" && present(block) && asRecord(block) === void 0) {
|
|
1132
1297
|
report("attributes", name2, `${shown(block)} is not a map`);
|
|
1133
1298
|
}
|
|
@@ -1142,10 +1307,12 @@ function fromLinkML(schema) {
|
|
|
1142
1307
|
report("slot_usage", name2);
|
|
1143
1308
|
continue;
|
|
1144
1309
|
}
|
|
1310
|
+
const written = slotKeysOf(def);
|
|
1311
|
+
const inherited = inheritedSlots(key2);
|
|
1145
1312
|
for (const slot of Object.keys(usage)) {
|
|
1146
|
-
if (
|
|
1313
|
+
if (written.includes(slot)) {
|
|
1147
1314
|
report("slot_usage", `${name2}.${fieldNameOf(key2, slot)}`);
|
|
1148
|
-
} else {
|
|
1315
|
+
} else if (!inherited.has(slot)) {
|
|
1149
1316
|
report("slot_usage", name2, `\`${slot}\` is inherited and not in the sketch`);
|
|
1150
1317
|
}
|
|
1151
1318
|
}
|
|
@@ -1153,9 +1320,10 @@ function fromLinkML(schema) {
|
|
|
1153
1320
|
}
|
|
1154
1321
|
report(other, name2);
|
|
1155
1322
|
}
|
|
1156
|
-
|
|
1323
|
+
const own = slotKeysOf(def);
|
|
1324
|
+
for (const slot of fieldKeysOf(key2)) {
|
|
1157
1325
|
const attributes = asRecord(get(def, "attributes")) ?? {};
|
|
1158
|
-
const rawSlot = Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
|
|
1326
|
+
const rawSlot = !own.includes(slot) ? get(asRecord(get(def, "slot_usage")) ?? {}, slot) : Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
|
|
1159
1327
|
const slotDef = asRecord(rawSlot);
|
|
1160
1328
|
if (slotDef === void 0 && present(rawSlot)) {
|
|
1161
1329
|
report("slot", `${name2}.${fieldNameOf(key2, slot)}`, "is not a slot definition");
|
|
@@ -1292,6 +1460,34 @@ function fromLinkML(schema) {
|
|
|
1292
1460
|
report("narrowed", element, `\`${range}\` is not a Skiss type; it falls back to string`);
|
|
1293
1461
|
return { kind: "unknown", name: at0(word), many };
|
|
1294
1462
|
}
|
|
1463
|
+
function parentKeyOf(classKey) {
|
|
1464
|
+
const def = asRecord(get(classDefs, classKey)) ?? {};
|
|
1465
|
+
const parent = stringOf(get(def, "is_a"));
|
|
1466
|
+
if (parent === void 0 || !Object.hasOwn(classDefs, parent)) return void 0;
|
|
1467
|
+
return parent;
|
|
1468
|
+
}
|
|
1469
|
+
function inheritedSlots(classKey) {
|
|
1470
|
+
const out = /* @__PURE__ */ new Set();
|
|
1471
|
+
const seen = /* @__PURE__ */ new Set([classKey]);
|
|
1472
|
+
let key2 = parentKeyOf(classKey);
|
|
1473
|
+
while (key2 !== void 0 && !seen.has(key2)) {
|
|
1474
|
+
seen.add(key2);
|
|
1475
|
+
for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) out.add(slot);
|
|
1476
|
+
key2 = parentKeyOf(key2);
|
|
1477
|
+
}
|
|
1478
|
+
return out;
|
|
1479
|
+
}
|
|
1480
|
+
function fieldKeysOf(classKey) {
|
|
1481
|
+
const def = asRecord(get(classDefs, classKey)) ?? {};
|
|
1482
|
+
const keys = slotKeysOf(def);
|
|
1483
|
+
const usage = asRecord(get(def, "slot_usage"));
|
|
1484
|
+
if (usage === void 0) return keys;
|
|
1485
|
+
const inherited = inheritedSlots(classKey);
|
|
1486
|
+
for (const slot of Object.keys(usage)) {
|
|
1487
|
+
if (inherited.has(slot) && !keys.includes(slot)) keys.push(slot);
|
|
1488
|
+
}
|
|
1489
|
+
return keys;
|
|
1490
|
+
}
|
|
1295
1491
|
function listedSomewhere(slot) {
|
|
1296
1492
|
for (const key2 of Object.keys(classDefs)) {
|
|
1297
1493
|
const def = asRecord(get(classDefs, key2)) ?? {};
|
|
@@ -1474,7 +1670,7 @@ function notYAML(error) {
|
|
|
1474
1670
|
}
|
|
1475
1671
|
|
|
1476
1672
|
// src/index.ts
|
|
1477
|
-
var VERSION = "0.
|
|
1673
|
+
var VERSION = "0.6.0";
|
|
1478
1674
|
|
|
1479
1675
|
// src/cli.ts
|
|
1480
1676
|
var HELP = `Usage: skiss <command> [options]
|
|
@@ -1483,15 +1679,22 @@ Commands:
|
|
|
1483
1679
|
diagram <file> Write a Mermaid class diagram for a .skiss file.
|
|
1484
1680
|
compile <file> Write a LinkML schema for a .skiss file.
|
|
1485
1681
|
import <file> Write Skiss for a LinkML schema file, YAML or JSON.
|
|
1682
|
+
render <file> Write an SVG or a PNG of the class diagram, through
|
|
1683
|
+
the Mermaid CLI (mmdc), which must be installed.
|
|
1486
1684
|
<file> may be - to read standard input.
|
|
1487
1685
|
|
|
1488
1686
|
Options:
|
|
1489
1687
|
-o, --output <path> Write the output to <path> instead of standard output
|
|
1490
|
-
--notes diagram: emit \`? text\` doubts as Mermaid notes
|
|
1688
|
+
--notes diagram, render: emit \`? text\` doubts as Mermaid notes
|
|
1491
1689
|
--json compile: write the schema as JSON instead of YAML
|
|
1492
1690
|
--name <name> compile: the schema name. Defaults to the file's
|
|
1493
1691
|
basename without its extension, or \`sketch\` for
|
|
1494
1692
|
standard input.
|
|
1693
|
+
--format <fmt> render: svg or png. Defaults to the extension of
|
|
1694
|
+
-o, and to svg when there is no -o. png needs an -o:
|
|
1695
|
+
only the SVG can go to standard output.
|
|
1696
|
+
--scale <n> render: the scale mmdc rasterises at. Defaults to 2
|
|
1697
|
+
for png, and to mmdc's own default for svg.
|
|
1495
1698
|
--strict Exit 1 when the input has any diagnostic; for import,
|
|
1496
1699
|
also when the projection dropped anything
|
|
1497
1700
|
-h, --help Show this help and exit
|
|
@@ -1508,8 +1711,11 @@ Exit codes:
|
|
|
1508
1711
|
0 the output was produced
|
|
1509
1712
|
1 --strict and the input has diagnostics (the output is still written)
|
|
1510
1713
|
2 usage error, missing file, or unreadable input: for import, a text no
|
|
1511
|
-
schema could be read out of, which produces no sketch at all
|
|
1714
|
+
schema could be read out of, which produces no sketch at all; for
|
|
1715
|
+
render, the Mermaid CLI not being installed
|
|
1512
1716
|
70 internal error, a bug in skiss
|
|
1717
|
+
|
|
1718
|
+
render exits with mmdc's own status when mmdc ran and failed.
|
|
1513
1719
|
`;
|
|
1514
1720
|
var EXIT_OK = 0;
|
|
1515
1721
|
var EXIT_STRICT = 1;
|
|
@@ -1518,7 +1724,8 @@ var EXIT_INTERNAL = 70;
|
|
|
1518
1724
|
var COMMANDS = {
|
|
1519
1725
|
diagram: ["output", "notes", "strict"],
|
|
1520
1726
|
compile: ["output", "json", "name", "strict"],
|
|
1521
|
-
import: ["output", "strict"]
|
|
1727
|
+
import: ["output", "strict"],
|
|
1728
|
+
render: ["output", "notes", "format", "scale", "strict"]
|
|
1522
1729
|
};
|
|
1523
1730
|
var isCommand = (text) => Object.hasOwn(COMMANDS, text);
|
|
1524
1731
|
var UsageError = class extends Error {
|
|
@@ -1540,6 +1747,8 @@ async function main(argv) {
|
|
|
1540
1747
|
notes: { type: "boolean" },
|
|
1541
1748
|
json: { type: "boolean" },
|
|
1542
1749
|
name: { type: "string" },
|
|
1750
|
+
format: { type: "string" },
|
|
1751
|
+
scale: { type: "string" },
|
|
1543
1752
|
strict: { type: "boolean" },
|
|
1544
1753
|
help: { type: "boolean", short: "h" },
|
|
1545
1754
|
version: { type: "boolean" }
|
|
@@ -1569,8 +1778,16 @@ async function main(argv) {
|
|
|
1569
1778
|
}
|
|
1570
1779
|
}
|
|
1571
1780
|
const source = file === "-" ? await readStdin() : await readSource(file);
|
|
1572
|
-
const { output, diagnostics, dropped } = runCommand(command, source, file, values);
|
|
1573
1781
|
const label = file === "-" ? "<stdin>" : file;
|
|
1782
|
+
if (command === "render") {
|
|
1783
|
+
const picture = compile(source, { target: "mermaid", notes: values.notes });
|
|
1784
|
+
for (const d of picture.diagnostics) process.stderr.write(`${formatDiagnostic(d, label)}
|
|
1785
|
+
`);
|
|
1786
|
+
const status = await render(picture.output, values);
|
|
1787
|
+
if (status !== EXIT_OK) return status;
|
|
1788
|
+
return values.strict === true && picture.diagnostics.length > 0 ? EXIT_STRICT : EXIT_OK;
|
|
1789
|
+
}
|
|
1790
|
+
const { output, diagnostics, dropped } = runCommand(command, source, file, values);
|
|
1574
1791
|
const report = formatDropped(dropped);
|
|
1575
1792
|
if (report !== "") process.stderr.write(`${report}
|
|
1576
1793
|
`);
|
|
@@ -1625,6 +1842,76 @@ async function writeOutput(path, text) {
|
|
|
1625
1842
|
throw new UsageError(`cannot write \`${path}\`: ${reason(error)}`);
|
|
1626
1843
|
}
|
|
1627
1844
|
}
|
|
1845
|
+
var MMDC_MISSING = "skiss render needs the Mermaid CLI: npm install -g @mermaid-js/mermaid-cli";
|
|
1846
|
+
var isFormat = (text) => text === "svg" || text === "png";
|
|
1847
|
+
async function render(mermaid, values) {
|
|
1848
|
+
const format = renderFormat(values);
|
|
1849
|
+
const scale = renderScale(values.scale, format);
|
|
1850
|
+
const mmdc = await findMmdc();
|
|
1851
|
+
if (mmdc === void 0) {
|
|
1852
|
+
process.stderr.write(`${MMDC_MISSING}
|
|
1853
|
+
`);
|
|
1854
|
+
return EXIT_USAGE;
|
|
1855
|
+
}
|
|
1856
|
+
const dir = await mkdtemp(join(tmpdir(), "skiss-render-"));
|
|
1857
|
+
try {
|
|
1858
|
+
const input = join(dir, "diagram.mmd");
|
|
1859
|
+
await writeFile(input, mermaid, "utf8");
|
|
1860
|
+
const target = values.output ?? join(dir, `diagram.${format}`);
|
|
1861
|
+
const args = ["-i", input, "-o", target, "-e", format, "-b", "transparent"];
|
|
1862
|
+
if (scale !== void 0) args.push("--scale", scale);
|
|
1863
|
+
const run = spawnSync(mmdc, args, {
|
|
1864
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
1865
|
+
encoding: "utf8"
|
|
1866
|
+
});
|
|
1867
|
+
if (run.error !== void 0)
|
|
1868
|
+
throw new UsageError(`cannot run \`${mmdc}\`: ${reason(run.error)}`);
|
|
1869
|
+
if (run.stdout !== "") process.stderr.write(run.stdout);
|
|
1870
|
+
if (run.status !== EXIT_OK) return run.status ?? EXIT_INTERNAL;
|
|
1871
|
+
if (values.output === void 0) process.stdout.write(await readFile(target, "utf8"));
|
|
1872
|
+
return EXIT_OK;
|
|
1873
|
+
} finally {
|
|
1874
|
+
await rm(dir, { recursive: true, force: true });
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function renderFormat(values) {
|
|
1878
|
+
if (values.format !== void 0) {
|
|
1879
|
+
if (!isFormat(values.format)) {
|
|
1880
|
+
throw new UsageError(`unknown \`--format ${values.format}\`: svg or png`);
|
|
1881
|
+
}
|
|
1882
|
+
if (values.format === "png" && values.output === void 0) {
|
|
1883
|
+
throw new UsageError(
|
|
1884
|
+
"render --format png needs -o <path>: a PNG cannot go to standard output"
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
return values.format;
|
|
1888
|
+
}
|
|
1889
|
+
if (values.output === void 0) return "svg";
|
|
1890
|
+
const extension = extname(values.output).slice(1).toLowerCase();
|
|
1891
|
+
if (isFormat(extension)) return extension;
|
|
1892
|
+
throw new UsageError(
|
|
1893
|
+
`cannot tell the format of \`${values.output}\`: give --format svg or --format png`
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
function renderScale(scale, format) {
|
|
1897
|
+
if (scale === void 0) return format === "png" ? "2" : void 0;
|
|
1898
|
+
const value = Number(scale);
|
|
1899
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1900
|
+
throw new UsageError(`\`--scale ${scale}\` is not a positive number`);
|
|
1901
|
+
}
|
|
1902
|
+
return scale;
|
|
1903
|
+
}
|
|
1904
|
+
async function findMmdc() {
|
|
1905
|
+
const onPath = (process.env.PATH ?? "").split(delimiter).filter((dir) => dir !== "").map((dir) => join(dir, "mmdc"));
|
|
1906
|
+
for (const candidate of [...onPath, join(process.cwd(), "node_modules", ".bin", "mmdc")]) {
|
|
1907
|
+
try {
|
|
1908
|
+
await access(candidate, constants.X_OK);
|
|
1909
|
+
return candidate;
|
|
1910
|
+
} catch {
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
return void 0;
|
|
1914
|
+
}
|
|
1628
1915
|
function reason(error) {
|
|
1629
1916
|
if (error instanceof Error) {
|
|
1630
1917
|
const code = error.code;
|