mevebu 2.0.0 → 2.2.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.
@@ -0,0 +1,181 @@
1
+ // Split a CSV into train, validation and test sets. Seeded, so the same seed
2
+ // and the same file always give the same split, and a row never lands in two
3
+ // sets.
4
+ import {
5
+ el, toast, download, bench, stack, panel, numberInput, textInput, selectInput,
6
+ button, actions, toolcard, blank, notice,
7
+ } from "../core.js";
8
+ import { dropzone, baseName, formatBytes } from "../files.js";
9
+ import { parseDelimited, detectDelimiter, headerKeys, toCsv } from "../csv.js";
10
+ import { seeded, shuffle } from "../random.js";
11
+ import { zip } from "../zip.js";
12
+
13
+ export function mount(slot) {
14
+ let file = null;
15
+ let header = [];
16
+ let rows = [];
17
+ let split = null;
18
+
19
+ const train = numberInput({ min: "0", max: "100", step: "5", value: "70", "aria-label": "Train percent" });
20
+ const validation = numberInput({ min: "0", max: "100", step: "5", value: "15", "aria-label": "Validation percent" });
21
+ const seed = textInput({ value: "mevebu", "aria-label": "Seed", class: "font-mono" });
22
+ const stratify = selectInput([{ value: "", label: "Do not stratify" }], { value: "", "aria-label": "Stratify by" });
23
+
24
+ const error = notice("warn");
25
+ const summary = el("p", { class: "text-sm text-ink-2" });
26
+ const tiles = el("div", { class: "grid grid-cols-3 gap-2" });
27
+ const empty = blank("table", "Drop a CSV to split it.");
28
+
29
+ const drop = dropzone({ label: "Drop a CSV here", accept: ".csv,text/csv,text/plain", iconName: "table" });
30
+ const clearButton = button("Clear", { icon: "trash", kind: "quiet" });
31
+ const splitButton = button("Split and download", { icon: "download", kind: "fill" });
32
+
33
+ function tile(label) {
34
+ const value = el("span", { class: "text-xl font-bold tracking-[-0.02em] tabular-nums" }, "0");
35
+ const rows = el("span", { class: "text-2xs text-ink-3" }, "");
36
+ const node = el("div", { class: "grid gap-0.5 rounded-r1 border border-orbit bg-deck-2 px-3 py-2.5" },
37
+ el("span", { class: "text-2xs font-semibold tracking-[0.06em] text-ink-3 uppercase" }, label),
38
+ value, rows);
39
+ node.set = (count, percent) => {
40
+ value.textContent = String(count);
41
+ rows.textContent = `${percent}% of rows`;
42
+ };
43
+ return node;
44
+ }
45
+
46
+ const trainTile = tile("Train");
47
+ const validationTile = tile("Validation");
48
+ const testTile = tile("Test");
49
+ tiles.append(trainTile, validationTile, testTile);
50
+
51
+ function weights() {
52
+ const a = Math.max(0, Number(train.value) || 0);
53
+ const b = Math.max(0, Number(validation.value) || 0);
54
+ const c = Math.max(0, 100 - a - b);
55
+ return { train: a, validation: b, test: c };
56
+ }
57
+
58
+ function compute() {
59
+ const w = weights();
60
+ const random = seeded(seed.value || "mevebu");
61
+ const key = stratify.value;
62
+ const keyIndex = key ? header.indexOf(key) : -1;
63
+
64
+ const buckets = new Map();
65
+ for (const row of rows) {
66
+ const group = keyIndex >= 0 ? row[keyIndex] ?? "" : "__all__";
67
+ if (!buckets.has(group)) buckets.set(group, []);
68
+ buckets.get(group).push(row);
69
+ }
70
+
71
+ const out = { train: [], validation: [], test: [] };
72
+ for (const group of buckets.values()) {
73
+ const list = shuffle(group, random);
74
+ const n = list.length;
75
+ const trainEnd = Math.round((w.train / 100) * n);
76
+ const validationEnd = trainEnd + Math.round((w.validation / 100) * n);
77
+ out.train.push(...list.slice(0, trainEnd));
78
+ out.validation.push(...list.slice(trainEnd, validationEnd));
79
+ out.test.push(...list.slice(validationEnd));
80
+ }
81
+
82
+ // Shuffle the final sets so a stratified split is not grouped by class.
83
+ for (const name of Object.keys(out)) out[name] = shuffle(out[name], random);
84
+ return out;
85
+ }
86
+
87
+ function render() {
88
+ empty.hidden = rows.length > 0;
89
+ tiles.hidden = rows.length === 0;
90
+ splitButton.disabled = rows.length === 0;
91
+ clearButton.disabled = rows.length === 0;
92
+ if (!rows.length) {
93
+ summary.textContent = "";
94
+ return;
95
+ }
96
+ const w = weights();
97
+ split = compute();
98
+ const total = rows.length;
99
+ trainTile.set(split.train.length, Math.round((split.train.length / total) * 100));
100
+ validationTile.set(split.validation.length, Math.round((split.validation.length / total) * 100));
101
+ testTile.set(split.test.length, Math.round((split.test.length / total) * 100));
102
+ summary.textContent = `${total} rows · ${header.length} columns · seed ${seed.value || "mevebu"}${w.test < 0 ? " · the percentages add to more than 100, so test is empty" : ""}`;
103
+ if (w.test < 0) error.write("Train and validation add to more than 100 percent. Test gets nothing.");
104
+ else error.write("");
105
+ }
106
+
107
+ async function open(files) {
108
+ const picked = Array.from(files).find((entry) => /\.(csv|tsv|txt)$/i.test(entry.name) || entry.type.startsWith("text/"));
109
+ if (!picked) {
110
+ error.write("That file is not a CSV.");
111
+ return;
112
+ }
113
+ try {
114
+ const text = await picked.text();
115
+ const parsed = parseDelimited(text, detectDelimiter(text));
116
+ if (parsed.length < 2) throw new Error("A CSV needs a header row and at least one data row.");
117
+ file = picked;
118
+ header = headerKeys(parsed[0]);
119
+ rows = parsed.slice(1).filter((row) => row.some((cell) => cell !== ""));
120
+ stratify.replaceChildren(
121
+ el("option", { value: "" }, "Do not stratify"),
122
+ ...header.map((name) => el("option", { value: name }, name))
123
+ );
124
+ stratify.value = "";
125
+ error.write("");
126
+ render();
127
+ } catch (caught) {
128
+ error.write(caught?.message || "This file could not be read.");
129
+ }
130
+ }
131
+
132
+ drop.onFiles(open);
133
+ train.addEventListener("input", render);
134
+ validation.addEventListener("input", render);
135
+ seed.addEventListener("input", render);
136
+ stratify.addEventListener("change", render);
137
+
138
+ splitButton.addEventListener("click", () => {
139
+ if (!split) return;
140
+ const base = baseName(file?.name || "data");
141
+ const files = ["train", "validation", "test"].map((name) => ({
142
+ name: `${base}-${name}.csv`,
143
+ bytes: new TextEncoder().encode(toCsv([header, ...split[name]])),
144
+ }));
145
+ download(`${base}-split.zip`, zip(files), "application/zip");
146
+ toast("Three files ready");
147
+ });
148
+
149
+ clearButton.addEventListener("click", () => {
150
+ file = null;
151
+ header = [];
152
+ rows = [];
153
+ split = null;
154
+ error.write("");
155
+ render();
156
+ });
157
+
158
+ render();
159
+
160
+ const shell = toolcard({
161
+ body: bench(
162
+ stack(
163
+ drop,
164
+ el("div", { class: "grid grid-cols-2 gap-3" }, panel("Train %", train), panel("Validation %", validation)),
165
+ panel("Seed", seed, "The same seed and file give the same split."),
166
+ panel("Stratify by", stratify, "Keeps each class proportional across the three sets."),
167
+ error
168
+ ),
169
+ stack(summary, empty, tiles,
170
+ el("p", { class: "m-0 text-2xs leading-[1.6] text-ink-3" },
171
+ "Every row goes to exactly one set. The test share is whatever is left after train and validation."))
172
+ ),
173
+ foot: actions(clearButton, splitButton),
174
+ });
175
+
176
+ slot.append(shell.node);
177
+
178
+ return {};
179
+ }
180
+
181
+ export default { mount };
@@ -0,0 +1,154 @@
1
+ // The hash of a file, to compare against a published checksum. The file is read
2
+ // here with Web Crypto, so a download can be verified before it is opened.
3
+ import {
4
+ el, icon, toast, bench, stack, panel, textInput, button, actions, toolcard,
5
+ blank, notice, copyButton, output,
6
+ } from "../core.js";
7
+ import { dropzone, formatBytes } from "../files.js";
8
+
9
+ const ALGORITHMS = [
10
+ { id: "SHA-1", label: "SHA-1" },
11
+ { id: "SHA-256", label: "SHA-256" },
12
+ { id: "SHA-512", label: "SHA-512" },
13
+ ];
14
+
15
+ function hex(bytes) {
16
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
17
+ }
18
+
19
+ export function mount(slot) {
20
+ let file = null;
21
+ let digests = {};
22
+
23
+ const expected = textInput({
24
+ placeholder: "Paste the published checksum",
25
+ "aria-label": "Expected checksum",
26
+ class: "font-mono text-sm",
27
+ autocomplete: "off",
28
+ spellcheck: "false",
29
+ });
30
+
31
+ const error = notice("warn");
32
+ const summary = el("p", { class: "text-sm text-ink-2" });
33
+ const verdictTitle = el("p", { class: "m-0 text-lg font-bold" }, "Waiting");
34
+ const verdictBody = el("p", { class: "m-0 text-sm text-ink-2" }, "Drop a file, then paste the checksum you were given.");
35
+ const verdict = el("div", { class: "grid gap-1 rounded-r2 border border-orbit bg-deck-2 p-4" }, verdictTitle, verdictBody);
36
+ const rows = el("div", { class: "grid gap-2" });
37
+ const empty = blank("file-key", "Drop a file to hash it.");
38
+
39
+ const drop = dropzone({ label: "Drop a file here", accept: "", iconName: "file-key" });
40
+ const clearButton = button("Clear", { icon: "trash", kind: "quiet" });
41
+
42
+ function paint(state, title, body) {
43
+ const tone = state === "ok"
44
+ ? "border-jade bg-jade-soft"
45
+ : state === "bad"
46
+ ? "border-danger/40 bg-danger/10"
47
+ : "border-orbit bg-deck-2";
48
+ verdict.className = `grid gap-1 rounded-r2 border p-4 ${tone}`;
49
+ verdictTitle.textContent = title;
50
+ verdictTitle.className = `m-0 text-lg font-bold ${state === "ok" ? "text-jade" : state === "bad" ? "text-danger" : ""}`;
51
+ verdictBody.textContent = body;
52
+ verdictBody.className = state === "ok" || state === "bad" ? "m-0 text-sm" : "m-0 text-sm text-ink-2";
53
+ }
54
+
55
+ function compare() {
56
+ if (!file) {
57
+ paint("idle", "Waiting", "Drop a file, then paste the checksum you were given.");
58
+ return;
59
+ }
60
+ const wanted = expected.value.replace(/\s+/g, "").toLowerCase();
61
+ if (!wanted) {
62
+ paint("idle", "Ready", "Paste a checksum to compare, or copy one below.");
63
+ return;
64
+ }
65
+ const match = Object.entries(digests).find(([, value]) => value === wanted);
66
+ if (match) {
67
+ paint("ok", "Match", `The file matches the ${match[0]} you pasted.`);
68
+ } else {
69
+ const length = wanted.length;
70
+ const sameLength = Object.entries(digests).find(([, value]) => value.length === length);
71
+ paint(
72
+ "bad",
73
+ "No match",
74
+ sameLength
75
+ ? `That is not the ${sameLength[0]} of this file. Either the file or the checksum is wrong.`
76
+ : `A checksum of ${length} characters is not a SHA-1, SHA-256 or SHA-512 digest.`
77
+ );
78
+ }
79
+ }
80
+
81
+ function renderRows() {
82
+ rows.replaceChildren(
83
+ ...ALGORITHMS.map((algorithm) =>
84
+ el("div", { class: "grid gap-1.5 rounded-r1 border border-orbit bg-deck-2 p-2.5" },
85
+ el("div", { class: "flex items-center justify-between gap-2" },
86
+ el("span", { class: "text-2xs font-bold tracking-[0.06em] text-ink-3 uppercase" }, algorithm.label),
87
+ copyButton(() => digests[algorithm.id] ?? "", "Copy")),
88
+ el("p", { class: "m-0 wrap-anywhere font-mono text-2xs leading-[1.6] text-ink" }, digests[algorithm.id] ?? "")))
89
+ );
90
+ }
91
+
92
+ async function open(files) {
93
+ const picked = Array.from(files)[0];
94
+ if (!picked) return;
95
+ error.write("");
96
+ summary.textContent = `Reading ${picked.name}`;
97
+ try {
98
+ const bytes = await picked.arrayBuffer();
99
+ const next = {};
100
+ for (const algorithm of ALGORITHMS) {
101
+ const digest = await crypto.subtle.digest(algorithm.id, bytes);
102
+ next[algorithm.id] = hex(new Uint8Array(digest));
103
+ }
104
+ file = picked;
105
+ digests = next;
106
+ summary.textContent = `${picked.name} · ${formatBytes(picked.size)}`;
107
+ empty.hidden = true;
108
+ rows.hidden = false;
109
+ renderRows();
110
+ compare();
111
+ } catch (caught) {
112
+ summary.textContent = "";
113
+ error.write(caught?.message || "This file could not be read.");
114
+ }
115
+ }
116
+
117
+ drop.onFiles(open);
118
+ expected.addEventListener("input", compare);
119
+ clearButton.addEventListener("click", () => {
120
+ file = null;
121
+ digests = {};
122
+ expected.value = "";
123
+ summary.textContent = "";
124
+ error.write("");
125
+ rows.replaceChildren();
126
+ rows.hidden = true;
127
+ empty.hidden = false;
128
+ paint("idle", "Waiting", "Drop a file, then paste the checksum you were given.");
129
+ });
130
+
131
+ paint("idle", "Waiting", "Drop a file, then paste the checksum you were given.");
132
+ rows.hidden = true;
133
+
134
+ const shell = toolcard({
135
+ body: bench(
136
+ stack(
137
+ drop,
138
+ panel("Expected checksum", expected, "SHA-1, SHA-256 or SHA-512. Case and spaces are ignored."),
139
+ verdict,
140
+ error,
141
+ el("p", { class: "m-0 text-2xs leading-[1.6] text-ink-3" },
142
+ "The file is read in this tab. Nothing is uploaded, and the size limit is whatever the browser can hold in memory.")
143
+ ),
144
+ stack(summary, empty, rows)
145
+ ),
146
+ foot: actions(clearButton),
147
+ });
148
+
149
+ slot.append(shell.node);
150
+
151
+ return {};
152
+ }
153
+
154
+ export default { mount };
@@ -0,0 +1,254 @@
1
+ // Sample documents from a JSON Schema. Covers the keywords that decide whether
2
+ // a document validates, and reports the ones it could not honour, so a passing
3
+ // sample is never mistaken for a complete implementation.
4
+ import {
5
+ el, toast, download, bench, stack, panel, textArea, numberInput, textInput,
6
+ button, actions, toolcard, blank, notice, output,
7
+ } from "../core.js";
8
+ import { seeded, pick, uuid, gaussian } from "../random.js";
9
+ import { zip } from "../zip.js";
10
+
11
+ const SUPPORTED = new Set([
12
+ "$schema", "$id", "title", "description", "type", "properties", "required",
13
+ "items", "enum", "const", "default", "examples", "minimum", "maximum",
14
+ "exclusiveMinimum", "exclusiveMaximum", "multipleOf", "minLength", "maxLength",
15
+ "pattern", "format", "minItems", "maxItems", "uniqueItems", "additionalProperties",
16
+ "minProperties", "maxProperties", "anyOf", "oneOf", "allOf", "not", "nullable",
17
+ ]);
18
+
19
+ const FORMATS = {
20
+ "date-time": () => new Date(Date.UTC(2025, 0, 1) + Math.floor(Math.random() * 3e10)).toISOString(),
21
+ date: (random) => new Date(Date.UTC(2025, 0, 1) + Math.floor(random() * 700) * 86400000).toISOString().slice(0, 10),
22
+ time: () => `${String(Math.floor(Math.random() * 24)).padStart(2, "0")}:00:00Z`,
23
+ email: (random) => `${pick(random, ["ani", "budi", "citra", "dewi"])}${Math.floor(random() * 99)}@example.com`,
24
+ uri: () => "https://example.com/resource",
25
+ uuid: (random) => uuid(random),
26
+ ipv4: () => `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
27
+ hostname: () => "example.com",
28
+ };
29
+
30
+ function regexFrom(pattern) {
31
+ try {
32
+ return new RegExp(pattern);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function stringValue(schema, random) {
39
+ if (schema.format && FORMATS[schema.format]) return FORMATS[schema.format](random);
40
+ if (schema.enum) return pick(random, schema.enum);
41
+ if (schema.const !== undefined) return schema.const;
42
+
43
+ const min = Math.max(0, schema.minLength ?? 0);
44
+ const max = Math.max(min, schema.maxLength ?? Math.max(min, 12));
45
+ const regex = schema.pattern ? regexFrom(schema.pattern) : null;
46
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
47
+
48
+ // Try a few random strings against the pattern before falling back, so a
49
+ // pattern like ^[A-Z]{3}$ still gets a plausible value.
50
+ for (let attempt = 0; attempt < 60; attempt++) {
51
+ const length = min + Math.floor(random() * Math.max(1, Math.min(max, 24) - min + 1));
52
+ let out = "";
53
+ for (let i = 0; i < length; i++) out += alphabet[Math.floor(random() * alphabet.length)];
54
+ if (!regex || regex.test(out)) return out;
55
+ }
56
+ const literal = schema.pattern?.replace(/[\^$.*+?()[\]{}|\\]/g, "") ?? "";
57
+ if (literal.length >= min) return literal.slice(0, Math.max(min, Math.min(max, literal.length)));
58
+ return "sample".padEnd(min, "x").slice(0, Math.max(min, 1));
59
+ }
60
+
61
+ function numberValue(schema, random, integer) {
62
+ if (schema.enum) return pick(random, schema.enum);
63
+ if (schema.const !== undefined) return schema.const;
64
+ const min = schema.minimum ?? schema.exclusiveMinimum ?? 0;
65
+ const max = schema.maximum ?? schema.exclusiveMaximum ?? (min + 1000);
66
+ let value = min + random() * (max - min);
67
+ if (integer) value = Math.round(value);
68
+ if (schema.multipleOf) value = Math.round(value / schema.multipleOf) * schema.multipleOf;
69
+ return value;
70
+ }
71
+
72
+ function sample(schema, random, path, unknown) {
73
+ if (!schema || typeof schema !== "object") return null;
74
+
75
+ if (schema.const !== undefined) return schema.const;
76
+ if (schema.enum) return pick(random, schema.enum);
77
+ if (schema.default !== undefined) return schema.default;
78
+ if (schema.examples?.length) return schema.examples[0];
79
+ if (schema.anyOf?.length) return sample(schema.anyOf[0], random, path, unknown);
80
+ if (schema.oneOf?.length) return sample(schema.oneOf[0], random, path, unknown);
81
+ if (schema.allOf?.length) {
82
+ // Merge the object branches, which is the common shape.
83
+ const merged = schema.allOf.reduce(
84
+ (acc, branch) => ({
85
+ ...acc,
86
+ ...branch,
87
+ properties: { ...(acc.properties ?? {}), ...(branch.properties ?? {}) },
88
+ required: [...new Set([...(acc.required ?? []), ...(branch.required ?? [])])],
89
+ }),
90
+ { ...schema, allOf: undefined }
91
+ );
92
+ return sample(merged, random, path, unknown);
93
+ }
94
+
95
+ const type = Array.isArray(schema.type)
96
+ ? schema.type.find((entry) => entry !== "null") ?? "null"
97
+ : schema.type ?? (schema.properties ? "object" : schema.items ? "array" : "string");
98
+
99
+ if (schema.nullable && random() < 0.1) return null;
100
+
101
+ switch (type) {
102
+ case "object": {
103
+ const out = {};
104
+ const properties = schema.properties ?? {};
105
+ for (const [key, child] of Object.entries(properties)) {
106
+ out[key] = sample(child, random, `${path}.${key}`, unknown);
107
+ }
108
+ for (const key of schema.required ?? []) {
109
+ if (!(key in out)) out[key] = sample({}, random, `${path}.${key}`, unknown);
110
+ }
111
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
112
+ out.extra = sample(schema.additionalProperties, random, `${path}.extra`, unknown);
113
+ }
114
+ return out;
115
+ }
116
+ case "array": {
117
+ const min = Math.max(0, schema.minItems ?? 1);
118
+ const max = Math.max(min, schema.maxItems ?? min + 2);
119
+ const count = min + Math.floor(random() * (Math.min(max, 8) - min + 1));
120
+ const items = Array.from({ length: count }, (_, index) =>
121
+ sample(schema.items ?? {}, random, `${path}[${index}]`, unknown)
122
+ );
123
+ return schema.uniqueItems ? [...new Set(items.map((item) => JSON.stringify(item)))].map((text) => JSON.parse(text)) : items;
124
+ }
125
+ case "integer":
126
+ return numberValue(schema, random, true);
127
+ case "number":
128
+ return Number(numberValue(schema, random, false).toFixed(3));
129
+ case "boolean":
130
+ return random() > 0.5;
131
+ case "null":
132
+ return null;
133
+ default:
134
+ return stringValue(schema, random);
135
+ }
136
+ }
137
+
138
+ function collectUnknown(schema, found, path = "$") {
139
+ if (!schema || typeof schema !== "object") return;
140
+ for (const [key, value] of Object.entries(schema)) {
141
+ if (["properties", "items", "anyOf", "oneOf", "allOf"].includes(key)) continue;
142
+ if (key.startsWith("$") || key.startsWith("x-")) continue;
143
+ if (!SUPPORTED.has(key) && !found.includes(key)) found.push(key);
144
+ }
145
+ for (const [name, child] of Object.entries(schema.properties ?? {})) {
146
+ collectUnknown(child, found, `${path}.${name}`);
147
+ }
148
+ if (schema.items && typeof schema.items === "object") collectUnknown(schema.items, found, `${path}[]`);
149
+ }
150
+
151
+ export function mount(slot) {
152
+ let documents = [];
153
+ let unknown = [];
154
+
155
+ const schema = textArea({
156
+ rows: 12,
157
+ value: JSON.stringify(
158
+ {
159
+ $schema: "https://json-schema.org/draft/2020-12/schema",
160
+ type: "object",
161
+ required: ["id", "name", "email"],
162
+ properties: {
163
+ id: { type: "string", format: "uuid" },
164
+ name: { type: "string", minLength: 3, maxLength: 24 },
165
+ email: { type: "string", format: "email" },
166
+ age: { type: "integer", minimum: 18, maximum: 90 },
167
+ tags: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 4 },
168
+ },
169
+ },
170
+ null,
171
+ 2
172
+ ),
173
+ "aria-label": "JSON Schema",
174
+ });
175
+
176
+ const count = numberInput({ min: "1", max: "200", step: "1", value: "3", "aria-label": "Documents" });
177
+ const seed = textInput({ value: "mevebu", "aria-label": "Seed", class: "font-mono" });
178
+ const error = notice("warn");
179
+ const summary = el("p", { class: "text-sm text-ink-2" });
180
+ const result = output({ rows: 16 });
181
+ const empty = blank("braces", "Paste a schema to generate documents.");
182
+
183
+ const generate = button("Generate", { icon: "refresh-cw", kind: "fill" });
184
+ const downloadJson = button("Download JSON", { icon: "download" });
185
+ const downloadZip = button("Download ZIP", { icon: "download" });
186
+
187
+ function run() {
188
+ error.write("");
189
+ try {
190
+ const parsed = JSON.parse(schema.value);
191
+ const random = seeded(seed.value || "mevebu");
192
+ unknown = [];
193
+ collectUnknown(parsed, unknown);
194
+ const total = Math.max(1, Math.min(200, Number(count.value) || 1));
195
+ documents = Array.from({ length: total }, () => sample(parsed, random, "$", unknown));
196
+ result.write(JSON.stringify(documents, null, 2));
197
+ summary.textContent = `${total} document${total === 1 ? "" : "s"} · seed ${seed.value || "mevebu"}`;
198
+ empty.hidden = true;
199
+ result.hidden = false;
200
+ if (unknown.length) {
201
+ error.write(`Not implemented, so left out: ${unknown.join(", ")}. Everything else follows the schema.`);
202
+ }
203
+ } catch (caught) {
204
+ documents = [];
205
+ result.write("");
206
+ result.hidden = true;
207
+ empty.hidden = false;
208
+ summary.textContent = "";
209
+ error.write(caught?.message || "That schema is not valid JSON.");
210
+ }
211
+ }
212
+
213
+ generate.addEventListener("click", () => {
214
+ run();
215
+ if (documents.length) toast("Documents ready");
216
+ });
217
+ schema.addEventListener("input", run);
218
+ count.addEventListener("input", run);
219
+ seed.addEventListener("input", run);
220
+
221
+ downloadJson.addEventListener("click", () => {
222
+ if (!documents.length) return;
223
+ download("samples.json", JSON.stringify(documents, null, 2), "application/json");
224
+ });
225
+ downloadZip.addEventListener("click", () => {
226
+ if (!documents.length) return;
227
+ const files = documents.map((doc, index) => ({
228
+ name: `sample-${String(index + 1).padStart(3, "0")}.json`,
229
+ bytes: new TextEncoder().encode(JSON.stringify(doc, null, 2)),
230
+ }));
231
+ download("samples.zip", zip(files), "application/zip");
232
+ });
233
+
234
+ run();
235
+
236
+ const shell = toolcard({
237
+ body: bench(
238
+ stack(
239
+ panel("Schema", schema),
240
+ panel("Documents", count),
241
+ panel("Seed", seed, "The same seed and schema give the same documents."),
242
+ error
243
+ ),
244
+ stack(summary, empty, result)
245
+ ),
246
+ foot: actions(downloadJson, downloadZip, generate),
247
+ });
248
+
249
+ slot.append(shell.node);
250
+
251
+ return {};
252
+ }
253
+
254
+ export default { mount };