@eriknaslund/skiss 0.7.0 → 0.8.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 +21 -4
- package/dist/cli.js +228 -27
- package/dist/index.d.ts +35 -9
- package/dist/index.js +208 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Skiss is text you can type while someone talks. A field is one line. A change is
|
|
|
46
46
|
- **`@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.
|
|
47
47
|
- **`~` 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.
|
|
48
48
|
- **`<` 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`.
|
|
49
|
-
- **`#` describes, `?` doubts.** A description ends up in generated documentation. A doubt never does.
|
|
49
|
+
- **`#` describes, `?` doubts.** A description ends up in generated documentation. A doubt never does. A description too long for the line continues on indented `#` lines under it, and the `#` block at the top of the file describes the whole sketch.
|
|
50
50
|
- **Names are ASCII.** `A`–`Z`, `a`–`z`, `0`–`9`, and nothing else: a name becomes an identifier and a URI in LinkML, a class in generated Python, a node in a diagram, and none of those take the rest of the alphabet. The words of the domain, in whatever alphabet they are spoken, go in the `#` description ([SPEC §4](docs/SPEC.md#4-grammar)).
|
|
51
51
|
- **Every line stands on its own.** A half-typed line breaks that line and nothing else, so the diagram keeps rendering while you type.
|
|
52
52
|
- **Everything you can write compiles to valid LinkML.** Skiss is a strict subset. Nothing is lost on the way up.
|
|
@@ -72,9 +72,26 @@ Or without installing: `npx @eriknaslund/skiss diagram model.skiss`.
|
|
|
72
72
|
|
|
73
73
|
`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`.
|
|
74
74
|
|
|
75
|
+
`skiss glossary model.skiss` writes what the sketch says about the words in it, as Markdown: one section per class, with its fields in a table and the `#` descriptions as the definitions. A sketch is where each thing is named, so the terminology page is generated from it rather than kept beside it and allowed to drift.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
# A build and release pipeline
|
|
79
|
+
|
|
80
|
+
## Environment
|
|
81
|
+
|
|
82
|
+
System: Platform. One installation of the platform, with its own data.
|
|
83
|
+
|
|
84
|
+
| Field | Type | Description |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| id | identifier | |
|
|
87
|
+
| purpose | build / test / playtest / live | |
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`--classes` writes the terms alone, as a `| Term | Definition |` table, which is the shape a glossary page usually wants; `--notes` adds the `? ` doubts as open questions, which nothing else in the output shows, because a doubt is never documentation. `--name` is the heading, and `-o`, `-`, `--strict` and the diagnostics work exactly as they do for `compile`.
|
|
91
|
+
|
|
75
92
|
`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.
|
|
76
93
|
|
|
77
|
-
`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.
|
|
94
|
+
`npm install @eriknaslund/skiss` also gives you the library: `compile`, `formatDiagnostic`, `parse`, `resolve`, `toLinkML`, `serialize`, `toMermaid`, `toGlossary`, `toSkiss`, `fromLinkML`, `formatDropped`, `importLinkML`, `tokenizeLine` and `VERSION`, browser-safe.
|
|
78
95
|
|
|
79
96
|
`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)).
|
|
80
97
|
|
|
@@ -94,9 +111,9 @@ Editors that render Skiss live are separate projects built on this package. This
|
|
|
94
111
|
|
|
95
112
|
## Status
|
|
96
113
|
|
|
97
|
-
0.
|
|
114
|
+
0.8.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.
|
|
98
115
|
|
|
99
|
-
The [specification](docs/SPEC.md) is at 0.
|
|
116
|
+
The [specification](docs/SPEC.md) is at 0.4.0 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.
|
|
100
117
|
|
|
101
118
|
## License
|
|
102
119
|
|
package/dist/cli.js
CHANGED
|
@@ -127,19 +127,47 @@ function primitiveFor(word) {
|
|
|
127
127
|
var DROPPED = "dropped-class";
|
|
128
128
|
var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
|
|
129
129
|
var CLASS_ORDER = "name, `< Parent`, `@System`, `~ Class`, then `#` and `?`";
|
|
130
|
+
var NO_ELEMENT_ABOVE = "A description line needs a class or a field above it";
|
|
130
131
|
function parse(source) {
|
|
131
132
|
const classes = [];
|
|
132
133
|
const diagnostics = [];
|
|
133
134
|
let current;
|
|
135
|
+
let target;
|
|
136
|
+
const header = [];
|
|
137
|
+
let inHeader = true;
|
|
134
138
|
const text0 = source.startsWith("\uFEFF") ? source.slice(1) : source;
|
|
135
139
|
const lines = text0.split(/\r\n|\n|\r/);
|
|
136
140
|
for (let i = 0; i < lines.length; i++) {
|
|
137
141
|
const line = i + 1;
|
|
138
142
|
const text = (lines[i] ?? "").replace(/[ \t]+$/, "");
|
|
139
143
|
const first = text[0];
|
|
140
|
-
if (first === void 0
|
|
144
|
+
if (first === void 0) {
|
|
145
|
+
inHeader = false;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (first === "#") {
|
|
149
|
+
if (inHeader) header.push(headerLine(text));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
inHeader = false;
|
|
141
153
|
if (isWs(first)) {
|
|
142
154
|
const start = text.search(/[^ \t]/);
|
|
155
|
+
if (text[start] === "#" || isDoubtMarker(text, start)) {
|
|
156
|
+
if (current === DROPPED) continue;
|
|
157
|
+
if (target === void 0) {
|
|
158
|
+
diagnostics.push({
|
|
159
|
+
severity: "error",
|
|
160
|
+
code: "E_UNPARSABLE",
|
|
161
|
+
message: NO_ELEMENT_ABOVE,
|
|
162
|
+
line,
|
|
163
|
+
col: start,
|
|
164
|
+
end: text.length
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
continueTrailer(target, splitTrailer(text));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
143
171
|
if (current === void 0) {
|
|
144
172
|
diagnostics.push({
|
|
145
173
|
severity: "error",
|
|
@@ -153,7 +181,10 @@ function parse(source) {
|
|
|
153
181
|
}
|
|
154
182
|
const result = parseFieldLine(text, line);
|
|
155
183
|
if (!result.ok) diagnostics.push(result.diagnostic);
|
|
156
|
-
else if (current !== DROPPED)
|
|
184
|
+
else if (current !== DROPPED) {
|
|
185
|
+
current.fields.push(result.value);
|
|
186
|
+
target = result.value;
|
|
187
|
+
}
|
|
157
188
|
continue;
|
|
158
189
|
}
|
|
159
190
|
if (/\p{L}/u.test(first)) {
|
|
@@ -161,9 +192,11 @@ function parse(source) {
|
|
|
161
192
|
if (result.ok) {
|
|
162
193
|
classes.push(result.value);
|
|
163
194
|
current = result.value;
|
|
195
|
+
target = result.value;
|
|
164
196
|
} else {
|
|
165
197
|
diagnostics.push(result.diagnostic);
|
|
166
198
|
current = result.atName ? DROPPED : void 0;
|
|
199
|
+
target = void 0;
|
|
167
200
|
}
|
|
168
201
|
continue;
|
|
169
202
|
}
|
|
@@ -176,7 +209,14 @@ function parse(source) {
|
|
|
176
209
|
end: text.length
|
|
177
210
|
});
|
|
178
211
|
}
|
|
179
|
-
|
|
212
|
+
const document = { classes, diagnostics };
|
|
213
|
+
const description = header.filter((part) => part !== "").join(" ");
|
|
214
|
+
if (description !== "") document.description = description;
|
|
215
|
+
return document;
|
|
216
|
+
}
|
|
217
|
+
function headerLine(text) {
|
|
218
|
+
const rest = text.slice(1);
|
|
219
|
+
return isWs(rest[0]) ? rest.slice(1) : rest;
|
|
180
220
|
}
|
|
181
221
|
function isDoubtMarker(text, i) {
|
|
182
222
|
return text[i] === "?" && isWs(text[i - 1]) && (i + 1 === text.length || isWs(text[i + 1]));
|
|
@@ -212,6 +252,15 @@ function splitTrailer(text) {
|
|
|
212
252
|
}
|
|
213
253
|
return trailer;
|
|
214
254
|
}
|
|
255
|
+
function continueTrailer(node, trailer) {
|
|
256
|
+
if (trailer.description !== void 0) {
|
|
257
|
+
node.description = node.description === void 0 ? trailer.description : `${node.description} ${trailer.description}`;
|
|
258
|
+
}
|
|
259
|
+
if (trailer.note !== void 0) {
|
|
260
|
+
node.note = node.note === void 0 ? trailer.note : `${node.note}
|
|
261
|
+
${trailer.note}`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
215
264
|
var WORD = /[\p{L}\p{N}_-]+/uy;
|
|
216
265
|
function wordAt(text, i) {
|
|
217
266
|
WORD.lastIndex = i;
|
|
@@ -332,8 +381,10 @@ function parseFieldLine(text, line) {
|
|
|
332
381
|
const head = take(c);
|
|
333
382
|
if (head === void 0) {
|
|
334
383
|
const col = text.search(/[^ \t]/);
|
|
335
|
-
|
|
336
|
-
|
|
384
|
+
return fail("E_UNPARSABLE", "A field line needs a field name before its trailer", line, {
|
|
385
|
+
col,
|
|
386
|
+
end: Math.max(col + 1, text.length)
|
|
387
|
+
});
|
|
337
388
|
}
|
|
338
389
|
if (head.kind !== "word") return unexpected(head, line, "field", FIELD_ORDER);
|
|
339
390
|
if (!FIELD_NAME.test(head.text)) {
|
|
@@ -793,6 +844,109 @@ function levenshtein(a, b) {
|
|
|
793
844
|
return previous[b.length] ?? 0;
|
|
794
845
|
}
|
|
795
846
|
|
|
847
|
+
// src/generators/glossary.ts
|
|
848
|
+
var DEFAULT_NAME = "Glossary";
|
|
849
|
+
function toGlossary(doc, opts) {
|
|
850
|
+
const resolved = resolve(doc);
|
|
851
|
+
const notes = opts?.notes === true;
|
|
852
|
+
const declared = new Set(resolved.classes.map((cls) => cls.name.text));
|
|
853
|
+
const blocks = [`# ${opts?.name ?? DEFAULT_NAME}`];
|
|
854
|
+
const description = resolved.description;
|
|
855
|
+
if (description !== void 0 && description !== "") blocks.push(description);
|
|
856
|
+
if (opts?.classes === true) blocks.push(termTable(resolved.classes, notes));
|
|
857
|
+
else for (const cls of resolved.classes) blocks.push(...section(cls, declared, notes));
|
|
858
|
+
return `${blocks.join("\n\n")}
|
|
859
|
+
`;
|
|
860
|
+
}
|
|
861
|
+
function section(cls, declared, notes) {
|
|
862
|
+
const out = [`## ${cls.name.text}`];
|
|
863
|
+
const paragraph = sentencesOf(cls).join(" ");
|
|
864
|
+
if (paragraph !== "") out.push(paragraph);
|
|
865
|
+
if (cls.fields.length > 0) out.push(fieldTable(cls, declared));
|
|
866
|
+
if (notes) {
|
|
867
|
+
const questions = openQuestions(cls);
|
|
868
|
+
if (questions.length > 0) out.push("**Open questions**", questions.join("\n"));
|
|
869
|
+
}
|
|
870
|
+
return out;
|
|
871
|
+
}
|
|
872
|
+
var FIELD_COLUMNS = ["Field", "Type", "Description"];
|
|
873
|
+
function fieldTable(cls, declared) {
|
|
874
|
+
const rows = [row(FIELD_COLUMNS), divider(FIELD_COLUMNS.length)];
|
|
875
|
+
for (const field of cls.fields) {
|
|
876
|
+
rows.push(row([field.name.text, typeCell(field, declared), descriptionCell(field)]));
|
|
877
|
+
}
|
|
878
|
+
return rows.join("\n");
|
|
879
|
+
}
|
|
880
|
+
function typeCell(field, declared) {
|
|
881
|
+
const parts2 = [];
|
|
882
|
+
if (field.identifier) parts2.push("identifier");
|
|
883
|
+
const type = typeText(field.type, declared);
|
|
884
|
+
if (type !== void 0) parts2.push(type);
|
|
885
|
+
return parts2.join(", ");
|
|
886
|
+
}
|
|
887
|
+
function typeText(type, declared) {
|
|
888
|
+
if (type === void 0) return void 0;
|
|
889
|
+
let text;
|
|
890
|
+
switch (type.kind) {
|
|
891
|
+
case "primitive":
|
|
892
|
+
text = type.written.text;
|
|
893
|
+
break;
|
|
894
|
+
case "unknown":
|
|
895
|
+
text = type.name.text;
|
|
896
|
+
break;
|
|
897
|
+
case "class":
|
|
898
|
+
text = declared.has(type.name.text) ? link(type.name.text) : type.name.text;
|
|
899
|
+
break;
|
|
900
|
+
case "enum":
|
|
901
|
+
text = type.values.map((value) => value.text).join(" / ");
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
return type.many ? `list of ${text}` : text;
|
|
905
|
+
}
|
|
906
|
+
var link = (name2) => `[${name2}](#${name2.toLowerCase()})`;
|
|
907
|
+
function descriptionCell(field) {
|
|
908
|
+
const sentences = [];
|
|
909
|
+
if (field.system !== void 0) sentences.push(`System: ${field.system.text}.`);
|
|
910
|
+
if (field.joinsTo !== void 0) {
|
|
911
|
+
sentences.push(`Joins to ${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}.`);
|
|
912
|
+
}
|
|
913
|
+
const description = field.description;
|
|
914
|
+
if (description === void 0 || description === "") return sentences.join(" ");
|
|
915
|
+
if (sentences.length === 0) return description;
|
|
916
|
+
return [stopped(description), ...sentences].join(" ");
|
|
917
|
+
}
|
|
918
|
+
function termTable(classes, notes) {
|
|
919
|
+
const columns = notes ? ["Term", "Definition", "Open questions"] : ["Term", "Definition"];
|
|
920
|
+
const rows = [row(columns), divider(columns.length)];
|
|
921
|
+
for (const cls of classes) {
|
|
922
|
+
const cells = [cls.name.text, sentencesOf(cls).join(" ")];
|
|
923
|
+
if (notes) cells.push(doubtsOf(cls.note).join("<br>"));
|
|
924
|
+
rows.push(row(cells));
|
|
925
|
+
}
|
|
926
|
+
return rows.join("\n");
|
|
927
|
+
}
|
|
928
|
+
function sentencesOf(cls) {
|
|
929
|
+
const out = [];
|
|
930
|
+
if (cls.system !== void 0) out.push(`System: ${cls.system.text}.`);
|
|
931
|
+
if (cls.parent !== void 0) out.push(`Extends ${cls.parent.text}.`);
|
|
932
|
+
if (cls.similarTo !== void 0) out.push(`Similar to ${cls.similarTo.text}.`);
|
|
933
|
+
if (cls.description !== void 0 && cls.description !== "") out.push(sentence(cls.description));
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
var sentence = (text) => stopped(text.charAt(0).toUpperCase() + text.slice(1));
|
|
937
|
+
var stopped = (text) => /[.!?]$/.test(text) ? text : `${text}.`;
|
|
938
|
+
var doubtsOf = (note) => note === void 0 ? [] : note.split("\n").filter((doubt) => doubt !== "");
|
|
939
|
+
function openQuestions(cls) {
|
|
940
|
+
const out = doubtsOf(cls.note).map((doubt) => `- ${doubt}`);
|
|
941
|
+
for (const field of cls.fields) {
|
|
942
|
+
for (const doubt of doubtsOf(field.note)) out.push(`- ${field.name.text}: ${doubt}`);
|
|
943
|
+
}
|
|
944
|
+
return out;
|
|
945
|
+
}
|
|
946
|
+
var row = (cells) => `|${cells.map((cell) => cell === "" ? " " : ` ${cellText(cell)} `).join("|")}|`;
|
|
947
|
+
var divider = (columns) => `|${"---|".repeat(columns)}`;
|
|
948
|
+
var cellText = (text) => text.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
949
|
+
|
|
796
950
|
// src/generators/linkml.ts
|
|
797
951
|
var BASE_URI = "https://example.org";
|
|
798
952
|
var RANGE = {
|
|
@@ -835,9 +989,11 @@ function toLinkML(doc, opts) {
|
|
|
835
989
|
if (system === void 0) return schemaName;
|
|
836
990
|
return systemPrefix.get(system) ?? schemaName;
|
|
837
991
|
};
|
|
992
|
+
const description = resolved.description;
|
|
838
993
|
const schema = {
|
|
839
994
|
id: `${BASE_URI}/${schemaName}`,
|
|
840
995
|
name: schemaName,
|
|
996
|
+
...description === void 0 || description === "" ? {} : { description },
|
|
841
997
|
default_prefix: schemaName,
|
|
842
998
|
default_range: "string",
|
|
843
999
|
prefixes,
|
|
@@ -996,10 +1152,12 @@ function toMermaid(doc, opts) {
|
|
|
996
1152
|
}
|
|
997
1153
|
if (notes) {
|
|
998
1154
|
for (const cls of resolved.classes) {
|
|
999
|
-
|
|
1155
|
+
for (const doubt of doubts(cls.note)) {
|
|
1156
|
+
lines.push(` note for ${cls.name.text} ${quote(doubt)}`);
|
|
1157
|
+
}
|
|
1000
1158
|
for (const field of cls.fields) {
|
|
1001
|
-
|
|
1002
|
-
lines.push(` note for ${cls.name.text} ${quote(`${field.name.text}: ${
|
|
1159
|
+
for (const doubt of doubts(field.note)) {
|
|
1160
|
+
lines.push(` note for ${cls.name.text} ${quote(`${field.name.text}: ${doubt}`)}`);
|
|
1003
1161
|
}
|
|
1004
1162
|
}
|
|
1005
1163
|
}
|
|
@@ -1024,11 +1182,11 @@ function toMermaid(doc, opts) {
|
|
|
1024
1182
|
}
|
|
1025
1183
|
function member(field) {
|
|
1026
1184
|
const marker = field.identifier ? "*" : "+";
|
|
1027
|
-
const type = field.type === void 0 ? "" : `${
|
|
1185
|
+
const type = field.type === void 0 ? "" : `${typeText2(field.type)} `;
|
|
1028
1186
|
const system = field.system === void 0 ? "" : ` @${field.system.text}`;
|
|
1029
1187
|
return `${marker}${type}${field.name.text}${system}`;
|
|
1030
1188
|
}
|
|
1031
|
-
function
|
|
1189
|
+
function typeText2(type) {
|
|
1032
1190
|
let text;
|
|
1033
1191
|
switch (type.kind) {
|
|
1034
1192
|
case "primitive":
|
|
@@ -1044,6 +1202,7 @@ function typeText(type) {
|
|
|
1044
1202
|
}
|
|
1045
1203
|
return type.many ? `${text}[]` : text;
|
|
1046
1204
|
}
|
|
1205
|
+
var doubts = (note) => note === void 0 ? [] : note.split("\n").filter((line) => line !== "");
|
|
1047
1206
|
var quote = (text) => `"${text.replaceAll('"', "'")}"`;
|
|
1048
1207
|
|
|
1049
1208
|
// src/serialize.ts
|
|
@@ -1093,8 +1252,17 @@ function hasControlCharacter(text) {
|
|
|
1093
1252
|
// src/compile.ts
|
|
1094
1253
|
function compile(source, opts) {
|
|
1095
1254
|
const doc = resolve(parse(source));
|
|
1096
|
-
|
|
1097
|
-
|
|
1255
|
+
return { output: generate(doc, opts), diagnostics: doc.diagnostics };
|
|
1256
|
+
}
|
|
1257
|
+
function generate(doc, opts) {
|
|
1258
|
+
switch (opts.target) {
|
|
1259
|
+
case "linkml":
|
|
1260
|
+
return serialize(toLinkML(doc, { schemaName: opts.schemaName }), opts.format ?? "yaml");
|
|
1261
|
+
case "glossary":
|
|
1262
|
+
return toGlossary(doc, { name: opts.name, classes: opts.classes, notes: opts.notes });
|
|
1263
|
+
case "mermaid":
|
|
1264
|
+
return toMermaid(doc, { notes: opts.notes });
|
|
1265
|
+
}
|
|
1098
1266
|
}
|
|
1099
1267
|
|
|
1100
1268
|
// src/diagnostics.ts
|
|
@@ -1106,20 +1274,27 @@ function formatDiagnostic(d, file) {
|
|
|
1106
1274
|
// src/generators/skiss.ts
|
|
1107
1275
|
var TRAILER_COLUMN = 40;
|
|
1108
1276
|
function toSkiss(doc) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1277
|
+
const description = doc.description;
|
|
1278
|
+
const blocks = description === void 0 || description === "" ? [] : [`# ${description}`];
|
|
1279
|
+
for (const cls of doc.classes) blocks.push(classBlock(cls));
|
|
1280
|
+
if (blocks.length === 0) return "";
|
|
1281
|
+
return `${blocks.join("\n\n")}
|
|
1111
1282
|
`;
|
|
1112
1283
|
}
|
|
1113
1284
|
function classBlock(cls) {
|
|
1114
1285
|
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}`);
|
|
1115
|
-
|
|
1286
|
+
const lines = [withTrailer(head, cls), ...doubtLines(cls.note, " ")];
|
|
1287
|
+
for (const field of cls.fields) lines.push(fieldLine(field), ...doubtLines(field.note, " "));
|
|
1288
|
+
return lines.join("\n");
|
|
1116
1289
|
}
|
|
1290
|
+
var doubtsOf2 = (note) => note === void 0 ? [] : note.split("\n").filter((doubt) => doubt !== "");
|
|
1291
|
+
var doubtLines = (note, indent) => doubtsOf2(note).slice(1).map((doubt) => `${indent}? ${doubt}`);
|
|
1117
1292
|
function fieldLine(field) {
|
|
1118
|
-
const type =
|
|
1293
|
+
const type = typeText3(field.type);
|
|
1119
1294
|
const head = ` ${field.name.text}` + (field.identifier ? "*" : "") + (type === void 0 ? "" : `: ${type}`) + (field.system === void 0 ? "" : ` @${field.system.text}`) + (field.joinsTo === void 0 ? "" : ` = ${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}`);
|
|
1120
1295
|
return withTrailer(head, field);
|
|
1121
1296
|
}
|
|
1122
|
-
function
|
|
1297
|
+
function typeText3(type) {
|
|
1123
1298
|
if (type === void 0) return void 0;
|
|
1124
1299
|
let text;
|
|
1125
1300
|
switch (type.kind) {
|
|
@@ -1146,7 +1321,8 @@ function withTrailer(head, node) {
|
|
|
1146
1321
|
const parts2 = [];
|
|
1147
1322
|
if (node.description !== void 0 && node.description !== "")
|
|
1148
1323
|
parts2.push(writeDescription(node.description));
|
|
1149
|
-
|
|
1324
|
+
const [doubt] = doubtsOf2(node.note);
|
|
1325
|
+
if (doubt !== void 0) parts2.push(`? ${doubt}`);
|
|
1150
1326
|
if (parts2.length === 0) return head;
|
|
1151
1327
|
const gap = head.length < TRAILER_COLUMN - 1 ? " ".repeat(TRAILER_COLUMN - head.length) : " ";
|
|
1152
1328
|
return head + gap + parts2.join(" ");
|
|
@@ -1179,6 +1355,8 @@ var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
|
|
|
1179
1355
|
var QUIET_SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
1180
1356
|
"id",
|
|
1181
1357
|
"name",
|
|
1358
|
+
// SPEC §8: the schema's own `description` is the document description.
|
|
1359
|
+
"description",
|
|
1182
1360
|
"prefixes",
|
|
1183
1361
|
"imports",
|
|
1184
1362
|
"default_prefix",
|
|
@@ -1282,7 +1460,7 @@ function fromLinkML(schema) {
|
|
|
1282
1460
|
} else if (system !== void 0) report("annotation", name2, notText("system", system));
|
|
1283
1461
|
const rawNote = annotations.get("note");
|
|
1284
1462
|
if (typeof rawNote === "string") {
|
|
1285
|
-
const note =
|
|
1463
|
+
const note = doubtLines2(rawNote);
|
|
1286
1464
|
if (note !== void 0) node.note = note;
|
|
1287
1465
|
} else if (rawNote !== void 0) report("annotation", name2, notText("note", rawNote));
|
|
1288
1466
|
const parent = stringOf(get(def, "is_a"));
|
|
@@ -1385,7 +1563,10 @@ function fromLinkML(schema) {
|
|
|
1385
1563
|
for (const key2 of Object.keys(root)) {
|
|
1386
1564
|
if (!QUIET_SCHEMA_KEYS.has(key2)) report("schema", schemaName, `\`${key2}\``);
|
|
1387
1565
|
}
|
|
1388
|
-
const
|
|
1566
|
+
const projected = { classes, diagnostics: [] };
|
|
1567
|
+
const schemaDescription = oneLine(stringOf(get(root, "description")));
|
|
1568
|
+
if (schemaDescription !== void 0) projected.description = schemaDescription;
|
|
1569
|
+
const source = toSkiss(projected);
|
|
1389
1570
|
return { document: parse(source), source, dropped };
|
|
1390
1571
|
function buildField(classKey, className, slot, def) {
|
|
1391
1572
|
const name2 = fieldNameOf(classKey, slot);
|
|
@@ -1428,7 +1609,7 @@ function fromLinkML(schema) {
|
|
|
1428
1609
|
} else if (joins !== void 0) report("annotation", element, notText("joins_to", joins));
|
|
1429
1610
|
const rawNote = annotations.get("note");
|
|
1430
1611
|
if (typeof rawNote === "string") {
|
|
1431
|
-
const note =
|
|
1612
|
+
const note = doubtLines2(rawNote);
|
|
1432
1613
|
if (note !== void 0) field.note = note;
|
|
1433
1614
|
} else if (rawNote !== void 0) report("annotation", element, notText("note", rawNote));
|
|
1434
1615
|
for (const other of Object.keys(def)) {
|
|
@@ -1654,6 +1835,10 @@ function oneLine(text) {
|
|
|
1654
1835
|
const line = text.replace(/\s*[\r\n]+\s*/g, " ").trim();
|
|
1655
1836
|
return line === "" ? void 0 : line;
|
|
1656
1837
|
}
|
|
1838
|
+
function doubtLines2(text) {
|
|
1839
|
+
const lines = text.split(/\r\n|\n|\r/).map((line) => line.trim()).filter((line) => line !== "");
|
|
1840
|
+
return lines.length === 0 ? void 0 : lines.join("\n");
|
|
1841
|
+
}
|
|
1657
1842
|
var list = (items) => items.map((item) => `\`${item}\``).join(", ");
|
|
1658
1843
|
var at0 = (text) => ({ text, line: 0, col: 0, end: 0 });
|
|
1659
1844
|
var stringType = (many) => ({
|
|
@@ -1693,7 +1878,7 @@ function notYAML(error) {
|
|
|
1693
1878
|
}
|
|
1694
1879
|
|
|
1695
1880
|
// src/index.ts
|
|
1696
|
-
var VERSION = "0.
|
|
1881
|
+
var VERSION = "0.8.0";
|
|
1697
1882
|
|
|
1698
1883
|
// src/cli.ts
|
|
1699
1884
|
var HELP = `Usage: skiss <command> [options]
|
|
@@ -1701,6 +1886,7 @@ var HELP = `Usage: skiss <command> [options]
|
|
|
1701
1886
|
Commands:
|
|
1702
1887
|
diagram <file> Write a Mermaid class diagram for a .skiss file.
|
|
1703
1888
|
compile <file> Write a LinkML schema for a .skiss file.
|
|
1889
|
+
glossary <file> Write the descriptions of a .skiss file as Markdown.
|
|
1704
1890
|
import <file> Write Skiss for a LinkML schema file, YAML or JSON.
|
|
1705
1891
|
render <file> Write an SVG or a PNG of the class diagram, through
|
|
1706
1892
|
the Mermaid CLI (mmdc), which must be installed.
|
|
@@ -1709,10 +1895,12 @@ Commands:
|
|
|
1709
1895
|
Options:
|
|
1710
1896
|
-o, --output <path> Write the output to <path> instead of standard output
|
|
1711
1897
|
--notes diagram, render: emit \`? text\` doubts as Mermaid notes
|
|
1898
|
+
glossary: list the doubts as open questions
|
|
1712
1899
|
--json compile: write the schema as JSON instead of YAML
|
|
1713
|
-
--
|
|
1714
|
-
|
|
1715
|
-
|
|
1900
|
+
--classes glossary: write the classes alone, as a table of terms
|
|
1901
|
+
--name <name> compile: the schema name; glossary: the heading.
|
|
1902
|
+
Defaults to the file's basename without its
|
|
1903
|
+
extension, or \`sketch\` for standard input.
|
|
1716
1904
|
--format <fmt> render: svg or png. Defaults to the extension of
|
|
1717
1905
|
-o, and to svg when there is no -o. png needs an -o:
|
|
1718
1906
|
only the SVG can go to standard output.
|
|
@@ -1747,6 +1935,7 @@ var EXIT_INTERNAL = 70;
|
|
|
1747
1935
|
var COMMANDS = {
|
|
1748
1936
|
diagram: ["output", "notes", "strict"],
|
|
1749
1937
|
compile: ["output", "json", "name", "strict"],
|
|
1938
|
+
glossary: ["output", "classes", "notes", "name", "strict"],
|
|
1750
1939
|
import: ["output", "strict"],
|
|
1751
1940
|
render: ["output", "notes", "format", "scale", "strict"]
|
|
1752
1941
|
};
|
|
@@ -1769,6 +1958,7 @@ async function main(argv) {
|
|
|
1769
1958
|
output: { type: "string", short: "o" },
|
|
1770
1959
|
notes: { type: "boolean" },
|
|
1771
1960
|
json: { type: "boolean" },
|
|
1961
|
+
classes: { type: "boolean" },
|
|
1772
1962
|
name: { type: "string" },
|
|
1773
1963
|
format: { type: "string" },
|
|
1774
1964
|
scale: { type: "string" },
|
|
@@ -1829,11 +2019,22 @@ function unreadableInput(command, diagnostics, dropped) {
|
|
|
1829
2019
|
}
|
|
1830
2020
|
function runCommand(command, source, file, values) {
|
|
1831
2021
|
if (command === "import") return importLinkML(source);
|
|
2022
|
+
if (command === "glossary") {
|
|
2023
|
+
return {
|
|
2024
|
+
...compile(source, {
|
|
2025
|
+
target: "glossary",
|
|
2026
|
+
name: values.name ?? defaultName(file),
|
|
2027
|
+
classes: values.classes,
|
|
2028
|
+
notes: values.notes
|
|
2029
|
+
}),
|
|
2030
|
+
dropped: []
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
1832
2033
|
if (command === "compile") {
|
|
1833
2034
|
return {
|
|
1834
2035
|
...compile(source, {
|
|
1835
2036
|
target: "linkml",
|
|
1836
|
-
schemaName: values.name ??
|
|
2037
|
+
schemaName: values.name ?? defaultName(file),
|
|
1837
2038
|
format: values.json === true ? "json" : "yaml"
|
|
1838
2039
|
}),
|
|
1839
2040
|
dropped: []
|
|
@@ -1841,7 +2042,7 @@ function runCommand(command, source, file, values) {
|
|
|
1841
2042
|
}
|
|
1842
2043
|
return { ...compile(source, { target: "mermaid", notes: values.notes }), dropped: [] };
|
|
1843
2044
|
}
|
|
1844
|
-
var
|
|
2045
|
+
var defaultName = (file) => file === "-" ? "sketch" : basename(file, extname(file));
|
|
1845
2046
|
async function readSource(file) {
|
|
1846
2047
|
try {
|
|
1847
2048
|
return await readFile(file, "utf8");
|
package/dist/index.d.ts
CHANGED
|
@@ -87,6 +87,14 @@ interface Diagnostic {
|
|
|
87
87
|
interface Document {
|
|
88
88
|
classes: ClassNode[];
|
|
89
89
|
diagnostics: Diagnostic[];
|
|
90
|
+
/**
|
|
91
|
+
* SPEC §3.1: the run of comment lines that starts on the first line of the
|
|
92
|
+
* file, each stripped of its `#` and one space, joined with a single space.
|
|
93
|
+
* Absent when the file does not start with one. It is the only column-0
|
|
94
|
+
* comment the document holds, and it compiles to the schema's own
|
|
95
|
+
* `description` (SPEC §5.1).
|
|
96
|
+
*/
|
|
97
|
+
description?: string;
|
|
90
98
|
/**
|
|
91
99
|
* Class names referenced by `:`, `~` or `=` but never declared, each once,
|
|
92
100
|
* in first-reference order and at its first reference's position. Absent
|
|
@@ -95,6 +103,22 @@ interface Document {
|
|
|
95
103
|
undeclared?: Name[];
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
/** What `resolve` returns: a `Document` whose `undeclared` list is always present. */
|
|
107
|
+
interface ResolvedDocument extends Document {
|
|
108
|
+
undeclared: Name[];
|
|
109
|
+
}
|
|
110
|
+
declare function resolve(doc: Document): ResolvedDocument;
|
|
111
|
+
|
|
112
|
+
interface GlossaryOptions {
|
|
113
|
+
/** The `#` heading. Normally the file name; defaults to `Glossary`. */
|
|
114
|
+
name?: string;
|
|
115
|
+
/** Write the terms alone: one row per class, no fields. Off by default. */
|
|
116
|
+
classes?: boolean;
|
|
117
|
+
/** Add the `? text` doubts as open questions. Off by default. */
|
|
118
|
+
notes?: boolean;
|
|
119
|
+
}
|
|
120
|
+
declare function toGlossary(doc: ResolvedDocument, opts?: GlossaryOptions): string;
|
|
121
|
+
|
|
98
122
|
interface MermaidOptions {
|
|
99
123
|
/** Emit `? text` doubts as `note for` lines. Off by default. */
|
|
100
124
|
notes?: boolean;
|
|
@@ -129,6 +153,8 @@ interface LinkMLEnum {
|
|
|
129
153
|
interface LinkMLSchema {
|
|
130
154
|
id: string;
|
|
131
155
|
name: string;
|
|
156
|
+
/** The document description (SPEC §3.1), written directly after `name`. */
|
|
157
|
+
description?: string;
|
|
132
158
|
default_prefix: string;
|
|
133
159
|
default_range: string;
|
|
134
160
|
prefixes: Record<string, string>;
|
|
@@ -157,12 +183,18 @@ interface LinkMLCompileOptions {
|
|
|
157
183
|
/** Defaults to `yaml`, the form LinkML itself reads. */
|
|
158
184
|
format?: SerializeFormat;
|
|
159
185
|
}
|
|
186
|
+
/** `toGlossary` text from the source (issue #90). */
|
|
187
|
+
interface GlossaryCompileOptions extends GlossaryOptions {
|
|
188
|
+
target: 'glossary';
|
|
189
|
+
/** The heading. Normally the file name, as `schemaName` is. */
|
|
190
|
+
name: string;
|
|
191
|
+
}
|
|
160
192
|
/**
|
|
161
193
|
* A union rather than one shape with optional keys, so `schemaName` is
|
|
162
194
|
* required exactly when it is used and a `mermaid` call cannot carry a
|
|
163
195
|
* `format` that would be ignored.
|
|
164
196
|
*/
|
|
165
|
-
type CompileOptions = MermaidCompileOptions | LinkMLCompileOptions;
|
|
197
|
+
type CompileOptions = MermaidCompileOptions | LinkMLCompileOptions | GlossaryCompileOptions;
|
|
166
198
|
interface CompileResult {
|
|
167
199
|
/** The generated text, `\n`-terminated, whatever the platform. */
|
|
168
200
|
output: string;
|
|
@@ -262,12 +294,6 @@ declare function importLinkML(text: string): ImportResult;
|
|
|
262
294
|
|
|
263
295
|
declare function parse(source: string): Document;
|
|
264
296
|
|
|
265
|
-
|
|
266
|
-
interface ResolvedDocument extends Document {
|
|
267
|
-
undeclared: Name[];
|
|
268
|
-
}
|
|
269
|
-
declare function resolve(doc: Document): ResolvedDocument;
|
|
270
|
-
|
|
271
|
-
declare const VERSION = "0.7.0";
|
|
297
|
+
declare const VERSION = "0.8.0";
|
|
272
298
|
|
|
273
|
-
export { type ClassNode, type CompileOptions, type CompileResult, type Diagnostic, type DiagnosticCode, type Document, type Dropped, type FieldNode, type FromLinkMLResult, type ImportResult, type JoinRef, type LinkMLAnnotations, type LinkMLAttribute, type LinkMLClass, type LinkMLCompileOptions, type LinkMLEnum, type LinkMLOptions, type LinkMLSchema, type MermaidCompileOptions, type MermaidOptions, type Name, type Primitive, type ResolvedDocument, type SerializeFormat, type Severity, type Token, type TokenKind, type TypeRef, VERSION, compile, formatDiagnostic, formatDropped, fromLinkML, importLinkML, parse, resolve, serialize, toLinkML, toMermaid, toSkiss, tokenizeLine };
|
|
299
|
+
export { type ClassNode, type CompileOptions, type CompileResult, type Diagnostic, type DiagnosticCode, type Document, type Dropped, type FieldNode, type FromLinkMLResult, type GlossaryCompileOptions, type GlossaryOptions, type ImportResult, type JoinRef, type LinkMLAnnotations, type LinkMLAttribute, type LinkMLClass, type LinkMLCompileOptions, type LinkMLEnum, type LinkMLOptions, type LinkMLSchema, type MermaidCompileOptions, type MermaidOptions, type Name, type Primitive, type ResolvedDocument, type SerializeFormat, type Severity, type Token, type TokenKind, type TypeRef, VERSION, compile, formatDiagnostic, formatDropped, fromLinkML, importLinkML, parse, resolve, serialize, toGlossary, toLinkML, toMermaid, toSkiss, tokenizeLine };
|
package/dist/index.js
CHANGED
|
@@ -117,19 +117,47 @@ function primitiveFor(word) {
|
|
|
117
117
|
var DROPPED = "dropped-class";
|
|
118
118
|
var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
|
|
119
119
|
var CLASS_ORDER = "name, `< Parent`, `@System`, `~ Class`, then `#` and `?`";
|
|
120
|
+
var NO_ELEMENT_ABOVE = "A description line needs a class or a field above it";
|
|
120
121
|
function parse(source) {
|
|
121
122
|
const classes = [];
|
|
122
123
|
const diagnostics = [];
|
|
123
124
|
let current;
|
|
125
|
+
let target;
|
|
126
|
+
const header = [];
|
|
127
|
+
let inHeader = true;
|
|
124
128
|
const text0 = source.startsWith("\uFEFF") ? source.slice(1) : source;
|
|
125
129
|
const lines = text0.split(/\r\n|\n|\r/);
|
|
126
130
|
for (let i = 0; i < lines.length; i++) {
|
|
127
131
|
const line = i + 1;
|
|
128
132
|
const text = (lines[i] ?? "").replace(/[ \t]+$/, "");
|
|
129
133
|
const first = text[0];
|
|
130
|
-
if (first === void 0
|
|
134
|
+
if (first === void 0) {
|
|
135
|
+
inHeader = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (first === "#") {
|
|
139
|
+
if (inHeader) header.push(headerLine(text));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
inHeader = false;
|
|
131
143
|
if (isWs(first)) {
|
|
132
144
|
const start = text.search(/[^ \t]/);
|
|
145
|
+
if (text[start] === "#" || isDoubtMarker(text, start)) {
|
|
146
|
+
if (current === DROPPED) continue;
|
|
147
|
+
if (target === void 0) {
|
|
148
|
+
diagnostics.push({
|
|
149
|
+
severity: "error",
|
|
150
|
+
code: "E_UNPARSABLE",
|
|
151
|
+
message: NO_ELEMENT_ABOVE,
|
|
152
|
+
line,
|
|
153
|
+
col: start,
|
|
154
|
+
end: text.length
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
continueTrailer(target, splitTrailer(text));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
133
161
|
if (current === void 0) {
|
|
134
162
|
diagnostics.push({
|
|
135
163
|
severity: "error",
|
|
@@ -143,7 +171,10 @@ function parse(source) {
|
|
|
143
171
|
}
|
|
144
172
|
const result = parseFieldLine(text, line);
|
|
145
173
|
if (!result.ok) diagnostics.push(result.diagnostic);
|
|
146
|
-
else if (current !== DROPPED)
|
|
174
|
+
else if (current !== DROPPED) {
|
|
175
|
+
current.fields.push(result.value);
|
|
176
|
+
target = result.value;
|
|
177
|
+
}
|
|
147
178
|
continue;
|
|
148
179
|
}
|
|
149
180
|
if (/\p{L}/u.test(first)) {
|
|
@@ -151,9 +182,11 @@ function parse(source) {
|
|
|
151
182
|
if (result.ok) {
|
|
152
183
|
classes.push(result.value);
|
|
153
184
|
current = result.value;
|
|
185
|
+
target = result.value;
|
|
154
186
|
} else {
|
|
155
187
|
diagnostics.push(result.diagnostic);
|
|
156
188
|
current = result.atName ? DROPPED : void 0;
|
|
189
|
+
target = void 0;
|
|
157
190
|
}
|
|
158
191
|
continue;
|
|
159
192
|
}
|
|
@@ -166,7 +199,14 @@ function parse(source) {
|
|
|
166
199
|
end: text.length
|
|
167
200
|
});
|
|
168
201
|
}
|
|
169
|
-
|
|
202
|
+
const document = { classes, diagnostics };
|
|
203
|
+
const description = header.filter((part) => part !== "").join(" ");
|
|
204
|
+
if (description !== "") document.description = description;
|
|
205
|
+
return document;
|
|
206
|
+
}
|
|
207
|
+
function headerLine(text) {
|
|
208
|
+
const rest = text.slice(1);
|
|
209
|
+
return isWs(rest[0]) ? rest.slice(1) : rest;
|
|
170
210
|
}
|
|
171
211
|
function isDoubtMarker(text, i) {
|
|
172
212
|
return text[i] === "?" && isWs(text[i - 1]) && (i + 1 === text.length || isWs(text[i + 1]));
|
|
@@ -202,6 +242,15 @@ function splitTrailer(text) {
|
|
|
202
242
|
}
|
|
203
243
|
return trailer;
|
|
204
244
|
}
|
|
245
|
+
function continueTrailer(node, trailer) {
|
|
246
|
+
if (trailer.description !== void 0) {
|
|
247
|
+
node.description = node.description === void 0 ? trailer.description : `${node.description} ${trailer.description}`;
|
|
248
|
+
}
|
|
249
|
+
if (trailer.note !== void 0) {
|
|
250
|
+
node.note = node.note === void 0 ? trailer.note : `${node.note}
|
|
251
|
+
${trailer.note}`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
205
254
|
var WORD = /[\p{L}\p{N}_-]+/uy;
|
|
206
255
|
function wordAt(text, i) {
|
|
207
256
|
WORD.lastIndex = i;
|
|
@@ -322,8 +371,10 @@ function parseFieldLine(text, line) {
|
|
|
322
371
|
const head = take(c);
|
|
323
372
|
if (head === void 0) {
|
|
324
373
|
const col = text.search(/[^ \t]/);
|
|
325
|
-
|
|
326
|
-
|
|
374
|
+
return fail("E_UNPARSABLE", "A field line needs a field name before its trailer", line, {
|
|
375
|
+
col,
|
|
376
|
+
end: Math.max(col + 1, text.length)
|
|
377
|
+
});
|
|
327
378
|
}
|
|
328
379
|
if (head.kind !== "word") return unexpected(head, line, "field", FIELD_ORDER);
|
|
329
380
|
if (!FIELD_NAME.test(head.text)) {
|
|
@@ -783,6 +834,109 @@ function levenshtein(a, b) {
|
|
|
783
834
|
return previous[b.length] ?? 0;
|
|
784
835
|
}
|
|
785
836
|
|
|
837
|
+
// src/generators/glossary.ts
|
|
838
|
+
var DEFAULT_NAME = "Glossary";
|
|
839
|
+
function toGlossary(doc, opts) {
|
|
840
|
+
const resolved = resolve(doc);
|
|
841
|
+
const notes = opts?.notes === true;
|
|
842
|
+
const declared = new Set(resolved.classes.map((cls) => cls.name.text));
|
|
843
|
+
const blocks = [`# ${opts?.name ?? DEFAULT_NAME}`];
|
|
844
|
+
const description = resolved.description;
|
|
845
|
+
if (description !== void 0 && description !== "") blocks.push(description);
|
|
846
|
+
if (opts?.classes === true) blocks.push(termTable(resolved.classes, notes));
|
|
847
|
+
else for (const cls of resolved.classes) blocks.push(...section(cls, declared, notes));
|
|
848
|
+
return `${blocks.join("\n\n")}
|
|
849
|
+
`;
|
|
850
|
+
}
|
|
851
|
+
function section(cls, declared, notes) {
|
|
852
|
+
const out = [`## ${cls.name.text}`];
|
|
853
|
+
const paragraph = sentencesOf(cls).join(" ");
|
|
854
|
+
if (paragraph !== "") out.push(paragraph);
|
|
855
|
+
if (cls.fields.length > 0) out.push(fieldTable(cls, declared));
|
|
856
|
+
if (notes) {
|
|
857
|
+
const questions = openQuestions(cls);
|
|
858
|
+
if (questions.length > 0) out.push("**Open questions**", questions.join("\n"));
|
|
859
|
+
}
|
|
860
|
+
return out;
|
|
861
|
+
}
|
|
862
|
+
var FIELD_COLUMNS = ["Field", "Type", "Description"];
|
|
863
|
+
function fieldTable(cls, declared) {
|
|
864
|
+
const rows = [row(FIELD_COLUMNS), divider(FIELD_COLUMNS.length)];
|
|
865
|
+
for (const field of cls.fields) {
|
|
866
|
+
rows.push(row([field.name.text, typeCell(field, declared), descriptionCell(field)]));
|
|
867
|
+
}
|
|
868
|
+
return rows.join("\n");
|
|
869
|
+
}
|
|
870
|
+
function typeCell(field, declared) {
|
|
871
|
+
const parts2 = [];
|
|
872
|
+
if (field.identifier) parts2.push("identifier");
|
|
873
|
+
const type = typeText(field.type, declared);
|
|
874
|
+
if (type !== void 0) parts2.push(type);
|
|
875
|
+
return parts2.join(", ");
|
|
876
|
+
}
|
|
877
|
+
function typeText(type, declared) {
|
|
878
|
+
if (type === void 0) return void 0;
|
|
879
|
+
let text;
|
|
880
|
+
switch (type.kind) {
|
|
881
|
+
case "primitive":
|
|
882
|
+
text = type.written.text;
|
|
883
|
+
break;
|
|
884
|
+
case "unknown":
|
|
885
|
+
text = type.name.text;
|
|
886
|
+
break;
|
|
887
|
+
case "class":
|
|
888
|
+
text = declared.has(type.name.text) ? link(type.name.text) : type.name.text;
|
|
889
|
+
break;
|
|
890
|
+
case "enum":
|
|
891
|
+
text = type.values.map((value) => value.text).join(" / ");
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
return type.many ? `list of ${text}` : text;
|
|
895
|
+
}
|
|
896
|
+
var link = (name2) => `[${name2}](#${name2.toLowerCase()})`;
|
|
897
|
+
function descriptionCell(field) {
|
|
898
|
+
const sentences = [];
|
|
899
|
+
if (field.system !== void 0) sentences.push(`System: ${field.system.text}.`);
|
|
900
|
+
if (field.joinsTo !== void 0) {
|
|
901
|
+
sentences.push(`Joins to ${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}.`);
|
|
902
|
+
}
|
|
903
|
+
const description = field.description;
|
|
904
|
+
if (description === void 0 || description === "") return sentences.join(" ");
|
|
905
|
+
if (sentences.length === 0) return description;
|
|
906
|
+
return [stopped(description), ...sentences].join(" ");
|
|
907
|
+
}
|
|
908
|
+
function termTable(classes, notes) {
|
|
909
|
+
const columns = notes ? ["Term", "Definition", "Open questions"] : ["Term", "Definition"];
|
|
910
|
+
const rows = [row(columns), divider(columns.length)];
|
|
911
|
+
for (const cls of classes) {
|
|
912
|
+
const cells = [cls.name.text, sentencesOf(cls).join(" ")];
|
|
913
|
+
if (notes) cells.push(doubtsOf(cls.note).join("<br>"));
|
|
914
|
+
rows.push(row(cells));
|
|
915
|
+
}
|
|
916
|
+
return rows.join("\n");
|
|
917
|
+
}
|
|
918
|
+
function sentencesOf(cls) {
|
|
919
|
+
const out = [];
|
|
920
|
+
if (cls.system !== void 0) out.push(`System: ${cls.system.text}.`);
|
|
921
|
+
if (cls.parent !== void 0) out.push(`Extends ${cls.parent.text}.`);
|
|
922
|
+
if (cls.similarTo !== void 0) out.push(`Similar to ${cls.similarTo.text}.`);
|
|
923
|
+
if (cls.description !== void 0 && cls.description !== "") out.push(sentence(cls.description));
|
|
924
|
+
return out;
|
|
925
|
+
}
|
|
926
|
+
var sentence = (text) => stopped(text.charAt(0).toUpperCase() + text.slice(1));
|
|
927
|
+
var stopped = (text) => /[.!?]$/.test(text) ? text : `${text}.`;
|
|
928
|
+
var doubtsOf = (note) => note === void 0 ? [] : note.split("\n").filter((doubt) => doubt !== "");
|
|
929
|
+
function openQuestions(cls) {
|
|
930
|
+
const out = doubtsOf(cls.note).map((doubt) => `- ${doubt}`);
|
|
931
|
+
for (const field of cls.fields) {
|
|
932
|
+
for (const doubt of doubtsOf(field.note)) out.push(`- ${field.name.text}: ${doubt}`);
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
var row = (cells) => `|${cells.map((cell) => cell === "" ? " " : ` ${cellText(cell)} `).join("|")}|`;
|
|
937
|
+
var divider = (columns) => `|${"---|".repeat(columns)}`;
|
|
938
|
+
var cellText = (text) => text.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
939
|
+
|
|
786
940
|
// src/generators/linkml.ts
|
|
787
941
|
var BASE_URI = "https://example.org";
|
|
788
942
|
var RANGE = {
|
|
@@ -825,9 +979,11 @@ function toLinkML(doc, opts) {
|
|
|
825
979
|
if (system === void 0) return schemaName;
|
|
826
980
|
return systemPrefix.get(system) ?? schemaName;
|
|
827
981
|
};
|
|
982
|
+
const description = resolved.description;
|
|
828
983
|
const schema = {
|
|
829
984
|
id: `${BASE_URI}/${schemaName}`,
|
|
830
985
|
name: schemaName,
|
|
986
|
+
...description === void 0 || description === "" ? {} : { description },
|
|
831
987
|
default_prefix: schemaName,
|
|
832
988
|
default_range: "string",
|
|
833
989
|
prefixes,
|
|
@@ -986,10 +1142,12 @@ function toMermaid(doc, opts) {
|
|
|
986
1142
|
}
|
|
987
1143
|
if (notes) {
|
|
988
1144
|
for (const cls of resolved.classes) {
|
|
989
|
-
|
|
1145
|
+
for (const doubt of doubts(cls.note)) {
|
|
1146
|
+
lines.push(` note for ${cls.name.text} ${quote(doubt)}`);
|
|
1147
|
+
}
|
|
990
1148
|
for (const field of cls.fields) {
|
|
991
|
-
|
|
992
|
-
lines.push(` note for ${cls.name.text} ${quote(`${field.name.text}: ${
|
|
1149
|
+
for (const doubt of doubts(field.note)) {
|
|
1150
|
+
lines.push(` note for ${cls.name.text} ${quote(`${field.name.text}: ${doubt}`)}`);
|
|
993
1151
|
}
|
|
994
1152
|
}
|
|
995
1153
|
}
|
|
@@ -1014,11 +1172,11 @@ function toMermaid(doc, opts) {
|
|
|
1014
1172
|
}
|
|
1015
1173
|
function member(field) {
|
|
1016
1174
|
const marker = field.identifier ? "*" : "+";
|
|
1017
|
-
const type = field.type === void 0 ? "" : `${
|
|
1175
|
+
const type = field.type === void 0 ? "" : `${typeText2(field.type)} `;
|
|
1018
1176
|
const system = field.system === void 0 ? "" : ` @${field.system.text}`;
|
|
1019
1177
|
return `${marker}${type}${field.name.text}${system}`;
|
|
1020
1178
|
}
|
|
1021
|
-
function
|
|
1179
|
+
function typeText2(type) {
|
|
1022
1180
|
let text;
|
|
1023
1181
|
switch (type.kind) {
|
|
1024
1182
|
case "primitive":
|
|
@@ -1034,6 +1192,7 @@ function typeText(type) {
|
|
|
1034
1192
|
}
|
|
1035
1193
|
return type.many ? `${text}[]` : text;
|
|
1036
1194
|
}
|
|
1195
|
+
var doubts = (note) => note === void 0 ? [] : note.split("\n").filter((line) => line !== "");
|
|
1037
1196
|
var quote = (text) => `"${text.replaceAll('"', "'")}"`;
|
|
1038
1197
|
|
|
1039
1198
|
// src/serialize.ts
|
|
@@ -1083,8 +1242,17 @@ function hasControlCharacter(text) {
|
|
|
1083
1242
|
// src/compile.ts
|
|
1084
1243
|
function compile(source, opts) {
|
|
1085
1244
|
const doc = resolve(parse(source));
|
|
1086
|
-
|
|
1087
|
-
|
|
1245
|
+
return { output: generate(doc, opts), diagnostics: doc.diagnostics };
|
|
1246
|
+
}
|
|
1247
|
+
function generate(doc, opts) {
|
|
1248
|
+
switch (opts.target) {
|
|
1249
|
+
case "linkml":
|
|
1250
|
+
return serialize(toLinkML(doc, { schemaName: opts.schemaName }), opts.format ?? "yaml");
|
|
1251
|
+
case "glossary":
|
|
1252
|
+
return toGlossary(doc, { name: opts.name, classes: opts.classes, notes: opts.notes });
|
|
1253
|
+
case "mermaid":
|
|
1254
|
+
return toMermaid(doc, { notes: opts.notes });
|
|
1255
|
+
}
|
|
1088
1256
|
}
|
|
1089
1257
|
|
|
1090
1258
|
// src/diagnostics.ts
|
|
@@ -1096,20 +1264,27 @@ function formatDiagnostic(d, file) {
|
|
|
1096
1264
|
// src/generators/skiss.ts
|
|
1097
1265
|
var TRAILER_COLUMN = 40;
|
|
1098
1266
|
function toSkiss(doc) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1267
|
+
const description = doc.description;
|
|
1268
|
+
const blocks = description === void 0 || description === "" ? [] : [`# ${description}`];
|
|
1269
|
+
for (const cls of doc.classes) blocks.push(classBlock(cls));
|
|
1270
|
+
if (blocks.length === 0) return "";
|
|
1271
|
+
return `${blocks.join("\n\n")}
|
|
1101
1272
|
`;
|
|
1102
1273
|
}
|
|
1103
1274
|
function classBlock(cls) {
|
|
1104
1275
|
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}`);
|
|
1105
|
-
|
|
1276
|
+
const lines = [withTrailer(head, cls), ...doubtLines(cls.note, " ")];
|
|
1277
|
+
for (const field of cls.fields) lines.push(fieldLine(field), ...doubtLines(field.note, " "));
|
|
1278
|
+
return lines.join("\n");
|
|
1106
1279
|
}
|
|
1280
|
+
var doubtsOf2 = (note) => note === void 0 ? [] : note.split("\n").filter((doubt) => doubt !== "");
|
|
1281
|
+
var doubtLines = (note, indent) => doubtsOf2(note).slice(1).map((doubt) => `${indent}? ${doubt}`);
|
|
1107
1282
|
function fieldLine(field) {
|
|
1108
|
-
const type =
|
|
1283
|
+
const type = typeText3(field.type);
|
|
1109
1284
|
const head = ` ${field.name.text}` + (field.identifier ? "*" : "") + (type === void 0 ? "" : `: ${type}`) + (field.system === void 0 ? "" : ` @${field.system.text}`) + (field.joinsTo === void 0 ? "" : ` = ${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}`);
|
|
1110
1285
|
return withTrailer(head, field);
|
|
1111
1286
|
}
|
|
1112
|
-
function
|
|
1287
|
+
function typeText3(type) {
|
|
1113
1288
|
if (type === void 0) return void 0;
|
|
1114
1289
|
let text;
|
|
1115
1290
|
switch (type.kind) {
|
|
@@ -1136,7 +1311,8 @@ function withTrailer(head, node) {
|
|
|
1136
1311
|
const parts2 = [];
|
|
1137
1312
|
if (node.description !== void 0 && node.description !== "")
|
|
1138
1313
|
parts2.push(writeDescription(node.description));
|
|
1139
|
-
|
|
1314
|
+
const [doubt] = doubtsOf2(node.note);
|
|
1315
|
+
if (doubt !== void 0) parts2.push(`? ${doubt}`);
|
|
1140
1316
|
if (parts2.length === 0) return head;
|
|
1141
1317
|
const gap = head.length < TRAILER_COLUMN - 1 ? " ".repeat(TRAILER_COLUMN - head.length) : " ";
|
|
1142
1318
|
return head + gap + parts2.join(" ");
|
|
@@ -1169,6 +1345,8 @@ var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
|
|
|
1169
1345
|
var QUIET_SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
1170
1346
|
"id",
|
|
1171
1347
|
"name",
|
|
1348
|
+
// SPEC §8: the schema's own `description` is the document description.
|
|
1349
|
+
"description",
|
|
1172
1350
|
"prefixes",
|
|
1173
1351
|
"imports",
|
|
1174
1352
|
"default_prefix",
|
|
@@ -1272,7 +1450,7 @@ function fromLinkML(schema) {
|
|
|
1272
1450
|
} else if (system !== void 0) report("annotation", name2, notText("system", system));
|
|
1273
1451
|
const rawNote = annotations.get("note");
|
|
1274
1452
|
if (typeof rawNote === "string") {
|
|
1275
|
-
const note =
|
|
1453
|
+
const note = doubtLines2(rawNote);
|
|
1276
1454
|
if (note !== void 0) node.note = note;
|
|
1277
1455
|
} else if (rawNote !== void 0) report("annotation", name2, notText("note", rawNote));
|
|
1278
1456
|
const parent = stringOf(get(def, "is_a"));
|
|
@@ -1375,7 +1553,10 @@ function fromLinkML(schema) {
|
|
|
1375
1553
|
for (const key2 of Object.keys(root)) {
|
|
1376
1554
|
if (!QUIET_SCHEMA_KEYS.has(key2)) report("schema", schemaName, `\`${key2}\``);
|
|
1377
1555
|
}
|
|
1378
|
-
const
|
|
1556
|
+
const projected = { classes, diagnostics: [] };
|
|
1557
|
+
const schemaDescription = oneLine(stringOf(get(root, "description")));
|
|
1558
|
+
if (schemaDescription !== void 0) projected.description = schemaDescription;
|
|
1559
|
+
const source = toSkiss(projected);
|
|
1379
1560
|
return { document: parse(source), source, dropped };
|
|
1380
1561
|
function buildField(classKey, className, slot, def) {
|
|
1381
1562
|
const name2 = fieldNameOf(classKey, slot);
|
|
@@ -1418,7 +1599,7 @@ function fromLinkML(schema) {
|
|
|
1418
1599
|
} else if (joins !== void 0) report("annotation", element, notText("joins_to", joins));
|
|
1419
1600
|
const rawNote = annotations.get("note");
|
|
1420
1601
|
if (typeof rawNote === "string") {
|
|
1421
|
-
const note =
|
|
1602
|
+
const note = doubtLines2(rawNote);
|
|
1422
1603
|
if (note !== void 0) field.note = note;
|
|
1423
1604
|
} else if (rawNote !== void 0) report("annotation", element, notText("note", rawNote));
|
|
1424
1605
|
for (const other of Object.keys(def)) {
|
|
@@ -1644,6 +1825,10 @@ function oneLine(text) {
|
|
|
1644
1825
|
const line = text.replace(/\s*[\r\n]+\s*/g, " ").trim();
|
|
1645
1826
|
return line === "" ? void 0 : line;
|
|
1646
1827
|
}
|
|
1828
|
+
function doubtLines2(text) {
|
|
1829
|
+
const lines = text.split(/\r\n|\n|\r/).map((line) => line.trim()).filter((line) => line !== "");
|
|
1830
|
+
return lines.length === 0 ? void 0 : lines.join("\n");
|
|
1831
|
+
}
|
|
1647
1832
|
var list = (items) => items.map((item) => `\`${item}\``).join(", ");
|
|
1648
1833
|
var at0 = (text) => ({ text, line: 0, col: 0, end: 0 });
|
|
1649
1834
|
var stringType = (many) => ({
|
|
@@ -1881,7 +2066,7 @@ function notYAML(error) {
|
|
|
1881
2066
|
}
|
|
1882
2067
|
|
|
1883
2068
|
// src/index.ts
|
|
1884
|
-
var VERSION = "0.
|
|
2069
|
+
var VERSION = "0.8.0";
|
|
1885
2070
|
export {
|
|
1886
2071
|
VERSION,
|
|
1887
2072
|
compile,
|
|
@@ -1892,6 +2077,7 @@ export {
|
|
|
1892
2077
|
parse,
|
|
1893
2078
|
resolve,
|
|
1894
2079
|
serialize,
|
|
2080
|
+
toGlossary,
|
|
1895
2081
|
toLinkML,
|
|
1896
2082
|
toMermaid,
|
|
1897
2083
|
toSkiss,
|