@eriknaslund/skiss 0.3.0 → 0.4.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
@@ -3,7 +3,8 @@ var isWs = (c) => c === " " || c === " ";
3
3
  var trimWs = (s) => s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
4
4
  var CLASS_NAME = /^[A-Z][A-Za-z0-9]*$/;
5
5
  var FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
6
- var SYSTEM_OR_VALUE = /^[A-Za-z][A-Za-z0-9_-]*$/;
6
+ var SYSTEM_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
7
+ var ENUM_VALUE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
7
8
  var PRIMITIVES = {
8
9
  string: "string",
9
10
  int: "int",
@@ -308,7 +309,7 @@ function parseSystem(c, at) {
308
309
  system ?? afterSpan(at)
309
310
  );
310
311
  }
311
- if (!SYSTEM_OR_VALUE.test(system.text)) {
312
+ if (!SYSTEM_NAME.test(system.text)) {
312
313
  return fail(
313
314
  "E_UNPARSABLE",
314
315
  `A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
@@ -366,10 +367,10 @@ function parseType(c, colon) {
366
367
  }
367
368
  if (words.length > 1) {
368
369
  for (const w of words) {
369
- if (!SYSTEM_OR_VALUE.test(w.text)) {
370
+ if (!ENUM_VALUE.test(w.text)) {
370
371
  return fail(
371
372
  "E_UNPARSABLE",
372
- `An enum value is letters, digits, \`-\` and \`_\`, starting with a letter: \`${w.text}\``,
373
+ `An enum value is letters, digits, \`-\` and \`_\`, and does not start with \`-\` or \`_\`: \`${w.text}\``,
373
374
  c.line,
374
375
  w
375
376
  );
@@ -506,6 +507,18 @@ function resolve(doc) {
506
507
  );
507
508
  } else if (field.type?.kind === "class") {
508
509
  reference(field.type.name);
510
+ } else if (field.type?.kind === "enum") {
511
+ const seen = /* @__PURE__ */ new Set();
512
+ for (const value of field.type.values) {
513
+ if (seen.has(value.text)) {
514
+ warn(
515
+ "W_DUPLICATE_ENUM_VALUE",
516
+ `value \`${value.text}\` is already in this enum; it is carried once`,
517
+ value
518
+ );
519
+ }
520
+ seen.add(value.text);
521
+ }
509
522
  }
510
523
  if (field.joinsTo !== void 0) {
511
524
  const { className, fieldName } = field.joinsTo;
@@ -619,7 +632,8 @@ function toLinkML(doc, opts) {
619
632
  const classNames = new Set(classes.map((cls) => cls.node.name.text));
620
633
  for (const name2 of resolved.undeclared) classNames.add(name2.text);
621
634
  const enums = nameEnums(classes, classNames);
622
- const prefixes = { linkml: "https://w3id.org/linkml/" };
635
+ const prefixes = nameKeyed();
636
+ prefixes.linkml = "https://w3id.org/linkml/";
623
637
  if (!Object.hasOwn(prefixes, schemaName)) prefixes[schemaName] = `${BASE_URI}/${schemaName}/`;
624
638
  const systemPrefix = /* @__PURE__ */ new Map();
625
639
  const addSystem = (system) => {
@@ -650,12 +664,12 @@ function toLinkML(doc, opts) {
650
664
  prefixes,
651
665
  imports: ["linkml:types"]
652
666
  };
653
- const out = {};
667
+ const out = nameKeyed();
654
668
  for (const cls of classes) {
655
669
  out[cls.node.name.text] = buildClass(cls, mappingPrefix, enums);
656
670
  }
657
671
  for (const name2 of resolved.undeclared) {
658
- if (!(name2.text in out)) out[name2.text] = { annotations: { undeclared: true } };
672
+ if (!Object.hasOwn(out, name2.text)) out[name2.text] = { annotations: { undeclared: true } };
659
673
  }
660
674
  if (Object.keys(out).length > 0) schema.classes = out;
661
675
  const enumBlock = buildEnums(classes, enums);
@@ -713,13 +727,13 @@ function nameEnums(classes, classNames) {
713
727
  return names;
714
728
  }
715
729
  function buildEnums(classes, enums) {
716
- const out = {};
730
+ const out = nameKeyed();
717
731
  for (const { node, fields } of classes) {
718
732
  for (const field of fields) {
719
733
  if (field.type?.kind !== "enum") continue;
720
734
  const name2 = enums.get(useKey(node.name.text, field.name.text));
721
- if (name2 === void 0 || name2 in out) continue;
722
- const values = {};
735
+ if (name2 === void 0 || Object.hasOwn(out, name2)) continue;
736
+ const values = nameKeyed();
723
737
  for (const value of field.type.values) values[value.text] = null;
724
738
  out[name2] = { permissible_values: values };
725
739
  }
@@ -738,7 +752,7 @@ function buildClass(cls, mappingPrefix, enums) {
738
752
  const target = node.similarTo.text;
739
753
  out.close_mappings = [`${mappingPrefix(target)}:${target}`];
740
754
  }
741
- const attributes = {};
755
+ const attributes = nameKeyed();
742
756
  for (const field of fields) {
743
757
  attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
744
758
  }
@@ -776,10 +790,12 @@ function rangeOf(field, className, enums) {
776
790
  }
777
791
  }
778
792
  var RESERVED_PREFIXES = /* @__PURE__ */ new Set(["linkml", "xsd", "shex", "schema"]);
793
+ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
794
+ var nameKeyed = () => /* @__PURE__ */ Object.create(null);
779
795
  var linkmlName = (text) => {
780
796
  const name2 = text.toLowerCase().replace(/[^a-z0-9]/g, "_");
781
797
  if (!/[a-z0-9]/.test(name2)) return "sketch";
782
- if (RESERVED_PREFIXES.has(name2)) return `${name2}_`;
798
+ if (RESERVED_PREFIXES.has(name2) || PROTOTYPE_KEYS.has(name2)) return `${name2}_`;
783
799
  return /^[0-9]/.test(name2) ? `_${name2}` : name2;
784
800
  };
785
801
  var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
@@ -942,10 +958,15 @@ function typeText2(type) {
942
958
  }
943
959
  return type.many ? `${text}[]` : text;
944
960
  }
961
+ function writeDescription(text) {
962
+ const attached = text.replace(/[ \t]+\?(?=[ \t]|$)/g, "?");
963
+ return /^\?([ \t]|$)/.test(attached) ? `#${attached}` : `# ${attached}`;
964
+ }
965
+ var rewordsDescription = (text) => writeDescription(text) !== `# ${text}`;
945
966
  function withTrailer(head, node) {
946
967
  const parts2 = [];
947
968
  if (node.description !== void 0 && node.description !== "")
948
- parts2.push(`# ${node.description}`);
969
+ parts2.push(writeDescription(node.description));
949
970
  if (node.note !== void 0 && node.note !== "") parts2.push(`? ${node.note}`);
950
971
  if (parts2.length === 0) return head;
951
972
  const gap = head.length < TRAILER_COLUMN - 1 ? " ".repeat(TRAILER_COLUMN - head.length) : " ";
@@ -983,6 +1004,7 @@ var QUIET_SCHEMA_KEYS = /* @__PURE__ */ new Set([
983
1004
  "slots",
984
1005
  "enums"
985
1006
  ]);
1007
+ var REWORDED = "a standalone `?` is attached to the word before it";
986
1008
  var UNKNOWN_TYPE = /^[a-z][a-z0-9_-]*$/;
987
1009
  function fromLinkML(schema) {
988
1010
  const root = asRecord(schema);
@@ -1026,7 +1048,7 @@ function fromLinkML(schema) {
1026
1048
  const rawValues = get(asRecord(get(enumDefs, key2)) ?? {}, "permissible_values");
1027
1049
  const values = asRecord(rawValues);
1028
1050
  const keys = Object.keys(values ?? {});
1029
- const bad = keys.find((value) => !SYSTEM_OR_VALUE.test(value));
1051
+ const bad = keys.find((value) => !ENUM_VALUE.test(value));
1030
1052
  if (values === void 0 && present(rawValues)) {
1031
1053
  unwritable.set(key2, `${shown(rawValues)} is not a map of permissible values`);
1032
1054
  } else if (keys.length < 2) {
@@ -1057,14 +1079,26 @@ function fromLinkML(schema) {
1057
1079
  }
1058
1080
  continue;
1059
1081
  }
1082
+ const undeclared = annotations.get("undeclared");
1083
+ if (undeclared !== void 0 && typeof undeclared !== "boolean") {
1084
+ report("annotation", name2, notText("undeclared", undeclared));
1085
+ }
1060
1086
  const node = { name: at0(name2), fields: [], line: 0 };
1061
1087
  const description = oneLine(stringOf(get(def, "description")));
1062
- if (description !== void 0) node.description = description;
1088
+ if (description !== void 0) {
1089
+ node.description = description;
1090
+ if (rewordsDescription(description)) report("reworded", name2, REWORDED);
1091
+ }
1063
1092
  const system = annotations.get("system");
1064
- if (typeof system === "string" && SYSTEM_OR_VALUE.test(system)) node.system = at0(system);
1065
- else if (system !== void 0) report("annotation", name2, "`system` is not a system name");
1066
- const note = oneLine(stringOf(annotations.get("note")));
1067
- if (note !== void 0) node.note = note;
1093
+ if (typeof system === "string") {
1094
+ if (SYSTEM_NAME.test(system)) node.system = at0(system);
1095
+ else report("annotation", name2, "`system` is not a system name");
1096
+ } else if (system !== void 0) report("annotation", name2, notText("system", system));
1097
+ const rawNote = annotations.get("note");
1098
+ if (typeof rawNote === "string") {
1099
+ const note = oneLine(rawNote);
1100
+ if (note !== void 0) node.note = note;
1101
+ } else if (rawNote !== void 0) report("annotation", name2, notText("note", rawNote));
1068
1102
  const mappings = stringList(get(def, "close_mappings"));
1069
1103
  const first = mappings[0];
1070
1104
  const target = first === void 0 ? "" : localName(first);
@@ -1169,13 +1203,18 @@ function fromLinkML(schema) {
1169
1203
  line: 0
1170
1204
  };
1171
1205
  const description = oneLine(stringOf(get(def, "description")));
1172
- if (description !== void 0) field.description = description;
1206
+ if (description !== void 0) {
1207
+ field.description = description;
1208
+ if (rewordsDescription(description)) report("reworded", element, REWORDED);
1209
+ }
1173
1210
  const type = typeFor(element, name2, def);
1174
1211
  if (type !== void 0) field.type = type;
1175
1212
  const annotations = annotationsOf(def);
1176
1213
  const system = annotations.get("system");
1177
- if (typeof system === "string" && SYSTEM_OR_VALUE.test(system)) field.system = at0(system);
1178
- else if (system !== void 0) report("annotation", element, "`system` is not a system name");
1214
+ if (typeof system === "string") {
1215
+ if (SYSTEM_NAME.test(system)) field.system = at0(system);
1216
+ else report("annotation", element, "`system` is not a system name");
1217
+ } else if (system !== void 0) report("annotation", element, notText("system", system));
1179
1218
  const joins = annotations.get("joins_to");
1180
1219
  if (typeof joins === "string") {
1181
1220
  const dot = joins.indexOf(".");
@@ -1188,9 +1227,12 @@ function fromLinkML(schema) {
1188
1227
  reference(element, target, targetClass);
1189
1228
  reference(element, targetField, targetName);
1190
1229
  } else report("annotation", element, "`joins_to` is not `Class.field`");
1191
- } else if (joins !== void 0) report("annotation", element, "`joins_to` is not text");
1192
- const note = oneLine(stringOf(annotations.get("note")));
1193
- if (note !== void 0) field.note = note;
1230
+ } else if (joins !== void 0) report("annotation", element, notText("joins_to", joins));
1231
+ const rawNote = annotations.get("note");
1232
+ if (typeof rawNote === "string") {
1233
+ const note = oneLine(rawNote);
1234
+ if (note !== void 0) field.note = note;
1235
+ } else if (rawNote !== void 0) report("annotation", element, notText("note", rawNote));
1194
1236
  for (const other of Object.keys(def)) {
1195
1237
  if (!SLOT_KEYS.has(other)) {
1196
1238
  report(other, element);
@@ -1265,13 +1307,17 @@ var COUNTED = {
1265
1307
  class: ["class", "classes"],
1266
1308
  slot: ["slot", "slots"],
1267
1309
  schema: ["schema key", "schema keys"],
1310
+ narrowed: ["narrowed range", "narrowed ranges"],
1311
+ enum_detail: ["enum detail", "enum details"],
1312
+ annotation: ["unreadable annotation", "unreadable annotations"],
1313
+ reworded: ["description reworded", "descriptions reworded"],
1268
1314
  // `mixin: true` marks the class; `mixins: [M]` points at another one.
1269
1315
  mixin: ["mixin class", "mixin classes"],
1270
1316
  mixins: ["mixin", "mixins"],
1271
1317
  pattern: ["pattern", "patterns"],
1272
1318
  unit: ["unit", "units"]
1273
1319
  };
1274
- var ENUM_KINDS = /* @__PURE__ */ new Set(["enum_detail"]);
1320
+ var UNREADABLE = "unreadable";
1275
1321
  function formatDropped(dropped) {
1276
1322
  const groups = /* @__PURE__ */ new Map();
1277
1323
  for (const entry of dropped) {
@@ -1280,15 +1326,21 @@ function formatDropped(dropped) {
1280
1326
  else group.push(entry);
1281
1327
  }
1282
1328
  const drops = [];
1329
+ const notRead = [];
1283
1330
  let renamed = "";
1284
1331
  let inlined = "";
1285
1332
  for (const [kind, entries] of groups) {
1333
+ if (kind === UNREADABLE) {
1334
+ for (const entry of entries) notRead.push(entry.detail ?? "the schema could not be read");
1335
+ continue;
1336
+ }
1286
1337
  const phrase = phraseFor(kind, entries);
1287
1338
  if (kind === "renamed") renamed = phrase;
1288
1339
  else if (kind === "inlined") inlined = phrase;
1289
1340
  else drops.push(phrase);
1290
1341
  }
1291
1342
  const sentences = [];
1343
+ if (notRead.length > 0) sentences.push(`Not read: ${notRead.join("; ")}.`);
1292
1344
  if (drops.length > 0) sentences.push(`Dropped: ${drops.join(", ")}.`);
1293
1345
  if (renamed !== "") sentences.push(`Renamed: ${renamed}.`);
1294
1346
  if (inlined !== "") sentences.push(`Inlined: ${inlined}.`);
@@ -1298,11 +1350,10 @@ function phraseFor(kind, entries) {
1298
1350
  const n = entries.length;
1299
1351
  const counted = Object.hasOwn(COUNTED, kind) ? COUNTED[kind] : void 0;
1300
1352
  if (counted !== void 0) return `${n} ${n === 1 ? counted[0] : counted[1]}`;
1301
- return `${kind} on ${n} ${nounFor(kind, entries)}`;
1353
+ return `${kind} on ${n} ${nounFor(entries)}`;
1302
1354
  }
1303
- function nounFor(kind, entries) {
1355
+ function nounFor(entries) {
1304
1356
  const one = entries.length === 1;
1305
- if (ENUM_KINDS.has(kind)) return one ? "enum" : "enums";
1306
1357
  const slots = entries.filter((entry) => entry.element.includes(".")).length;
1307
1358
  if (slots === entries.length) return one ? "slot" : "slots";
1308
1359
  if (slots === 0) return one ? "class" : "classes";
@@ -1353,12 +1404,18 @@ function annotationsOf(def) {
1353
1404
  if (annotations === void 0) return out;
1354
1405
  for (const tag of Object.keys(annotations)) {
1355
1406
  const raw = get(annotations, tag);
1356
- const value = asRecord(raw) === void 0 ? raw : get(asRecord(raw) ?? {}, "value");
1357
- if (typeof value === "string" || typeof value === "boolean") out.set(tag, value);
1358
- else out.set(tag, String(value));
1407
+ const long = asRecord(raw);
1408
+ const value = long === void 0 ? raw : get(long, "value");
1409
+ out.set(tag, value ?? null);
1359
1410
  }
1360
1411
  return out;
1361
1412
  }
1413
+ function notText(tag, value) {
1414
+ if (value === null) return `\`${tag}\` was written with no value`;
1415
+ if (Array.isArray(value)) return `\`${tag}\` is a list, not text`;
1416
+ if (typeof value === "object") return `\`${tag}\` is a map, not text`;
1417
+ return `\`${tag}\` is a ${typeof value}, not text`;
1418
+ }
1362
1419
  function slotKeysOf(def) {
1363
1420
  const keys = Object.keys(asRecord(get(def, "attributes")) ?? {});
1364
1421
  for (const slot of stringList(get(def, "slots"))) {
@@ -1383,7 +1440,7 @@ var schemaElement = (root) => stringOf(get(root, "name")) ?? "schema";
1383
1440
  var nothing = (element, detail) => ({
1384
1441
  document: parse(""),
1385
1442
  source: "",
1386
- dropped: [{ kind: "schema", element, detail }]
1443
+ dropped: [{ kind: UNREADABLE, element, detail }]
1387
1444
  });
1388
1445
 
1389
1446
  // src/import.ts
@@ -1410,7 +1467,7 @@ function notYAML(error) {
1410
1467
  }
1411
1468
 
1412
1469
  // src/index.ts
1413
- var VERSION = "0.3.0";
1470
+ var VERSION = "0.4.0";
1414
1471
  export {
1415
1472
  VERSION,
1416
1473
  compile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eriknaslund/skiss",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A text notation for sketching data models, compiled to LinkML",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,8 +12,10 @@
12
12
  "node": ">=20"
13
13
  },
14
14
  "packageManager": "pnpm@9.15.9",
15
+ "sideEffects": false,
15
16
  "files": [
16
- "dist"
17
+ "dist",
18
+ "!dist/**/*.map"
17
19
  ],
18
20
  "exports": {
19
21
  ".": {