keepset 0.19.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.
@@ -410,7 +410,7 @@ const CitationSchema = z.object({
410
410
  const RelativePathSchema = z.string().refine((path) => path.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".."), { message: "must be a relative path of non-empty segments with no '.' or '..'" });
411
411
  /** Which file of an archive to read: a member path, matched against a member's whole path or its trailing segments (so `net.gml` finds `net/net.gml`), or the member whose name ends with one of a list of extensions (a format can have several, such as Pajek's `.net` and `.paj`). Either must match exactly one member when the recipe runs; macOS resource-fork members (`__MACOSX/` and `._` files) never match. */
412
412
  const ArchiveMemberSchema = z.union([z.object({ path: RelativePathSchema }).strict(), z.object({ extensions: z.array(z.string().regex(/^\.[A-Za-z0-9][A-Za-z0-9.]*$/, { message: "must be a file extension such as .mtx" })).min(1) }).strict()]);
413
- /** How the downloaded bytes unpack into the one file the parser reads. */
413
+ /** How the downloaded bytes unpack into the one file the parser reads. A workbook (`xlsx`) is read as the CSV of its named sheet, with dates as ISO 8601 text, for the `csv-table` parser. */
414
414
  const ArchiveSchema = z.discriminatedUnion("kind", [
415
415
  z.object({ kind: z.literal("none") }).strict(),
416
416
  z.object({ kind: z.literal("gzip") }).strict(),
@@ -425,6 +425,10 @@ const ArchiveSchema = z.discriminatedUnion("kind", [
425
425
  z.object({
426
426
  kind: z.literal("tar.gz"),
427
427
  member: ArchiveMemberSchema
428
+ }).strict(),
429
+ z.object({
430
+ kind: z.literal("xlsx"),
431
+ sheet: z.string().min(1)
428
432
  }).strict()
429
433
  ]);
430
434
  const ColumnSchema = z.int().min(0);
@@ -889,7 +893,7 @@ var catalogue_default = {
889
893
  "id": "keepset",
890
894
  "name": "keepset",
891
895
  "description": "The keepset catalogue: datasets with machine-readable recipes that fetch each one and normalise it into its category's target shape.",
892
- "version": "1.7.0",
896
+ "version": "1.8.0",
893
897
  "repository": "https://github.com/Mearman/keepset",
894
898
  "namespaceAliases": {
895
899
  "networkrepository": ["nr"],
@@ -5191,6 +5195,10 @@ var catalogue_default = {
5191
5195
  "url": "https://environment.data.gov.uk/dataset/9c7728fc-68e2-46e1-addb-63790b6b5313",
5192
5196
  "retrieved": "2026-10-19",
5193
5197
  "status": "live"
5198
+ }, {
5199
+ "url": "https://environment.data.gov.uk/api/file/download?fileDataSetId=e06036f5-7f41-46a3-93ff-fbd0fa1a83cc&fileName=Environment_Agency_Prosecutions.xlsx",
5200
+ "retrieved": "2026-10-19",
5201
+ "status": "live"
5194
5202
  }],
5195
5203
  "licence": {
5196
5204
  "kind": "spdx",
@@ -5198,10 +5206,73 @@ var catalogue_default = {
5198
5206
  },
5199
5207
  "citations": [],
5200
5208
  "recipe": {
5201
- "status": "not-fetchable",
5202
- "reason": "The download is an xlsx workbook with an explanation sheet and a data sheet, and keepset has no spreadsheet parser yet."
5209
+ "status": "fetchable",
5210
+ "source": 1,
5211
+ "archive": {
5212
+ "kind": "xlsx",
5213
+ "sheet": "Environment Agency Prosecutions"
5214
+ },
5215
+ "parser": {
5216
+ "format": "csv-table",
5217
+ "encoding": "utf-8",
5218
+ "delimiter": "comma",
5219
+ "skipLines": 0,
5220
+ "idColumn": null,
5221
+ "numericColumns": [{
5222
+ "column": "Fine",
5223
+ "factor": 1
5224
+ }],
5225
+ "columns": [
5226
+ "Date_of_Action",
5227
+ "Fine",
5228
+ "Incident_Type",
5229
+ "Verdict",
5230
+ "Incident_Area"
5231
+ ]
5232
+ }
5233
+ },
5234
+ "crosswalk": {
5235
+ "source": "csv-table",
5236
+ "target": "or/v1",
5237
+ "rules": [
5238
+ {
5239
+ "op": "move",
5240
+ "from": "meta",
5241
+ "to": "meta"
5242
+ },
5243
+ {
5244
+ "op": "move",
5245
+ "from": "nodes[].id",
5246
+ "to": "nodes[].id"
5247
+ },
5248
+ {
5249
+ "op": "move",
5250
+ "from": "nodes[].Date_of_Action",
5251
+ "to": "nodes[].closed"
5252
+ },
5253
+ {
5254
+ "op": "move",
5255
+ "from": "nodes[].Fine",
5256
+ "to": "nodes[].prize"
5257
+ },
5258
+ {
5259
+ "op": "move",
5260
+ "from": "nodes[].Incident_Type",
5261
+ "to": "nodes[].skill"
5262
+ },
5263
+ {
5264
+ "op": "move",
5265
+ "from": "nodes[].Verdict",
5266
+ "to": "nodes[].status"
5267
+ },
5268
+ {
5269
+ "op": "move",
5270
+ "from": "nodes[].Incident_Area",
5271
+ "to": "nodes[].territory"
5272
+ }
5273
+ ],
5274
+ "oneWay": []
5203
5275
  },
5204
- "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
5205
5276
  "expectedCounts": {},
5206
5277
  "properties": { "surveyId": "or-50" }
5207
5278
  },
@@ -70,7 +70,7 @@ declare const ArchiveMemberSchema: z.ZodUnion<readonly [z.ZodObject<{
70
70
  extensions: z.ZodArray<z.ZodString>;
71
71
  }, z.core.$strict>]>;
72
72
  type ArchiveMember = z.infer<typeof ArchiveMemberSchema>;
73
- /** How the downloaded bytes unpack into the one file the parser reads. */
73
+ /** How the downloaded bytes unpack into the one file the parser reads. A workbook (`xlsx`) is read as the CSV of its named sheet, with dates as ISO 8601 text, for the `csv-table` parser. */
74
74
  declare const ArchiveSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
75
75
  kind: z.ZodLiteral<"none">;
76
76
  }, z.core.$strict>, z.ZodObject<{
@@ -96,6 +96,9 @@ declare const ArchiveSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
96
96
  }, z.core.$strict>, z.ZodObject<{
97
97
  extensions: z.ZodArray<z.ZodString>;
98
98
  }, z.core.$strict>]>;
99
+ }, z.core.$strict>, z.ZodObject<{
100
+ kind: z.ZodLiteral<"xlsx">;
101
+ sheet: z.ZodString;
99
102
  }, z.core.$strict>], "kind">;
100
103
  type Archive = z.infer<typeof ArchiveSchema>;
101
104
  /** A column of a table as the table parser names it: the header with every run of characters outside letters, digits and underscore replaced by one underscore, so it is a valid segment of a twindex path. */
@@ -199,6 +202,9 @@ declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
199
202
  }, z.core.$strict>, z.ZodObject<{
200
203
  extensions: z.ZodArray<z.ZodString>;
201
204
  }, z.core.$strict>]>;
205
+ }, z.core.$strict>, z.ZodObject<{
206
+ kind: z.ZodLiteral<"xlsx">;
207
+ sheet: z.ZodString;
202
208
  }, z.core.$strict>], "kind">;
203
209
  parser: z.ZodDiscriminatedUnion<[z.ZodObject<{
204
210
  format: z.ZodLiteral<"edge-list">;
@@ -352,6 +358,9 @@ declare const EntrySchema: z.ZodObject<{
352
358
  }, z.core.$strict>, z.ZodObject<{
353
359
  extensions: z.ZodArray<z.ZodString>;
354
360
  }, z.core.$strict>]>;
361
+ }, z.core.$strict>, z.ZodObject<{
362
+ kind: z.ZodLiteral<"xlsx">;
363
+ sheet: z.ZodString;
355
364
  }, z.core.$strict>], "kind">;
356
365
  parser: z.ZodDiscriminatedUnion<[z.ZodObject<{
357
366
  format: z.ZodLiteral<"edge-list">;
@@ -486,6 +495,9 @@ declare const EntrySchema: z.ZodObject<{
486
495
  } | {
487
496
  extensions: string[];
488
497
  };
498
+ } | {
499
+ kind: "xlsx";
500
+ sheet: string;
489
501
  };
490
502
  parser: {
491
503
  format: "edge-list";
@@ -709,6 +721,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
709
721
  }, z.core.$strict>, z.ZodObject<{
710
722
  extensions: z.ZodArray<z.ZodString>;
711
723
  }, z.core.$strict>]>;
724
+ }, z.core.$strict>, z.ZodObject<{
725
+ kind: z.ZodLiteral<"xlsx">;
726
+ sheet: z.ZodString;
712
727
  }, z.core.$strict>], "kind">;
713
728
  parser: z.ZodDiscriminatedUnion<[z.ZodObject<{
714
729
  format: z.ZodLiteral<"edge-list">;
@@ -843,6 +858,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
843
858
  } | {
844
859
  extensions: string[];
845
860
  };
861
+ } | {
862
+ kind: "xlsx";
863
+ sheet: string;
846
864
  };
847
865
  parser: {
848
866
  format: "edge-list";
@@ -975,6 +993,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
975
993
  } | {
976
994
  extensions: string[];
977
995
  };
996
+ } | {
997
+ kind: "xlsx";
998
+ sheet: string;
978
999
  };
979
1000
  parser: {
980
1001
  format: "edge-list";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as Parser, A as LineageSchema, B as Citation, C as CatalogueIndex, D as NamespaceNode, E as DatasetQuery, F as AliasSchema, G as Entry, H as ColumnNameSchema, I as Archive, J as IdSchema, K as EntrySchema, L as ArchiveMember, M as ManifestSchema, N as SemVerSchema, O as createCatalogue, P as parseManifest, Q as NamespaceSchema, R as ArchiveMemberSchema, S as Catalogue, T as Dataset, U as CrosswalkReference, V as CitationSchema, W as CrosswalkReferenceSchema, X as LicenceSchema, Y as Licence, Z as Namespace, _ as GraphEdgeSchema, a as CategoryTargets, at as Snapshot, b as GraphV1, c as getCategory, ct as SourceSchema, d as listCategories, dt as waybackUrl, et as ParserSchema, f as OrNode, g as GraphEdge, h as OrV1Schema, i as CategoryTarget, it as RelativePathSchema, j as Manifest, k as Lineage, l as getTarget, lt as WaybackTimestampSchema, m as OrV1, n as CategoryId, nt as Recipe, o as CountedKey, ot as SnapshotSchema, p as OrNodeSchema, q as HttpUrlSchema, r as CategoryIdSchema, rt as RecipeSchema, s as countTarget, st as Source, t as Category, tt as RESERVED_META_KEYS, u as kindOf, ut as parseEntry, v as GraphNode, w as CatalogueIndexSchema, x as GraphV1Schema, y as GraphNodeSchema, z as ArchiveSchema } from "./categories-tH53cHmu.js";
1
+ import { $ as Parser, A as LineageSchema, B as Citation, C as CatalogueIndex, D as NamespaceNode, E as DatasetQuery, F as AliasSchema, G as Entry, H as ColumnNameSchema, I as Archive, J as IdSchema, K as EntrySchema, L as ArchiveMember, M as ManifestSchema, N as SemVerSchema, O as createCatalogue, P as parseManifest, Q as NamespaceSchema, R as ArchiveMemberSchema, S as Catalogue, T as Dataset, U as CrosswalkReference, V as CitationSchema, W as CrosswalkReferenceSchema, X as LicenceSchema, Y as Licence, Z as Namespace, _ as GraphEdgeSchema, a as CategoryTargets, at as Snapshot, b as GraphV1, c as getCategory, ct as SourceSchema, d as listCategories, dt as waybackUrl, et as ParserSchema, f as OrNode, g as GraphEdge, h as OrV1Schema, i as CategoryTarget, it as RelativePathSchema, j as Manifest, k as Lineage, l as getTarget, lt as WaybackTimestampSchema, m as OrV1, n as CategoryId, nt as Recipe, o as CountedKey, ot as SnapshotSchema, p as OrNodeSchema, q as HttpUrlSchema, r as CategoryIdSchema, rt as RecipeSchema, s as countTarget, st as Source, t as Category, tt as RESERVED_META_KEYS, u as kindOf, ut as parseEntry, v as GraphNode, w as CatalogueIndexSchema, x as GraphV1Schema, y as GraphNodeSchema, z as ArchiveSchema } from "./categories-BVROO4tM.js";
2
2
  import { Crosswalk } from "twindex";
3
3
  import { z } from "zod";
4
4
  //#region src/errors.d.ts
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as NativeParseError, A as toIssues, B as kindOf, C as RESERVED_META_KEYS, D as SourceSchema, E as SnapshotSchema, G as GraphNodeSchema, H as OrNodeSchema, I as CategoryIdSchema, J as ArchiveError, K as GraphV1Schema, L as countTarget, M as NAME_SEPARATOR, N as groupsOf, O as WaybackTimestampSchema, P as namesOf, Q as KeepsetError, R as getCategory, S as ParserSchema, T as RelativePathSchema, U as OrV1Schema, V as listCategories, W as GraphEdgeSchema, X as DatasetAmbiguousError, Y as CatalogueSyntaxError, Z as DatasetNotFoundError, _ as EntrySchema, a as CatalogueIndexSchema, b as LicenceSchema, c as ManifestSchema, d as AliasSchema, et as RecipeError, f as ArchiveMemberSchema, g as CrosswalkReferenceSchema, h as ColumnNameSchema, i as listNamespaces, j as waybackUrl, k as parseEntry, l as SemVerSchema, m as CitationSchema, n as getManifest, o as createCatalogue, p as ArchiveSchema, q as JsonScalarSchema, r as listDatasets, s as LineageSchema, t as getDataset, tt as UnknownCategoryError, u as parseManifest, v as HttpUrlSchema, w as RecipeSchema, x as NamespaceSchema, y as IdSchema, z as getTarget } from "./bundled-8oHQPxcd.js";
1
+ import { $ as NativeParseError, A as toIssues, B as kindOf, C as RESERVED_META_KEYS, D as SourceSchema, E as SnapshotSchema, G as GraphNodeSchema, H as OrNodeSchema, I as CategoryIdSchema, J as ArchiveError, K as GraphV1Schema, L as countTarget, M as NAME_SEPARATOR, N as groupsOf, O as WaybackTimestampSchema, P as namesOf, Q as KeepsetError, R as getCategory, S as ParserSchema, T as RelativePathSchema, U as OrV1Schema, V as listCategories, W as GraphEdgeSchema, X as DatasetAmbiguousError, Y as CatalogueSyntaxError, Z as DatasetNotFoundError, _ as EntrySchema, a as CatalogueIndexSchema, b as LicenceSchema, c as ManifestSchema, d as AliasSchema, et as RecipeError, f as ArchiveMemberSchema, g as CrosswalkReferenceSchema, h as ColumnNameSchema, i as listNamespaces, j as waybackUrl, k as parseEntry, l as SemVerSchema, m as CitationSchema, n as getManifest, o as createCatalogue, p as ArchiveSchema, q as JsonScalarSchema, r as listDatasets, s as LineageSchema, t as getDataset, tt as UnknownCategoryError, u as parseManifest, v as HttpUrlSchema, w as RecipeSchema, x as NamespaceSchema, y as IdSchema, z as getTarget } from "./bundled-BmqpHjOH.js";
2
2
  import { validateCrosswalk } from "twindex";
3
3
  //#region src/serialise.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { G as Entry, T as Dataset, at as Snapshot, i as CategoryTarget, st as Source } from "../categories-tH53cHmu.js";
1
+ import { G as Entry, T as Dataset, at as Snapshot, i as CategoryTarget, st as Source } from "../categories-BVROO4tM.js";
2
2
  //#region src/node/fetch-dataset.d.ts
3
3
  /** The network port a recipe downloads through: `downloadRaw` by default, which returns the bytes exactly as served, or a stand-in under test. */
4
4
  export type FetchPort = (url: string, init: {
@@ -1,4 +1,4 @@
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-8oHQPxcd.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
3
  import { z } from "zod";
4
4
  import { createHash } from "node:crypto";
@@ -356,7 +356,7 @@ const DELIMITERS = {
356
356
  semicolon: ";"
357
357
  };
358
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. */
359
- const DECIMAL = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
359
+ const DECIMAL$1 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
360
360
  /** The byte order mark some exporters put before the first header. */
361
361
  const BYTE_ORDER_MARK = "";
362
362
  /**
@@ -559,7 +559,7 @@ function* withoutByteOrderMark(text) {
559
559
  */
560
560
  function numeric$1(cell, factor, column, line) {
561
561
  if (cell === "") return null;
562
- 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)}`);
563
563
  return Number(cell) * factor;
564
564
  }
565
565
  /**
@@ -933,7 +933,7 @@ const BYTE_RADIX = 256;
933
933
  * @param length - Field length.
934
934
  * @returns The field text.
935
935
  */
936
- function field(bytes, offset, length) {
936
+ function field$1(bytes, offset, length) {
937
937
  const slice = bytes.subarray(offset, offset + length);
938
938
  const end = slice.indexOf(0);
939
939
  return new TextDecoder().decode(end === -1 ? slice : slice.subarray(0, end));
@@ -952,7 +952,7 @@ function numeric(bytes, offset, length) {
952
952
  for (const byte of bytes.subarray(offset + 1, offset + length)) value = value * BYTE_RADIX + byte;
953
953
  return value;
954
954
  }
955
- const text = field(bytes, offset, length).trim();
955
+ const text = field$1(bytes, offset, length).trim();
956
956
  if (!/^[0-7]*$/.test(text)) throw new ArchiveError(`tar header field is not octal: ${JSON.stringify(text)}`);
957
957
  return text === "" ? 0 : Number.parseInt(text, 8);
958
958
  }
@@ -988,21 +988,21 @@ function readTar(bytes) {
988
988
  for (const [index, byte] of header.entries()) sum += index >= checksum.offset && index < checksum.offset + checksum.length ? 32 : byte;
989
989
  if (sum !== numeric(header, checksum.offset, checksum.length)) throw new ArchiveError(`tar header at byte ${String(offset)} fails its checksum`);
990
990
  const size = numeric(header, HEADER.size.offset, HEADER.size.length);
991
- const type = field(header, HEADER.type.offset, HEADER.type.length);
991
+ const type = field$1(header, HEADER.type.offset, HEADER.type.length);
992
992
  const dataStart = offset + 512;
993
993
  if (dataStart + size > bytes.length) throw new ArchiveError(`tar member at byte ${String(offset)} runs past the end of the archive`);
994
994
  const data = bytes.subarray(dataStart, dataStart + size);
995
995
  offset = dataStart + Math.ceil(size / 512) * 512;
996
996
  if (type === "L") {
997
- longName = field(data, 0, data.length);
997
+ longName = field$1(data, 0, data.length);
998
998
  continue;
999
999
  }
1000
1000
  if (type === "x") {
1001
1001
  longName = paxPath(data);
1002
1002
  continue;
1003
1003
  }
1004
- const name = field(header, HEADER.name.offset, HEADER.name.length);
1005
- 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) : "";
1006
1006
  const path = longName ?? (prefix === "" ? name : `${prefix}/${name}`);
1007
1007
  longName = void 0;
1008
1008
  if (type === "0" || type === "" || type === "7") members.push({
@@ -1013,6 +1013,255 @@ function readTar(bytes) {
1013
1013
  return members;
1014
1014
  }
1015
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
1016
1265
  //#region src/node/archive.ts
1017
1266
  /**
1018
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.
@@ -1059,6 +1308,10 @@ function pick(paths, member) {
1059
1308
  function unpack(bytes, archive) {
1060
1309
  if (archive.kind === "none") return { bytes };
1061
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
+ };
1062
1315
  if (archive.kind === "zip") {
1063
1316
  const names = [];
1064
1317
  unzip(bytes, (file) => {
@@ -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.19.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.19.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"}]},"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.19.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.19.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"}