@eriknaslund/skiss 0.5.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/README.md CHANGED
@@ -69,7 +69,9 @@ Or without installing: `npx @eriknaslund/skiss diagram model.skiss`.
69
69
 
70
70
  `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
71
 
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.
72
+ `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
+ `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)).
73
75
 
74
76
  Editors that render Skiss live are separate projects built on this package. This repository is the language, the library and the command line.
75
77
 
@@ -87,7 +89,7 @@ Editors that render Skiss live are separate projects built on this package. This
87
89
 
88
90
  ## Status
89
91
 
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.
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.
91
93
 
92
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.
93
95
 
package/dist/cli.js CHANGED
@@ -197,6 +197,10 @@ function splitTrailer(text) {
197
197
  return trailer;
198
198
  }
199
199
  var WORD = /[\p{L}\p{N}_-]+/uy;
200
+ function wordAt(text, i) {
201
+ WORD.lastIndex = i;
202
+ return WORD.exec(text)?.[0];
203
+ }
200
204
  var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "<", ",", "["];
201
205
  function tokenize(head) {
202
206
  const tokens = [];
@@ -207,11 +211,10 @@ function tokenize(head) {
207
211
  i++;
208
212
  continue;
209
213
  }
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;
214
+ const word = wordAt(head, i);
215
+ if (word !== void 0) {
216
+ tokens.push({ kind: "word", text: word, col: i, end: i + word.length });
217
+ i += word.length;
215
218
  continue;
216
219
  }
217
220
  if (c === "[" && head[i + 1] === "]") {
@@ -1667,7 +1670,7 @@ function notYAML(error) {
1667
1670
  }
1668
1671
 
1669
1672
  // src/index.ts
1670
- var VERSION = "0.5.0";
1673
+ var VERSION = "0.6.0";
1671
1674
 
1672
1675
  // src/cli.ts
1673
1676
  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.0";
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
@@ -187,6 +187,10 @@ function splitTrailer(text) {
187
187
  return trailer;
188
188
  }
189
189
  var WORD = /[\p{L}\p{N}_-]+/uy;
190
+ function wordAt(text, i) {
191
+ WORD.lastIndex = i;
192
+ return WORD.exec(text)?.[0];
193
+ }
190
194
  var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "<", ",", "["];
191
195
  function tokenize(head) {
192
196
  const tokens = [];
@@ -197,11 +201,10 @@ function tokenize(head) {
197
201
  i++;
198
202
  continue;
199
203
  }
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;
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;
205
208
  continue;
206
209
  }
207
210
  if (c === "[" && head[i + 1] === "]") {
@@ -518,36 +521,36 @@ function parseType(c, colon) {
518
521
  first
519
522
  );
520
523
  }
521
- function unexpected(token, line, kind, order) {
524
+ function unexpected(token2, line, kind, order) {
522
525
  const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["<", "@", "~"];
523
- if (modifiers.includes(token.kind)) {
526
+ if (modifiers.includes(token2.kind)) {
524
527
  return fail(
525
528
  "E_UNPARSABLE",
526
- `\`${token.text}\` is out of order; a ${kind} line reads: ${order}`,
529
+ `\`${token2.text}\` is out of order; a ${kind} line reads: ${order}`,
527
530
  line,
528
- token
531
+ token2
529
532
  );
530
533
  }
531
- if (token.kind === "<") {
534
+ if (token2.kind === "<") {
532
535
  return fail(
533
536
  "E_UNPARSABLE",
534
537
  "`<` marks inheritance and belongs on the class line: `Child < Parent`",
535
538
  line,
536
- token
539
+ token2
537
540
  );
538
541
  }
539
- if (token.text === "?") {
542
+ if (token2.text === "?") {
540
543
  return fail(
541
544
  "E_UNPARSABLE",
542
545
  "`?` attached to a word is not a marker; a doubt needs whitespace before `?` and after it",
543
546
  line,
544
- token
547
+ token2
545
548
  );
546
549
  }
547
- if (token.text === "#") {
548
- 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);
549
552
  }
550
- 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);
551
554
  }
552
555
 
553
556
  // src/resolve.ts
@@ -1633,6 +1636,204 @@ var nothing = (element, detail) => ({
1633
1636
  dropped: [{ kind: UNREADABLE, element, detail }]
1634
1637
  });
1635
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
+
1636
1837
  // src/import.ts
1637
1838
  import { parse as parseYAML } from "yaml";
1638
1839
  function importLinkML(text) {
@@ -1657,7 +1858,7 @@ function notYAML(error) {
1657
1858
  }
1658
1859
 
1659
1860
  // src/index.ts
1660
- var VERSION = "0.5.0";
1861
+ var VERSION = "0.6.0";
1661
1862
  export {
1662
1863
  VERSION,
1663
1864
  compile,
@@ -1670,6 +1871,7 @@ export {
1670
1871
  serialize,
1671
1872
  toLinkML,
1672
1873
  toMermaid,
1673
- toSkiss
1874
+ toSkiss,
1875
+ tokenizeLine
1674
1876
  };
1675
1877
  //# 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.0",
4
4
  "description": "A text notation for sketching data models, compiled to LinkML",
5
5
  "type": "module",
6
6
  "license": "MIT",