@eriknaslund/skiss 0.4.0 → 0.6.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/dist/index.js CHANGED
@@ -1,3 +1,82 @@
1
+ // src/inheritance.ts
2
+ function inheritance(classes) {
3
+ const declared = /* @__PURE__ */ new Map();
4
+ for (const cls of classes) {
5
+ if (!declared.has(cls.name.text)) declared.set(cls.name.text, cls);
6
+ }
7
+ const circles = /* @__PURE__ */ new Map();
8
+ const parentOf = (cls) => {
9
+ if (cls.parent === void 0 || circles.has(cls)) return void 0;
10
+ return declared.get(cls.parent.text);
11
+ };
12
+ for (const cls of declared.values()) {
13
+ const path = [cls];
14
+ const seen = new Set(path);
15
+ for (let next = parentOf(cls); next !== void 0; next = parentOf(next)) {
16
+ if (seen.has(next)) {
17
+ const circle = path.slice(path.indexOf(next));
18
+ let closer = circle[0];
19
+ for (const member2 of circle) {
20
+ if (member2.line > closer.line) closer = member2;
21
+ }
22
+ const at = circle.indexOf(closer);
23
+ circles.set(closer, [...circle.slice(at), ...circle.slice(0, at)]);
24
+ break;
25
+ }
26
+ path.push(next);
27
+ seen.add(next);
28
+ }
29
+ }
30
+ const ancestors = (cls) => {
31
+ const out = [];
32
+ const seen = /* @__PURE__ */ new Set([cls]);
33
+ for (let next = parentOf(cls); next !== void 0; next = parentOf(next)) {
34
+ if (seen.has(next)) break;
35
+ out.push(next);
36
+ seen.add(next);
37
+ }
38
+ return out;
39
+ };
40
+ const find = (cls, matches) => {
41
+ for (const from of ancestors(cls)) {
42
+ const field = from.fields.find(matches);
43
+ if (field !== void 0) return { field, from };
44
+ }
45
+ return void 0;
46
+ };
47
+ return {
48
+ ancestors,
49
+ inherited: (cls, fieldName) => find(cls, (field) => field.name.text === fieldName),
50
+ inheritedIdentifier: (cls) => find(cls, (field) => field.identifier),
51
+ circle: (cls) => circles.get(cls)
52
+ };
53
+ }
54
+ function sameField(child, inheritedField) {
55
+ return meaning(child) === meaning(inheritedField);
56
+ }
57
+ var meaning = (field) => JSON.stringify([
58
+ field.name.text,
59
+ field.identifier,
60
+ typeMeaning(field),
61
+ field.system?.text,
62
+ field.joinsTo === void 0 ? void 0 : [field.joinsTo.className.text, field.joinsTo.fieldName.text],
63
+ field.description,
64
+ field.note
65
+ ]);
66
+ function typeMeaning(field) {
67
+ const type = field.type;
68
+ if (type === void 0) return void 0;
69
+ switch (type.kind) {
70
+ case "primitive":
71
+ return [type.kind, type.name, type.many];
72
+ case "class":
73
+ case "unknown":
74
+ return [type.kind, type.name.text, type.many];
75
+ case "enum":
76
+ return [type.kind, type.values.map((v) => v.text), type.many];
77
+ }
78
+ }
79
+
1
80
  // src/parse.ts
2
81
  var isWs = (c) => c === " " || c === " ";
3
82
  var trimWs = (s) => s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
@@ -21,7 +100,7 @@ function primitiveFor(word) {
21
100
  return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
22
101
  }
23
102
  var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
24
- var CLASS_ORDER = "name, `@System`, `~ Class`, then `#` and `?`";
103
+ var CLASS_ORDER = "name, `< Parent`, `@System`, `~ Class`, then `#` and `?`";
25
104
  function parse(source) {
26
105
  const classes = [];
27
106
  const diagnostics = [];
@@ -108,7 +187,11 @@ function splitTrailer(text) {
108
187
  return trailer;
109
188
  }
110
189
  var WORD = /[\p{L}\p{N}_-]+/uy;
111
- var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "["];
190
+ function wordAt(text, i) {
191
+ WORD.lastIndex = i;
192
+ return WORD.exec(text)?.[0];
193
+ }
194
+ var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "<", ",", "["];
112
195
  function tokenize(head) {
113
196
  const tokens = [];
114
197
  let i = 0;
@@ -118,11 +201,10 @@ function tokenize(head) {
118
201
  i++;
119
202
  continue;
120
203
  }
121
- WORD.lastIndex = i;
122
- const word = WORD.exec(head);
123
- if (word !== null) {
124
- tokens.push({ kind: "word", text: word[0], col: i, end: i + word[0].length });
125
- i += word[0].length;
204
+ const word = wordAt(head, i);
205
+ if (word !== void 0) {
206
+ tokens.push({ kind: "word", text: word, col: i, end: i + word.length });
207
+ i += word.length;
126
208
  continue;
127
209
  }
128
210
  if (c === "[" && head[i + 1] === "]") {
@@ -170,6 +252,13 @@ function parseClassLine(text, line) {
170
252
  );
171
253
  }
172
254
  const node = { name: name(head, line), line };
255
+ const lt = peek(c);
256
+ if (lt?.kind === "<") {
257
+ take(c);
258
+ const parent = parseParent(c, lt);
259
+ if (!parent.ok) return parent;
260
+ node.parent = parent.value;
261
+ }
173
262
  const at = peek(c);
174
263
  if (at?.kind === "@") {
175
264
  take(c);
@@ -299,6 +388,31 @@ function parseFieldLine(text, line) {
299
388
  if (trailer.note !== void 0) node.note = trailer.note;
300
389
  return { ok: true, value: node };
301
390
  }
391
+ function parseParent(c, lt) {
392
+ const parent = take(c);
393
+ if (parent === void 0 || parent.kind !== "word") {
394
+ return fail(
395
+ "E_UNPARSABLE",
396
+ "Expected a class name after `<`, e.g. `Jedi < Character`",
397
+ c.line,
398
+ parent ?? afterSpan(lt)
399
+ );
400
+ }
401
+ 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);
404
+ }
405
+ const next = peek(c);
406
+ if (next?.kind === "," || next?.kind === "<") {
407
+ return fail(
408
+ "E_UNPARSABLE",
409
+ "A class has one parent; `<` takes a single class name",
410
+ c.line,
411
+ next
412
+ );
413
+ }
414
+ return { ok: true, value: name(parent, c.line) };
415
+ }
302
416
  function parseSystem(c, at) {
303
417
  const system = take(c);
304
418
  if (system === void 0 || system.kind !== "word") {
@@ -407,36 +521,36 @@ function parseType(c, colon) {
407
521
  first
408
522
  );
409
523
  }
410
- function unexpected(token, line, kind, order) {
411
- const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["@", "~"];
412
- if (modifiers.includes(token.kind)) {
524
+ function unexpected(token2, line, kind, order) {
525
+ const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["<", "@", "~"];
526
+ if (modifiers.includes(token2.kind)) {
413
527
  return fail(
414
528
  "E_UNPARSABLE",
415
- `\`${token.text}\` is out of order; a ${kind} line reads: ${order}`,
529
+ `\`${token2.text}\` is out of order; a ${kind} line reads: ${order}`,
416
530
  line,
417
- token
531
+ token2
418
532
  );
419
533
  }
420
- if (token.text === "<") {
534
+ if (token2.kind === "<") {
421
535
  return fail(
422
536
  "E_UNPARSABLE",
423
- "`<` is reserved for inheritance and not yet part of the language",
537
+ "`<` marks inheritance and belongs on the class line: `Child < Parent`",
424
538
  line,
425
- token
539
+ token2
426
540
  );
427
541
  }
428
- if (token.text === "?") {
542
+ if (token2.text === "?") {
429
543
  return fail(
430
544
  "E_UNPARSABLE",
431
545
  "`?` attached to a word is not a marker; a doubt needs whitespace before `?` and after it",
432
546
  line,
433
- token
547
+ token2
434
548
  );
435
549
  }
436
- if (token.text === "#") {
437
- return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line, token);
550
+ if (token2.text === "#") {
551
+ return fail("E_UNPARSABLE", "A description needs whitespace before `#`", line, token2);
438
552
  }
439
- return fail("E_UNPARSABLE", `Unexpected \`${token.text}\` on a ${kind} line`, line, token);
553
+ return fail("E_UNPARSABLE", `Unexpected \`${token2.text}\` on a ${kind} line`, line, token2);
440
554
  }
441
555
 
442
556
  // src/resolve.ts
@@ -449,10 +563,14 @@ function resolve(doc) {
449
563
  };
450
564
  const classes = out.classes;
451
565
  const declared = new Set(classes.map((c) => c.name.text));
452
- const warnings = [];
566
+ const found = [];
453
567
  const undeclared = /* @__PURE__ */ new Map();
568
+ const tree = inheritance(classes);
454
569
  const warn = (code, message, at) => {
455
- warnings.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
570
+ found.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
571
+ };
572
+ const error = (code, message, at) => {
573
+ found.push({ severity: "error", code, message, line: at.line, col: at.col, end: at.end });
456
574
  };
457
575
  const reference = (name2) => {
458
576
  if (declared.has(name2.text)) return true;
@@ -473,9 +591,25 @@ function resolve(doc) {
473
591
  cls.name
474
592
  );
475
593
  }
594
+ const parent = cls.parent;
595
+ if (parent !== void 0) {
596
+ reference(parent);
597
+ const circle = tree.circle(cls);
598
+ if (circle !== void 0) {
599
+ const through = circle.slice(1).map((member2) => `\`${member2.name.text}\``);
600
+ const path = through.length === 0 ? "" : ` through ${through.join(", ")}`;
601
+ error(
602
+ "E_INHERITANCE_CYCLE",
603
+ `class \`${cls.name.text}\` inherits from itself${path}; this \`<\` is not carried`,
604
+ parent
605
+ );
606
+ cls.parent = void 0;
607
+ }
608
+ }
476
609
  if (cls.similarTo !== void 0) reference(cls.similarTo);
477
610
  const firstField = /* @__PURE__ */ new Map();
478
- let identifier;
611
+ const inheritedIdentifier = tree.inheritedIdentifier(cls);
612
+ let identifier = inheritedIdentifier?.field;
479
613
  for (const field of cls.fields) {
480
614
  const earlierField = firstField.get(field.name.text);
481
615
  if (earlierField === void 0) firstField.set(field.name.text, field);
@@ -489,14 +623,23 @@ function resolve(doc) {
489
623
  if (field.identifier) {
490
624
  if (identifier === void 0) identifier = field;
491
625
  else {
626
+ const from = identifier === inheritedIdentifier?.field ? ` inherits the identifier \`${identifier.name.text}\` from class \`${inheritedIdentifier.from.name.text}\`, line` : ` already has the identifier \`${identifier.name.text}\` on line`;
492
627
  warn(
493
628
  "W_MULTIPLE_IDENTIFIERS",
494
- `\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\` already has the identifier \`${identifier.name.text}\` on line ${identifier.line}`,
629
+ `\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\`${from} ${identifier.line}`,
495
630
  field.identifierAt ?? field.name
496
631
  );
497
632
  field.identifier = false;
498
633
  }
499
634
  }
635
+ const replaced = tree.inherited(cls, field.name.text);
636
+ if (replaced !== void 0 && sameField(field, replaced.field)) {
637
+ warn(
638
+ "W_REDUNDANT_OVERRIDE",
639
+ `field \`${field.name.text}\` is identical to the one inherited from class \`${replaced.from.name.text}\` on line ${replaced.field.line}`,
640
+ field.name
641
+ );
642
+ }
500
643
  if (field.type?.kind === "unknown") {
501
644
  const word = field.type.name.text;
502
645
  const suggestion = closestTypeWord(word);
@@ -534,8 +677,8 @@ function resolve(doc) {
534
677
  }
535
678
  }
536
679
  const present2 = new Set(out.diagnostics.map(key));
537
- for (const w of warnings) {
538
- if (!present2.has(key(w))) out.diagnostics.push(w);
680
+ for (const d of found) {
681
+ if (!present2.has(key(d))) out.diagnostics.push(d);
539
682
  }
540
683
  out.diagnostics.sort((a, b) => a.line - b.line);
541
684
  out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
@@ -566,6 +709,7 @@ function copyField(f) {
566
709
  }
567
710
  function copyClass(c) {
568
711
  const out = { ...c, name: copyName(c.name), fields: c.fields.map(copyField) };
712
+ if (c.parent !== void 0) out.parent = copyName(c.parent);
569
713
  if (c.system !== void 0) out.system = copyName(c.system);
570
714
  if (c.similarTo !== void 0) out.similarTo = copyName(c.similarTo);
571
715
  return out;
@@ -744,6 +888,7 @@ function buildClass(cls, mappingPrefix, enums) {
744
888
  const { node, fields } = cls;
745
889
  const out = {};
746
890
  if (node.description !== void 0) out.description = node.description;
891
+ if (node.parent !== void 0) out.is_a = node.parent.text;
747
892
  const annotations = {};
748
893
  if (node.system !== void 0) annotations.system = node.system.text;
749
894
  if (node.note !== void 0) annotations.note = node.note;
@@ -826,6 +971,7 @@ function toMermaid(doc, opts) {
826
971
  }
827
972
  for (const cls of resolved.classes) {
828
973
  const from = cls.name.text;
974
+ if (cls.parent !== void 0) lines.push(` ${cls.parent.text} <|-- ${from}`);
829
975
  if (cls.similarTo !== void 0) lines.push(` ${from} ..> ${cls.similarTo.text} : similar`);
830
976
  for (const field of cls.fields) {
831
977
  const type = field.type;
@@ -932,7 +1078,7 @@ function toSkiss(doc) {
932
1078
  `;
933
1079
  }
934
1080
  function classBlock(cls) {
935
- const head = cls.name.text + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
1081
+ const head = cls.name.text + (cls.parent === void 0 ? "" : ` < ${cls.parent.text}`) + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
936
1082
  return [withTrailer(head, cls), ...cls.fields.map(fieldLine)].join("\n");
937
1083
  }
938
1084
  function fieldLine(field) {
@@ -986,7 +1132,14 @@ var PRIMITIVE_RANGE = {
986
1132
  uri: "uri",
987
1133
  uriorcurie: "uri"
988
1134
  };
989
- var CLASS_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "close_mappings", "attributes", "slots"]);
1135
+ var CLASS_KEYS = /* @__PURE__ */ new Set([
1136
+ "description",
1137
+ "is_a",
1138
+ "annotations",
1139
+ "close_mappings",
1140
+ "attributes",
1141
+ "slots"
1142
+ ]);
990
1143
  var SLOT_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "range", "multivalued", "identifier"]);
991
1144
  var CLASS_TAGS = /* @__PURE__ */ new Set(["system", "note", "undeclared"]);
992
1145
  var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
@@ -1029,7 +1182,7 @@ function fromLinkML(schema) {
1029
1182
  for (const key2 of Object.keys(classDefs)) {
1030
1183
  const names = /* @__PURE__ */ new Map();
1031
1184
  const taken = /* @__PURE__ */ new Set();
1032
- for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) {
1185
+ for (const slot of fieldKeysOf(key2)) {
1033
1186
  names.set(slot, unique(toFieldName(slot), taken));
1034
1187
  }
1035
1188
  fieldNames.set(key2, names);
@@ -1099,6 +1252,12 @@ function fromLinkML(schema) {
1099
1252
  const note = oneLine(rawNote);
1100
1253
  if (note !== void 0) node.note = note;
1101
1254
  } else if (rawNote !== void 0) report("annotation", name2, notText("note", rawNote));
1255
+ const parent = stringOf(get(def, "is_a"));
1256
+ if (parent !== void 0) {
1257
+ const parentName = Object.hasOwn(classDefs, parent) ? classNameOf(parent) : toClassName(parent);
1258
+ node.parent = at0(parentName);
1259
+ reference(name2, parent, parentName);
1260
+ }
1102
1261
  const mappings = stringList(get(def, "close_mappings"));
1103
1262
  const first = mappings[0];
1104
1263
  const target = first === void 0 ? "" : localName(first);
@@ -1121,6 +1280,9 @@ function fromLinkML(schema) {
1121
1280
  }
1122
1281
  }
1123
1282
  const block = get(def, other);
1283
+ if (other === "is_a" && present(block) && stringOf(block) === void 0) {
1284
+ report("is_a", name2, `${shown(block)} is not a class name`);
1285
+ }
1124
1286
  if (other === "attributes" && present(block) && asRecord(block) === void 0) {
1125
1287
  report("attributes", name2, `${shown(block)} is not a map`);
1126
1288
  }
@@ -1135,10 +1297,12 @@ function fromLinkML(schema) {
1135
1297
  report("slot_usage", name2);
1136
1298
  continue;
1137
1299
  }
1300
+ const written = slotKeysOf(def);
1301
+ const inherited = inheritedSlots(key2);
1138
1302
  for (const slot of Object.keys(usage)) {
1139
- if (fieldNames.get(key2)?.has(slot) === true) {
1303
+ if (written.includes(slot)) {
1140
1304
  report("slot_usage", `${name2}.${fieldNameOf(key2, slot)}`);
1141
- } else {
1305
+ } else if (!inherited.has(slot)) {
1142
1306
  report("slot_usage", name2, `\`${slot}\` is inherited and not in the sketch`);
1143
1307
  }
1144
1308
  }
@@ -1146,9 +1310,10 @@ function fromLinkML(schema) {
1146
1310
  }
1147
1311
  report(other, name2);
1148
1312
  }
1149
- for (const slot of slotKeysOf(def)) {
1313
+ const own = slotKeysOf(def);
1314
+ for (const slot of fieldKeysOf(key2)) {
1150
1315
  const attributes = asRecord(get(def, "attributes")) ?? {};
1151
- const rawSlot = Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
1316
+ const rawSlot = !own.includes(slot) ? get(asRecord(get(def, "slot_usage")) ?? {}, slot) : Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
1152
1317
  const slotDef = asRecord(rawSlot);
1153
1318
  if (slotDef === void 0 && present(rawSlot)) {
1154
1319
  report("slot", `${name2}.${fieldNameOf(key2, slot)}`, "is not a slot definition");
@@ -1285,6 +1450,34 @@ function fromLinkML(schema) {
1285
1450
  report("narrowed", element, `\`${range}\` is not a Skiss type; it falls back to string`);
1286
1451
  return { kind: "unknown", name: at0(word), many };
1287
1452
  }
1453
+ function parentKeyOf(classKey) {
1454
+ const def = asRecord(get(classDefs, classKey)) ?? {};
1455
+ const parent = stringOf(get(def, "is_a"));
1456
+ if (parent === void 0 || !Object.hasOwn(classDefs, parent)) return void 0;
1457
+ return parent;
1458
+ }
1459
+ function inheritedSlots(classKey) {
1460
+ const out = /* @__PURE__ */ new Set();
1461
+ const seen = /* @__PURE__ */ new Set([classKey]);
1462
+ let key2 = parentKeyOf(classKey);
1463
+ while (key2 !== void 0 && !seen.has(key2)) {
1464
+ seen.add(key2);
1465
+ for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) out.add(slot);
1466
+ key2 = parentKeyOf(key2);
1467
+ }
1468
+ return out;
1469
+ }
1470
+ function fieldKeysOf(classKey) {
1471
+ const def = asRecord(get(classDefs, classKey)) ?? {};
1472
+ const keys = slotKeysOf(def);
1473
+ const usage = asRecord(get(def, "slot_usage"));
1474
+ if (usage === void 0) return keys;
1475
+ const inherited = inheritedSlots(classKey);
1476
+ for (const slot of Object.keys(usage)) {
1477
+ if (inherited.has(slot) && !keys.includes(slot)) keys.push(slot);
1478
+ }
1479
+ return keys;
1480
+ }
1288
1481
  function listedSomewhere(slot) {
1289
1482
  for (const key2 of Object.keys(classDefs)) {
1290
1483
  const def = asRecord(get(classDefs, key2)) ?? {};
@@ -1443,6 +1636,204 @@ var nothing = (element, detail) => ({
1443
1636
  dropped: [{ kind: UNREADABLE, element, detail }]
1444
1637
  });
1445
1638
 
1639
+ // src/highlight.ts
1640
+ var TRAILING_WS = /[ \t]+$/;
1641
+ function tokenizeLine(line) {
1642
+ const text = line.replace(TRAILING_WS, "");
1643
+ const first = text[0];
1644
+ if (first === void 0) {
1645
+ return [];
1646
+ }
1647
+ if (first === "#") {
1648
+ return [{ kind: "comment", from: 0, to: text.length }];
1649
+ }
1650
+ const { head, trailers } = splitTrailer2(text);
1651
+ return [...headTokens(head, isWs(first)), ...trailers];
1652
+ }
1653
+ function splitTrailer2(text) {
1654
+ let start = -1;
1655
+ for (let i = 1; i < text.length; i++) {
1656
+ if (!isWs(text[i - 1])) {
1657
+ continue;
1658
+ }
1659
+ if (text[i] === "#" || isDoubtMarker(text, i)) {
1660
+ start = i;
1661
+ break;
1662
+ }
1663
+ }
1664
+ if (start < 0) {
1665
+ return { head: text, trailers: [] };
1666
+ }
1667
+ const head = text.slice(0, start);
1668
+ if (text[start] === "?") {
1669
+ return { head, trailers: [{ kind: "doubt", from: start, to: text.length }] };
1670
+ }
1671
+ let doubt = -1;
1672
+ for (let i = start + 1; i < text.length; i++) {
1673
+ if (isDoubtMarker(text, i)) {
1674
+ doubt = i;
1675
+ break;
1676
+ }
1677
+ }
1678
+ if (doubt < 0) {
1679
+ return { head, trailers: [{ kind: "description", from: start, to: text.length }] };
1680
+ }
1681
+ return {
1682
+ head,
1683
+ trailers: [
1684
+ // The whitespace between the two belongs to neither.
1685
+ {
1686
+ kind: "description",
1687
+ from: start,
1688
+ to: text.slice(0, doubt).replace(TRAILING_WS, "").length
1689
+ },
1690
+ { kind: "doubt", from: doubt, to: text.length }
1691
+ ]
1692
+ };
1693
+ }
1694
+ function scan(head) {
1695
+ const raws = [];
1696
+ let i = 0;
1697
+ while (i < head.length) {
1698
+ const c = head[i] ?? "";
1699
+ if (isWs(c)) {
1700
+ i += 1;
1701
+ continue;
1702
+ }
1703
+ const word = wordAt(head, i);
1704
+ if (word !== void 0) {
1705
+ raws.push({ text: word, from: i, to: i + word.length, word: true });
1706
+ i += word.length;
1707
+ continue;
1708
+ }
1709
+ if (c === "[" && head[i + 1] === "]") {
1710
+ raws.push({ text: "[]", from: i, to: i + 2, word: false });
1711
+ i += 2;
1712
+ continue;
1713
+ }
1714
+ const ch = String.fromCodePoint(head.codePointAt(i) ?? 0);
1715
+ raws.push({ text: ch, from: i, to: i + ch.length, word: false });
1716
+ i += ch.length;
1717
+ }
1718
+ return raws;
1719
+ }
1720
+ var token = (kind, raw) => ({ kind, from: raw.from, to: raw.to });
1721
+ function headTokens(head, field) {
1722
+ const raws = scan(head);
1723
+ const tokens = [];
1724
+ let i = 0;
1725
+ const name2 = raws[0];
1726
+ if (name2?.word === true && (field ? FIELD_NAME : CLASS_NAME).test(name2.text)) {
1727
+ tokens.push(token(field ? "field" : "class", name2));
1728
+ i = 1;
1729
+ }
1730
+ while (i < raws.length) {
1731
+ const raw = raws[i];
1732
+ i += 1;
1733
+ if (raw === void 0 || raw.word) {
1734
+ continue;
1735
+ }
1736
+ switch (raw.text) {
1737
+ case "*":
1738
+ tokens.push(token("marker", raw));
1739
+ break;
1740
+ case ":":
1741
+ tokens.push(token("operator", raw));
1742
+ i = typeTokens(raws, i, tokens);
1743
+ break;
1744
+ case "@":
1745
+ tokens.push(token("operator", raw));
1746
+ i = nameToken(raws, i, tokens, "system", SYSTEM_NAME);
1747
+ break;
1748
+ case "~":
1749
+ case "<":
1750
+ tokens.push(token("operator", raw));
1751
+ i = nameToken(raws, i, tokens, "class", CLASS_NAME);
1752
+ break;
1753
+ case "=":
1754
+ tokens.push(token("operator", raw));
1755
+ i = joinTokens(raws, i, tokens);
1756
+ break;
1757
+ case "|":
1758
+ case "[]":
1759
+ tokens.push(token("operator", raw));
1760
+ break;
1761
+ default:
1762
+ break;
1763
+ }
1764
+ }
1765
+ return tokens;
1766
+ }
1767
+ function nameToken(raws, i, tokens, kind, shape) {
1768
+ const raw = raws[i];
1769
+ if (raw === void 0 || !raw.word) {
1770
+ return i;
1771
+ }
1772
+ if (shape.test(raw.text)) {
1773
+ tokens.push(token(kind, raw));
1774
+ }
1775
+ return i + 1;
1776
+ }
1777
+ function joinTokens(raws, start, tokens) {
1778
+ let i = nameToken(raws, start, tokens, "class", CLASS_NAME);
1779
+ const dot = raws[i];
1780
+ if (dot === void 0 || dot.text !== "." || dot.word) {
1781
+ return i;
1782
+ }
1783
+ tokens.push(token("operator", dot));
1784
+ i += 1;
1785
+ return nameToken(raws, i, tokens, "field", FIELD_NAME);
1786
+ }
1787
+ function typeTokens(raws, start, tokens) {
1788
+ const first = raws[start];
1789
+ if (first === void 0 || !first.word) {
1790
+ return start;
1791
+ }
1792
+ const words = [first];
1793
+ const operators = [];
1794
+ let pipes = 0;
1795
+ let i = start + 1;
1796
+ while (raws[i]?.text === "|") {
1797
+ const pipe = raws[i];
1798
+ if (pipe === void 0) {
1799
+ break;
1800
+ }
1801
+ operators.push(pipe);
1802
+ pipes += 1;
1803
+ i += 1;
1804
+ const value = raws[i];
1805
+ if (value === void 0 || !value.word) {
1806
+ break;
1807
+ }
1808
+ words.push(value);
1809
+ i += 1;
1810
+ }
1811
+ const many = raws[i];
1812
+ if (many !== void 0 && many.text === "[]" && !many.word) {
1813
+ operators.push(many);
1814
+ i += 1;
1815
+ }
1816
+ const typed = pipes > 0 ? words.filter((word) => ENUM_VALUE.test(word.text)).map((word) => token("enum", word)) : nameTypeTokens(first);
1817
+ tokens.push(
1818
+ ...[...typed, ...operators.map((raw) => token("operator", raw))].sort(
1819
+ (a, b) => a.from - b.from
1820
+ )
1821
+ );
1822
+ return i;
1823
+ }
1824
+ function nameTypeTokens(word) {
1825
+ if (primitiveFor(word.text) !== void 0) {
1826
+ return [token("primitive", word)];
1827
+ }
1828
+ if (CLASS_NAME.test(word.text)) {
1829
+ return [token("class", word)];
1830
+ }
1831
+ if (/^[a-z]/.test(word.text)) {
1832
+ return [token("type", word)];
1833
+ }
1834
+ return [];
1835
+ }
1836
+
1446
1837
  // src/import.ts
1447
1838
  import { parse as parseYAML } from "yaml";
1448
1839
  function importLinkML(text) {
@@ -1467,7 +1858,7 @@ function notYAML(error) {
1467
1858
  }
1468
1859
 
1469
1860
  // src/index.ts
1470
- var VERSION = "0.4.0";
1861
+ var VERSION = "0.6.0";
1471
1862
  export {
1472
1863
  VERSION,
1473
1864
  compile,
@@ -1480,6 +1871,7 @@ export {
1480
1871
  serialize,
1481
1872
  toLinkML,
1482
1873
  toMermaid,
1483
- toSkiss
1874
+ toSkiss,
1875
+ tokenizeLine
1484
1876
  };
1485
1877
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eriknaslund/skiss",
3
- "version": "0.4.0",
3
+ "version": "0.6.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,8 @@
41
41
  },
42
42
  "devDependencies": {
43
43
  "@biomejs/biome": "^2.5.13",
44
- "@types/node": "^24.0.0",
44
+ "@mermaid-js/mermaid-cli": "^11.17.0",
45
+ "@types/node": "^26.6.1",
45
46
  "jsdom": "^30.0.1",
46
47
  "mermaid": "^12.0.0",
47
48
  "tsup": "^8.5.1",