@eriknaslund/skiss 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/cli.js +60 -32
- package/dist/index.d.ts +47 -2
- package/dist/index.js +271 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ Skiss is text you can type while someone talks. A field is one line. A change is
|
|
|
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
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`.
|
|
45
45
|
- **`#` describes, `?` doubts.** A description ends up in generated documentation. A doubt never does.
|
|
46
|
+
- **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)).
|
|
46
47
|
- **Every line stands on its own.** A half-typed line breaks that line and nothing else, so the diagram keeps rendering while you type.
|
|
47
48
|
- **Everything you can write compiles to valid LinkML.** Skiss is a strict subset. Nothing is lost on the way up.
|
|
48
49
|
|
|
@@ -69,9 +70,11 @@ Or without installing: `npx @eriknaslund/skiss diagram model.skiss`.
|
|
|
69
70
|
|
|
70
71
|
`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
|
|
|
72
|
-
`npm install @eriknaslund/skiss` also gives you the library: `compile`, `formatDiagnostic`, `parse`, `resolve`, `toLinkML`, `serialize`, `toMermaid`, `toSkiss`, `fromLinkML`, `formatDropped`, `importLinkML` and `VERSION`, browser-safe.
|
|
73
|
+
`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.
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
`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)).
|
|
76
|
+
|
|
77
|
+
Editors that render Skiss live are separate projects built on this package. This repository is the language, the library and the command line. To try the language without installing anything, open the [playground](https://erik-naslund.github.io/skiss-playground/): write a sketch, watch the diagram, share a link. Inside Obsidian, the [obsidian-skiss](https://github.com/erik-naslund/obsidian-skiss) plugin renders `skiss` code blocks in your notes.
|
|
75
78
|
|
|
76
79
|
## Where to look
|
|
77
80
|
|
|
@@ -87,9 +90,9 @@ Editors that render Skiss live are separate projects built on this package. This
|
|
|
87
90
|
|
|
88
91
|
## Status
|
|
89
92
|
|
|
90
|
-
0.
|
|
93
|
+
0.6.1, 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.
|
|
91
94
|
|
|
92
|
-
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.
|
|
95
|
+
The [specification](docs/SPEC.md) is at 0.3.1 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.
|
|
93
96
|
|
|
94
97
|
## License
|
|
95
98
|
|
package/dist/cli.js
CHANGED
|
@@ -94,6 +94,21 @@ var CLASS_NAME = /^[A-Z][A-Za-z0-9]*$/;
|
|
|
94
94
|
var FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
|
|
95
95
|
var SYSTEM_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
96
96
|
var ENUM_VALUE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
97
|
+
var NAME_CHAR = /^[A-Za-z0-9]$/;
|
|
98
|
+
var WORD_CHAR = /^[A-Za-z0-9_-]$/;
|
|
99
|
+
var CLASS_RULE = "UpperCamelCase in ASCII letters and digits (A-Z, a-z, 0-9), uppercase first";
|
|
100
|
+
var FIELD_RULE = "lowerCamelCase in ASCII letters and digits (a-z, A-Z, 0-9), lowercase first";
|
|
101
|
+
function nameFault(text, allowed) {
|
|
102
|
+
for (const ch of text) {
|
|
103
|
+
if (!allowed.test(ch)) return `\`${ch}\` is not one of them.`;
|
|
104
|
+
}
|
|
105
|
+
const first = text.slice(0, 1);
|
|
106
|
+
const kind = /[A-Z]/.test(first) ? "an uppercase letter" : /[a-z]/.test(first) ? "a lowercase letter" : /[0-9]/.test(first) ? "a digit" : `\`${first}\``;
|
|
107
|
+
return `\`${text}\` starts with ${kind}.`;
|
|
108
|
+
}
|
|
109
|
+
function badName(rule, text, allowed = NAME_CHAR) {
|
|
110
|
+
return `${rule}: \`${text}\`. ${nameFault(text, allowed)}`;
|
|
111
|
+
}
|
|
97
112
|
var PRIMITIVES = {
|
|
98
113
|
string: "string",
|
|
99
114
|
int: "int",
|
|
@@ -109,6 +124,7 @@ var PRIMITIVES = {
|
|
|
109
124
|
function primitiveFor(word) {
|
|
110
125
|
return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
|
|
111
126
|
}
|
|
127
|
+
var DROPPED = "dropped-class";
|
|
112
128
|
var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
|
|
113
129
|
var CLASS_ORDER = "name, `< Parent`, `@System`, `~ Class`, then `#` and `?`";
|
|
114
130
|
function parse(source) {
|
|
@@ -136,8 +152,8 @@ function parse(source) {
|
|
|
136
152
|
continue;
|
|
137
153
|
}
|
|
138
154
|
const result = parseFieldLine(text, line);
|
|
139
|
-
if (result.ok)
|
|
140
|
-
else
|
|
155
|
+
if (!result.ok) diagnostics.push(result.diagnostic);
|
|
156
|
+
else if (current !== DROPPED) current.fields.push(result.value);
|
|
141
157
|
continue;
|
|
142
158
|
}
|
|
143
159
|
if (/\p{L}/u.test(first)) {
|
|
@@ -147,7 +163,7 @@ function parse(source) {
|
|
|
147
163
|
current = result.value;
|
|
148
164
|
} else {
|
|
149
165
|
diagnostics.push(result.diagnostic);
|
|
150
|
-
current = void 0;
|
|
166
|
+
current = result.atName ? DROPPED : void 0;
|
|
151
167
|
}
|
|
152
168
|
continue;
|
|
153
169
|
}
|
|
@@ -197,6 +213,10 @@ function splitTrailer(text) {
|
|
|
197
213
|
return trailer;
|
|
198
214
|
}
|
|
199
215
|
var WORD = /[\p{L}\p{N}_-]+/uy;
|
|
216
|
+
function wordAt(text, i) {
|
|
217
|
+
WORD.lastIndex = i;
|
|
218
|
+
return WORD.exec(text)?.[0];
|
|
219
|
+
}
|
|
200
220
|
var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "<", ",", "["];
|
|
201
221
|
function tokenize(head) {
|
|
202
222
|
const tokens = [];
|
|
@@ -207,11 +227,10 @@ function tokenize(head) {
|
|
|
207
227
|
i++;
|
|
208
228
|
continue;
|
|
209
229
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
i += word[0].length;
|
|
230
|
+
const word = wordAt(head, i);
|
|
231
|
+
if (word !== void 0) {
|
|
232
|
+
tokens.push({ kind: "word", text: word, col: i, end: i + word.length });
|
|
233
|
+
i += word.length;
|
|
215
234
|
continue;
|
|
216
235
|
}
|
|
217
236
|
if (c === "[" && head[i + 1] === "]") {
|
|
@@ -233,10 +252,11 @@ function take(c) {
|
|
|
233
252
|
return t;
|
|
234
253
|
}
|
|
235
254
|
var name = (t, line) => ({ text: t.text, line, col: t.col, end: t.end });
|
|
236
|
-
function fail(code, message, line, span) {
|
|
255
|
+
function fail(code, message, line, span, atName = false) {
|
|
237
256
|
return {
|
|
238
257
|
ok: false,
|
|
239
|
-
diagnostic: { severity: "error", code, message, line, col: span.col, end: span.end }
|
|
258
|
+
diagnostic: { severity: "error", code, message, line, col: span.col, end: span.end },
|
|
259
|
+
atName
|
|
240
260
|
};
|
|
241
261
|
}
|
|
242
262
|
var afterSpan = (after) => ({ col: after.end, end: after.end });
|
|
@@ -245,17 +265,21 @@ function parseClassLine(text, line) {
|
|
|
245
265
|
const c = { tokens: tokenize(trailer.head), pos: 0, line };
|
|
246
266
|
const head = take(c);
|
|
247
267
|
if (head === void 0 || head.kind !== "word") {
|
|
248
|
-
return fail(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
268
|
+
return fail(
|
|
269
|
+
"E_UNPARSABLE",
|
|
270
|
+
"A class line must start with a class name",
|
|
271
|
+
line,
|
|
272
|
+
{ col: 0, end: Math.max(1, trailer.head.length) },
|
|
273
|
+
true
|
|
274
|
+
);
|
|
252
275
|
}
|
|
253
276
|
if (!CLASS_NAME.test(head.text)) {
|
|
254
277
|
return fail(
|
|
255
278
|
"E_BAD_NAME",
|
|
256
|
-
`Class names are
|
|
279
|
+
badName(`Class names are ${CLASS_RULE}`, head.text),
|
|
257
280
|
line,
|
|
258
|
-
head
|
|
281
|
+
head,
|
|
282
|
+
true
|
|
259
283
|
);
|
|
260
284
|
}
|
|
261
285
|
const node = { name: name(head, line), line };
|
|
@@ -288,9 +312,10 @@ function parseClassLine(text, line) {
|
|
|
288
312
|
if (!CLASS_NAME.test(target.text)) {
|
|
289
313
|
return fail(
|
|
290
314
|
"E_BAD_NAME",
|
|
291
|
-
`The class after \`~\` must be
|
|
315
|
+
badName(`The class after \`~\` must be ${CLASS_RULE}`, target.text),
|
|
292
316
|
line,
|
|
293
|
-
target
|
|
317
|
+
target,
|
|
318
|
+
true
|
|
294
319
|
);
|
|
295
320
|
}
|
|
296
321
|
node.similarTo = name(target, line);
|
|
@@ -312,12 +337,7 @@ function parseFieldLine(text, line) {
|
|
|
312
337
|
}
|
|
313
338
|
if (head.kind !== "word") return unexpected(head, line, "field", FIELD_ORDER);
|
|
314
339
|
if (!FIELD_NAME.test(head.text)) {
|
|
315
|
-
return fail(
|
|
316
|
-
"E_BAD_NAME",
|
|
317
|
-
`Field names are lowerCamelCase (letters and digits, lowercase first): \`${head.text}\``,
|
|
318
|
-
line,
|
|
319
|
-
head
|
|
320
|
-
);
|
|
340
|
+
return fail("E_BAD_NAME", badName(`Field names are ${FIELD_RULE}`, head.text), line, head);
|
|
321
341
|
}
|
|
322
342
|
const node = { name: name(head, line), line, identifier: false };
|
|
323
343
|
const star = peek(c);
|
|
@@ -356,7 +376,7 @@ function parseFieldLine(text, line) {
|
|
|
356
376
|
if (!CLASS_NAME.test(className.text)) {
|
|
357
377
|
return fail(
|
|
358
378
|
"E_BAD_NAME",
|
|
359
|
-
`The class after \`=\` must be
|
|
379
|
+
badName(`The class after \`=\` must be ${CLASS_RULE}`, className.text),
|
|
360
380
|
line,
|
|
361
381
|
className
|
|
362
382
|
);
|
|
@@ -382,7 +402,7 @@ function parseFieldLine(text, line) {
|
|
|
382
402
|
if (!FIELD_NAME.test(fieldName.text)) {
|
|
383
403
|
return fail(
|
|
384
404
|
"E_BAD_NAME",
|
|
385
|
-
`The field after \`= ${className.text}.\` must be
|
|
405
|
+
badName(`The field after \`= ${className.text}.\` must be ${FIELD_RULE}`, fieldName.text),
|
|
386
406
|
line,
|
|
387
407
|
fieldName
|
|
388
408
|
);
|
|
@@ -406,8 +426,8 @@ function parseParent(c, lt) {
|
|
|
406
426
|
);
|
|
407
427
|
}
|
|
408
428
|
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
|
|
410
|
-
return fail("E_BAD_NAME", message, c.line, parent);
|
|
429
|
+
const message = Object.hasOwn(PRIMITIVES, parent.text) ? `\`${parent.text}\` is a primitive, not a class; a parent is a class` : badName(`The class after \`<\` must be ${CLASS_RULE}`, parent.text);
|
|
430
|
+
return fail("E_BAD_NAME", message, c.line, parent, true);
|
|
411
431
|
}
|
|
412
432
|
const next = peek(c);
|
|
413
433
|
if (next?.kind === "," || next?.kind === "<") {
|
|
@@ -433,7 +453,11 @@ function parseSystem(c, at) {
|
|
|
433
453
|
if (!SYSTEM_NAME.test(system.text)) {
|
|
434
454
|
return fail(
|
|
435
455
|
"E_UNPARSABLE",
|
|
436
|
-
|
|
456
|
+
badName(
|
|
457
|
+
"A system name is ASCII letters, digits, `-` and `_` (A-Z, a-z, 0-9), starting with a letter",
|
|
458
|
+
system.text,
|
|
459
|
+
WORD_CHAR
|
|
460
|
+
),
|
|
437
461
|
c.line,
|
|
438
462
|
system
|
|
439
463
|
);
|
|
@@ -491,7 +515,11 @@ function parseType(c, colon) {
|
|
|
491
515
|
if (!ENUM_VALUE.test(w.text)) {
|
|
492
516
|
return fail(
|
|
493
517
|
"E_UNPARSABLE",
|
|
494
|
-
|
|
518
|
+
badName(
|
|
519
|
+
"An enum value is ASCII letters, digits, `-` and `_` (A-Z, a-z, 0-9), and does not start with `-` or `_`",
|
|
520
|
+
w.text,
|
|
521
|
+
WORD_CHAR
|
|
522
|
+
),
|
|
495
523
|
c.line,
|
|
496
524
|
w
|
|
497
525
|
);
|
|
@@ -511,7 +539,7 @@ function parseType(c, colon) {
|
|
|
511
539
|
if (!CLASS_NAME.test(word)) {
|
|
512
540
|
return fail(
|
|
513
541
|
"E_BAD_NAME",
|
|
514
|
-
`A referenced class must be
|
|
542
|
+
badName(`A referenced class must be ${CLASS_RULE}`, word),
|
|
515
543
|
c.line,
|
|
516
544
|
first
|
|
517
545
|
);
|
|
@@ -1667,7 +1695,7 @@ function notYAML(error) {
|
|
|
1667
1695
|
}
|
|
1668
1696
|
|
|
1669
1697
|
// src/index.ts
|
|
1670
|
-
var VERSION = "0.
|
|
1698
|
+
var VERSION = "0.6.1";
|
|
1671
1699
|
|
|
1672
1700
|
// src/cli.ts
|
|
1673
1701
|
var HELP = `Usage: skiss <command> [options]
|
package/dist/index.d.ts
CHANGED
|
@@ -205,6 +205,51 @@ declare function formatDropped(dropped: Dropped[]): string;
|
|
|
205
205
|
|
|
206
206
|
declare function toSkiss(doc: Document): string;
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Where the words of a Skiss line are, so an editor can colour them.
|
|
210
|
+
*
|
|
211
|
+
* This is not a parser and holds no language logic (AGENTS.md §2): nothing
|
|
212
|
+
* here decides whether a line is valid, what it means or what it compiles to.
|
|
213
|
+
* It reads the shapes of SPEC §3 and §4 far enough to say "this run of
|
|
214
|
+
* characters is a class name, that one is an operator", and says nothing at
|
|
215
|
+
* all about a run it does not recognise, which is what leaves a half-typed
|
|
216
|
+
* line looking calm instead of wrong. Diagnostics stay the compiler's, and
|
|
217
|
+
* reach the editor through the gutter.
|
|
218
|
+
*
|
|
219
|
+
* Every line is read on its own, as the language is (SPEC §4): no state is
|
|
220
|
+
* carried from one line to the next, so an edit is never more than one line
|
|
221
|
+
* of work.
|
|
222
|
+
*
|
|
223
|
+
* The productions come from `parse.ts` — the name shapes, the primitive
|
|
224
|
+
* table, what a word is and where a trailer starts — so that the colours and
|
|
225
|
+
* the compiler can never disagree about them (ADR 0010).
|
|
226
|
+
*/
|
|
227
|
+
/** What a run of characters is, as far as colour is concerned. */
|
|
228
|
+
type TokenKind = 'class' | 'field' | 'system' | 'primitive'
|
|
229
|
+
/** A lowercase word after `:` that is no primitive. The compiler warns; the colour does not. */
|
|
230
|
+
| 'type' | 'enum'
|
|
231
|
+
/** The operators of SPEC §4, `<` included, and the `.` of a `= Class.field` join. */
|
|
232
|
+
| 'operator'
|
|
233
|
+
/** The `*` that marks the identifier. */
|
|
234
|
+
| 'marker'
|
|
235
|
+
/** A `#` trailer, the `#` included. */
|
|
236
|
+
| 'description'
|
|
237
|
+
/** A `?` trailer, the `?` included. */
|
|
238
|
+
| 'doubt'
|
|
239
|
+
/** A whole `#` line at column 0. */
|
|
240
|
+
| 'comment';
|
|
241
|
+
/** A run of one line, from `from` up to but not including `to`. */
|
|
242
|
+
interface Token {
|
|
243
|
+
kind: TokenKind;
|
|
244
|
+
from: number;
|
|
245
|
+
to: number;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The tokens of one line, in the order they appear, never overlapping. A blank
|
|
249
|
+
* line, and a line of nothing the tokenizer recognises, has none.
|
|
250
|
+
*/
|
|
251
|
+
declare function tokenizeLine(line: string): Token[];
|
|
252
|
+
|
|
208
253
|
interface ImportResult {
|
|
209
254
|
/** The projected sketch as canonical Skiss text, `\n`-terminated. */
|
|
210
255
|
output: string;
|
|
@@ -223,6 +268,6 @@ interface ResolvedDocument extends Document {
|
|
|
223
268
|
}
|
|
224
269
|
declare function resolve(doc: Document): ResolvedDocument;
|
|
225
270
|
|
|
226
|
-
declare const VERSION = "0.
|
|
271
|
+
declare const VERSION = "0.6.1";
|
|
227
272
|
|
|
228
|
-
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 TypeRef, VERSION, compile, formatDiagnostic, formatDropped, fromLinkML, importLinkML, parse, resolve, serialize, toLinkML, toMermaid, toSkiss };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -84,6 +84,21 @@ var CLASS_NAME = /^[A-Z][A-Za-z0-9]*$/;
|
|
|
84
84
|
var FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
|
|
85
85
|
var SYSTEM_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
86
86
|
var ENUM_VALUE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
87
|
+
var NAME_CHAR = /^[A-Za-z0-9]$/;
|
|
88
|
+
var WORD_CHAR = /^[A-Za-z0-9_-]$/;
|
|
89
|
+
var CLASS_RULE = "UpperCamelCase in ASCII letters and digits (A-Z, a-z, 0-9), uppercase first";
|
|
90
|
+
var FIELD_RULE = "lowerCamelCase in ASCII letters and digits (a-z, A-Z, 0-9), lowercase first";
|
|
91
|
+
function nameFault(text, allowed) {
|
|
92
|
+
for (const ch of text) {
|
|
93
|
+
if (!allowed.test(ch)) return `\`${ch}\` is not one of them.`;
|
|
94
|
+
}
|
|
95
|
+
const first = text.slice(0, 1);
|
|
96
|
+
const kind = /[A-Z]/.test(first) ? "an uppercase letter" : /[a-z]/.test(first) ? "a lowercase letter" : /[0-9]/.test(first) ? "a digit" : `\`${first}\``;
|
|
97
|
+
return `\`${text}\` starts with ${kind}.`;
|
|
98
|
+
}
|
|
99
|
+
function badName(rule, text, allowed = NAME_CHAR) {
|
|
100
|
+
return `${rule}: \`${text}\`. ${nameFault(text, allowed)}`;
|
|
101
|
+
}
|
|
87
102
|
var PRIMITIVES = {
|
|
88
103
|
string: "string",
|
|
89
104
|
int: "int",
|
|
@@ -99,6 +114,7 @@ var PRIMITIVES = {
|
|
|
99
114
|
function primitiveFor(word) {
|
|
100
115
|
return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
|
|
101
116
|
}
|
|
117
|
+
var DROPPED = "dropped-class";
|
|
102
118
|
var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
|
|
103
119
|
var CLASS_ORDER = "name, `< Parent`, `@System`, `~ Class`, then `#` and `?`";
|
|
104
120
|
function parse(source) {
|
|
@@ -126,8 +142,8 @@ function parse(source) {
|
|
|
126
142
|
continue;
|
|
127
143
|
}
|
|
128
144
|
const result = parseFieldLine(text, line);
|
|
129
|
-
if (result.ok)
|
|
130
|
-
else
|
|
145
|
+
if (!result.ok) diagnostics.push(result.diagnostic);
|
|
146
|
+
else if (current !== DROPPED) current.fields.push(result.value);
|
|
131
147
|
continue;
|
|
132
148
|
}
|
|
133
149
|
if (/\p{L}/u.test(first)) {
|
|
@@ -137,7 +153,7 @@ function parse(source) {
|
|
|
137
153
|
current = result.value;
|
|
138
154
|
} else {
|
|
139
155
|
diagnostics.push(result.diagnostic);
|
|
140
|
-
current = void 0;
|
|
156
|
+
current = result.atName ? DROPPED : void 0;
|
|
141
157
|
}
|
|
142
158
|
continue;
|
|
143
159
|
}
|
|
@@ -187,6 +203,10 @@ function splitTrailer(text) {
|
|
|
187
203
|
return trailer;
|
|
188
204
|
}
|
|
189
205
|
var WORD = /[\p{L}\p{N}_-]+/uy;
|
|
206
|
+
function wordAt(text, i) {
|
|
207
|
+
WORD.lastIndex = i;
|
|
208
|
+
return WORD.exec(text)?.[0];
|
|
209
|
+
}
|
|
190
210
|
var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "<", ",", "["];
|
|
191
211
|
function tokenize(head) {
|
|
192
212
|
const tokens = [];
|
|
@@ -197,11 +217,10 @@ function tokenize(head) {
|
|
|
197
217
|
i++;
|
|
198
218
|
continue;
|
|
199
219
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
i += word[0].length;
|
|
220
|
+
const word = wordAt(head, i);
|
|
221
|
+
if (word !== void 0) {
|
|
222
|
+
tokens.push({ kind: "word", text: word, col: i, end: i + word.length });
|
|
223
|
+
i += word.length;
|
|
205
224
|
continue;
|
|
206
225
|
}
|
|
207
226
|
if (c === "[" && head[i + 1] === "]") {
|
|
@@ -223,10 +242,11 @@ function take(c) {
|
|
|
223
242
|
return t;
|
|
224
243
|
}
|
|
225
244
|
var name = (t, line) => ({ text: t.text, line, col: t.col, end: t.end });
|
|
226
|
-
function fail(code, message, line, span) {
|
|
245
|
+
function fail(code, message, line, span, atName = false) {
|
|
227
246
|
return {
|
|
228
247
|
ok: false,
|
|
229
|
-
diagnostic: { severity: "error", code, message, line, col: span.col, end: span.end }
|
|
248
|
+
diagnostic: { severity: "error", code, message, line, col: span.col, end: span.end },
|
|
249
|
+
atName
|
|
230
250
|
};
|
|
231
251
|
}
|
|
232
252
|
var afterSpan = (after) => ({ col: after.end, end: after.end });
|
|
@@ -235,17 +255,21 @@ function parseClassLine(text, line) {
|
|
|
235
255
|
const c = { tokens: tokenize(trailer.head), pos: 0, line };
|
|
236
256
|
const head = take(c);
|
|
237
257
|
if (head === void 0 || head.kind !== "word") {
|
|
238
|
-
return fail(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
258
|
+
return fail(
|
|
259
|
+
"E_UNPARSABLE",
|
|
260
|
+
"A class line must start with a class name",
|
|
261
|
+
line,
|
|
262
|
+
{ col: 0, end: Math.max(1, trailer.head.length) },
|
|
263
|
+
true
|
|
264
|
+
);
|
|
242
265
|
}
|
|
243
266
|
if (!CLASS_NAME.test(head.text)) {
|
|
244
267
|
return fail(
|
|
245
268
|
"E_BAD_NAME",
|
|
246
|
-
`Class names are
|
|
269
|
+
badName(`Class names are ${CLASS_RULE}`, head.text),
|
|
247
270
|
line,
|
|
248
|
-
head
|
|
271
|
+
head,
|
|
272
|
+
true
|
|
249
273
|
);
|
|
250
274
|
}
|
|
251
275
|
const node = { name: name(head, line), line };
|
|
@@ -278,9 +302,10 @@ function parseClassLine(text, line) {
|
|
|
278
302
|
if (!CLASS_NAME.test(target.text)) {
|
|
279
303
|
return fail(
|
|
280
304
|
"E_BAD_NAME",
|
|
281
|
-
`The class after \`~\` must be
|
|
305
|
+
badName(`The class after \`~\` must be ${CLASS_RULE}`, target.text),
|
|
282
306
|
line,
|
|
283
|
-
target
|
|
307
|
+
target,
|
|
308
|
+
true
|
|
284
309
|
);
|
|
285
310
|
}
|
|
286
311
|
node.similarTo = name(target, line);
|
|
@@ -302,12 +327,7 @@ function parseFieldLine(text, line) {
|
|
|
302
327
|
}
|
|
303
328
|
if (head.kind !== "word") return unexpected(head, line, "field", FIELD_ORDER);
|
|
304
329
|
if (!FIELD_NAME.test(head.text)) {
|
|
305
|
-
return fail(
|
|
306
|
-
"E_BAD_NAME",
|
|
307
|
-
`Field names are lowerCamelCase (letters and digits, lowercase first): \`${head.text}\``,
|
|
308
|
-
line,
|
|
309
|
-
head
|
|
310
|
-
);
|
|
330
|
+
return fail("E_BAD_NAME", badName(`Field names are ${FIELD_RULE}`, head.text), line, head);
|
|
311
331
|
}
|
|
312
332
|
const node = { name: name(head, line), line, identifier: false };
|
|
313
333
|
const star = peek(c);
|
|
@@ -346,7 +366,7 @@ function parseFieldLine(text, line) {
|
|
|
346
366
|
if (!CLASS_NAME.test(className.text)) {
|
|
347
367
|
return fail(
|
|
348
368
|
"E_BAD_NAME",
|
|
349
|
-
`The class after \`=\` must be
|
|
369
|
+
badName(`The class after \`=\` must be ${CLASS_RULE}`, className.text),
|
|
350
370
|
line,
|
|
351
371
|
className
|
|
352
372
|
);
|
|
@@ -372,7 +392,7 @@ function parseFieldLine(text, line) {
|
|
|
372
392
|
if (!FIELD_NAME.test(fieldName.text)) {
|
|
373
393
|
return fail(
|
|
374
394
|
"E_BAD_NAME",
|
|
375
|
-
`The field after \`= ${className.text}.\` must be
|
|
395
|
+
badName(`The field after \`= ${className.text}.\` must be ${FIELD_RULE}`, fieldName.text),
|
|
376
396
|
line,
|
|
377
397
|
fieldName
|
|
378
398
|
);
|
|
@@ -396,8 +416,8 @@ function parseParent(c, lt) {
|
|
|
396
416
|
);
|
|
397
417
|
}
|
|
398
418
|
if (!CLASS_NAME.test(parent.text)) {
|
|
399
|
-
const message = Object.hasOwn(PRIMITIVES, parent.text) ? `\`${parent.text}\` is a primitive, not a class; a parent is a class` : `The class after \`<\` must be
|
|
400
|
-
return fail("E_BAD_NAME", message, c.line, parent);
|
|
419
|
+
const message = Object.hasOwn(PRIMITIVES, parent.text) ? `\`${parent.text}\` is a primitive, not a class; a parent is a class` : badName(`The class after \`<\` must be ${CLASS_RULE}`, parent.text);
|
|
420
|
+
return fail("E_BAD_NAME", message, c.line, parent, true);
|
|
401
421
|
}
|
|
402
422
|
const next = peek(c);
|
|
403
423
|
if (next?.kind === "," || next?.kind === "<") {
|
|
@@ -423,7 +443,11 @@ function parseSystem(c, at) {
|
|
|
423
443
|
if (!SYSTEM_NAME.test(system.text)) {
|
|
424
444
|
return fail(
|
|
425
445
|
"E_UNPARSABLE",
|
|
426
|
-
|
|
446
|
+
badName(
|
|
447
|
+
"A system name is ASCII letters, digits, `-` and `_` (A-Z, a-z, 0-9), starting with a letter",
|
|
448
|
+
system.text,
|
|
449
|
+
WORD_CHAR
|
|
450
|
+
),
|
|
427
451
|
c.line,
|
|
428
452
|
system
|
|
429
453
|
);
|
|
@@ -481,7 +505,11 @@ function parseType(c, colon) {
|
|
|
481
505
|
if (!ENUM_VALUE.test(w.text)) {
|
|
482
506
|
return fail(
|
|
483
507
|
"E_UNPARSABLE",
|
|
484
|
-
|
|
508
|
+
badName(
|
|
509
|
+
"An enum value is ASCII letters, digits, `-` and `_` (A-Z, a-z, 0-9), and does not start with `-` or `_`",
|
|
510
|
+
w.text,
|
|
511
|
+
WORD_CHAR
|
|
512
|
+
),
|
|
485
513
|
c.line,
|
|
486
514
|
w
|
|
487
515
|
);
|
|
@@ -501,7 +529,7 @@ function parseType(c, colon) {
|
|
|
501
529
|
if (!CLASS_NAME.test(word)) {
|
|
502
530
|
return fail(
|
|
503
531
|
"E_BAD_NAME",
|
|
504
|
-
`A referenced class must be
|
|
532
|
+
badName(`A referenced class must be ${CLASS_RULE}`, word),
|
|
505
533
|
c.line,
|
|
506
534
|
first
|
|
507
535
|
);
|
|
@@ -518,36 +546,36 @@ function parseType(c, colon) {
|
|
|
518
546
|
first
|
|
519
547
|
);
|
|
520
548
|
}
|
|
521
|
-
function unexpected(
|
|
549
|
+
function unexpected(token2, line, kind, order) {
|
|
522
550
|
const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["<", "@", "~"];
|
|
523
|
-
if (modifiers.includes(
|
|
551
|
+
if (modifiers.includes(token2.kind)) {
|
|
524
552
|
return fail(
|
|
525
553
|
"E_UNPARSABLE",
|
|
526
|
-
`\`${
|
|
554
|
+
`\`${token2.text}\` is out of order; a ${kind} line reads: ${order}`,
|
|
527
555
|
line,
|
|
528
|
-
|
|
556
|
+
token2
|
|
529
557
|
);
|
|
530
558
|
}
|
|
531
|
-
if (
|
|
559
|
+
if (token2.kind === "<") {
|
|
532
560
|
return fail(
|
|
533
561
|
"E_UNPARSABLE",
|
|
534
562
|
"`<` marks inheritance and belongs on the class line: `Child < Parent`",
|
|
535
563
|
line,
|
|
536
|
-
|
|
564
|
+
token2
|
|
537
565
|
);
|
|
538
566
|
}
|
|
539
|
-
if (
|
|
567
|
+
if (token2.text === "?") {
|
|
540
568
|
return fail(
|
|
541
569
|
"E_UNPARSABLE",
|
|
542
570
|
"`?` attached to a word is not a marker; a doubt needs whitespace before `?` and after it",
|
|
543
571
|
line,
|
|
544
|
-
|
|
572
|
+
token2
|
|
545
573
|
);
|
|
546
574
|
}
|
|
547
|
-
if (
|
|
548
|
-
return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line,
|
|
575
|
+
if (token2.text === "#") {
|
|
576
|
+
return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line, token2);
|
|
549
577
|
}
|
|
550
|
-
return fail("E_UNPARSABLE", `Unexpected \`${
|
|
578
|
+
return fail("E_UNPARSABLE", `Unexpected \`${token2.text}\` on a ${kind} line`, line, token2);
|
|
551
579
|
}
|
|
552
580
|
|
|
553
581
|
// src/resolve.ts
|
|
@@ -1633,6 +1661,204 @@ var nothing = (element, detail) => ({
|
|
|
1633
1661
|
dropped: [{ kind: UNREADABLE, element, detail }]
|
|
1634
1662
|
});
|
|
1635
1663
|
|
|
1664
|
+
// src/highlight.ts
|
|
1665
|
+
var TRAILING_WS = /[ \t]+$/;
|
|
1666
|
+
function tokenizeLine(line) {
|
|
1667
|
+
const text = line.replace(TRAILING_WS, "");
|
|
1668
|
+
const first = text[0];
|
|
1669
|
+
if (first === void 0) {
|
|
1670
|
+
return [];
|
|
1671
|
+
}
|
|
1672
|
+
if (first === "#") {
|
|
1673
|
+
return [{ kind: "comment", from: 0, to: text.length }];
|
|
1674
|
+
}
|
|
1675
|
+
const { head, trailers } = splitTrailer2(text);
|
|
1676
|
+
return [...headTokens(head, isWs(first)), ...trailers];
|
|
1677
|
+
}
|
|
1678
|
+
function splitTrailer2(text) {
|
|
1679
|
+
let start = -1;
|
|
1680
|
+
for (let i = 1; i < text.length; i++) {
|
|
1681
|
+
if (!isWs(text[i - 1])) {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
if (text[i] === "#" || isDoubtMarker(text, i)) {
|
|
1685
|
+
start = i;
|
|
1686
|
+
break;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (start < 0) {
|
|
1690
|
+
return { head: text, trailers: [] };
|
|
1691
|
+
}
|
|
1692
|
+
const head = text.slice(0, start);
|
|
1693
|
+
if (text[start] === "?") {
|
|
1694
|
+
return { head, trailers: [{ kind: "doubt", from: start, to: text.length }] };
|
|
1695
|
+
}
|
|
1696
|
+
let doubt = -1;
|
|
1697
|
+
for (let i = start + 1; i < text.length; i++) {
|
|
1698
|
+
if (isDoubtMarker(text, i)) {
|
|
1699
|
+
doubt = i;
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (doubt < 0) {
|
|
1704
|
+
return { head, trailers: [{ kind: "description", from: start, to: text.length }] };
|
|
1705
|
+
}
|
|
1706
|
+
return {
|
|
1707
|
+
head,
|
|
1708
|
+
trailers: [
|
|
1709
|
+
// The whitespace between the two belongs to neither.
|
|
1710
|
+
{
|
|
1711
|
+
kind: "description",
|
|
1712
|
+
from: start,
|
|
1713
|
+
to: text.slice(0, doubt).replace(TRAILING_WS, "").length
|
|
1714
|
+
},
|
|
1715
|
+
{ kind: "doubt", from: doubt, to: text.length }
|
|
1716
|
+
]
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function scan(head) {
|
|
1720
|
+
const raws = [];
|
|
1721
|
+
let i = 0;
|
|
1722
|
+
while (i < head.length) {
|
|
1723
|
+
const c = head[i] ?? "";
|
|
1724
|
+
if (isWs(c)) {
|
|
1725
|
+
i += 1;
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
const word = wordAt(head, i);
|
|
1729
|
+
if (word !== void 0) {
|
|
1730
|
+
raws.push({ text: word, from: i, to: i + word.length, word: true });
|
|
1731
|
+
i += word.length;
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
if (c === "[" && head[i + 1] === "]") {
|
|
1735
|
+
raws.push({ text: "[]", from: i, to: i + 2, word: false });
|
|
1736
|
+
i += 2;
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
const ch = String.fromCodePoint(head.codePointAt(i) ?? 0);
|
|
1740
|
+
raws.push({ text: ch, from: i, to: i + ch.length, word: false });
|
|
1741
|
+
i += ch.length;
|
|
1742
|
+
}
|
|
1743
|
+
return raws;
|
|
1744
|
+
}
|
|
1745
|
+
var token = (kind, raw) => ({ kind, from: raw.from, to: raw.to });
|
|
1746
|
+
function headTokens(head, field) {
|
|
1747
|
+
const raws = scan(head);
|
|
1748
|
+
const tokens = [];
|
|
1749
|
+
let i = 0;
|
|
1750
|
+
const name2 = raws[0];
|
|
1751
|
+
if (name2?.word === true && (field ? FIELD_NAME : CLASS_NAME).test(name2.text)) {
|
|
1752
|
+
tokens.push(token(field ? "field" : "class", name2));
|
|
1753
|
+
i = 1;
|
|
1754
|
+
}
|
|
1755
|
+
while (i < raws.length) {
|
|
1756
|
+
const raw = raws[i];
|
|
1757
|
+
i += 1;
|
|
1758
|
+
if (raw === void 0 || raw.word) {
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
switch (raw.text) {
|
|
1762
|
+
case "*":
|
|
1763
|
+
tokens.push(token("marker", raw));
|
|
1764
|
+
break;
|
|
1765
|
+
case ":":
|
|
1766
|
+
tokens.push(token("operator", raw));
|
|
1767
|
+
i = typeTokens(raws, i, tokens);
|
|
1768
|
+
break;
|
|
1769
|
+
case "@":
|
|
1770
|
+
tokens.push(token("operator", raw));
|
|
1771
|
+
i = nameToken(raws, i, tokens, "system", SYSTEM_NAME);
|
|
1772
|
+
break;
|
|
1773
|
+
case "~":
|
|
1774
|
+
case "<":
|
|
1775
|
+
tokens.push(token("operator", raw));
|
|
1776
|
+
i = nameToken(raws, i, tokens, "class", CLASS_NAME);
|
|
1777
|
+
break;
|
|
1778
|
+
case "=":
|
|
1779
|
+
tokens.push(token("operator", raw));
|
|
1780
|
+
i = joinTokens(raws, i, tokens);
|
|
1781
|
+
break;
|
|
1782
|
+
case "|":
|
|
1783
|
+
case "[]":
|
|
1784
|
+
tokens.push(token("operator", raw));
|
|
1785
|
+
break;
|
|
1786
|
+
default:
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return tokens;
|
|
1791
|
+
}
|
|
1792
|
+
function nameToken(raws, i, tokens, kind, shape) {
|
|
1793
|
+
const raw = raws[i];
|
|
1794
|
+
if (raw === void 0 || !raw.word) {
|
|
1795
|
+
return i;
|
|
1796
|
+
}
|
|
1797
|
+
if (shape.test(raw.text)) {
|
|
1798
|
+
tokens.push(token(kind, raw));
|
|
1799
|
+
}
|
|
1800
|
+
return i + 1;
|
|
1801
|
+
}
|
|
1802
|
+
function joinTokens(raws, start, tokens) {
|
|
1803
|
+
let i = nameToken(raws, start, tokens, "class", CLASS_NAME);
|
|
1804
|
+
const dot = raws[i];
|
|
1805
|
+
if (dot === void 0 || dot.text !== "." || dot.word) {
|
|
1806
|
+
return i;
|
|
1807
|
+
}
|
|
1808
|
+
tokens.push(token("operator", dot));
|
|
1809
|
+
i += 1;
|
|
1810
|
+
return nameToken(raws, i, tokens, "field", FIELD_NAME);
|
|
1811
|
+
}
|
|
1812
|
+
function typeTokens(raws, start, tokens) {
|
|
1813
|
+
const first = raws[start];
|
|
1814
|
+
if (first === void 0 || !first.word) {
|
|
1815
|
+
return start;
|
|
1816
|
+
}
|
|
1817
|
+
const words = [first];
|
|
1818
|
+
const operators = [];
|
|
1819
|
+
let pipes = 0;
|
|
1820
|
+
let i = start + 1;
|
|
1821
|
+
while (raws[i]?.text === "|") {
|
|
1822
|
+
const pipe = raws[i];
|
|
1823
|
+
if (pipe === void 0) {
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
operators.push(pipe);
|
|
1827
|
+
pipes += 1;
|
|
1828
|
+
i += 1;
|
|
1829
|
+
const value = raws[i];
|
|
1830
|
+
if (value === void 0 || !value.word) {
|
|
1831
|
+
break;
|
|
1832
|
+
}
|
|
1833
|
+
words.push(value);
|
|
1834
|
+
i += 1;
|
|
1835
|
+
}
|
|
1836
|
+
const many = raws[i];
|
|
1837
|
+
if (many !== void 0 && many.text === "[]" && !many.word) {
|
|
1838
|
+
operators.push(many);
|
|
1839
|
+
i += 1;
|
|
1840
|
+
}
|
|
1841
|
+
const typed = pipes > 0 ? words.filter((word) => ENUM_VALUE.test(word.text)).map((word) => token("enum", word)) : nameTypeTokens(first);
|
|
1842
|
+
tokens.push(
|
|
1843
|
+
...[...typed, ...operators.map((raw) => token("operator", raw))].sort(
|
|
1844
|
+
(a, b) => a.from - b.from
|
|
1845
|
+
)
|
|
1846
|
+
);
|
|
1847
|
+
return i;
|
|
1848
|
+
}
|
|
1849
|
+
function nameTypeTokens(word) {
|
|
1850
|
+
if (primitiveFor(word.text) !== void 0) {
|
|
1851
|
+
return [token("primitive", word)];
|
|
1852
|
+
}
|
|
1853
|
+
if (CLASS_NAME.test(word.text)) {
|
|
1854
|
+
return [token("class", word)];
|
|
1855
|
+
}
|
|
1856
|
+
if (/^[a-z]/.test(word.text)) {
|
|
1857
|
+
return [token("type", word)];
|
|
1858
|
+
}
|
|
1859
|
+
return [];
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1636
1862
|
// src/import.ts
|
|
1637
1863
|
import { parse as parseYAML } from "yaml";
|
|
1638
1864
|
function importLinkML(text) {
|
|
@@ -1657,7 +1883,7 @@ function notYAML(error) {
|
|
|
1657
1883
|
}
|
|
1658
1884
|
|
|
1659
1885
|
// src/index.ts
|
|
1660
|
-
var VERSION = "0.
|
|
1886
|
+
var VERSION = "0.6.1";
|
|
1661
1887
|
export {
|
|
1662
1888
|
VERSION,
|
|
1663
1889
|
compile,
|
|
@@ -1670,6 +1896,7 @@ export {
|
|
|
1670
1896
|
serialize,
|
|
1671
1897
|
toLinkML,
|
|
1672
1898
|
toMermaid,
|
|
1673
|
-
toSkiss
|
|
1899
|
+
toSkiss,
|
|
1900
|
+
tokenizeLine
|
|
1674
1901
|
};
|
|
1675
1902
|
//# sourceMappingURL=index.js.map
|