keepset 0.18.0 → 0.20.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.
@@ -1,5 +1,6 @@
1
- import { $ as NativeParseError, F as getOwn, J as ArchiveError, L as countTarget, Q as KeepsetError, et as RecipeError, t as getDataset, z as getTarget } from "../bundled-BiuG_xLd.js";
1
+ import { $ as NativeParseError, F as getOwn, J as ArchiveError, L as countTarget, Q as KeepsetError, et as RecipeError, t as getDataset, z as getTarget } from "../bundled-BmqpHjOH.js";
2
2
  import { RoundTripMismatchError, assertRoundTrip, encode } from "twindex";
3
+ import { z } from "zod";
3
4
  import { createHash } from "node:crypto";
4
5
  import { gunzipSync } from "node:zlib";
5
6
  import { unzipSync } from "fflate";
@@ -355,7 +356,7 @@ const DELIMITERS = {
355
356
  semicolon: ";"
356
357
  };
357
358
  /** A number as a decimal literal, with an optional sign and exponent. `Number` alone would also accept hex, binary, `Infinity` and the empty string. */
358
- const DECIMAL = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
359
+ const DECIMAL$1 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
359
360
  /** The byte order mark some exporters put before the first header. */
360
361
  const BYTE_ORDER_MARK = "";
361
362
  /**
@@ -558,7 +559,7 @@ function* withoutByteOrderMark(text) {
558
559
  */
559
560
  function numeric$1(cell, factor, column, line) {
560
561
  if (cell === "") return null;
561
- if (!DECIMAL.test(cell)) throw new NativeParseError("csv-table", line, `column ${column} is declared numeric but holds ${JSON.stringify(cell)}`);
562
+ if (!DECIMAL$1.test(cell)) throw new NativeParseError("csv-table", line, `column ${column} is declared numeric but holds ${JSON.stringify(cell)}`);
562
563
  return Number(cell) * factor;
563
564
  }
564
565
  /**
@@ -617,6 +618,106 @@ function parseCsvTable(text, options) {
617
618
  return { nodes };
618
619
  }
619
620
  //#endregion
621
+ //#region src/parsers/geojson.ts
622
+ /** The columns a point feature's position fills, which no property may share a name with: the latitude and the longitude of a `Point` geometry, and null for a feature with any other geometry or none. */
623
+ const POINT_COLUMNS = {
624
+ latitude: "geometry_latitude",
625
+ longitude: "geometry_longitude"
626
+ };
627
+ const FeatureCollectionSchema = z.object({
628
+ type: z.literal("FeatureCollection"),
629
+ features: z.array(z.object({
630
+ geometry: z.object({
631
+ type: z.string(),
632
+ coordinates: z.unknown()
633
+ }).nullable(),
634
+ properties: z.record(z.string(), z.unknown()).nullable()
635
+ }))
636
+ });
637
+ /**
638
+ * The position of a point geometry.
639
+ * @param geometry - The feature's geometry.
640
+ * @returns The longitude and latitude of a `Point`, and undefined for any other geometry or none.
641
+ */
642
+ function pointOf(geometry) {
643
+ const coordinates = z.array(z.unknown()).safeParse(geometry?.type === "Point" ? geometry.coordinates : void 0);
644
+ if (!coordinates.success) return;
645
+ const [longitude, latitude] = coordinates.data;
646
+ return typeof longitude === "number" && typeof latitude === "number" ? {
647
+ longitude,
648
+ latitude
649
+ } : void 0;
650
+ }
651
+ /**
652
+ * One cell of a feature, in the form its column is declared to take.
653
+ * @param value - The property's value, or undefined when the feature lacks it.
654
+ * @param column - The column, for the error.
655
+ * @param feature - The 1-based feature number, for the error.
656
+ * @param kind - How the column is read: as a number times a factor, as an epoch-millisecond date written in ISO 8601, or as text.
657
+ * @returns The cell: null for a missing, null or empty value.
658
+ * @throws NativeParseError when the value is not the kind the recipe declares.
659
+ */
660
+ function cellOf(value, column, feature, kind) {
661
+ if (value === void 0 || value === null) return null;
662
+ if (kind === "date") {
663
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new NativeParseError("geojson", feature, `column ${column} is declared a date but holds ${JSON.stringify(value)}`);
664
+ return new Date(value).toISOString();
665
+ }
666
+ if (kind !== "text") {
667
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new NativeParseError("geojson", feature, `column ${column} is declared numeric but holds ${JSON.stringify(value)}`);
668
+ return value * kind.factor;
669
+ }
670
+ if (typeof value === "string") return value.trim() === "" ? null : value.trim();
671
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
672
+ throw new NativeParseError("geojson", feature, `column ${column} holds a ${Array.isArray(value) ? "list" : "nested object"}, which a column cannot`);
673
+ }
674
+ /**
675
+ * Parses a GeoJSON feature collection into rows, one per feature in file order: the properties the recipe lists become columns, a point's position fills `geometry_latitude` and `geometry_longitude` when they are listed, and an id comes from the recipe's id column (not also a column) or the feature's 1-based number. A column declared numeric is a number times its factor, one declared a date is an epoch-millisecond number written as an ISO 8601 text, and any other is text (a number or flag is written as its text), an empty or missing value being null so every row carries every column.
676
+ * @param text - The file text.
677
+ * @param options - The recipe's parser.
678
+ * @returns The rows.
679
+ * @throws NativeParseError when the text is not a GeoJSON feature collection, a value is not the kind its column is declared, a listed column is also a point column of another name, or an id is empty.
680
+ */
681
+ function parseGeojson(text, options) {
682
+ let document;
683
+ try {
684
+ document = JSON.parse(text);
685
+ } catch (error) {
686
+ throw new NativeParseError("geojson", 1, `the file is not JSON (${error instanceof Error ? error.message : String(error)})`);
687
+ }
688
+ const collection = FeatureCollectionSchema.safeParse(document);
689
+ if (!collection.success) throw new NativeParseError("geojson", 1, `the file is not a GeoJSON feature collection (${collection.error.issues.map((issue) => `${issue.path.map(String).join(".") || "(root)"}: ${issue.message}`).join("; ")})`);
690
+ const factors = new Map(options.numericColumns.map((numeric) => [numeric.column, numeric.factor]));
691
+ const dates = new Set(options.dateColumns);
692
+ const kinds = options.columns.map((column) => {
693
+ const factor = factors.get(column);
694
+ return {
695
+ column,
696
+ kind: factor === void 0 ? dates.has(column) ? "date" : "text" : { factor }
697
+ };
698
+ });
699
+ return { nodes: collection.data.features.map((feature, index) => {
700
+ const number = index + 1;
701
+ const properties = feature.properties ?? {};
702
+ const point = pointOf(feature.geometry);
703
+ const own = (column) => Object.hasOwn(properties, column) ? properties[column] : void 0;
704
+ const id = options.idColumn === null ? String(number) : cellOf(own(options.idColumn), options.idColumn, number, "text");
705
+ if (id === null) throw new NativeParseError("geojson", number, `the id column ${options.idColumn ?? ""} is empty`);
706
+ const cells = kinds.map(({ column, kind }) => {
707
+ if (column === POINT_COLUMNS.latitude || column === POINT_COLUMNS.longitude) {
708
+ if (Object.hasOwn(properties, column)) throw new NativeParseError("geojson", number, `a property is named ${column}, which is the column of the point's position`);
709
+ const position = column === POINT_COLUMNS.latitude ? point?.latitude : point?.longitude;
710
+ return [column, position === void 0 ? null : position * (typeof kind === "object" ? kind.factor : 1)];
711
+ }
712
+ return [column, cellOf(own(column), column, number, kind)];
713
+ });
714
+ return {
715
+ ...Object.fromEntries(cells),
716
+ id: String(id)
717
+ };
718
+ }) };
719
+ }
720
+ //#endregion
620
721
  //#region src/parsers/pajek.ts
621
722
  /** Section keywords, lower-cased, mapped to how their lines are read. `*Network` names the network and starts no section. `*Partition` and `*Vector` hold node attributes this version does not carry, so their lines are skipped, as are the `*Vertices` lines inside them. */
622
723
  const sections = /* @__PURE__ */ new Map([
@@ -780,6 +881,7 @@ function* decodePieces(bytes, parser, pieceBytes = DECODE_PIECE_BYTES) {
780
881
  function parseNative(bytes, parser) {
781
882
  if (parser.format === "csv-table") return parseCsvTable(decodePieces(bytes, parser), parser);
782
883
  const text = decode(bytes, parser);
884
+ if (parser.format === "geojson") return parseGeojson(text, parser);
783
885
  if (parser.format === "edge-list") return parseEdgeList(text, parser);
784
886
  if (parser.format === "matrix-market") return parseMatrixMarket(text);
785
887
  if (parser.format === "gml") return parseGml(text);
@@ -831,7 +933,7 @@ const BYTE_RADIX = 256;
831
933
  * @param length - Field length.
832
934
  * @returns The field text.
833
935
  */
834
- function field(bytes, offset, length) {
936
+ function field$1(bytes, offset, length) {
835
937
  const slice = bytes.subarray(offset, offset + length);
836
938
  const end = slice.indexOf(0);
837
939
  return new TextDecoder().decode(end === -1 ? slice : slice.subarray(0, end));
@@ -850,7 +952,7 @@ function numeric(bytes, offset, length) {
850
952
  for (const byte of bytes.subarray(offset + 1, offset + length)) value = value * BYTE_RADIX + byte;
851
953
  return value;
852
954
  }
853
- const text = field(bytes, offset, length).trim();
955
+ const text = field$1(bytes, offset, length).trim();
854
956
  if (!/^[0-7]*$/.test(text)) throw new ArchiveError(`tar header field is not octal: ${JSON.stringify(text)}`);
855
957
  return text === "" ? 0 : Number.parseInt(text, 8);
856
958
  }
@@ -886,21 +988,21 @@ function readTar(bytes) {
886
988
  for (const [index, byte] of header.entries()) sum += index >= checksum.offset && index < checksum.offset + checksum.length ? 32 : byte;
887
989
  if (sum !== numeric(header, checksum.offset, checksum.length)) throw new ArchiveError(`tar header at byte ${String(offset)} fails its checksum`);
888
990
  const size = numeric(header, HEADER.size.offset, HEADER.size.length);
889
- const type = field(header, HEADER.type.offset, HEADER.type.length);
991
+ const type = field$1(header, HEADER.type.offset, HEADER.type.length);
890
992
  const dataStart = offset + 512;
891
993
  if (dataStart + size > bytes.length) throw new ArchiveError(`tar member at byte ${String(offset)} runs past the end of the archive`);
892
994
  const data = bytes.subarray(dataStart, dataStart + size);
893
995
  offset = dataStart + Math.ceil(size / 512) * 512;
894
996
  if (type === "L") {
895
- longName = field(data, 0, data.length);
997
+ longName = field$1(data, 0, data.length);
896
998
  continue;
897
999
  }
898
1000
  if (type === "x") {
899
1001
  longName = paxPath(data);
900
1002
  continue;
901
1003
  }
902
- const name = field(header, HEADER.name.offset, HEADER.name.length);
903
- const prefix = field(header, HEADER.magic.offset, HEADER.magic.length).startsWith("ustar") ? field(header, HEADER.prefix.offset, HEADER.prefix.length) : "";
1004
+ const name = field$1(header, HEADER.name.offset, HEADER.name.length);
1005
+ const prefix = field$1(header, HEADER.magic.offset, HEADER.magic.length).startsWith("ustar") ? field$1(header, HEADER.prefix.offset, HEADER.prefix.length) : "";
904
1006
  const path = longName ?? (prefix === "" ? name : `${prefix}/${name}`);
905
1007
  longName = void 0;
906
1008
  if (type === "0" || type === "" || type === "7") members.push({
@@ -911,6 +1013,255 @@ function readTar(bytes) {
911
1013
  return members;
912
1014
  }
913
1015
  //#endregion
1016
+ //#region src/node/xlsx.ts
1017
+ const HEXADECIMAL = 16;
1018
+ const DECIMAL = 10;
1019
+ const LETTERS = 26;
1020
+ const FIRST_LETTER = "A".charCodeAt(0);
1021
+ const SECONDS_PER_DAY = 86400;
1022
+ const MILLISECONDS = 1e3;
1023
+ /** The serial number Excel gives its 29th of February 1900, a day that never existed: serials above it count from 30 December 1899 and serials below it from 31 December 1899. */
1024
+ const LEAP_BUG_SERIAL = 60;
1025
+ /** The day a serial number counts from: 1 January 1904 in a workbook that says so, otherwise 31 December 1899 for the serials before Excel's false 29th of February 1900 and 30 December 1899 for those after it. */
1026
+ const EPOCH_1904 = "1904-01-01T00:00:00Z";
1027
+ const EPOCH_BEFORE_LEAP_BUG = "1899-12-31T00:00:00Z";
1028
+ const EPOCH_AFTER_LEAP_BUG = "1899-12-30T00:00:00Z";
1029
+ const DATE_LENGTH = 10;
1030
+ const DATE_TIME_LENGTH = 19;
1031
+ const SECONDS_PER_MINUTE = 60;
1032
+ const MINUTES_PER_HOUR = 60;
1033
+ /**
1034
+ * Replaces the XML entities and character references of a text.
1035
+ * @param text - Escaped text.
1036
+ * @returns The text.
1037
+ */
1038
+ function unescape(text) {
1039
+ return text.replaceAll(/&(#x[0-9A-Fa-f]+|#[0-9]+|amp|lt|gt|quot|apos);/g, (_whole, entity) => {
1040
+ if (entity.startsWith("#x")) return String.fromCodePoint(Number.parseInt(entity.slice(2), HEXADECIMAL));
1041
+ if (entity.startsWith("#")) return String.fromCodePoint(Number.parseInt(entity.slice(1), DECIMAL));
1042
+ return {
1043
+ amp: "&",
1044
+ lt: "<",
1045
+ gt: ">",
1046
+ quot: "\"",
1047
+ apos: "'"
1048
+ }[entity] ?? "";
1049
+ });
1050
+ }
1051
+ /**
1052
+ * The attributes of a start tag's text.
1053
+ * @param tag - The text between `<` and `>`, name included.
1054
+ * @returns The attributes, unescaped.
1055
+ */
1056
+ function attributesOf(tag) {
1057
+ const attributes = {};
1058
+ for (const [, name, value] of tag.matchAll(/([\w:.-]+)\s*=\s*"([^"]*)"/g)) if (name !== void 0 && value !== void 0) attributes[name] = unescape(value);
1059
+ return attributes;
1060
+ }
1061
+ /**
1062
+ * The start tags of one element name in an XML part, in order.
1063
+ * @param xml - The part's text.
1064
+ * @param name - The element name.
1065
+ * @returns The attributes of each start tag.
1066
+ */
1067
+ function elements(xml, name) {
1068
+ return [...xml.matchAll(new RegExp(`<${name}(?=[\\s/>])([^>]*)>`, "g"))].map((match) => attributesOf(match[1] ?? ""));
1069
+ }
1070
+ /**
1071
+ * The strings of a workbook's shared string table, in index order: each item's text runs joined, the phonetic runs of East Asian text left out.
1072
+ * @param xml - The `sharedStrings.xml` text.
1073
+ * @returns The strings.
1074
+ */
1075
+ function sharedStrings(xml) {
1076
+ return [...xml.matchAll(/<si(?:\s[^>]*)?>([\s\S]*?)<\/si>/g)].map((item) => {
1077
+ return [...(item[1] ?? "").replaceAll(/<rPh[\s\S]*?<\/rPh>/g, "").matchAll(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g)].map((text) => unescape(text[1] ?? "")).join("");
1078
+ });
1079
+ }
1080
+ /** The built-in number format ids that show a date or a time, as inclusive ranges (ECMA-376, 18.8.30): the short and long dates and times, the locale dates, and the elapsed and minute times. */
1081
+ const BUILT_IN_DATE_FORMATS = [
1082
+ {
1083
+ first: 14,
1084
+ last: 22
1085
+ },
1086
+ {
1087
+ first: 27,
1088
+ last: 36
1089
+ },
1090
+ {
1091
+ first: 45,
1092
+ last: 47
1093
+ },
1094
+ {
1095
+ first: 50,
1096
+ last: 58
1097
+ }
1098
+ ];
1099
+ /**
1100
+ * Whether a custom number format shows a date or a time: it names a year, month, day, hour or second once its quoted text, escaped characters and bracketed conditions and colours are removed.
1101
+ * @param code - The format code.
1102
+ * @returns True for a date or time format.
1103
+ */
1104
+ function isDateFormat(code) {
1105
+ return /[ymdhs]/i.test(code.replaceAll(/"[^"]*"|\\.|\[[^\]]*\]/g, ""));
1106
+ }
1107
+ /**
1108
+ * Which cell styles show dates, by the style's index.
1109
+ * @param xml - The `styles.xml` text, or undefined when the workbook has none.
1110
+ * @returns The style indexes whose number format is a date or a time.
1111
+ */
1112
+ function dateStyles(xml) {
1113
+ if (xml === void 0) return /* @__PURE__ */ new Set();
1114
+ const custom = /* @__PURE__ */ new Map();
1115
+ for (const format of elements(xml, "numFmt")) if (format["numFmtId"] !== void 0 && format["formatCode"] !== void 0) custom.set(Number(format["numFmtId"]), format["formatCode"]);
1116
+ const cellFormats = /<cellXfs(?:\s[^>]*)?>([\s\S]*?)<\/cellXfs>/.exec(xml)?.[1] ?? "";
1117
+ const styles = /* @__PURE__ */ new Set();
1118
+ for (const [index, format] of elements(cellFormats, "xf").entries()) {
1119
+ const id = Number(format["numFmtId"] ?? "0");
1120
+ const code = custom.get(id);
1121
+ if (code === void 0 ? BUILT_IN_DATE_FORMATS.some((range) => id >= range.first && id <= range.last) : isDateFormat(code)) styles.add(index);
1122
+ }
1123
+ return styles;
1124
+ }
1125
+ /**
1126
+ * A date serial number as ISO 8601 text: a date when it has no time of day, a date and time when it has, and a time alone for a serial below one day in a 1900 workbook.
1127
+ * @param serial - The serial number, with the time of day as its fraction.
1128
+ * @param date1904 - Whether the workbook counts days from 1 January 1904.
1129
+ * @returns The text.
1130
+ * @throws ArchiveError for the serial of the 29th of February 1900, which is not a date.
1131
+ */
1132
+ function isoOf(serial, date1904) {
1133
+ const whole = Math.floor(serial);
1134
+ const seconds = Math.round((serial - whole) * SECONDS_PER_DAY);
1135
+ if (!date1904 && whole === 0) return [
1136
+ Math.floor(seconds / 3600),
1137
+ Math.floor(seconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR,
1138
+ seconds % SECONDS_PER_MINUTE
1139
+ ].map((part) => String(part).padStart(2, "0")).join(":");
1140
+ if (!date1904 && whole === LEAP_BUG_SERIAL) throw new ArchiveError("a cell holds the serial of 29 February 1900, which is not a date");
1141
+ return new Date(Date.parse(date1904 ? EPOCH_1904 : whole < LEAP_BUG_SERIAL ? EPOCH_BEFORE_LEAP_BUG : EPOCH_AFTER_LEAP_BUG) + (whole * SECONDS_PER_DAY + seconds) * MILLISECONDS).toISOString().slice(0, seconds === 0 ? DATE_LENGTH : DATE_TIME_LENGTH);
1142
+ }
1143
+ /**
1144
+ * The 0-based column of a cell reference such as `AB12`.
1145
+ * @param reference - The reference.
1146
+ * @returns The column index.
1147
+ */
1148
+ function columnOf(reference) {
1149
+ let index = 0;
1150
+ for (const letter of reference.replaceAll(/[^A-Za-z]/g, "")) index = index * LETTERS + (letter.toUpperCase().charCodeAt(0) - FIRST_LETTER + 1);
1151
+ return index - 1;
1152
+ }
1153
+ /**
1154
+ * One CSV field: quoted when it holds a comma, a quote or a line break.
1155
+ * @param text - The cell text.
1156
+ * @returns The field.
1157
+ */
1158
+ function field(text) {
1159
+ return /[",\r\n]/.test(text) ? `"${text.replaceAll("\"", "\"\"")}"` : text;
1160
+ }
1161
+ /**
1162
+ * Finds the part that holds a named sheet.
1163
+ * @param workbook - The `workbook.xml` text.
1164
+ * @param rels - The workbook's relationships text.
1165
+ * @param name - The sheet's name.
1166
+ * @returns The part's path inside the archive.
1167
+ * @throws ArchiveError when the workbook has no such sheet or its relationship is missing.
1168
+ */
1169
+ function sheetPath(workbook, rels, name) {
1170
+ const sheets = elements(workbook, "sheet");
1171
+ const wanted = sheets.find((sheet) => sheet["name"] === name);
1172
+ if (wanted === void 0) throw new ArchiveError(`the workbook has no sheet ${name}; it has ${sheets.map((sheet) => sheet["name"] ?? "").join(", ")}`);
1173
+ const target = elements(rels, "Relationship").find((relation) => relation["Id"] === wanted["r:id"])?.["Target"];
1174
+ if (target === void 0) throw new ArchiveError(`the workbook has no relationship for the sheet ${name}`);
1175
+ return target.startsWith("/") ? target.slice(1) : `xl/${target}`;
1176
+ }
1177
+ /**
1178
+ * Reads a worksheet's cells into text by row and column.
1179
+ * @param xml - The worksheet's text.
1180
+ * @param strings - The shared strings.
1181
+ * @param dates - The date styles.
1182
+ * @param date1904 - Whether the workbook counts days from 1 January 1904.
1183
+ * @returns The rows by 1-based row number, each a sparse list of the cells' text, and the widest row's column count.
1184
+ * @throws ArchiveError when a cell names a shared string the workbook lacks or a date cell holds no number.
1185
+ */
1186
+ function readRows(xml, strings, dates, date1904) {
1187
+ const rows = /* @__PURE__ */ new Map();
1188
+ let width = 0;
1189
+ let next = 1;
1190
+ for (const row of xml.matchAll(/<row(?=[\s>])([^>]*?)(?:\/>|>([\s\S]*?)<\/row>)/g)) {
1191
+ const number = Number(attributesOf(row[1] ?? "")["r"] ?? next);
1192
+ next = number + 1;
1193
+ const cells = [];
1194
+ for (const cell of (row[2] ?? "").matchAll(/<c(?=[\s>])([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/g)) {
1195
+ const attributes = attributesOf(cell[1] ?? "");
1196
+ const column = attributes["r"] === void 0 ? cells.length : columnOf(attributes["r"]);
1197
+ const body = cell[2] ?? "";
1198
+ const value = /<v(?:\s[^>]*)?>([\s\S]*?)<\/v>/.exec(body)?.[1];
1199
+ let text;
1200
+ if (attributes["t"] === "inlineStr") text = [...body.matchAll(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g)].map((run) => unescape(run[1] ?? "")).join("");
1201
+ else if (value === void 0) text = void 0;
1202
+ else if (attributes["t"] === "s") {
1203
+ text = strings[Number(value)];
1204
+ if (text === void 0) throw new ArchiveError(`row ${String(number)} names the shared string ${value}, which the workbook does not have`);
1205
+ } else if (attributes["t"] === "b") text = value === "0" ? "FALSE" : "TRUE";
1206
+ else if (attributes["t"] === "str" || attributes["t"] === "e") text = unescape(value);
1207
+ else if (dates.has(Number(attributes["s"] ?? "0"))) {
1208
+ const serial = Number(value);
1209
+ if (!Number.isFinite(serial) || serial < 0) throw new ArchiveError(`row ${String(number)} has a date cell holding ${value}`);
1210
+ text = isoOf(serial, date1904);
1211
+ } else text = unescape(value);
1212
+ if (text !== void 0 && text !== "") {
1213
+ cells[column] = text;
1214
+ width = Math.max(width, column + 1);
1215
+ }
1216
+ }
1217
+ rows.set(number, cells);
1218
+ }
1219
+ return {
1220
+ rows,
1221
+ width
1222
+ };
1223
+ }
1224
+ /**
1225
+ * Writes one sheet of an xlsx workbook as CSV text, so a recipe reads it with the table parser: one line for every row number from the first to the last (a row the sheet leaves out is an empty line, so the lines above the header are the rows an editor shows), every line the width of the sheet's widest row, a date cell as ISO 8601 text, a boolean as TRUE or FALSE, a number as the text the workbook stores, and a formula as its stored result.
1226
+ * @param bytes - The workbook.
1227
+ * @param sheet - The sheet's name.
1228
+ * @returns The CSV, in UTF-8.
1229
+ * @throws ArchiveError when the bytes are not a workbook, the sheet is missing, or a cell cannot be read.
1230
+ */
1231
+ function sheetToCsv(bytes, sheet) {
1232
+ let parts;
1233
+ try {
1234
+ const wanted = /* @__PURE__ */ new Set([
1235
+ "xl/workbook.xml",
1236
+ "xl/_rels/workbook.xml.rels",
1237
+ "xl/sharedStrings.xml",
1238
+ "xl/styles.xml"
1239
+ ]);
1240
+ parts = unzipSync(bytes, { filter: (file) => wanted.has(file.name) });
1241
+ } catch (error) {
1242
+ throw new ArchiveError(`not a valid workbook: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1243
+ }
1244
+ const text = (path) => {
1245
+ const part = getOwn(parts, path);
1246
+ return part === void 0 ? void 0 : new TextDecoder("utf-8").decode(part);
1247
+ };
1248
+ const workbook = text("xl/workbook.xml");
1249
+ const rels = text("xl/_rels/workbook.xml.rels");
1250
+ if (workbook === void 0 || rels === void 0) throw new ArchiveError("the file is not an xlsx workbook: it has no xl/workbook.xml with its relationships");
1251
+ const path = sheetPath(workbook, rels, sheet);
1252
+ const sheetBytes = getOwn(unzipSync(bytes, { filter: (file) => file.name === path }), path);
1253
+ if (sheetBytes === void 0) throw new ArchiveError(`the workbook names the sheet ${sheet} in ${path}, which the file does not have`);
1254
+ const strings = text("xl/sharedStrings.xml");
1255
+ const { rows, width } = readRows(new TextDecoder("utf-8").decode(sheetBytes), strings === void 0 ? [] : sharedStrings(strings), dateStyles(text("xl/styles.xml")), /<workbookPr(?=[\s/>])[^>]*date1904="(?:1|true)"/.test(workbook));
1256
+ const last = Math.max(0, ...rows.keys());
1257
+ const lines = [];
1258
+ for (let number = 1; number <= last; number += 1) {
1259
+ const cells = rows.get(number) ?? [];
1260
+ lines.push(Array.from({ length: width }, (_empty, column) => field(cells[column] ?? "")).join(","));
1261
+ }
1262
+ return new TextEncoder().encode(lines.length === 0 ? "" : `${lines.join("\n")}\n`);
1263
+ }
1264
+ //#endregion
914
1265
  //#region src/node/archive.ts
915
1266
  /**
916
1267
  * Whether an archive member is a macOS resource fork (`__MACOSX/` or an AppleDouble `._` file), which carries Finder metadata rather than data and never matches a selector.
@@ -957,6 +1308,10 @@ function pick(paths, member) {
957
1308
  function unpack(bytes, archive) {
958
1309
  if (archive.kind === "none") return { bytes };
959
1310
  if (archive.kind === "gzip") return { bytes: gunzip(bytes) };
1311
+ if (archive.kind === "xlsx") return {
1312
+ bytes: sheetToCsv(bytes, archive.sheet),
1313
+ member: archive.sheet
1314
+ };
960
1315
  if (archive.kind === "zip") {
961
1316
  const names = [];
962
1317
  unzip(bytes, (file) => {
@@ -1082,6 +1437,93 @@ async function downloadRaw(url, init) {
1082
1437
  return follow(new URL(url), init.signal, 0);
1083
1438
  }
1084
1439
  //#endregion
1440
+ //#region src/node/paged.ts
1441
+ const countResponse = (value) => typeof value === "object" && value !== null && "count" in value && typeof value.count === "number" && Number.isInteger(value.count) && value.count >= 0 ? value.count : void 0;
1442
+ /**
1443
+ * The error message an ArcGIS server returns in a 200 response, if the body is one.
1444
+ * @param value - A parsed response body.
1445
+ * @returns The message, or undefined when the body is not an error.
1446
+ */
1447
+ function arcgisError(value) {
1448
+ if (typeof value !== "object" || value === null || !("error" in value)) return;
1449
+ const { error } = value;
1450
+ return typeof error === "object" && error !== null && "message" in error && typeof error.message === "string" ? error.message : "the server returned an error";
1451
+ }
1452
+ /**
1453
+ * Reads one JSON response of the layer's query endpoint.
1454
+ * @param port - The network port.
1455
+ * @param url - The request URL.
1456
+ * @param signal - Aborts the request.
1457
+ * @returns The parsed body.
1458
+ * @throws KeepsetError when the server does not answer successfully, the body is not JSON, or it is an ArcGIS error.
1459
+ */
1460
+ async function getJson(port, url, signal) {
1461
+ const response = await port(url, signal === void 0 ? {} : { signal });
1462
+ if (!response.ok) throw new KeepsetError(`${url} answered ${String(response.status)} ${response.statusText}`);
1463
+ let body;
1464
+ try {
1465
+ body = JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(await response.arrayBuffer())));
1466
+ } catch (error) {
1467
+ throw new KeepsetError(`${url} did not answer JSON (${error instanceof Error ? error.message : String(error)})`);
1468
+ }
1469
+ const message = arcgisError(body);
1470
+ if (message !== void 0) throw new KeepsetError(`${url} answered an error: ${message}`);
1471
+ return body;
1472
+ }
1473
+ const FeatureListSchema = z.array(z.unknown());
1474
+ /**
1475
+ * Reads a layer's features page by page, each request starting where the features held so far end.
1476
+ * @param layer - The layer read.
1477
+ * @param held - The features read so far.
1478
+ * @returns Every feature of the layer.
1479
+ * @throws KeepsetError when a page is empty before the count is reached or the server answers an error.
1480
+ */
1481
+ async function readPages(layer, held) {
1482
+ if (held.length >= layer.count) return held;
1483
+ const url = `${layer.query}?where=1%3D1&outFields=*&orderByFields=${encodeURIComponent(layer.orderBy)}&resultOffset=${String(held.length)}&f=geojson`;
1484
+ const page = await getJson(layer.port, url, layer.signal);
1485
+ const features = FeatureListSchema.safeParse(typeof page === "object" && page !== null && "features" in page ? page.features : void 0);
1486
+ if (!features.success || features.data.length === 0) throw new KeepsetError(`${layer.query} gave no features at offset ${String(held.length)}, before the ${String(layer.count)} it reports, so it does not page by offset`);
1487
+ return readPages(layer, [...held, ...features.data]);
1488
+ }
1489
+ /**
1490
+ * Downloads a whole ArcGIS feature layer as one GeoJSON feature collection: the layer's feature count is read first, then its pages in `orderBy` order by offset until every feature is held, so a server that ignores the offset is an error and not an endless loop.
1491
+ * @param layerUrl - The layer's URL, such as `.../FeatureServer/0`.
1492
+ * @param orderBy - The field the pages are ordered by.
1493
+ * @param port - The network port.
1494
+ * @param signal - Aborts the download.
1495
+ * @returns The bytes of one `FeatureCollection`, which is what the parser reads and what the provenance hashes.
1496
+ * @throws KeepsetError when the layer does not report a count, a page is empty before the count is reached, or the server answers an error.
1497
+ */
1498
+ async function downloadArcgisLayer(layerUrl, orderBy, port, signal) {
1499
+ const query = `${layerUrl.replace(/\/+$/, "")}/query`;
1500
+ const count = countResponse(await getJson(port, `${query}?where=1%3D1&returnCountOnly=true&f=json`, signal));
1501
+ if (count === void 0) throw new KeepsetError(`${layerUrl} did not report a feature count`);
1502
+ const pages = await readPages({
1503
+ query,
1504
+ orderBy,
1505
+ count,
1506
+ port,
1507
+ signal
1508
+ }, []);
1509
+ return new TextEncoder().encode(JSON.stringify({
1510
+ type: "FeatureCollection",
1511
+ features: pages
1512
+ }));
1513
+ }
1514
+ /**
1515
+ * Downloads a paged source as the one file its pages make, in the way its recipe's paging says.
1516
+ * @param url - The source's URL.
1517
+ * @param paging - How the source is paged.
1518
+ * @param port - The network port.
1519
+ * @param signal - Aborts the download.
1520
+ * @returns The bytes the parser reads.
1521
+ * @throws KeepsetError when a page cannot be read or the pages do not add up to what the source reports.
1522
+ */
1523
+ async function downloadPaged(url, paging, port, signal) {
1524
+ return downloadArcgisLayer(url, paging.orderBy, port, signal);
1525
+ }
1526
+ //#endregion
1085
1527
  //#region src/node/fetch-dataset.ts
1086
1528
  /**
1087
1529
  * The URL a recipe downloads a source from. A dead source is fetched from its pinned snapshot in the Wayback Machine's `id_` form, which serves the archived bytes as they were captured rather than wrapped in the archive's page chrome.
@@ -1138,6 +1580,7 @@ async function runToNative(dataset, options) {
1138
1580
  const fetchedUrl = downloadUrl(source);
1139
1581
  const retrievedAt = now().toISOString();
1140
1582
  const bytes = await step(entry.id, "download", async () => {
1583
+ if (recipe.paging !== void 0) return downloadPaged(fetchedUrl, recipe.paging, fetchPort, signal);
1141
1584
  const response = await fetchPort(fetchedUrl, signal === void 0 ? {} : { signal });
1142
1585
  if (!response.ok) throw new KeepsetError(`${fetchedUrl} answered ${String(response.status)} ${response.statusText}`);
1143
1586
  return new Uint8Array(await response.arrayBuffer());
@@ -4,7 +4,7 @@ import { KeepsetError, NAME_SEPARATOR, countTarget, getDataset, listDatasets, li
4
4
  import { fetchDataset, roundTripDataset } from "keepset/node";
5
5
  import { parseArgs } from "node:util";
6
6
  //#region package.json
7
- var version = "0.18.0";
7
+ var version = "0.20.0";
8
8
  const usage = `Usage: keepset <command> [options]
9
9
 
10
10
  Commands:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "keepset",
3
3
  "description": "Library, CLI and statically generated catalogue for dataset fetch-and-normalise recipes",
4
- "version": "0.18.0",
4
+ "version": "0.20.0",
5
5
  "bin": {
6
6
  "keepset": "./dist-cli/keepset.mjs"
7
7
  },
@@ -1 +1 @@
1
- {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.4/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"aliases":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*(?:\\/[A-Za-z0-9][A-Za-z0-9._-]*)*$","type":"string"},"minItems":1,"type":"array"},"category":{"enum":["graph/v1","or/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"namespace":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar.gz","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","whitespace","tab","comma","space"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"edge-list","type":"string"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"sourceColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"targetColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","commentPrefixes","sourceColumn","targetColumn"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"matrix-market","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"gml","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"pajek","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"delimiter":{"enum":["comma","tab","semicolon"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"csv-table","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","idColumn","numericColumns"],"type":"object"}]},"source":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"status":{"const":"fetchable","type":"string"}},"required":["status","source","archive","parser"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"minLength":1,"type":"string"},"status":{"const":"not-fetchable","type":"string"}},"required":["status","reason"],"type":"object"}]},"sources":{"items":{"additionalProperties":false,"properties":{"retrieved":{"format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$","type":"string"},"snapshot":{"additionalProperties":false,"properties":{"timestamp":{"pattern":"^\\d{8}(?:\\d{6})?$","type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","timestamp"],"type":"object"},"status":{"enum":["live","dead"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","retrieved","status"],"type":"object"},"minItems":1,"type":"array"},"tags":{"items":{"minLength":1,"type":"string"},"type":"array"}},"required":["id","name","tags","category","sources","licence","citations","recipe","crosswalk","expectedCounts","properties"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.18.0/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
1
+ {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.4/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"aliases":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*(?:\\/[A-Za-z0-9][A-Za-z0-9._-]*)*$","type":"string"},"minItems":1,"type":"array"},"category":{"enum":["graph/v1","or/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"namespace":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar.gz","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"xlsx","type":"string"},"sheet":{"minLength":1,"type":"string"}},"required":["kind","sheet"],"type":"object"}]},"paging":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"arcgis-query","type":"string"},"orderBy":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"}},"required":["kind","orderBy"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","whitespace","tab","comma","space"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"edge-list","type":"string"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"sourceColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"targetColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","commentPrefixes","sourceColumn","targetColumn"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"matrix-market","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"gml","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"pajek","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"delimiter":{"enum":["comma","tab","semicolon"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"csv-table","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","idColumn","numericColumns"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"minItems":1,"type":"array"},"dateColumns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"encoding":{"const":"utf-8","type":"string"},"format":{"const":"geojson","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"}},"required":["format","encoding","idColumn","numericColumns","dateColumns","columns"],"type":"object"}]},"source":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"status":{"const":"fetchable","type":"string"}},"required":["status","source","archive","parser"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"minLength":1,"type":"string"},"status":{"const":"not-fetchable","type":"string"}},"required":["status","reason"],"type":"object"}]},"sources":{"items":{"additionalProperties":false,"properties":{"retrieved":{"format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$","type":"string"},"snapshot":{"additionalProperties":false,"properties":{"timestamp":{"pattern":"^\\d{8}(?:\\d{6})?$","type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","timestamp"],"type":"object"},"status":{"enum":["live","dead"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","retrieved","status"],"type":"object"},"minItems":1,"type":"array"},"tags":{"items":{"minLength":1,"type":"string"},"type":"array"}},"required":["id","name","tags","category","sources","licence","citations","recipe","crosswalk","expectedCounts","properties"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.20.0/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
@@ -1 +1 @@
1
- {"$defs":{"Manifest":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"lineage":{"additionalProperties":false,"properties":{"catalogue":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["catalogue","version","repository"],"type":"object"},"name":{"minLength":1,"type":"string"},"namespaceAliases":{"additionalProperties":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"propertyNames":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"type":"object"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["id","name","description","version","repository"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.18.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}
1
+ {"$defs":{"Manifest":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"lineage":{"additionalProperties":false,"properties":{"catalogue":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["catalogue","version","repository"],"type":"object"},"name":{"minLength":1,"type":"string"},"namespaceAliases":{"additionalProperties":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"propertyNames":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"type":"object"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["id","name","description","version","repository"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.20.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}