@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 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
- Editors that render Skiss live are separate projects built on this package. This repository is the language, the library and the command line.
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.5.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
+ 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) 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
  }
@@ -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
- WORD.lastIndex = i;
211
- const word = WORD.exec(head);
212
- if (word !== null) {
213
- tokens.push({ kind: "word", text: word[0], col: i, end: i + word[0].length });
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("E_UNPARSABLE", "A class line must start with a class name", line, {
249
- col: 0,
250
- end: Math.max(1, trailer.head.length)
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 UpperCamelCase (letters and digits, uppercase first): \`${head.text}\``,
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 UpperCamelCase: \`${target.text}\``,
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 UpperCamelCase: \`${className.text}\``,
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 lowerCamelCase: \`${fieldName.text}\``,
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 UpperCamelCase: \`${parent.text}\``;
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
- `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
+ ),
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
- `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
+ ),
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 UpperCamelCase: \`${word}\``,
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.5.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.5.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) 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
  }
@@ -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
- WORD.lastIndex = i;
201
- const word = WORD.exec(head);
202
- if (word !== null) {
203
- tokens.push({ kind: "word", text: word[0], col: i, end: i + word[0].length });
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("E_UNPARSABLE", "A class line must start with a class name", line, {
239
- col: 0,
240
- end: Math.max(1, trailer.head.length)
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 UpperCamelCase (letters and digits, uppercase first): \`${head.text}\``,
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 UpperCamelCase: \`${target.text}\``,
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 UpperCamelCase: \`${className.text}\``,
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 lowerCamelCase: \`${fieldName.text}\``,
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 UpperCamelCase: \`${parent.text}\``;
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
- `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
+ ),
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
- `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
+ ),
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 UpperCamelCase: \`${word}\``,
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(token, line, kind, order) {
549
+ function unexpected(token2, line, kind, order) {
522
550
  const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["<", "@", "~"];
523
- if (modifiers.includes(token.kind)) {
551
+ if (modifiers.includes(token2.kind)) {
524
552
  return fail(
525
553
  "E_UNPARSABLE",
526
- `\`${token.text}\` is out of order; a ${kind} line reads: ${order}`,
554
+ `\`${token2.text}\` is out of order; a ${kind} line reads: ${order}`,
527
555
  line,
528
- token
556
+ token2
529
557
  );
530
558
  }
531
- if (token.kind === "<") {
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
- token
564
+ token2
537
565
  );
538
566
  }
539
- if (token.text === "?") {
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
- token
572
+ token2
545
573
  );
546
574
  }
547
- if (token.text === "#") {
548
- return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line, token);
575
+ if (token2.text === "#") {
576
+ return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line, token2);
549
577
  }
550
- return fail("E_UNPARSABLE", `Unexpected \`${token.text}\` on a ${kind} line`, line, token);
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.5.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eriknaslund/skiss",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "A text notation for sketching data models, compiled to LinkML",
5
5
  "type": "module",
6
6
  "license": "MIT",