@eriknaslund/skiss 0.2.1 → 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/cli.js CHANGED
@@ -10,7 +10,8 @@ var isWs = (c) => c === " " || c === " ";
10
10
  var trimWs = (s) => s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
11
11
  var CLASS_NAME = /^[A-Z][A-Za-z0-9]*$/;
12
12
  var FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
13
- var SYSTEM_OR_VALUE = /^[A-Za-z][A-Za-z0-9_-]*$/;
13
+ var SYSTEM_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
14
+ var ENUM_VALUE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
14
15
  var PRIMITIVES = {
15
16
  string: "string",
16
17
  int: "int",
@@ -315,7 +316,7 @@ function parseSystem(c, at) {
315
316
  system ?? afterSpan(at)
316
317
  );
317
318
  }
318
- if (!SYSTEM_OR_VALUE.test(system.text)) {
319
+ if (!SYSTEM_NAME.test(system.text)) {
319
320
  return fail(
320
321
  "E_UNPARSABLE",
321
322
  `A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
@@ -373,10 +374,10 @@ function parseType(c, colon) {
373
374
  }
374
375
  if (words.length > 1) {
375
376
  for (const w of words) {
376
- if (!SYSTEM_OR_VALUE.test(w.text)) {
377
+ if (!ENUM_VALUE.test(w.text)) {
377
378
  return fail(
378
379
  "E_UNPARSABLE",
379
- `An enum value is letters, digits, \`-\` and \`_\`, starting with a letter: \`${w.text}\``,
380
+ `An enum value is letters, digits, \`-\` and \`_\`, and does not start with \`-\` or \`_\`: \`${w.text}\``,
380
381
  c.line,
381
382
  w
382
383
  );
@@ -513,6 +514,18 @@ function resolve(doc) {
513
514
  );
514
515
  } else if (field.type?.kind === "class") {
515
516
  reference(field.type.name);
517
+ } else if (field.type?.kind === "enum") {
518
+ const seen = /* @__PURE__ */ new Set();
519
+ for (const value of field.type.values) {
520
+ if (seen.has(value.text)) {
521
+ warn(
522
+ "W_DUPLICATE_ENUM_VALUE",
523
+ `value \`${value.text}\` is already in this enum; it is carried once`,
524
+ value
525
+ );
526
+ }
527
+ seen.add(value.text);
528
+ }
516
529
  }
517
530
  if (field.joinsTo !== void 0) {
518
531
  const { className, fieldName } = field.joinsTo;
@@ -527,9 +540,9 @@ function resolve(doc) {
527
540
  }
528
541
  }
529
542
  }
530
- const present = new Set(out.diagnostics.map(key));
543
+ const present2 = new Set(out.diagnostics.map(key));
531
544
  for (const w of warnings) {
532
- if (!present.has(key(w))) out.diagnostics.push(w);
545
+ if (!present2.has(key(w))) out.diagnostics.push(w);
533
546
  }
534
547
  out.diagnostics.sort((a, b) => a.line - b.line);
535
548
  out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
@@ -626,7 +639,8 @@ function toLinkML(doc, opts) {
626
639
  const classNames = new Set(classes.map((cls) => cls.node.name.text));
627
640
  for (const name2 of resolved.undeclared) classNames.add(name2.text);
628
641
  const enums = nameEnums(classes, classNames);
629
- const prefixes = { linkml: "https://w3id.org/linkml/" };
642
+ const prefixes = nameKeyed();
643
+ prefixes.linkml = "https://w3id.org/linkml/";
630
644
  if (!Object.hasOwn(prefixes, schemaName)) prefixes[schemaName] = `${BASE_URI}/${schemaName}/`;
631
645
  const systemPrefix = /* @__PURE__ */ new Map();
632
646
  const addSystem = (system) => {
@@ -657,12 +671,12 @@ function toLinkML(doc, opts) {
657
671
  prefixes,
658
672
  imports: ["linkml:types"]
659
673
  };
660
- const out = {};
674
+ const out = nameKeyed();
661
675
  for (const cls of classes) {
662
676
  out[cls.node.name.text] = buildClass(cls, mappingPrefix, enums);
663
677
  }
664
678
  for (const name2 of resolved.undeclared) {
665
- if (!(name2.text in out)) out[name2.text] = { annotations: { undeclared: true } };
679
+ if (!Object.hasOwn(out, name2.text)) out[name2.text] = { annotations: { undeclared: true } };
666
680
  }
667
681
  if (Object.keys(out).length > 0) schema.classes = out;
668
682
  const enumBlock = buildEnums(classes, enums);
@@ -720,13 +734,13 @@ function nameEnums(classes, classNames) {
720
734
  return names;
721
735
  }
722
736
  function buildEnums(classes, enums) {
723
- const out = {};
737
+ const out = nameKeyed();
724
738
  for (const { node, fields } of classes) {
725
739
  for (const field of fields) {
726
740
  if (field.type?.kind !== "enum") continue;
727
741
  const name2 = enums.get(useKey(node.name.text, field.name.text));
728
- if (name2 === void 0 || name2 in out) continue;
729
- const values = {};
742
+ if (name2 === void 0 || Object.hasOwn(out, name2)) continue;
743
+ const values = nameKeyed();
730
744
  for (const value of field.type.values) values[value.text] = null;
731
745
  out[name2] = { permissible_values: values };
732
746
  }
@@ -745,7 +759,7 @@ function buildClass(cls, mappingPrefix, enums) {
745
759
  const target = node.similarTo.text;
746
760
  out.close_mappings = [`${mappingPrefix(target)}:${target}`];
747
761
  }
748
- const attributes = {};
762
+ const attributes = nameKeyed();
749
763
  for (const field of fields) {
750
764
  attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
751
765
  }
@@ -783,10 +797,12 @@ function rangeOf(field, className, enums) {
783
797
  }
784
798
  }
785
799
  var RESERVED_PREFIXES = /* @__PURE__ */ new Set(["linkml", "xsd", "shex", "schema"]);
800
+ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
801
+ var nameKeyed = () => /* @__PURE__ */ Object.create(null);
786
802
  var linkmlName = (text) => {
787
803
  const name2 = text.toLowerCase().replace(/[^a-z0-9]/g, "_");
788
804
  if (!/[a-z0-9]/.test(name2)) return "sketch";
789
- if (RESERVED_PREFIXES.has(name2)) return `${name2}_`;
805
+ if (RESERVED_PREFIXES.has(name2) || PROTOTYPE_KEYS.has(name2)) return `${name2}_`;
790
806
  return /^[0-9]/.test(name2) ? `_${name2}` : name2;
791
807
  };
792
808
  var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
@@ -915,8 +931,550 @@ function formatDiagnostic(d, file) {
915
931
  return `${where}: ${d.severity} ${d.code} ${d.message}`;
916
932
  }
917
933
 
934
+ // src/generators/skiss.ts
935
+ var TRAILER_COLUMN = 40;
936
+ function toSkiss(doc) {
937
+ if (doc.classes.length === 0) return "";
938
+ return `${doc.classes.map(classBlock).join("\n\n")}
939
+ `;
940
+ }
941
+ function classBlock(cls) {
942
+ const head = cls.name.text + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
943
+ return [withTrailer(head, cls), ...cls.fields.map(fieldLine)].join("\n");
944
+ }
945
+ function fieldLine(field) {
946
+ const type = typeText2(field.type);
947
+ const head = ` ${field.name.text}` + (field.identifier ? "*" : "") + (type === void 0 ? "" : `: ${type}`) + (field.system === void 0 ? "" : ` @${field.system.text}`) + (field.joinsTo === void 0 ? "" : ` = ${field.joinsTo.className.text}.${field.joinsTo.fieldName.text}`);
948
+ return withTrailer(head, field);
949
+ }
950
+ function typeText2(type) {
951
+ if (type === void 0) return void 0;
952
+ let text;
953
+ switch (type.kind) {
954
+ case "primitive":
955
+ if (type.name === "string" && !type.many) return void 0;
956
+ text = type.name;
957
+ break;
958
+ case "class":
959
+ case "unknown":
960
+ text = type.name.text;
961
+ break;
962
+ case "enum":
963
+ text = type.values.map((v) => v.text).join("|");
964
+ break;
965
+ }
966
+ return type.many ? `${text}[]` : text;
967
+ }
968
+ function writeDescription(text) {
969
+ const attached = text.replace(/[ \t]+\?(?=[ \t]|$)/g, "?");
970
+ return /^\?([ \t]|$)/.test(attached) ? `#${attached}` : `# ${attached}`;
971
+ }
972
+ var rewordsDescription = (text) => writeDescription(text) !== `# ${text}`;
973
+ function withTrailer(head, node) {
974
+ const parts2 = [];
975
+ if (node.description !== void 0 && node.description !== "")
976
+ parts2.push(writeDescription(node.description));
977
+ if (node.note !== void 0 && node.note !== "") parts2.push(`? ${node.note}`);
978
+ if (parts2.length === 0) return head;
979
+ const gap = head.length < TRAILER_COLUMN - 1 ? " ".repeat(TRAILER_COLUMN - head.length) : " ";
980
+ return head + gap + parts2.join(" ");
981
+ }
982
+
983
+ // src/generators/from-linkml.ts
984
+ var PRIMITIVE_RANGE = {
985
+ string: "string",
986
+ integer: "int",
987
+ float: "float",
988
+ double: "float",
989
+ decimal: "float",
990
+ boolean: "bool",
991
+ date: "date",
992
+ datetime: "datetime",
993
+ uri: "uri",
994
+ uriorcurie: "uri"
995
+ };
996
+ var CLASS_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "close_mappings", "attributes", "slots"]);
997
+ var SLOT_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "range", "multivalued", "identifier"]);
998
+ var CLASS_TAGS = /* @__PURE__ */ new Set(["system", "note", "undeclared"]);
999
+ var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
1000
+ var QUIET_SCHEMA_KEYS = /* @__PURE__ */ new Set([
1001
+ "id",
1002
+ "name",
1003
+ "prefixes",
1004
+ "imports",
1005
+ "default_prefix",
1006
+ "default_range",
1007
+ "title",
1008
+ "license",
1009
+ "version",
1010
+ "classes",
1011
+ "slots",
1012
+ "enums"
1013
+ ]);
1014
+ var REWORDED = "a standalone `?` is attached to the word before it";
1015
+ var UNKNOWN_TYPE = /^[a-z][a-z0-9_-]*$/;
1016
+ function fromLinkML(schema) {
1017
+ const root = asRecord(schema);
1018
+ if (root === void 0) return nothing("schema", "the schema is not an object");
1019
+ const schemaName = schemaElement(root);
1020
+ const classBlock2 = asRecord(get(root, "classes"));
1021
+ if (classBlock2 === void 0) return nothing(schemaName, "the schema has no `classes`");
1022
+ const classDefs = classBlock2;
1023
+ const dropped = [];
1024
+ const report = (kind, element, detail) => {
1025
+ dropped.push(detail === void 0 ? { kind, element } : { kind, element, detail });
1026
+ };
1027
+ const slotDefs = asRecord(get(root, "slots")) ?? {};
1028
+ const enumDefs = asRecord(get(root, "enums")) ?? {};
1029
+ const defaultRange = stringOf(get(root, "default_range")) ?? "string";
1030
+ const classNames = /* @__PURE__ */ new Map();
1031
+ const takenClasses = /* @__PURE__ */ new Set();
1032
+ for (const key2 of Object.keys(classDefs)) {
1033
+ classNames.set(key2, unique(toClassName(key2), takenClasses));
1034
+ }
1035
+ const fieldNames = /* @__PURE__ */ new Map();
1036
+ for (const key2 of Object.keys(classDefs)) {
1037
+ const names = /* @__PURE__ */ new Map();
1038
+ const taken = /* @__PURE__ */ new Set();
1039
+ for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) {
1040
+ names.set(slot, unique(toFieldName(slot), taken));
1041
+ }
1042
+ fieldNames.set(key2, names);
1043
+ }
1044
+ const referenced = /* @__PURE__ */ new Set();
1045
+ const reference = (element, from, to) => {
1046
+ if (from === to || referenced.has(`${element}\0${to}`)) return;
1047
+ referenced.add(`${element}\0${to}`);
1048
+ report("renamed", element, `\`${to}\` from \`${from}\``);
1049
+ };
1050
+ const classNameOf = (key2) => classNames.get(key2) ?? toClassName(key2);
1051
+ const fieldNameOf = (classKey, slot) => fieldNames.get(classKey)?.get(slot) ?? toFieldName(slot);
1052
+ const enumValues = /* @__PURE__ */ new Map();
1053
+ const unwritable = /* @__PURE__ */ new Map();
1054
+ for (const key2 of Object.keys(enumDefs)) {
1055
+ const rawValues = get(asRecord(get(enumDefs, key2)) ?? {}, "permissible_values");
1056
+ const values = asRecord(rawValues);
1057
+ const keys = Object.keys(values ?? {});
1058
+ const bad = keys.find((value) => !ENUM_VALUE.test(value));
1059
+ if (values === void 0 && present(rawValues)) {
1060
+ unwritable.set(key2, `${shown(rawValues)} is not a map of permissible values`);
1061
+ } else if (keys.length < 2) {
1062
+ const count = `${keys.length} permissible value${keys.length === 1 ? "" : "s"}`;
1063
+ unwritable.set(key2, `${count} cannot be an inline enum (\xA73.4)`);
1064
+ } else if (bad !== void 0) {
1065
+ unwritable.set(key2, `\`${bad}\` is not a value Skiss can write (\xA74)`);
1066
+ } else {
1067
+ enumValues.set(key2, keys);
1068
+ }
1069
+ }
1070
+ const enumUses = [];
1071
+ const classes = [];
1072
+ for (const key2 of Object.keys(classDefs)) {
1073
+ const raw = get(classDefs, key2);
1074
+ const body = asRecord(raw);
1075
+ const name2 = classNameOf(key2);
1076
+ if (body === void 0 && raw !== null && raw !== void 0) {
1077
+ report("class", name2, "is not a class definition");
1078
+ continue;
1079
+ }
1080
+ const def = body ?? {};
1081
+ if (name2 !== key2) report("renamed", name2, `from \`${key2}\``);
1082
+ const annotations = annotationsOf(def);
1083
+ if (annotations.get("undeclared") === true) {
1084
+ for (const other of Object.keys(def)) {
1085
+ if (other !== "annotations") report(other, name2);
1086
+ }
1087
+ continue;
1088
+ }
1089
+ const undeclared = annotations.get("undeclared");
1090
+ if (undeclared !== void 0 && typeof undeclared !== "boolean") {
1091
+ report("annotation", name2, notText("undeclared", undeclared));
1092
+ }
1093
+ const node = { name: at0(name2), fields: [], line: 0 };
1094
+ const description = oneLine(stringOf(get(def, "description")));
1095
+ if (description !== void 0) {
1096
+ node.description = description;
1097
+ if (rewordsDescription(description)) report("reworded", name2, REWORDED);
1098
+ }
1099
+ const system = annotations.get("system");
1100
+ if (typeof system === "string") {
1101
+ if (SYSTEM_NAME.test(system)) node.system = at0(system);
1102
+ else report("annotation", name2, "`system` is not a system name");
1103
+ } else if (system !== void 0) report("annotation", name2, notText("system", system));
1104
+ const rawNote = annotations.get("note");
1105
+ if (typeof rawNote === "string") {
1106
+ const note = oneLine(rawNote);
1107
+ if (note !== void 0) node.note = note;
1108
+ } else if (rawNote !== void 0) report("annotation", name2, notText("note", rawNote));
1109
+ const mappings = stringList(get(def, "close_mappings"));
1110
+ const first = mappings[0];
1111
+ const target = first === void 0 ? "" : localName(first);
1112
+ if (target !== "") {
1113
+ const similar = Object.hasOwn(classDefs, target) ? classNameOf(target) : toClassName(target);
1114
+ node.similarTo = at0(similar);
1115
+ reference(name2, target, similar);
1116
+ }
1117
+ for (const other of Object.keys(def)) {
1118
+ if (CLASS_KEYS.has(other)) {
1119
+ if (other === "annotations") {
1120
+ for (const tag of annotations.keys()) {
1121
+ if (!CLASS_TAGS.has(tag)) report("annotation", name2, `\`${tag}\``);
1122
+ }
1123
+ }
1124
+ if (other === "close_mappings") {
1125
+ if (target === "") report("close_mappings", name2, "no mapping this can read");
1126
+ else if (mappings.length > 1) {
1127
+ report("close_mappings", name2, `${mappings.length - 1} beyond the first`);
1128
+ }
1129
+ }
1130
+ const block = get(def, other);
1131
+ if (other === "attributes" && present(block) && asRecord(block) === void 0) {
1132
+ report("attributes", name2, `${shown(block)} is not a map`);
1133
+ }
1134
+ if (other === "slots" && present(block) && !Array.isArray(block)) {
1135
+ report("slots", name2, `${shown(block)} is not a list`);
1136
+ }
1137
+ continue;
1138
+ }
1139
+ if (other === "slot_usage") {
1140
+ const usage = asRecord(get(def, other));
1141
+ if (usage === void 0) {
1142
+ report("slot_usage", name2);
1143
+ continue;
1144
+ }
1145
+ for (const slot of Object.keys(usage)) {
1146
+ if (fieldNames.get(key2)?.has(slot) === true) {
1147
+ report("slot_usage", `${name2}.${fieldNameOf(key2, slot)}`);
1148
+ } else {
1149
+ report("slot_usage", name2, `\`${slot}\` is inherited and not in the sketch`);
1150
+ }
1151
+ }
1152
+ continue;
1153
+ }
1154
+ report(other, name2);
1155
+ }
1156
+ for (const slot of slotKeysOf(def)) {
1157
+ const attributes = asRecord(get(def, "attributes")) ?? {};
1158
+ const rawSlot = Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
1159
+ const slotDef = asRecord(rawSlot);
1160
+ if (slotDef === void 0 && present(rawSlot)) {
1161
+ report("slot", `${name2}.${fieldNameOf(key2, slot)}`, "is not a slot definition");
1162
+ continue;
1163
+ }
1164
+ node.fields.push(buildField(key2, name2, slot, slotDef ?? {}));
1165
+ }
1166
+ classes.push(node);
1167
+ }
1168
+ for (const key2 of Object.keys(enumDefs)) {
1169
+ const def = asRecord(get(enumDefs, key2)) ?? {};
1170
+ const uses = enumUses.filter((use) => use.enumName === key2);
1171
+ const why = unwritable.get(key2);
1172
+ if (why !== void 0) report("enum", key2, `${why}; those attributes keep no type`);
1173
+ else if (uses.length === 0) report("unused_enum", key2, "no attribute has it as its range");
1174
+ else if (uses.length > 1 && sharingLost(uses, enumUses)) {
1175
+ report("inlined", key2, `inlined at ${uses.length} attributes`);
1176
+ }
1177
+ const detail = [];
1178
+ const values = asRecord(get(def, "permissible_values")) ?? {};
1179
+ const detailed = Object.keys(values).filter((value) => {
1180
+ const valueBody = asRecord(get(values, value));
1181
+ return valueBody !== void 0 && Object.keys(valueBody).length > 0;
1182
+ });
1183
+ if (detailed.length > 0) {
1184
+ const bodies = detailed.length === 1 ? "carries a body of its own" : "carry bodies of their own";
1185
+ detail.push(`${list(detailed)} ${bodies}`);
1186
+ }
1187
+ const others = Object.keys(def).filter((other) => other !== "permissible_values");
1188
+ if (others.length > 0) detail.push(`the enum carries ${list(others)}`);
1189
+ if (detail.length > 0) report("enum_detail", key2, detail.join("; "));
1190
+ }
1191
+ for (const slot of Object.keys(slotDefs)) {
1192
+ if (!listedSomewhere(slot)) report("slot", slot, "no class lists it");
1193
+ }
1194
+ for (const key2 of Object.keys(root)) {
1195
+ if (!QUIET_SCHEMA_KEYS.has(key2)) report("schema", schemaName, `\`${key2}\``);
1196
+ }
1197
+ const source = toSkiss({ classes, diagnostics: [] });
1198
+ return { document: parse(source), source, dropped };
1199
+ function buildField(classKey, className, slot, def) {
1200
+ const name2 = fieldNameOf(classKey, slot);
1201
+ const element = `${className}.${name2}`;
1202
+ if (name2 !== slot) report("renamed", element, `from \`${slot}\``);
1203
+ const identifier = get(def, "identifier");
1204
+ if (present(identifier) && typeof identifier !== "boolean") {
1205
+ report("identifier", element, `${shown(identifier)} is not a boolean`);
1206
+ }
1207
+ const field = {
1208
+ name: at0(name2),
1209
+ identifier: identifier === true,
1210
+ line: 0
1211
+ };
1212
+ const description = oneLine(stringOf(get(def, "description")));
1213
+ if (description !== void 0) {
1214
+ field.description = description;
1215
+ if (rewordsDescription(description)) report("reworded", element, REWORDED);
1216
+ }
1217
+ const type = typeFor(element, name2, def);
1218
+ if (type !== void 0) field.type = type;
1219
+ const annotations = annotationsOf(def);
1220
+ const system = annotations.get("system");
1221
+ if (typeof system === "string") {
1222
+ if (SYSTEM_NAME.test(system)) field.system = at0(system);
1223
+ else report("annotation", element, "`system` is not a system name");
1224
+ } else if (system !== void 0) report("annotation", element, notText("system", system));
1225
+ const joins = annotations.get("joins_to");
1226
+ if (typeof joins === "string") {
1227
+ const dot = joins.indexOf(".");
1228
+ const target = joins.slice(0, dot);
1229
+ const targetField = joins.slice(dot + 1);
1230
+ if (dot > 0 && targetField !== "") {
1231
+ const targetClass = classNameOf(target);
1232
+ const targetName = fieldNameOf(target, targetField);
1233
+ field.joinsTo = { className: at0(targetClass), fieldName: at0(targetName) };
1234
+ reference(element, target, targetClass);
1235
+ reference(element, targetField, targetName);
1236
+ } else report("annotation", element, "`joins_to` is not `Class.field`");
1237
+ } else if (joins !== void 0) report("annotation", element, notText("joins_to", joins));
1238
+ const rawNote = annotations.get("note");
1239
+ if (typeof rawNote === "string") {
1240
+ const note = oneLine(rawNote);
1241
+ if (note !== void 0) field.note = note;
1242
+ } else if (rawNote !== void 0) report("annotation", element, notText("note", rawNote));
1243
+ for (const other of Object.keys(def)) {
1244
+ if (!SLOT_KEYS.has(other)) {
1245
+ report(other, element);
1246
+ continue;
1247
+ }
1248
+ if (other !== "annotations") continue;
1249
+ for (const tag of annotations.keys()) {
1250
+ if (!SLOT_TAGS.has(tag)) report("annotation", element, `\`${tag}\``);
1251
+ }
1252
+ }
1253
+ return field;
1254
+ }
1255
+ function typeFor(element, field, def) {
1256
+ const rawMany = get(def, "multivalued");
1257
+ if (present(rawMany) && typeof rawMany !== "boolean") {
1258
+ report("multivalued", element, `${shown(rawMany)} is not a boolean`);
1259
+ }
1260
+ const many = rawMany === true;
1261
+ const rawRange = get(def, "range");
1262
+ if (present(rawRange) && typeof rawRange !== "string") {
1263
+ report("range", element, `${shown(rawRange)} is not a string`);
1264
+ } else if (rawRange === "") report("range", element, "the range is empty");
1265
+ const range = stringOf(rawRange) ?? defaultRange;
1266
+ if (Object.hasOwn(classDefs, range)) {
1267
+ const target = classNameOf(range);
1268
+ reference(element, range, target);
1269
+ return { kind: "class", name: at0(target), many };
1270
+ }
1271
+ if (Object.hasOwn(enumDefs, range)) {
1272
+ const values = enumValues.get(range);
1273
+ if (values === void 0) return many ? stringType(true) : void 0;
1274
+ enumUses.push({ enumName: range, field, values: values.join("|") });
1275
+ return { kind: "enum", values: values.map(at0), many };
1276
+ }
1277
+ if (Object.hasOwn(PRIMITIVE_RANGE, range)) {
1278
+ const primitive = PRIMITIVE_RANGE[range] ?? "string";
1279
+ if (primitive === "string") return many ? stringType(true) : void 0;
1280
+ return { kind: "primitive", name: primitive, written: at0(primitive), many };
1281
+ }
1282
+ if (/^[A-Z]/.test(range)) {
1283
+ const target = toClassName(range);
1284
+ reference(element, range, target);
1285
+ return { kind: "class", name: at0(target), many };
1286
+ }
1287
+ const word = range.toLowerCase();
1288
+ if (!UNKNOWN_TYPE.test(word)) {
1289
+ report("narrowed", element, `\`${range}\` is not a type Skiss can write; it reads as string`);
1290
+ return many ? stringType(true) : void 0;
1291
+ }
1292
+ report("narrowed", element, `\`${range}\` is not a Skiss type; it falls back to string`);
1293
+ return { kind: "unknown", name: at0(word), many };
1294
+ }
1295
+ function listedSomewhere(slot) {
1296
+ for (const key2 of Object.keys(classDefs)) {
1297
+ const def = asRecord(get(classDefs, key2)) ?? {};
1298
+ if (stringList(get(def, "slots")).includes(slot)) return true;
1299
+ }
1300
+ return false;
1301
+ }
1302
+ }
1303
+ function sharingLost(uses, all) {
1304
+ const first = uses[0];
1305
+ if (first === void 0) return false;
1306
+ if (uses.some((use) => use.field !== first.field)) return true;
1307
+ return all.some((use) => use.field === first.field && use.values !== first.values);
1308
+ }
1309
+ var COUNTED = {
1310
+ renamed: ["name", "names"],
1311
+ inlined: ["enum", "enums"],
1312
+ enum: ["unwritable enum", "unwritable enums"],
1313
+ unused_enum: ["unused enum", "unused enums"],
1314
+ class: ["class", "classes"],
1315
+ slot: ["slot", "slots"],
1316
+ schema: ["schema key", "schema keys"],
1317
+ narrowed: ["narrowed range", "narrowed ranges"],
1318
+ enum_detail: ["enum detail", "enum details"],
1319
+ annotation: ["unreadable annotation", "unreadable annotations"],
1320
+ reworded: ["description reworded", "descriptions reworded"],
1321
+ // `mixin: true` marks the class; `mixins: [M]` points at another one.
1322
+ mixin: ["mixin class", "mixin classes"],
1323
+ mixins: ["mixin", "mixins"],
1324
+ pattern: ["pattern", "patterns"],
1325
+ unit: ["unit", "units"]
1326
+ };
1327
+ var UNREADABLE = "unreadable";
1328
+ function formatDropped(dropped) {
1329
+ const groups = /* @__PURE__ */ new Map();
1330
+ for (const entry of dropped) {
1331
+ const group = groups.get(entry.kind);
1332
+ if (group === void 0) groups.set(entry.kind, [entry]);
1333
+ else group.push(entry);
1334
+ }
1335
+ const drops = [];
1336
+ const notRead = [];
1337
+ let renamed = "";
1338
+ let inlined = "";
1339
+ for (const [kind, entries] of groups) {
1340
+ if (kind === UNREADABLE) {
1341
+ for (const entry of entries) notRead.push(entry.detail ?? "the schema could not be read");
1342
+ continue;
1343
+ }
1344
+ const phrase = phraseFor(kind, entries);
1345
+ if (kind === "renamed") renamed = phrase;
1346
+ else if (kind === "inlined") inlined = phrase;
1347
+ else drops.push(phrase);
1348
+ }
1349
+ const sentences = [];
1350
+ if (notRead.length > 0) sentences.push(`Not read: ${notRead.join("; ")}.`);
1351
+ if (drops.length > 0) sentences.push(`Dropped: ${drops.join(", ")}.`);
1352
+ if (renamed !== "") sentences.push(`Renamed: ${renamed}.`);
1353
+ if (inlined !== "") sentences.push(`Inlined: ${inlined}.`);
1354
+ return sentences.join(" ");
1355
+ }
1356
+ function phraseFor(kind, entries) {
1357
+ const n = entries.length;
1358
+ const counted = Object.hasOwn(COUNTED, kind) ? COUNTED[kind] : void 0;
1359
+ if (counted !== void 0) return `${n} ${n === 1 ? counted[0] : counted[1]}`;
1360
+ return `${kind} on ${n} ${nounFor(entries)}`;
1361
+ }
1362
+ function nounFor(entries) {
1363
+ const one = entries.length === 1;
1364
+ const slots = entries.filter((entry) => entry.element.includes(".")).length;
1365
+ if (slots === entries.length) return one ? "slot" : "slots";
1366
+ if (slots === 0) return one ? "class" : "classes";
1367
+ return one ? "element" : "elements";
1368
+ }
1369
+ var capitalise2 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
1370
+ var decapitalise = (text) => text.charAt(0).toLowerCase() + text.slice(1);
1371
+ var parts = (text) => text.split(/[^A-Za-z0-9]+/).filter((part) => part !== "");
1372
+ function toClassName(text) {
1373
+ if (CLASS_NAME.test(text)) return text;
1374
+ const name2 = parts(text).map(capitalise2).join("");
1375
+ if (name2 === "") return "Class";
1376
+ return CLASS_NAME.test(name2) ? name2 : `X${name2}`;
1377
+ }
1378
+ function toFieldName(text) {
1379
+ if (FIELD_NAME.test(text)) return text;
1380
+ const name2 = parts(text).map((part, i) => i === 0 ? decapitalise(part) : capitalise2(part)).join("");
1381
+ if (name2 === "") return "field";
1382
+ return FIELD_NAME.test(name2) ? name2 : `x${capitalise2(name2)}`;
1383
+ }
1384
+ function unique(base, taken) {
1385
+ let name2 = base;
1386
+ for (let n = 2; taken.has(name2); n++) name2 = `${base}${n}`;
1387
+ taken.add(name2);
1388
+ return name2;
1389
+ }
1390
+ var get = (record, key2) => Object.hasOwn(record, key2) ? record[key2] : void 0;
1391
+ function asRecord(value) {
1392
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1393
+ return value;
1394
+ }
1395
+ var present = (value) => value !== void 0 && value !== null;
1396
+ function shown(value) {
1397
+ if (Array.isArray(value)) return "a list";
1398
+ if (typeof value === "object" && value !== null) return "a map";
1399
+ return `\`${String(value)}\``;
1400
+ }
1401
+ function localName(mapping) {
1402
+ let cut = -1;
1403
+ for (const mark of [":", "/", "#"]) cut = Math.max(cut, mapping.lastIndexOf(mark));
1404
+ return mapping.slice(cut + 1);
1405
+ }
1406
+ var stringOf = (value) => typeof value === "string" && value !== "" ? value : void 0;
1407
+ var stringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1408
+ function annotationsOf(def) {
1409
+ const out = /* @__PURE__ */ new Map();
1410
+ const annotations = asRecord(get(def, "annotations"));
1411
+ if (annotations === void 0) return out;
1412
+ for (const tag of Object.keys(annotations)) {
1413
+ const raw = get(annotations, tag);
1414
+ const long = asRecord(raw);
1415
+ const value = long === void 0 ? raw : get(long, "value");
1416
+ out.set(tag, value ?? null);
1417
+ }
1418
+ return out;
1419
+ }
1420
+ function notText(tag, value) {
1421
+ if (value === null) return `\`${tag}\` was written with no value`;
1422
+ if (Array.isArray(value)) return `\`${tag}\` is a list, not text`;
1423
+ if (typeof value === "object") return `\`${tag}\` is a map, not text`;
1424
+ return `\`${tag}\` is a ${typeof value}, not text`;
1425
+ }
1426
+ function slotKeysOf(def) {
1427
+ const keys = Object.keys(asRecord(get(def, "attributes")) ?? {});
1428
+ for (const slot of stringList(get(def, "slots"))) {
1429
+ if (!keys.includes(slot)) keys.push(slot);
1430
+ }
1431
+ return keys;
1432
+ }
1433
+ function oneLine(text) {
1434
+ if (text === void 0) return void 0;
1435
+ const line = text.replace(/\s*[\r\n]+\s*/g, " ").trim();
1436
+ return line === "" ? void 0 : line;
1437
+ }
1438
+ var list = (items) => items.map((item) => `\`${item}\``).join(", ");
1439
+ var at0 = (text) => ({ text, line: 0, col: 0, end: 0 });
1440
+ var stringType = (many) => ({
1441
+ kind: "primitive",
1442
+ name: "string",
1443
+ written: at0("string"),
1444
+ many
1445
+ });
1446
+ var schemaElement = (root) => stringOf(get(root, "name")) ?? "schema";
1447
+ var nothing = (element, detail) => ({
1448
+ document: parse(""),
1449
+ source: "",
1450
+ dropped: [{ kind: UNREADABLE, element, detail }]
1451
+ });
1452
+
1453
+ // src/import.ts
1454
+ import { parse as parseYAML } from "yaml";
1455
+ function importLinkML(text) {
1456
+ let schema;
1457
+ try {
1458
+ schema = parseYAML(text);
1459
+ } catch (error) {
1460
+ return { output: "", dropped: [], diagnostics: [notYAML(error)] };
1461
+ }
1462
+ const { document, source, dropped } = fromLinkML(schema);
1463
+ return { output: source, dropped, diagnostics: resolve(document).diagnostics };
1464
+ }
1465
+ function notYAML(error) {
1466
+ const first = error instanceof Error ? error.message.split("\n")[0] ?? "" : String(error);
1467
+ const detail = first.replace(/:$/, "");
1468
+ return {
1469
+ severity: "error",
1470
+ code: "E_NOT_YAML",
1471
+ message: `the input is not YAML: ${detail}`,
1472
+ line: 1
1473
+ };
1474
+ }
1475
+
918
1476
  // src/index.ts
919
- var VERSION = "0.2.1";
1477
+ var VERSION = "0.4.0";
920
1478
 
921
1479
  // src/cli.ts
922
1480
  var HELP = `Usage: skiss <command> [options]
@@ -924,6 +1482,7 @@ var HELP = `Usage: skiss <command> [options]
924
1482
  Commands:
925
1483
  diagram <file> Write a Mermaid class diagram for a .skiss file.
926
1484
  compile <file> Write a LinkML schema for a .skiss file.
1485
+ import <file> Write Skiss for a LinkML schema file, YAML or JSON.
927
1486
  <file> may be - to read standard input.
928
1487
 
929
1488
  Options:
@@ -933,17 +1492,23 @@ Options:
933
1492
  --name <name> compile: the schema name. Defaults to the file's
934
1493
  basename without its extension, or \`sketch\` for
935
1494
  standard input.
936
- --strict Exit 1 when the input has any diagnostic
1495
+ --strict Exit 1 when the input has any diagnostic; for import,
1496
+ also when the projection dropped anything
937
1497
  -h, --help Show this help and exit
938
1498
  --version Print the version and exit
939
1499
 
940
1500
  Diagnostics go to standard error, one per line:
941
1501
  <file>:<line>:<col>: <severity> <CODE> <message>
942
1502
 
1503
+ import writes what the projection could not carry to standard error too, as
1504
+ one line before the diagnostics. Its diagnostics are the ones the sketch it
1505
+ wrote has, so their line numbers are lines of that sketch.
1506
+
943
1507
  Exit codes:
944
1508
  0 the output was produced
945
1509
  1 --strict and the input has diagnostics (the output is still written)
946
- 2 usage error, missing file or unreadable input
1510
+ 2 usage error, missing file, or unreadable input: for import, a text no
1511
+ schema could be read out of, which produces no sketch at all
947
1512
  70 internal error, a bug in skiss
948
1513
  `;
949
1514
  var EXIT_OK = 0;
@@ -952,7 +1517,8 @@ var EXIT_USAGE = 2;
952
1517
  var EXIT_INTERNAL = 70;
953
1518
  var COMMANDS = {
954
1519
  diagram: ["output", "notes", "strict"],
955
- compile: ["output", "json", "name", "strict"]
1520
+ compile: ["output", "json", "name", "strict"],
1521
+ import: ["output", "strict"]
956
1522
  };
957
1523
  var isCommand = (text) => Object.hasOwn(COMMANDS, text);
958
1524
  var UsageError = class extends Error {
@@ -1003,17 +1569,37 @@ async function main(argv) {
1003
1569
  }
1004
1570
  }
1005
1571
  const source = file === "-" ? await readStdin() : await readSource(file);
1006
- const { output, diagnostics } = command === "compile" ? compile(source, {
1007
- target: "linkml",
1008
- schemaName: values.name ?? defaultSchemaName(file),
1009
- format: values.json === true ? "json" : "yaml"
1010
- }) : compile(source, { target: "mermaid", notes: values.notes });
1572
+ const { output, diagnostics, dropped } = runCommand(command, source, file, values);
1011
1573
  const label = file === "-" ? "<stdin>" : file;
1574
+ const report = formatDropped(dropped);
1575
+ if (report !== "") process.stderr.write(`${report}
1576
+ `);
1012
1577
  for (const d of diagnostics) process.stderr.write(`${formatDiagnostic(d, label)}
1013
1578
  `);
1014
1579
  if (values.output === void 0) process.stdout.write(output);
1015
1580
  else await writeOutput(values.output, output);
1016
- return values.strict === true && diagnostics.length > 0 ? EXIT_STRICT : EXIT_OK;
1581
+ if (unreadableInput(command, diagnostics, dropped)) return EXIT_USAGE;
1582
+ const strict = values.strict === true && (diagnostics.length > 0 || dropped.length > 0);
1583
+ return strict ? EXIT_STRICT : EXIT_OK;
1584
+ }
1585
+ function unreadableInput(command, diagnostics, dropped) {
1586
+ if (command !== "import") return false;
1587
+ if (dropped.some((entry) => entry.kind === "unreadable")) return true;
1588
+ return diagnostics.some((d) => d.code === "E_NOT_YAML");
1589
+ }
1590
+ function runCommand(command, source, file, values) {
1591
+ if (command === "import") return importLinkML(source);
1592
+ if (command === "compile") {
1593
+ return {
1594
+ ...compile(source, {
1595
+ target: "linkml",
1596
+ schemaName: values.name ?? defaultSchemaName(file),
1597
+ format: values.json === true ? "json" : "yaml"
1598
+ }),
1599
+ dropped: []
1600
+ };
1601
+ }
1602
+ return { ...compile(source, { target: "mermaid", notes: values.notes }), dropped: [] };
1017
1603
  }
1018
1604
  var defaultSchemaName = (file) => file === "-" ? "sketch" : basename(file, extname(file));
1019
1605
  async function readSource(file) {
@@ -1046,6 +1632,12 @@ function reason(error) {
1046
1632
  }
1047
1633
  return String(error);
1048
1634
  }
1635
+ for (const stream of [process.stdout, process.stderr]) {
1636
+ stream.on("error", (error) => {
1637
+ if (error.code === "EPIPE") process.exit(EXIT_OK);
1638
+ throw error;
1639
+ });
1640
+ }
1049
1641
  main(process.argv.slice(2)).then(
1050
1642
  (code) => {
1051
1643
  process.exitCode = code;