@eriknaslund/skiss 0.2.1 → 0.3.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
@@ -64,7 +64,9 @@ Or without installing: `npx @eriknaslund/skiss diagram model.skiss`.
64
64
 
65
65
  `skiss compile model.skiss` writes a LinkML schema instead, as YAML on standard output. `--json` writes the same schema as JSON, `--name` sets the schema name (it defaults to the file name without its extension, and to `sketch` when reading standard input), and `-o`, `-`, `--strict` and the diagnostics work exactly as they do for `diagram`.
66
66
 
67
- `npm install @eriknaslund/skiss` also gives you the library: `compile`, `formatDiagnostic`, `parse`, `resolve`, `toLinkML`, `serialize`, `toMermaid` and `VERSION`, browser-safe.
67
+ `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 — `is_a`, patterns, `required`, 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.
68
+
69
+ `npm install @eriknaslund/skiss` also gives you the library: `compile`, `formatDiagnostic`, `parse`, `resolve`, `toLinkML`, `serialize`, `toMermaid`, `toSkiss`, `fromLinkML`, `formatDropped`, `importLinkML` and `VERSION`, browser-safe.
68
70
 
69
71
  Editors that render Skiss live are separate projects built on this package. This repository is the language, the library and the command line.
70
72
 
package/dist/cli.js CHANGED
@@ -527,9 +527,9 @@ function resolve(doc) {
527
527
  }
528
528
  }
529
529
  }
530
- const present = new Set(out.diagnostics.map(key));
530
+ const present2 = new Set(out.diagnostics.map(key));
531
531
  for (const w of warnings) {
532
- if (!present.has(key(w))) out.diagnostics.push(w);
532
+ if (!present2.has(key(w))) out.diagnostics.push(w);
533
533
  }
534
534
  out.diagnostics.sort((a, b) => a.line - b.line);
535
535
  out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
@@ -915,8 +915,509 @@ function formatDiagnostic(d, file) {
915
915
  return `${where}: ${d.severity} ${d.code} ${d.message}`;
916
916
  }
917
917
 
918
+ // src/generators/skiss.ts
919
+ var TRAILER_COLUMN = 40;
920
+ function toSkiss(doc) {
921
+ if (doc.classes.length === 0) return "";
922
+ return `${doc.classes.map(classBlock).join("\n\n")}
923
+ `;
924
+ }
925
+ function classBlock(cls) {
926
+ const head = cls.name.text + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
927
+ return [withTrailer(head, cls), ...cls.fields.map(fieldLine)].join("\n");
928
+ }
929
+ function fieldLine(field) {
930
+ const type = typeText2(field.type);
931
+ 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}`);
932
+ return withTrailer(head, field);
933
+ }
934
+ function typeText2(type) {
935
+ if (type === void 0) return void 0;
936
+ let text;
937
+ switch (type.kind) {
938
+ case "primitive":
939
+ if (type.name === "string" && !type.many) return void 0;
940
+ text = type.name;
941
+ break;
942
+ case "class":
943
+ case "unknown":
944
+ text = type.name.text;
945
+ break;
946
+ case "enum":
947
+ text = type.values.map((v) => v.text).join("|");
948
+ break;
949
+ }
950
+ return type.many ? `${text}[]` : text;
951
+ }
952
+ function withTrailer(head, node) {
953
+ const parts2 = [];
954
+ if (node.description !== void 0 && node.description !== "")
955
+ parts2.push(`# ${node.description}`);
956
+ if (node.note !== void 0 && node.note !== "") parts2.push(`? ${node.note}`);
957
+ if (parts2.length === 0) return head;
958
+ const gap = head.length < TRAILER_COLUMN - 1 ? " ".repeat(TRAILER_COLUMN - head.length) : " ";
959
+ return head + gap + parts2.join(" ");
960
+ }
961
+
962
+ // src/generators/from-linkml.ts
963
+ var PRIMITIVE_RANGE = {
964
+ string: "string",
965
+ integer: "int",
966
+ float: "float",
967
+ double: "float",
968
+ decimal: "float",
969
+ boolean: "bool",
970
+ date: "date",
971
+ datetime: "datetime",
972
+ uri: "uri",
973
+ uriorcurie: "uri"
974
+ };
975
+ var CLASS_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "close_mappings", "attributes", "slots"]);
976
+ var SLOT_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "range", "multivalued", "identifier"]);
977
+ var CLASS_TAGS = /* @__PURE__ */ new Set(["system", "note", "undeclared"]);
978
+ var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
979
+ var QUIET_SCHEMA_KEYS = /* @__PURE__ */ new Set([
980
+ "id",
981
+ "name",
982
+ "prefixes",
983
+ "imports",
984
+ "default_prefix",
985
+ "default_range",
986
+ "title",
987
+ "license",
988
+ "version",
989
+ "classes",
990
+ "slots",
991
+ "enums"
992
+ ]);
993
+ var UNKNOWN_TYPE = /^[a-z][a-z0-9_-]*$/;
994
+ function fromLinkML(schema) {
995
+ const root = asRecord(schema);
996
+ if (root === void 0) return nothing("schema", "the schema is not an object");
997
+ const schemaName = schemaElement(root);
998
+ const classBlock2 = asRecord(get(root, "classes"));
999
+ if (classBlock2 === void 0) return nothing(schemaName, "the schema has no `classes`");
1000
+ const classDefs = classBlock2;
1001
+ const dropped = [];
1002
+ const report = (kind, element, detail) => {
1003
+ dropped.push(detail === void 0 ? { kind, element } : { kind, element, detail });
1004
+ };
1005
+ const slotDefs = asRecord(get(root, "slots")) ?? {};
1006
+ const enumDefs = asRecord(get(root, "enums")) ?? {};
1007
+ const defaultRange = stringOf(get(root, "default_range")) ?? "string";
1008
+ const classNames = /* @__PURE__ */ new Map();
1009
+ const takenClasses = /* @__PURE__ */ new Set();
1010
+ for (const key2 of Object.keys(classDefs)) {
1011
+ classNames.set(key2, unique(toClassName(key2), takenClasses));
1012
+ }
1013
+ const fieldNames = /* @__PURE__ */ new Map();
1014
+ for (const key2 of Object.keys(classDefs)) {
1015
+ const names = /* @__PURE__ */ new Map();
1016
+ const taken = /* @__PURE__ */ new Set();
1017
+ for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) {
1018
+ names.set(slot, unique(toFieldName(slot), taken));
1019
+ }
1020
+ fieldNames.set(key2, names);
1021
+ }
1022
+ const referenced = /* @__PURE__ */ new Set();
1023
+ const reference = (element, from, to) => {
1024
+ if (from === to || referenced.has(`${element}\0${to}`)) return;
1025
+ referenced.add(`${element}\0${to}`);
1026
+ report("renamed", element, `\`${to}\` from \`${from}\``);
1027
+ };
1028
+ const classNameOf = (key2) => classNames.get(key2) ?? toClassName(key2);
1029
+ const fieldNameOf = (classKey, slot) => fieldNames.get(classKey)?.get(slot) ?? toFieldName(slot);
1030
+ const enumValues = /* @__PURE__ */ new Map();
1031
+ const unwritable = /* @__PURE__ */ new Map();
1032
+ for (const key2 of Object.keys(enumDefs)) {
1033
+ const rawValues = get(asRecord(get(enumDefs, key2)) ?? {}, "permissible_values");
1034
+ const values = asRecord(rawValues);
1035
+ const keys = Object.keys(values ?? {});
1036
+ const bad = keys.find((value) => !SYSTEM_OR_VALUE.test(value));
1037
+ if (values === void 0 && present(rawValues)) {
1038
+ unwritable.set(key2, `${shown(rawValues)} is not a map of permissible values`);
1039
+ } else if (keys.length < 2) {
1040
+ const count = `${keys.length} permissible value${keys.length === 1 ? "" : "s"}`;
1041
+ unwritable.set(key2, `${count} cannot be an inline enum (\xA73.4)`);
1042
+ } else if (bad !== void 0) {
1043
+ unwritable.set(key2, `\`${bad}\` is not a value Skiss can write (\xA74)`);
1044
+ } else {
1045
+ enumValues.set(key2, keys);
1046
+ }
1047
+ }
1048
+ const enumUses = [];
1049
+ const classes = [];
1050
+ for (const key2 of Object.keys(classDefs)) {
1051
+ const raw = get(classDefs, key2);
1052
+ const body = asRecord(raw);
1053
+ const name2 = classNameOf(key2);
1054
+ if (body === void 0 && raw !== null && raw !== void 0) {
1055
+ report("class", name2, "is not a class definition");
1056
+ continue;
1057
+ }
1058
+ const def = body ?? {};
1059
+ if (name2 !== key2) report("renamed", name2, `from \`${key2}\``);
1060
+ const annotations = annotationsOf(def);
1061
+ if (annotations.get("undeclared") === true) {
1062
+ for (const other of Object.keys(def)) {
1063
+ if (other !== "annotations") report(other, name2);
1064
+ }
1065
+ continue;
1066
+ }
1067
+ const node = { name: at0(name2), fields: [], line: 0 };
1068
+ const description = oneLine(stringOf(get(def, "description")));
1069
+ if (description !== void 0) node.description = description;
1070
+ const system = annotations.get("system");
1071
+ if (typeof system === "string" && SYSTEM_OR_VALUE.test(system)) node.system = at0(system);
1072
+ else if (system !== void 0) report("annotation", name2, "`system` is not a system name");
1073
+ const note = oneLine(stringOf(annotations.get("note")));
1074
+ if (note !== void 0) node.note = note;
1075
+ const mappings = stringList(get(def, "close_mappings"));
1076
+ const first = mappings[0];
1077
+ const target = first === void 0 ? "" : localName(first);
1078
+ if (target !== "") {
1079
+ const similar = Object.hasOwn(classDefs, target) ? classNameOf(target) : toClassName(target);
1080
+ node.similarTo = at0(similar);
1081
+ reference(name2, target, similar);
1082
+ }
1083
+ for (const other of Object.keys(def)) {
1084
+ if (CLASS_KEYS.has(other)) {
1085
+ if (other === "annotations") {
1086
+ for (const tag of annotations.keys()) {
1087
+ if (!CLASS_TAGS.has(tag)) report("annotation", name2, `\`${tag}\``);
1088
+ }
1089
+ }
1090
+ if (other === "close_mappings") {
1091
+ if (target === "") report("close_mappings", name2, "no mapping this can read");
1092
+ else if (mappings.length > 1) {
1093
+ report("close_mappings", name2, `${mappings.length - 1} beyond the first`);
1094
+ }
1095
+ }
1096
+ const block = get(def, other);
1097
+ if (other === "attributes" && present(block) && asRecord(block) === void 0) {
1098
+ report("attributes", name2, `${shown(block)} is not a map`);
1099
+ }
1100
+ if (other === "slots" && present(block) && !Array.isArray(block)) {
1101
+ report("slots", name2, `${shown(block)} is not a list`);
1102
+ }
1103
+ continue;
1104
+ }
1105
+ if (other === "slot_usage") {
1106
+ const usage = asRecord(get(def, other));
1107
+ if (usage === void 0) {
1108
+ report("slot_usage", name2);
1109
+ continue;
1110
+ }
1111
+ for (const slot of Object.keys(usage)) {
1112
+ if (fieldNames.get(key2)?.has(slot) === true) {
1113
+ report("slot_usage", `${name2}.${fieldNameOf(key2, slot)}`);
1114
+ } else {
1115
+ report("slot_usage", name2, `\`${slot}\` is inherited and not in the sketch`);
1116
+ }
1117
+ }
1118
+ continue;
1119
+ }
1120
+ report(other, name2);
1121
+ }
1122
+ for (const slot of slotKeysOf(def)) {
1123
+ const attributes = asRecord(get(def, "attributes")) ?? {};
1124
+ const rawSlot = Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
1125
+ const slotDef = asRecord(rawSlot);
1126
+ if (slotDef === void 0 && present(rawSlot)) {
1127
+ report("slot", `${name2}.${fieldNameOf(key2, slot)}`, "is not a slot definition");
1128
+ continue;
1129
+ }
1130
+ node.fields.push(buildField(key2, name2, slot, slotDef ?? {}));
1131
+ }
1132
+ classes.push(node);
1133
+ }
1134
+ for (const key2 of Object.keys(enumDefs)) {
1135
+ const def = asRecord(get(enumDefs, key2)) ?? {};
1136
+ const uses = enumUses.filter((use) => use.enumName === key2);
1137
+ const why = unwritable.get(key2);
1138
+ if (why !== void 0) report("enum", key2, `${why}; those attributes keep no type`);
1139
+ else if (uses.length === 0) report("unused_enum", key2, "no attribute has it as its range");
1140
+ else if (uses.length > 1 && sharingLost(uses, enumUses)) {
1141
+ report("inlined", key2, `inlined at ${uses.length} attributes`);
1142
+ }
1143
+ const detail = [];
1144
+ const values = asRecord(get(def, "permissible_values")) ?? {};
1145
+ const detailed = Object.keys(values).filter((value) => {
1146
+ const valueBody = asRecord(get(values, value));
1147
+ return valueBody !== void 0 && Object.keys(valueBody).length > 0;
1148
+ });
1149
+ if (detailed.length > 0) {
1150
+ const bodies = detailed.length === 1 ? "carries a body of its own" : "carry bodies of their own";
1151
+ detail.push(`${list(detailed)} ${bodies}`);
1152
+ }
1153
+ const others = Object.keys(def).filter((other) => other !== "permissible_values");
1154
+ if (others.length > 0) detail.push(`the enum carries ${list(others)}`);
1155
+ if (detail.length > 0) report("enum_detail", key2, detail.join("; "));
1156
+ }
1157
+ for (const slot of Object.keys(slotDefs)) {
1158
+ if (!listedSomewhere(slot)) report("slot", slot, "no class lists it");
1159
+ }
1160
+ for (const key2 of Object.keys(root)) {
1161
+ if (!QUIET_SCHEMA_KEYS.has(key2)) report("schema", schemaName, `\`${key2}\``);
1162
+ }
1163
+ const source = toSkiss({ classes, diagnostics: [] });
1164
+ return { document: parse(source), source, dropped };
1165
+ function buildField(classKey, className, slot, def) {
1166
+ const name2 = fieldNameOf(classKey, slot);
1167
+ const element = `${className}.${name2}`;
1168
+ if (name2 !== slot) report("renamed", element, `from \`${slot}\``);
1169
+ const identifier = get(def, "identifier");
1170
+ if (present(identifier) && typeof identifier !== "boolean") {
1171
+ report("identifier", element, `${shown(identifier)} is not a boolean`);
1172
+ }
1173
+ const field = {
1174
+ name: at0(name2),
1175
+ identifier: identifier === true,
1176
+ line: 0
1177
+ };
1178
+ const description = oneLine(stringOf(get(def, "description")));
1179
+ if (description !== void 0) field.description = description;
1180
+ const type = typeFor(element, name2, def);
1181
+ if (type !== void 0) field.type = type;
1182
+ const annotations = annotationsOf(def);
1183
+ const system = annotations.get("system");
1184
+ if (typeof system === "string" && SYSTEM_OR_VALUE.test(system)) field.system = at0(system);
1185
+ else if (system !== void 0) report("annotation", element, "`system` is not a system name");
1186
+ const joins = annotations.get("joins_to");
1187
+ if (typeof joins === "string") {
1188
+ const dot = joins.indexOf(".");
1189
+ const target = joins.slice(0, dot);
1190
+ const targetField = joins.slice(dot + 1);
1191
+ if (dot > 0 && targetField !== "") {
1192
+ const targetClass = classNameOf(target);
1193
+ const targetName = fieldNameOf(target, targetField);
1194
+ field.joinsTo = { className: at0(targetClass), fieldName: at0(targetName) };
1195
+ reference(element, target, targetClass);
1196
+ reference(element, targetField, targetName);
1197
+ } else report("annotation", element, "`joins_to` is not `Class.field`");
1198
+ } else if (joins !== void 0) report("annotation", element, "`joins_to` is not text");
1199
+ const note = oneLine(stringOf(annotations.get("note")));
1200
+ if (note !== void 0) field.note = note;
1201
+ for (const other of Object.keys(def)) {
1202
+ if (!SLOT_KEYS.has(other)) {
1203
+ report(other, element);
1204
+ continue;
1205
+ }
1206
+ if (other !== "annotations") continue;
1207
+ for (const tag of annotations.keys()) {
1208
+ if (!SLOT_TAGS.has(tag)) report("annotation", element, `\`${tag}\``);
1209
+ }
1210
+ }
1211
+ return field;
1212
+ }
1213
+ function typeFor(element, field, def) {
1214
+ const rawMany = get(def, "multivalued");
1215
+ if (present(rawMany) && typeof rawMany !== "boolean") {
1216
+ report("multivalued", element, `${shown(rawMany)} is not a boolean`);
1217
+ }
1218
+ const many = rawMany === true;
1219
+ const rawRange = get(def, "range");
1220
+ if (present(rawRange) && typeof rawRange !== "string") {
1221
+ report("range", element, `${shown(rawRange)} is not a string`);
1222
+ } else if (rawRange === "") report("range", element, "the range is empty");
1223
+ const range = stringOf(rawRange) ?? defaultRange;
1224
+ if (Object.hasOwn(classDefs, range)) {
1225
+ const target = classNameOf(range);
1226
+ reference(element, range, target);
1227
+ return { kind: "class", name: at0(target), many };
1228
+ }
1229
+ if (Object.hasOwn(enumDefs, range)) {
1230
+ const values = enumValues.get(range);
1231
+ if (values === void 0) return many ? stringType(true) : void 0;
1232
+ enumUses.push({ enumName: range, field, values: values.join("|") });
1233
+ return { kind: "enum", values: values.map(at0), many };
1234
+ }
1235
+ if (Object.hasOwn(PRIMITIVE_RANGE, range)) {
1236
+ const primitive = PRIMITIVE_RANGE[range] ?? "string";
1237
+ if (primitive === "string") return many ? stringType(true) : void 0;
1238
+ return { kind: "primitive", name: primitive, written: at0(primitive), many };
1239
+ }
1240
+ if (/^[A-Z]/.test(range)) {
1241
+ const target = toClassName(range);
1242
+ reference(element, range, target);
1243
+ return { kind: "class", name: at0(target), many };
1244
+ }
1245
+ const word = range.toLowerCase();
1246
+ if (!UNKNOWN_TYPE.test(word)) {
1247
+ report("narrowed", element, `\`${range}\` is not a type Skiss can write; it reads as string`);
1248
+ return many ? stringType(true) : void 0;
1249
+ }
1250
+ report("narrowed", element, `\`${range}\` is not a Skiss type; it falls back to string`);
1251
+ return { kind: "unknown", name: at0(word), many };
1252
+ }
1253
+ function listedSomewhere(slot) {
1254
+ for (const key2 of Object.keys(classDefs)) {
1255
+ const def = asRecord(get(classDefs, key2)) ?? {};
1256
+ if (stringList(get(def, "slots")).includes(slot)) return true;
1257
+ }
1258
+ return false;
1259
+ }
1260
+ }
1261
+ function sharingLost(uses, all) {
1262
+ const first = uses[0];
1263
+ if (first === void 0) return false;
1264
+ if (uses.some((use) => use.field !== first.field)) return true;
1265
+ return all.some((use) => use.field === first.field && use.values !== first.values);
1266
+ }
1267
+ var COUNTED = {
1268
+ renamed: ["name", "names"],
1269
+ inlined: ["enum", "enums"],
1270
+ enum: ["unwritable enum", "unwritable enums"],
1271
+ unused_enum: ["unused enum", "unused enums"],
1272
+ class: ["class", "classes"],
1273
+ slot: ["slot", "slots"],
1274
+ schema: ["schema key", "schema keys"],
1275
+ // `mixin: true` marks the class; `mixins: [M]` points at another one.
1276
+ mixin: ["mixin class", "mixin classes"],
1277
+ mixins: ["mixin", "mixins"],
1278
+ pattern: ["pattern", "patterns"],
1279
+ unit: ["unit", "units"]
1280
+ };
1281
+ var ENUM_KINDS = /* @__PURE__ */ new Set(["enum_detail"]);
1282
+ function formatDropped(dropped) {
1283
+ const groups = /* @__PURE__ */ new Map();
1284
+ for (const entry of dropped) {
1285
+ const group = groups.get(entry.kind);
1286
+ if (group === void 0) groups.set(entry.kind, [entry]);
1287
+ else group.push(entry);
1288
+ }
1289
+ const drops = [];
1290
+ let renamed = "";
1291
+ let inlined = "";
1292
+ for (const [kind, entries] of groups) {
1293
+ const phrase = phraseFor(kind, entries);
1294
+ if (kind === "renamed") renamed = phrase;
1295
+ else if (kind === "inlined") inlined = phrase;
1296
+ else drops.push(phrase);
1297
+ }
1298
+ const sentences = [];
1299
+ if (drops.length > 0) sentences.push(`Dropped: ${drops.join(", ")}.`);
1300
+ if (renamed !== "") sentences.push(`Renamed: ${renamed}.`);
1301
+ if (inlined !== "") sentences.push(`Inlined: ${inlined}.`);
1302
+ return sentences.join(" ");
1303
+ }
1304
+ function phraseFor(kind, entries) {
1305
+ const n = entries.length;
1306
+ const counted = Object.hasOwn(COUNTED, kind) ? COUNTED[kind] : void 0;
1307
+ if (counted !== void 0) return `${n} ${n === 1 ? counted[0] : counted[1]}`;
1308
+ return `${kind} on ${n} ${nounFor(kind, entries)}`;
1309
+ }
1310
+ function nounFor(kind, entries) {
1311
+ const one = entries.length === 1;
1312
+ if (ENUM_KINDS.has(kind)) return one ? "enum" : "enums";
1313
+ const slots = entries.filter((entry) => entry.element.includes(".")).length;
1314
+ if (slots === entries.length) return one ? "slot" : "slots";
1315
+ if (slots === 0) return one ? "class" : "classes";
1316
+ return one ? "element" : "elements";
1317
+ }
1318
+ var capitalise2 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
1319
+ var decapitalise = (text) => text.charAt(0).toLowerCase() + text.slice(1);
1320
+ var parts = (text) => text.split(/[^A-Za-z0-9]+/).filter((part) => part !== "");
1321
+ function toClassName(text) {
1322
+ if (CLASS_NAME.test(text)) return text;
1323
+ const name2 = parts(text).map(capitalise2).join("");
1324
+ if (name2 === "") return "Class";
1325
+ return CLASS_NAME.test(name2) ? name2 : `X${name2}`;
1326
+ }
1327
+ function toFieldName(text) {
1328
+ if (FIELD_NAME.test(text)) return text;
1329
+ const name2 = parts(text).map((part, i) => i === 0 ? decapitalise(part) : capitalise2(part)).join("");
1330
+ if (name2 === "") return "field";
1331
+ return FIELD_NAME.test(name2) ? name2 : `x${capitalise2(name2)}`;
1332
+ }
1333
+ function unique(base, taken) {
1334
+ let name2 = base;
1335
+ for (let n = 2; taken.has(name2); n++) name2 = `${base}${n}`;
1336
+ taken.add(name2);
1337
+ return name2;
1338
+ }
1339
+ var get = (record, key2) => Object.hasOwn(record, key2) ? record[key2] : void 0;
1340
+ function asRecord(value) {
1341
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1342
+ return value;
1343
+ }
1344
+ var present = (value) => value !== void 0 && value !== null;
1345
+ function shown(value) {
1346
+ if (Array.isArray(value)) return "a list";
1347
+ if (typeof value === "object" && value !== null) return "a map";
1348
+ return `\`${String(value)}\``;
1349
+ }
1350
+ function localName(mapping) {
1351
+ let cut = -1;
1352
+ for (const mark of [":", "/", "#"]) cut = Math.max(cut, mapping.lastIndexOf(mark));
1353
+ return mapping.slice(cut + 1);
1354
+ }
1355
+ var stringOf = (value) => typeof value === "string" && value !== "" ? value : void 0;
1356
+ var stringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1357
+ function annotationsOf(def) {
1358
+ const out = /* @__PURE__ */ new Map();
1359
+ const annotations = asRecord(get(def, "annotations"));
1360
+ if (annotations === void 0) return out;
1361
+ for (const tag of Object.keys(annotations)) {
1362
+ const raw = get(annotations, tag);
1363
+ const value = asRecord(raw) === void 0 ? raw : get(asRecord(raw) ?? {}, "value");
1364
+ if (typeof value === "string" || typeof value === "boolean") out.set(tag, value);
1365
+ else out.set(tag, String(value));
1366
+ }
1367
+ return out;
1368
+ }
1369
+ function slotKeysOf(def) {
1370
+ const keys = Object.keys(asRecord(get(def, "attributes")) ?? {});
1371
+ for (const slot of stringList(get(def, "slots"))) {
1372
+ if (!keys.includes(slot)) keys.push(slot);
1373
+ }
1374
+ return keys;
1375
+ }
1376
+ function oneLine(text) {
1377
+ if (text === void 0) return void 0;
1378
+ const line = text.replace(/\s*[\r\n]+\s*/g, " ").trim();
1379
+ return line === "" ? void 0 : line;
1380
+ }
1381
+ var list = (items) => items.map((item) => `\`${item}\``).join(", ");
1382
+ var at0 = (text) => ({ text, line: 0, col: 0, end: 0 });
1383
+ var stringType = (many) => ({
1384
+ kind: "primitive",
1385
+ name: "string",
1386
+ written: at0("string"),
1387
+ many
1388
+ });
1389
+ var schemaElement = (root) => stringOf(get(root, "name")) ?? "schema";
1390
+ var nothing = (element, detail) => ({
1391
+ document: parse(""),
1392
+ source: "",
1393
+ dropped: [{ kind: "schema", element, detail }]
1394
+ });
1395
+
1396
+ // src/import.ts
1397
+ import { parse as parseYAML } from "yaml";
1398
+ function importLinkML(text) {
1399
+ let schema;
1400
+ try {
1401
+ schema = parseYAML(text);
1402
+ } catch (error) {
1403
+ return { output: "", dropped: [], diagnostics: [notYAML(error)] };
1404
+ }
1405
+ const { document, source, dropped } = fromLinkML(schema);
1406
+ return { output: source, dropped, diagnostics: resolve(document).diagnostics };
1407
+ }
1408
+ function notYAML(error) {
1409
+ const first = error instanceof Error ? error.message.split("\n")[0] ?? "" : String(error);
1410
+ const detail = first.replace(/:$/, "");
1411
+ return {
1412
+ severity: "error",
1413
+ code: "E_NOT_YAML",
1414
+ message: `the input is not YAML: ${detail}`,
1415
+ line: 1
1416
+ };
1417
+ }
1418
+
918
1419
  // src/index.ts
919
- var VERSION = "0.2.1";
1420
+ var VERSION = "0.3.0";
920
1421
 
921
1422
  // src/cli.ts
922
1423
  var HELP = `Usage: skiss <command> [options]
@@ -924,6 +1425,7 @@ var HELP = `Usage: skiss <command> [options]
924
1425
  Commands:
925
1426
  diagram <file> Write a Mermaid class diagram for a .skiss file.
926
1427
  compile <file> Write a LinkML schema for a .skiss file.
1428
+ import <file> Write Skiss for a LinkML schema file, YAML or JSON.
927
1429
  <file> may be - to read standard input.
928
1430
 
929
1431
  Options:
@@ -933,13 +1435,18 @@ Options:
933
1435
  --name <name> compile: the schema name. Defaults to the file's
934
1436
  basename without its extension, or \`sketch\` for
935
1437
  standard input.
936
- --strict Exit 1 when the input has any diagnostic
1438
+ --strict Exit 1 when the input has any diagnostic; for import,
1439
+ also when the projection dropped anything
937
1440
  -h, --help Show this help and exit
938
1441
  --version Print the version and exit
939
1442
 
940
1443
  Diagnostics go to standard error, one per line:
941
1444
  <file>:<line>:<col>: <severity> <CODE> <message>
942
1445
 
1446
+ import writes what the projection could not carry to standard error too, as
1447
+ one line before the diagnostics. Its diagnostics are the ones the sketch it
1448
+ wrote has, so their line numbers are lines of that sketch.
1449
+
943
1450
  Exit codes:
944
1451
  0 the output was produced
945
1452
  1 --strict and the input has diagnostics (the output is still written)
@@ -952,7 +1459,8 @@ var EXIT_USAGE = 2;
952
1459
  var EXIT_INTERNAL = 70;
953
1460
  var COMMANDS = {
954
1461
  diagram: ["output", "notes", "strict"],
955
- compile: ["output", "json", "name", "strict"]
1462
+ compile: ["output", "json", "name", "strict"],
1463
+ import: ["output", "strict"]
956
1464
  };
957
1465
  var isCommand = (text) => Object.hasOwn(COMMANDS, text);
958
1466
  var UsageError = class extends Error {
@@ -1003,17 +1511,31 @@ async function main(argv) {
1003
1511
  }
1004
1512
  }
1005
1513
  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 });
1514
+ const { output, diagnostics, dropped } = runCommand(command, source, file, values);
1011
1515
  const label = file === "-" ? "<stdin>" : file;
1516
+ const report = formatDropped(dropped);
1517
+ if (report !== "") process.stderr.write(`${report}
1518
+ `);
1012
1519
  for (const d of diagnostics) process.stderr.write(`${formatDiagnostic(d, label)}
1013
1520
  `);
1014
1521
  if (values.output === void 0) process.stdout.write(output);
1015
1522
  else await writeOutput(values.output, output);
1016
- return values.strict === true && diagnostics.length > 0 ? EXIT_STRICT : EXIT_OK;
1523
+ const strict = values.strict === true && (diagnostics.length > 0 || dropped.length > 0);
1524
+ return strict ? EXIT_STRICT : EXIT_OK;
1525
+ }
1526
+ function runCommand(command, source, file, values) {
1527
+ if (command === "import") return importLinkML(source);
1528
+ if (command === "compile") {
1529
+ return {
1530
+ ...compile(source, {
1531
+ target: "linkml",
1532
+ schemaName: values.name ?? defaultSchemaName(file),
1533
+ format: values.json === true ? "json" : "yaml"
1534
+ }),
1535
+ dropped: []
1536
+ };
1537
+ }
1538
+ return { ...compile(source, { target: "mermaid", notes: values.notes }), dropped: [] };
1017
1539
  }
1018
1540
  var defaultSchemaName = (file) => file === "-" ? "sketch" : basename(file, extname(file));
1019
1541
  async function readSource(file) {