@eriknaslund/skiss 0.6.0 → 0.7.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 +8 -3
- package/dist/cli.js +53 -30
- package/dist/index.d.ts +1 -1
- package/dist/index.js +53 -30
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,6 +25,10 @@ CharacterPage @Community ~ Character # the community wiki's version
|
|
|
25
25
|
|
|
26
26
|
That is the whole language. Classes at column 0, fields indented under them, a handful of punctuation marks.
|
|
27
27
|
|
|
28
|
+
**Try it live:** <https://erik-naslund.github.io/skiss-playground/> draws a sketch as you type, shares it as a link and exports SVG, PNG and LinkML. Nothing to install, nothing leaves your browser.
|
|
29
|
+
|
|
30
|
+
**Let an assistant write it:** [`skills/skiss`](skills/skiss/SKILL.md) is an agent skill for Claude, a custom GPT or any assistant with a system prompt, so it writes valid Skiss and draws it, with or without the compiler at hand.
|
|
31
|
+
|
|
28
32
|
## Why
|
|
29
33
|
|
|
30
34
|
You sit down with someone who knows the domain. Within the hour you want a shared picture of it: what the things are, what they are called, which system owns them, how they hang together.
|
|
@@ -43,6 +47,7 @@ Skiss is text you can type while someone talks. A field is one line. A change is
|
|
|
43
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.
|
|
44
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`.
|
|
45
49
|
- **`#` describes, `?` doubts.** A description ends up in generated documentation. A doubt never does.
|
|
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)).
|
|
46
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.
|
|
47
52
|
- **Everything you can write compiles to valid LinkML.** Skiss is a strict subset. Nothing is lost on the way up.
|
|
48
53
|
|
|
@@ -73,7 +78,7 @@ Or without installing: `npx @eriknaslund/skiss diagram model.skiss`.
|
|
|
73
78
|
|
|
74
79
|
`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)).
|
|
75
80
|
|
|
76
|
-
Editors that render Skiss live are separate projects built on this package. This repository is the language, the library and the command line.
|
|
81
|
+
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.
|
|
77
82
|
|
|
78
83
|
## Where to look
|
|
79
84
|
|
|
@@ -89,9 +94,9 @@ Editors that render Skiss live are separate projects built on this package. This
|
|
|
89
94
|
|
|
90
95
|
## Status
|
|
91
96
|
|
|
92
|
-
0.
|
|
97
|
+
0.7.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.
|
|
93
98
|
|
|
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.
|
|
99
|
+
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.
|
|
95
100
|
|
|
96
101
|
## License
|
|
97
102
|
|
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
|
}
|
|
@@ -236,10 +252,11 @@ function take(c) {
|
|
|
236
252
|
return t;
|
|
237
253
|
}
|
|
238
254
|
var name = (t, line) => ({ text: t.text, line, col: t.col, end: t.end });
|
|
239
|
-
function fail(code, message, line, span) {
|
|
255
|
+
function fail(code, message, line, span, atName = false) {
|
|
240
256
|
return {
|
|
241
257
|
ok: false,
|
|
242
|
-
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
|
|
243
260
|
};
|
|
244
261
|
}
|
|
245
262
|
var afterSpan = (after) => ({ col: after.end, end: after.end });
|
|
@@ -248,17 +265,21 @@ function parseClassLine(text, line) {
|
|
|
248
265
|
const c = { tokens: tokenize(trailer.head), pos: 0, line };
|
|
249
266
|
const head = take(c);
|
|
250
267
|
if (head === void 0 || head.kind !== "word") {
|
|
251
|
-
return fail(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
);
|
|
255
275
|
}
|
|
256
276
|
if (!CLASS_NAME.test(head.text)) {
|
|
257
277
|
return fail(
|
|
258
278
|
"E_BAD_NAME",
|
|
259
|
-
`Class names are
|
|
279
|
+
badName(`Class names are ${CLASS_RULE}`, head.text),
|
|
260
280
|
line,
|
|
261
|
-
head
|
|
281
|
+
head,
|
|
282
|
+
true
|
|
262
283
|
);
|
|
263
284
|
}
|
|
264
285
|
const node = { name: name(head, line), line };
|
|
@@ -291,9 +312,10 @@ function parseClassLine(text, line) {
|
|
|
291
312
|
if (!CLASS_NAME.test(target.text)) {
|
|
292
313
|
return fail(
|
|
293
314
|
"E_BAD_NAME",
|
|
294
|
-
`The class after \`~\` must be
|
|
315
|
+
badName(`The class after \`~\` must be ${CLASS_RULE}`, target.text),
|
|
295
316
|
line,
|
|
296
|
-
target
|
|
317
|
+
target,
|
|
318
|
+
true
|
|
297
319
|
);
|
|
298
320
|
}
|
|
299
321
|
node.similarTo = name(target, line);
|
|
@@ -315,12 +337,7 @@ function parseFieldLine(text, line) {
|
|
|
315
337
|
}
|
|
316
338
|
if (head.kind !== "word") return unexpected(head, line, "field", FIELD_ORDER);
|
|
317
339
|
if (!FIELD_NAME.test(head.text)) {
|
|
318
|
-
return fail(
|
|
319
|
-
"E_BAD_NAME",
|
|
320
|
-
`Field names are lowerCamelCase (letters and digits, lowercase first): \`${head.text}\``,
|
|
321
|
-
line,
|
|
322
|
-
head
|
|
323
|
-
);
|
|
340
|
+
return fail("E_BAD_NAME", badName(`Field names are ${FIELD_RULE}`, head.text), line, head);
|
|
324
341
|
}
|
|
325
342
|
const node = { name: name(head, line), line, identifier: false };
|
|
326
343
|
const star = peek(c);
|
|
@@ -359,7 +376,7 @@ function parseFieldLine(text, line) {
|
|
|
359
376
|
if (!CLASS_NAME.test(className.text)) {
|
|
360
377
|
return fail(
|
|
361
378
|
"E_BAD_NAME",
|
|
362
|
-
`The class after \`=\` must be
|
|
379
|
+
badName(`The class after \`=\` must be ${CLASS_RULE}`, className.text),
|
|
363
380
|
line,
|
|
364
381
|
className
|
|
365
382
|
);
|
|
@@ -385,7 +402,7 @@ function parseFieldLine(text, line) {
|
|
|
385
402
|
if (!FIELD_NAME.test(fieldName.text)) {
|
|
386
403
|
return fail(
|
|
387
404
|
"E_BAD_NAME",
|
|
388
|
-
`The field after \`= ${className.text}.\` must be
|
|
405
|
+
badName(`The field after \`= ${className.text}.\` must be ${FIELD_RULE}`, fieldName.text),
|
|
389
406
|
line,
|
|
390
407
|
fieldName
|
|
391
408
|
);
|
|
@@ -409,8 +426,8 @@ function parseParent(c, lt) {
|
|
|
409
426
|
);
|
|
410
427
|
}
|
|
411
428
|
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
|
|
413
|
-
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);
|
|
414
431
|
}
|
|
415
432
|
const next = peek(c);
|
|
416
433
|
if (next?.kind === "," || next?.kind === "<") {
|
|
@@ -436,7 +453,11 @@ function parseSystem(c, at) {
|
|
|
436
453
|
if (!SYSTEM_NAME.test(system.text)) {
|
|
437
454
|
return fail(
|
|
438
455
|
"E_UNPARSABLE",
|
|
439
|
-
|
|
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
|
+
),
|
|
440
461
|
c.line,
|
|
441
462
|
system
|
|
442
463
|
);
|
|
@@ -494,7 +515,11 @@ function parseType(c, colon) {
|
|
|
494
515
|
if (!ENUM_VALUE.test(w.text)) {
|
|
495
516
|
return fail(
|
|
496
517
|
"E_UNPARSABLE",
|
|
497
|
-
|
|
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
|
+
),
|
|
498
523
|
c.line,
|
|
499
524
|
w
|
|
500
525
|
);
|
|
@@ -514,7 +539,7 @@ function parseType(c, colon) {
|
|
|
514
539
|
if (!CLASS_NAME.test(word)) {
|
|
515
540
|
return fail(
|
|
516
541
|
"E_BAD_NAME",
|
|
517
|
-
`A referenced class must be
|
|
542
|
+
badName(`A referenced class must be ${CLASS_RULE}`, word),
|
|
518
543
|
c.line,
|
|
519
544
|
first
|
|
520
545
|
);
|
|
@@ -986,9 +1011,7 @@ function toMermaid(doc, opts) {
|
|
|
986
1011
|
for (const field of cls.fields) {
|
|
987
1012
|
const type = field.type;
|
|
988
1013
|
if (type?.kind === "class") {
|
|
989
|
-
lines.push(
|
|
990
|
-
type.many ? ` ${from} "1" --> "*" ${type.name.text} : ${field.name.text}` : ` ${from} --> ${type.name.text} : ${field.name.text}`
|
|
991
|
-
);
|
|
1014
|
+
lines.push(` ${from} --> ${type.name.text} : ${field.name.text}`);
|
|
992
1015
|
}
|
|
993
1016
|
if (field.joinsTo !== void 0) {
|
|
994
1017
|
const { className, fieldName } = field.joinsTo;
|
|
@@ -1670,7 +1693,7 @@ function notYAML(error) {
|
|
|
1670
1693
|
}
|
|
1671
1694
|
|
|
1672
1695
|
// src/index.ts
|
|
1673
|
-
var VERSION = "0.
|
|
1696
|
+
var VERSION = "0.7.0";
|
|
1674
1697
|
|
|
1675
1698
|
// src/cli.ts
|
|
1676
1699
|
var HELP = `Usage: skiss <command> [options]
|
package/dist/index.d.ts
CHANGED
|
@@ -268,6 +268,6 @@ interface ResolvedDocument extends Document {
|
|
|
268
268
|
}
|
|
269
269
|
declare function resolve(doc: Document): ResolvedDocument;
|
|
270
270
|
|
|
271
|
-
declare const VERSION = "0.
|
|
271
|
+
declare const VERSION = "0.7.0";
|
|
272
272
|
|
|
273
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
|
}
|
|
@@ -226,10 +242,11 @@ function take(c) {
|
|
|
226
242
|
return t;
|
|
227
243
|
}
|
|
228
244
|
var name = (t, line) => ({ text: t.text, line, col: t.col, end: t.end });
|
|
229
|
-
function fail(code, message, line, span) {
|
|
245
|
+
function fail(code, message, line, span, atName = false) {
|
|
230
246
|
return {
|
|
231
247
|
ok: false,
|
|
232
|
-
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
|
|
233
250
|
};
|
|
234
251
|
}
|
|
235
252
|
var afterSpan = (after) => ({ col: after.end, end: after.end });
|
|
@@ -238,17 +255,21 @@ function parseClassLine(text, line) {
|
|
|
238
255
|
const c = { tokens: tokenize(trailer.head), pos: 0, line };
|
|
239
256
|
const head = take(c);
|
|
240
257
|
if (head === void 0 || head.kind !== "word") {
|
|
241
|
-
return fail(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
);
|
|
245
265
|
}
|
|
246
266
|
if (!CLASS_NAME.test(head.text)) {
|
|
247
267
|
return fail(
|
|
248
268
|
"E_BAD_NAME",
|
|
249
|
-
`Class names are
|
|
269
|
+
badName(`Class names are ${CLASS_RULE}`, head.text),
|
|
250
270
|
line,
|
|
251
|
-
head
|
|
271
|
+
head,
|
|
272
|
+
true
|
|
252
273
|
);
|
|
253
274
|
}
|
|
254
275
|
const node = { name: name(head, line), line };
|
|
@@ -281,9 +302,10 @@ function parseClassLine(text, line) {
|
|
|
281
302
|
if (!CLASS_NAME.test(target.text)) {
|
|
282
303
|
return fail(
|
|
283
304
|
"E_BAD_NAME",
|
|
284
|
-
`The class after \`~\` must be
|
|
305
|
+
badName(`The class after \`~\` must be ${CLASS_RULE}`, target.text),
|
|
285
306
|
line,
|
|
286
|
-
target
|
|
307
|
+
target,
|
|
308
|
+
true
|
|
287
309
|
);
|
|
288
310
|
}
|
|
289
311
|
node.similarTo = name(target, line);
|
|
@@ -305,12 +327,7 @@ function parseFieldLine(text, line) {
|
|
|
305
327
|
}
|
|
306
328
|
if (head.kind !== "word") return unexpected(head, line, "field", FIELD_ORDER);
|
|
307
329
|
if (!FIELD_NAME.test(head.text)) {
|
|
308
|
-
return fail(
|
|
309
|
-
"E_BAD_NAME",
|
|
310
|
-
`Field names are lowerCamelCase (letters and digits, lowercase first): \`${head.text}\``,
|
|
311
|
-
line,
|
|
312
|
-
head
|
|
313
|
-
);
|
|
330
|
+
return fail("E_BAD_NAME", badName(`Field names are ${FIELD_RULE}`, head.text), line, head);
|
|
314
331
|
}
|
|
315
332
|
const node = { name: name(head, line), line, identifier: false };
|
|
316
333
|
const star = peek(c);
|
|
@@ -349,7 +366,7 @@ function parseFieldLine(text, line) {
|
|
|
349
366
|
if (!CLASS_NAME.test(className.text)) {
|
|
350
367
|
return fail(
|
|
351
368
|
"E_BAD_NAME",
|
|
352
|
-
`The class after \`=\` must be
|
|
369
|
+
badName(`The class after \`=\` must be ${CLASS_RULE}`, className.text),
|
|
353
370
|
line,
|
|
354
371
|
className
|
|
355
372
|
);
|
|
@@ -375,7 +392,7 @@ function parseFieldLine(text, line) {
|
|
|
375
392
|
if (!FIELD_NAME.test(fieldName.text)) {
|
|
376
393
|
return fail(
|
|
377
394
|
"E_BAD_NAME",
|
|
378
|
-
`The field after \`= ${className.text}.\` must be
|
|
395
|
+
badName(`The field after \`= ${className.text}.\` must be ${FIELD_RULE}`, fieldName.text),
|
|
379
396
|
line,
|
|
380
397
|
fieldName
|
|
381
398
|
);
|
|
@@ -399,8 +416,8 @@ function parseParent(c, lt) {
|
|
|
399
416
|
);
|
|
400
417
|
}
|
|
401
418
|
if (!CLASS_NAME.test(parent.text)) {
|
|
402
|
-
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
|
|
403
|
-
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);
|
|
404
421
|
}
|
|
405
422
|
const next = peek(c);
|
|
406
423
|
if (next?.kind === "," || next?.kind === "<") {
|
|
@@ -426,7 +443,11 @@ function parseSystem(c, at) {
|
|
|
426
443
|
if (!SYSTEM_NAME.test(system.text)) {
|
|
427
444
|
return fail(
|
|
428
445
|
"E_UNPARSABLE",
|
|
429
|
-
|
|
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
|
+
),
|
|
430
451
|
c.line,
|
|
431
452
|
system
|
|
432
453
|
);
|
|
@@ -484,7 +505,11 @@ function parseType(c, colon) {
|
|
|
484
505
|
if (!ENUM_VALUE.test(w.text)) {
|
|
485
506
|
return fail(
|
|
486
507
|
"E_UNPARSABLE",
|
|
487
|
-
|
|
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
|
+
),
|
|
488
513
|
c.line,
|
|
489
514
|
w
|
|
490
515
|
);
|
|
@@ -504,7 +529,7 @@ function parseType(c, colon) {
|
|
|
504
529
|
if (!CLASS_NAME.test(word)) {
|
|
505
530
|
return fail(
|
|
506
531
|
"E_BAD_NAME",
|
|
507
|
-
`A referenced class must be
|
|
532
|
+
badName(`A referenced class must be ${CLASS_RULE}`, word),
|
|
508
533
|
c.line,
|
|
509
534
|
first
|
|
510
535
|
);
|
|
@@ -976,9 +1001,7 @@ function toMermaid(doc, opts) {
|
|
|
976
1001
|
for (const field of cls.fields) {
|
|
977
1002
|
const type = field.type;
|
|
978
1003
|
if (type?.kind === "class") {
|
|
979
|
-
lines.push(
|
|
980
|
-
type.many ? ` ${from} "1" --> "*" ${type.name.text} : ${field.name.text}` : ` ${from} --> ${type.name.text} : ${field.name.text}`
|
|
981
|
-
);
|
|
1004
|
+
lines.push(` ${from} --> ${type.name.text} : ${field.name.text}`);
|
|
982
1005
|
}
|
|
983
1006
|
if (field.joinsTo !== void 0) {
|
|
984
1007
|
const { className, fieldName } = field.joinsTo;
|
|
@@ -1858,7 +1881,7 @@ function notYAML(error) {
|
|
|
1858
1881
|
}
|
|
1859
1882
|
|
|
1860
1883
|
// src/index.ts
|
|
1861
|
-
var VERSION = "0.
|
|
1884
|
+
var VERSION = "0.7.0";
|
|
1862
1885
|
export {
|
|
1863
1886
|
VERSION,
|
|
1864
1887
|
compile,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eriknaslund/skiss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A text notation for sketching data models, compiled to LinkML",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@biomejs/biome": "^2.5.13",
|
|
44
|
-
"@mermaid-js/mermaid-cli": "^
|
|
44
|
+
"@mermaid-js/mermaid-cli": "^12.0.0",
|
|
45
45
|
"@types/node": "^26.6.1",
|
|
46
46
|
"jsdom": "^30.0.1",
|
|
47
47
|
"mermaid": "^12.0.0",
|