@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 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.6.0, published on npm as [`@eriknaslund/skiss`](https://www.npmjs.com/package/@eriknaslund/skiss). The language parses, resolves and diagnoses; `skiss diagram` draws it, `skiss compile` writes LinkML that the real LinkML toolchain validates in CI, and `skiss import` reads a LinkML schema back into a sketch. The library exports the same in both directions and is browser-safe.
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) current.fields.push(result.value);
140
- else diagnostics.push(result.diagnostic);
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("E_UNPARSABLE", "A class line must start with a class name", line, {
252
- col: 0,
253
- end: Math.max(1, trailer.head.length)
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 UpperCamelCase (letters and digits, uppercase first): \`${head.text}\``,
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 UpperCamelCase: \`${target.text}\``,
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 UpperCamelCase: \`${className.text}\``,
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 lowerCamelCase: \`${fieldName.text}\``,
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 UpperCamelCase: \`${parent.text}\``;
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
- `A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
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
- `An enum value is letters, digits, \`-\` and \`_\`, and does not start with \`-\` or \`_\`: \`${w.text}\``,
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 UpperCamelCase: \`${word}\``,
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.6.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.6.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) current.fields.push(result.value);
130
- else diagnostics.push(result.diagnostic);
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("E_UNPARSABLE", "A class line must start with a class name", line, {
242
- col: 0,
243
- end: Math.max(1, trailer.head.length)
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 UpperCamelCase (letters and digits, uppercase first): \`${head.text}\``,
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 UpperCamelCase: \`${target.text}\``,
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 UpperCamelCase: \`${className.text}\``,
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 lowerCamelCase: \`${fieldName.text}\``,
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 UpperCamelCase: \`${parent.text}\``;
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
- `A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
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
- `An enum value is letters, digits, \`-\` and \`_\`, and does not start with \`-\` or \`_\`: \`${w.text}\``,
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 UpperCamelCase: \`${word}\``,
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.6.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.6.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": "^11.17.0",
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",