mevebu 2.3.0 → 2.5.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,111 @@
1
+ // Compares two JSON documents by path, so a reformatted file does not look
2
+ // like a rewrite.
3
+ import { el, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
4
+
5
+ const EXAMPLE_OLD = '{\n "name": "mevebu",\n "tools": 220,\n "tags": ["offline", "browser"]\n}';
6
+ const EXAMPLE_NEW = '{\n "name": "mevebu",\n "tools": 225,\n "tags": ["offline", "browser", "free"],\n "license": "MIT"\n}';
7
+
8
+ const kindLabel = { added: "Added", removed: "Removed", changed: "Changed" };
9
+
10
+ function isObject(value) {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+
14
+ function walk(oldValue, newValue, path, output) {
15
+ if (Array.isArray(oldValue) && Array.isArray(newValue)) {
16
+ const length = Math.max(oldValue.length, newValue.length);
17
+ for (let index = 0; index < length; index++) {
18
+ const child = `${path}[${index}]`;
19
+ if (index >= oldValue.length) output.push({ kind: "added", path: child, after: newValue[index] });
20
+ else if (index >= newValue.length) output.push({ kind: "removed", path: child, before: oldValue[index] });
21
+ else walk(oldValue[index], newValue[index], child, output);
22
+ }
23
+ return;
24
+ }
25
+ if (isObject(oldValue) && isObject(newValue)) {
26
+ const keys = new Set([...Object.keys(oldValue), ...Object.keys(newValue)]);
27
+ for (const key of keys) {
28
+ const child = path ? `${path}.${key}` : key;
29
+ if (!(key in newValue)) output.push({ kind: "removed", path: child, before: oldValue[key] });
30
+ else if (!(key in oldValue)) output.push({ kind: "added", path: child, after: newValue[key] });
31
+ else walk(oldValue[key], newValue[key], child, output);
32
+ }
33
+ return;
34
+ }
35
+ if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
36
+ output.push({ kind: "changed", path, before: oldValue, after: newValue });
37
+ }
38
+ }
39
+
40
+ const show = (value) => {
41
+ if (value === undefined) return "";
42
+ const text = JSON.stringify(value);
43
+ return text.length > 80 ? `${text.slice(0, 77)}...` : text;
44
+ };
45
+
46
+ export function mount(slot) {
47
+ const before = textArea({ rows: 10, "aria-label": "Old JSON", value: EXAMPLE_OLD });
48
+ const after = textArea({ rows: 10, "aria-label": "New JSON", value: EXAMPLE_NEW });
49
+ const error = notice("error");
50
+ const list = el("div", { class: "grid gap-2" });
51
+ const summary = el("div", { class: "flex flex-wrap gap-2 text-2xs font-semibold" });
52
+ let rows = [];
53
+
54
+ function run() {
55
+ error.write("");
56
+ rows = [];
57
+ if (!before.value.trim() || !after.value.trim()) {
58
+ list.replaceChildren(el("p", { class: "text-sm text-ink-3" }, "Paste both documents."));
59
+ summary.replaceChildren();
60
+ return;
61
+ }
62
+ let oldValue;
63
+ let newValue;
64
+ try {
65
+ oldValue = JSON.parse(before.value);
66
+ newValue = JSON.parse(after.value);
67
+ } catch (caught) {
68
+ list.replaceChildren();
69
+ summary.replaceChildren();
70
+ error.write(caught.message);
71
+ return;
72
+ }
73
+ walk(oldValue, newValue, "$", rows);
74
+ const counts = rows.reduce((total, row) => ({ ...total, [row.kind]: (total[row.kind] ?? 0) + 1 }), {});
75
+ summary.replaceChildren(...["added", "removed", "changed"].map((kind) =>
76
+ el("span", { class: `rounded-full border px-2.5 py-1 ${kind === "added" ? "border-emerald-500/40 text-emerald-600 dark:text-emerald-400" : kind === "removed" ? "border-rose-500/40 text-rose-600 dark:text-rose-400" : "border-amber-500/40 text-amber-600 dark:text-amber-400"}` },
77
+ `${counts[kind] ?? 0} ${kind}`)));
78
+ if (!rows.length) {
79
+ list.replaceChildren(el("p", { class: "text-sm text-ink-2" }, "No differences."));
80
+ return;
81
+ }
82
+ list.replaceChildren(...rows.map((row) =>
83
+ el("div", { class: "grid gap-1 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3" },
84
+ el("div", { class: "flex flex-wrap items-baseline gap-2" },
85
+ el("span", { class: "rounded-full border border-orbit px-2 py-0.5 text-2xs font-semibold text-ink-2" }, kindLabel[row.kind]),
86
+ el("code", { class: "wrap-anywhere text-sm font-semibold" }, row.path)),
87
+ el("p", { class: "wrap-anywhere font-mono text-xs text-ink-2" },
88
+ row.kind === "added" ? show(row.after)
89
+ : row.kind === "removed" ? show(row.before)
90
+ : `${show(row.before)} -> ${show(row.after)}`))));
91
+ }
92
+
93
+ before.addEventListener("input", run);
94
+ after.addEventListener("input", run);
95
+
96
+ const report = () => rows.map((row) => `${kindLabel[row.kind]}: ${row.path}`).join("\n");
97
+
98
+ const card = toolcard({
99
+ body: bench(
100
+ stack(panel("Old", before), panel("New", after), error),
101
+ stack(panel("Changes", el("div", { class: "grid gap-2" }, summary, list))),
102
+ ),
103
+ foot: actions(copyButton(report, "Copy report", "fill")),
104
+ });
105
+ slot.append(card.node);
106
+ run();
107
+
108
+ return {};
109
+ }
110
+
111
+ export default { mount };
@@ -0,0 +1,215 @@
1
+ // Runs a documented JSONPath subset against a document. No library, no
2
+ // network: the parser and the evaluator live here.
3
+ import { el, bench, stack, panel, textArea, textInput, notice, copyButton, actions, toolcard } from "../core.js";
4
+
5
+ const EXAMPLE = '{\n "items": [\n { "name": "pen", "price": 2 },\n { "name": "lamp", "price": 18 },\n { "name": "desk", "price": 120 }\n ],\n "meta": { "currency": "USD" }\n}';
6
+
7
+ function tokenizePath(expr) {
8
+ if (!expr.startsWith("$")) throw new Error("Start with $.");
9
+ const steps = [];
10
+ let i = 1;
11
+ while (i < expr.length) {
12
+ const ch = expr[i];
13
+ if (ch === ".") {
14
+ if (expr[i + 1] === ".") {
15
+ let end = i + 2;
16
+ while (end < expr.length && /[\w$-]/.test(expr[end])) end++;
17
+ const name = expr.slice(i + 2, end);
18
+ if (!name) throw new Error("Expected a name after ..");
19
+ steps.push({ kind: "descend", name });
20
+ i = end;
21
+ } else {
22
+ let end = i + 1;
23
+ if (expr[end] === "*") {
24
+ steps.push({ kind: "wildcard" });
25
+ i = end + 1;
26
+ continue;
27
+ }
28
+ while (end < expr.length && /[\w$-]/.test(expr[end])) end++;
29
+ const name = expr.slice(i + 1, end);
30
+ if (!name) throw new Error("Expected a name after .");
31
+ steps.push({ kind: "child", name });
32
+ i = end;
33
+ }
34
+ continue;
35
+ }
36
+ if (ch === "[") {
37
+ let end = i + 1;
38
+ let quote = null;
39
+ while (end < expr.length) {
40
+ const c = expr[end];
41
+ if (quote) { if (c === quote) quote = null; }
42
+ else if (c === "'" || c === '"') quote = c;
43
+ else if (c === "]") break;
44
+ end++;
45
+ }
46
+ if (end >= expr.length) throw new Error("Expected ].");
47
+ const body = expr.slice(i + 1, end).trim();
48
+ if (body === "*") steps.push({ kind: "wildcard" });
49
+ else if (/^['"]/.test(body)) steps.push({ kind: "child", name: body.slice(1, -1) });
50
+ else if (body.startsWith("?(") && body.endsWith(")")) steps.push({ kind: "filter", test: body.slice(2, -1).trim() });
51
+ else if (/^-?\d+$/.test(body)) steps.push({ kind: "index", index: Number(body) });
52
+ else if (/^-?\d*:-?\d*$/.test(body)) {
53
+ const [from, to] = body.split(":");
54
+ steps.push({ kind: "slice", from: from === "" ? null : Number(from), to: to === "" ? null : Number(to) });
55
+ } else throw new Error(`Unknown selector [${body}].`);
56
+ i = end + 1;
57
+ continue;
58
+ }
59
+ throw new Error(`Unexpected ${ch}.`);
60
+ }
61
+ return steps;
62
+ }
63
+
64
+ function readField(value, path) {
65
+ const parts = path.split(".").filter(Boolean);
66
+ let current = value;
67
+ for (const part of parts) {
68
+ if (current === null || typeof current !== "object") return undefined;
69
+ current = current[part];
70
+ }
71
+ return current;
72
+ }
73
+
74
+ function makeTest(source) {
75
+ const match = source.match(/^@((?:\.[\w$-]+)+)\s*(?:(>=|<=|==|!=|>|<)\s*(.+))?$/);
76
+ if (!match) throw new Error("Filter must look like @.price > 10.");
77
+ const [, path, operator, raw] = match;
78
+ let expected;
79
+ if (operator) {
80
+ const text = raw.trim();
81
+ if (/^-?\d+(?:\.\d+)?$/.test(text)) expected = Number(text);
82
+ else if (text === "true" || text === "false") expected = text === "true";
83
+ else if (text === "null") expected = null;
84
+ else expected = text.replace(/^['"]|['"]$/g, "");
85
+ }
86
+ return (value) => {
87
+ const actual = readField(value, path.slice(1));
88
+ if (!operator) return Boolean(actual);
89
+ if (actual === undefined) return false;
90
+ if (operator === "==") return actual === expected;
91
+ if (operator === "!=") return actual !== expected;
92
+ if (operator === ">") return actual > expected;
93
+ if (operator === "<") return actual < expected;
94
+ if (operator === ">=") return actual >= expected;
95
+ return actual <= expected;
96
+ };
97
+ }
98
+
99
+ function select(step, entries) {
100
+ const output = [];
101
+ for (const entry of entries) {
102
+ const value = entry.value;
103
+ const path = entry.path;
104
+ if (step.kind === "child") {
105
+ if (value !== null && typeof value === "object" && step.name in value) {
106
+ output.push({ path: `${path}.${step.name}`, value: value[step.name] });
107
+ }
108
+ } else if (step.kind === "index") {
109
+ if (Array.isArray(value)) {
110
+ const index = step.index < 0 ? value.length + step.index : step.index;
111
+ if (index >= 0 && index < value.length) output.push({ path: `${path}[${index}]`, value: value[index] });
112
+ }
113
+ } else if (step.kind === "slice") {
114
+ if (Array.isArray(value)) {
115
+ const from = step.from ?? 0;
116
+ const to = step.to ?? value.length;
117
+ const start = from < 0 ? Math.max(value.length + from, 0) : from;
118
+ value.slice(from, to).forEach((item, offset) =>
119
+ output.push({ path: `${path}[${start + offset}]`, value: item }));
120
+ }
121
+ } else if (step.kind === "wildcard") {
122
+ if (Array.isArray(value)) value.forEach((item, index) => output.push({ path: `${path}[${index}]`, value: item }));
123
+ else if (value !== null && typeof value === "object") {
124
+ Object.entries(value).forEach(([key, item]) => output.push({ path: `${path}.${key}`, value: item }));
125
+ }
126
+ } else if (step.kind === "filter") {
127
+ const test = makeTest(step.test);
128
+ if (Array.isArray(value)) value.forEach((item, index) => {
129
+ if (item !== null && typeof item === "object" && test(item)) output.push({ path: `${path}[${index}]`, value: item });
130
+ });
131
+ } else if (step.kind === "descend") {
132
+ const visit = (node, at) => {
133
+ if (node === null || typeof node !== "object") return;
134
+ for (const [key, item] of Object.entries(node)) {
135
+ const childPath = Array.isArray(node) ? `${at}[${key}]` : `${at}.${key}`;
136
+ if (step.name === "*" || key === step.name) output.push({ path: childPath, value: item });
137
+ visit(item, childPath);
138
+ }
139
+ };
140
+ visit(value, path);
141
+ }
142
+ }
143
+ return output;
144
+ }
145
+
146
+ const show = (value) => {
147
+ const text = JSON.stringify(value) ?? String(value);
148
+ return text.length > 90 ? `${text.slice(0, 87)}...` : text;
149
+ };
150
+
151
+ export function mount(slot) {
152
+ const source = textArea({ rows: 13, "aria-label": "JSON", value: EXAMPLE });
153
+ const expression = textInput({ value: "$.items[?(@.price > 10)].name", "aria-label": "JSONPath", class: "font-mono" });
154
+ const error = notice("error");
155
+ const list = el("div", { class: "grid gap-2" });
156
+ const count = el("p", { class: "text-sm text-ink-3" });
157
+ let results = [];
158
+
159
+ function run() {
160
+ error.write("");
161
+ results = [];
162
+ if (!source.value.trim() || !expression.value.trim()) {
163
+ list.replaceChildren();
164
+ count.textContent = "";
165
+ return;
166
+ }
167
+ let document;
168
+ try {
169
+ document = JSON.parse(source.value);
170
+ } catch (caught) {
171
+ error.write(caught.message);
172
+ list.replaceChildren();
173
+ count.textContent = "";
174
+ return;
175
+ }
176
+ try {
177
+ let entries = [{ path: "$", value: document }];
178
+ for (const step of tokenizePath(expression.value.trim())) entries = select(step, entries);
179
+ results = entries;
180
+ } catch (caught) {
181
+ error.write(caught.message);
182
+ list.replaceChildren();
183
+ count.textContent = "";
184
+ return;
185
+ }
186
+ count.textContent = `${results.length} ${results.length === 1 ? "match" : "matches"}`;
187
+ if (!results.length) {
188
+ list.replaceChildren(el("p", { class: "text-sm text-ink-2" }, "No match."));
189
+ return;
190
+ }
191
+ list.replaceChildren(...results.map((row) =>
192
+ el("div", { class: "grid gap-1 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3" },
193
+ el("code", { class: "wrap-anywhere text-sm font-semibold" }, row.path),
194
+ el("p", { class: "wrap-anywhere font-mono text-xs text-ink-2" }, show(row.value)))));
195
+ }
196
+
197
+ source.addEventListener("input", run);
198
+ expression.addEventListener("input", run);
199
+
200
+ const report = () => results.map((row) => `${row.path}: ${JSON.stringify(row.value)}`).join("\n");
201
+
202
+ const card = toolcard({
203
+ body: bench(
204
+ stack(panel("JSON", source), panel("JSONPath", expression), error),
205
+ stack(panel("Matches", el("div", { class: "grid gap-2" }, count, list))),
206
+ ),
207
+ foot: actions(copyButton(report, "Copy matches", "fill")),
208
+ });
209
+ slot.append(card.node);
210
+ run();
211
+
212
+ return {};
213
+ }
214
+
215
+ export default { mount };
@@ -0,0 +1,59 @@
1
+ // Converts JSON to YAML with a vendored writer. Block style by default, flow
2
+ // style on request.
3
+ import { bench, stack, panel, textArea, output, notice, chipRow, downloadButton, copyButton, actions, toolcard } from "../core.js";
4
+ import { dump } from "../vendor/js-yaml.mjs";
5
+
6
+ const EXAMPLE = '{\n "service": "api",\n "replicas": 2,\n "ports": [8080, 8443],\n "labels": { "tier": "backend" }\n}';
7
+
8
+ export function mount(slot) {
9
+ const source = textArea({ rows: 14, "aria-label": "JSON", value: EXAMPLE });
10
+ const result = output({ tag: "textarea", rows: 12 });
11
+ const error = notice("error");
12
+ let style = "block";
13
+
14
+ const chips = chipRow(
15
+ [{ value: "block", label: "Block" }, { value: "flow", label: "Flow" }],
16
+ { value: "block", label: "Style", onChange: (value) => { style = value; run(); } },
17
+ );
18
+
19
+ function run() {
20
+ error.write("");
21
+ if (!source.value.trim()) {
22
+ result.value = "";
23
+ return;
24
+ }
25
+ let value;
26
+ try {
27
+ value = JSON.parse(source.value);
28
+ } catch (caught) {
29
+ result.value = "";
30
+ error.write(caught.message);
31
+ return;
32
+ }
33
+ result.value = dump(value, {
34
+ indent: 2,
35
+ lineWidth: -1,
36
+ noRefs: true,
37
+ flowLevel: style === "flow" ? 0 : -1,
38
+ });
39
+ }
40
+
41
+ source.addEventListener("input", run);
42
+
43
+ const card = toolcard({
44
+ body: bench(
45
+ stack(panel("Style", chips), panel("JSON", source), error),
46
+ stack(panel("YAML", result)),
47
+ ),
48
+ foot: actions(
49
+ downloadButton(() => result.value, "data.yaml", "Download", "text/yaml"),
50
+ copyButton(() => result.value, "Copy YAML", "fill"),
51
+ ),
52
+ });
53
+ slot.append(card.node);
54
+ run();
55
+
56
+ return {};
57
+ }
58
+
59
+ export default { mount };
@@ -0,0 +1,100 @@
1
+ // A searchable reference for JavaScript regular expressions. Each row shows
2
+ // the token, what it matches, and an example with the match highlighted.
3
+ import { el, bench, stack, panel, textInput, toolcard } from "../core.js";
4
+
5
+ const ROWS = [
6
+ { token: ".", meaning: "Any character except a newline.", pattern: "a.c", example: "abc adc a\nc" },
7
+ { token: "\\d", meaning: "A digit, 0 to 9.", pattern: "\\d+", example: "Order 42 shipped" },
8
+ { token: "\\D", meaning: "Anything that is not a digit.", pattern: "\\D+", example: "42 and 7" },
9
+ { token: "\\w", meaning: "A letter, digit or underscore.", pattern: "\\w+", example: "hello_world!" },
10
+ { token: "\\W", meaning: "Anything that is not a word character.", pattern: "\\W+", example: "a-b c" },
11
+ { token: "\\s", meaning: "A space, tab or line break.", pattern: "\\s", example: "a b\tc" },
12
+ { token: "\\S", meaning: "Anything that is not whitespace.", pattern: "\\S+", example: " keep this" },
13
+ { token: "^", meaning: "The start of the string, or of a line with m.", pattern: "^abc", example: "abc abc" },
14
+ { token: "$", meaning: "The end of the string, or of a line with m.", pattern: "abc$", example: "abc abc" },
15
+ { token: "*", meaning: "Zero or more of the previous token.", pattern: "ab*", example: "a ab abb" },
16
+ { token: "+", meaning: "One or more of the previous token.", pattern: "ab+", example: "a ab abb" },
17
+ { token: "?", meaning: "Zero or one of the previous token.", pattern: "colou?r", example: "color colour" },
18
+ { token: "{n,m}", meaning: "Between n and m repeats.", pattern: "\\d{2,3}", example: "5 42 1234" },
19
+ { token: "[abc]", meaning: "One character from the set.", pattern: "[aeiou]", example: "mevebu" },
20
+ { token: "[^abc]", meaning: "One character outside the set.", pattern: "[^aeiou]", example: "mevebu" },
21
+ { token: "[a-z]", meaning: "One character from a range.", pattern: "[a-c]+", example: "abcxyz" },
22
+ { token: "(...)", meaning: "A capturing group.", pattern: "(\\d{4})-(\\d{2})", example: "2026-10-20" },
23
+ { token: "(?:...)", meaning: "A group that is not captured.", pattern: "(?:cat|dog)s", example: "cats dogs" },
24
+ { token: "(?<name>...)", meaning: "A named capturing group.", pattern: "(?<year>\\d{4})", example: "in 2026" },
25
+ { token: "\\b", meaning: "A word boundary.", pattern: "\\bcat\\b", example: "cat scatter cat" },
26
+ { token: "|", meaning: "Either the left or the right side.", pattern: "cat|dog", example: "cat dog cow" },
27
+ { token: "\\.", meaning: "A literal dot, escaped.", pattern: "\\d+\\.\\d+", example: "3.14 and 42" },
28
+ { token: "(?=...)", meaning: "Lookahead: what comes next.", pattern: "\\d+(?= USD)", example: "10 USD 20 EUR" },
29
+ { token: "(?<=...)", meaning: "Lookbehind: what came before.", pattern: "(?<=\\$)\\d+", example: "$5 and €7" },
30
+ { token: "*?", meaning: "Lazy: match as little as possible.", pattern: "<.+?>", example: "<a><b>" },
31
+ { token: "i", meaning: "Ignore case. A flag, not a token.", pattern: "[a-z]+", flags: "gi", example: "AbC abc" },
32
+ { token: "g", meaning: "Find every match, not just the first.", pattern: "\\d+", flags: "g", example: "1 22 333" },
33
+ { token: "m", meaning: "Make ^ and $ work per line.", pattern: "^\\w+", flags: "gm", example: "one\ntwo" },
34
+ { token: "s", meaning: "Let . match a newline too.", pattern: "a.b", flags: "s", example: "a\nb" },
35
+ { token: "u", meaning: "Unicode mode, for full code points.", pattern: "\\u{1F600}", flags: "u", example: "hi \u{1F600}" },
36
+ ];
37
+
38
+ function highlight(example, pattern, flags = "") {
39
+ let regex;
40
+ try {
41
+ regex = new RegExp(pattern, flags.includes("g") ? flags : `${flags}g`);
42
+ } catch {
43
+ return [document.createTextNode(example)];
44
+ }
45
+ const nodes = [];
46
+ let last = 0;
47
+ for (const match of example.matchAll(regex)) {
48
+ if (match.index > last) nodes.push(document.createTextNode(example.slice(last, match.index)));
49
+ if (!match[0]) break;
50
+ nodes.push(el("mark", { class: "rounded-xs bg-amber-300/60 px-0.5 text-inherit dark:bg-amber-400/40" }, match[0]));
51
+ last = match.index + match[0].length;
52
+ }
53
+ if (last < example.length) nodes.push(document.createTextNode(example.slice(last)));
54
+ return nodes;
55
+ }
56
+
57
+ export function mount(slot) {
58
+ const search = textInput({ placeholder: "Filter tokens", "aria-label": "Filter" });
59
+ const list = el("div", { class: "grid gap-2" });
60
+
61
+ function render() {
62
+ const query = search.value.trim().toLowerCase();
63
+ const rows = ROWS.filter((row) =>
64
+ !query || row.token.toLowerCase().includes(query) || row.meaning.toLowerCase().includes(query));
65
+ if (!rows.length) {
66
+ list.replaceChildren(el("p", { class: "text-sm text-ink-2" }, "No token matches that."));
67
+ return;
68
+ }
69
+ list.replaceChildren(...rows.map((row) =>
70
+ el("div", { class: "grid gap-1.5 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3" },
71
+ el("div", { class: "flex flex-wrap items-baseline gap-2" },
72
+ el("code", { class: "rounded-xs border border-orbit bg-deck px-1.5 py-0.5 font-mono text-sm font-semibold" }, row.token),
73
+ el("span", { class: "text-sm text-ink-2" }, row.meaning)),
74
+ el("p", { class: "font-mono text-sm" },
75
+ ...highlight(row.example, row.pattern, row.flags)),
76
+ el("div", { class: "flex flex-wrap items-center gap-2 text-xs text-ink-3" },
77
+ el("code", { class: "font-mono" }, `/${row.pattern}/${row.flags ?? ""}`),
78
+ el("a", { href: "#/developer/regex-tester", class: "underline decoration-dotted underline-offset-2 hover:text-ink-2" }, "Try it")))));
79
+ }
80
+
81
+ search.addEventListener("input", render);
82
+
83
+ const link = el("p", { class: "text-sm text-ink-3" },
84
+ "Matches are JavaScript flavoured. Open ",
85
+ el("a", { href: "#/developer/regex-tester", class: "underline decoration-dotted underline-offset-2 hover:text-ink-2" }, "Regex Tester"),
86
+ " to run a full pattern.");
87
+
88
+ const card = toolcard({
89
+ body: bench(
90
+ stack(panel("Find a token", search), link),
91
+ stack(panel("Tokens", list)),
92
+ ),
93
+ });
94
+ slot.append(card.node);
95
+ render();
96
+
97
+ return {};
98
+ }
99
+
100
+ export default { mount };
@@ -0,0 +1,103 @@
1
+ // Compacts XML by dropping indentation between tags. CDATA, comments,
2
+ // processing instructions and attribute values are copied unchanged.
3
+ import { el, bench, stack, panel, textArea, output, notice, copyButton, actions, toolcard } from "../core.js";
4
+ import { bytes } from "../js-tokens.js";
5
+
6
+ const EXAMPLE = '<?xml version="1.0"?>\n<config>\n <server host="localhost" port="8080">\n <name>api</name>\n </server>\n <note><![CDATA[keep this spacing]]></note>\n</config>';
7
+
8
+ const tile = (key, value) =>
9
+ el("div", { class: "grid gap-0.5 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3" },
10
+ el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, value),
11
+ el("span", { class: "text-2xs font-semibold text-ink-3" }, key));
12
+
13
+ function minify(source) {
14
+ let out = "";
15
+ let i = 0;
16
+
17
+ while (i < source.length) {
18
+ if (source.startsWith("<![CDATA[", i)) {
19
+ const end = source.indexOf("]]>", i + 9);
20
+ if (end === -1) return { error: "Unclosed CDATA section." };
21
+ out += source.slice(i, end + 3);
22
+ i = end + 3;
23
+ continue;
24
+ }
25
+ if (source.startsWith("<!--", i)) {
26
+ const end = source.indexOf("-->", i + 4);
27
+ if (end === -1) return { error: "Unclosed comment." };
28
+ out += source.slice(i, end + 3);
29
+ i = end + 3;
30
+ continue;
31
+ }
32
+ if (source[i] === "<") {
33
+ let j = i + 1;
34
+ let quote = null;
35
+ while (j < source.length) {
36
+ const ch = source[j];
37
+ if (quote) {
38
+ if (ch === quote) quote = null;
39
+ } else if (ch === '"' || ch === "'") {
40
+ quote = ch;
41
+ } else if (ch === ">") {
42
+ break;
43
+ }
44
+ j++;
45
+ }
46
+ if (j >= source.length) return { error: "Unclosed tag." };
47
+ out += source.slice(i, j + 1);
48
+ i = j + 1;
49
+ continue;
50
+ }
51
+
52
+ let j = source.indexOf("<", i);
53
+ if (j === -1) j = source.length;
54
+ const text = source.slice(i, j);
55
+ if (!/^\s+$/.test(text) || !text.includes("\n")) out += text;
56
+ i = j;
57
+ }
58
+
59
+ return { text: out };
60
+ }
61
+
62
+ export function mount(slot) {
63
+ const source = textArea({ rows: 14, "aria-label": "XML", value: EXAMPLE });
64
+ const result = output({ tag: "textarea", rows: 12 });
65
+ const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" });
66
+ const error = notice("error");
67
+
68
+ function run() {
69
+ error.write("");
70
+ stats.replaceChildren();
71
+ if (!source.value.trim()) {
72
+ result.value = "";
73
+ return;
74
+ }
75
+ const output = minify(source.value);
76
+ if (output.error) {
77
+ result.value = "";
78
+ error.write(output.error);
79
+ return;
80
+ }
81
+ const before = bytes(source.value);
82
+ const after = bytes(output.text);
83
+ const saved = before ? Math.max(0, Math.round((1 - after / before) * 100)) : 0;
84
+ result.value = output.text;
85
+ stats.append(tile("Before", `${before} B`), tile("After", `${after} B`), tile("Saved", `${saved}%`));
86
+ }
87
+
88
+ source.addEventListener("input", run);
89
+
90
+ const card = toolcard({
91
+ body: bench(
92
+ stack(panel("XML", source), error),
93
+ stack(panel("Minified", result), stats),
94
+ ),
95
+ foot: actions(copyButton(() => result.value, "Copy XML", "fill")),
96
+ });
97
+ slot.append(card.node);
98
+ run();
99
+
100
+ return {};
101
+ }
102
+
103
+ export default { mount };
@@ -0,0 +1,46 @@
1
+ // Converts YAML to JSON with a vendored parser. Anchors are resolved and the
2
+ // key order is kept.
3
+ import { bench, stack, panel, textArea, output, notice, downloadButton, copyButton, actions, toolcard } from "../core.js";
4
+ import { load } from "../vendor/js-yaml.mjs";
5
+
6
+ const EXAMPLE = "service: api\nreplicas: 2\nports:\n - 8080\n - 8443\nlabels:\n tier: backend";
7
+
8
+ export function mount(slot) {
9
+ const source = textArea({ rows: 14, "aria-label": "YAML", value: EXAMPLE });
10
+ const result = output({ tag: "textarea", rows: 12 });
11
+ const error = notice("error");
12
+ function run() {
13
+ error.write("");
14
+ if (!source.value.trim()) {
15
+ result.value = "";
16
+ return;
17
+ }
18
+ try {
19
+ result.value = JSON.stringify(load(source.value), null, 2);
20
+ } catch (caught) {
21
+ result.value = "";
22
+ const line = caught.mark?.line;
23
+ const where = typeof line === "number" ? `Line ${line + 1}: ` : "";
24
+ error.write(`${where}${caught.reason ?? caught.message}`);
25
+ }
26
+ }
27
+
28
+ source.addEventListener("input", run);
29
+
30
+ const card = toolcard({
31
+ body: bench(
32
+ stack(panel("YAML", source), error),
33
+ stack(panel("JSON", result)),
34
+ ),
35
+ foot: actions(
36
+ downloadButton(() => result.value, "data.json", "Download", "application/json"),
37
+ copyButton(() => result.value, "Copy JSON", "fill"),
38
+ ),
39
+ });
40
+ slot.append(card.node);
41
+ run();
42
+
43
+ return {};
44
+ }
45
+
46
+ export default { mount };
@@ -0,0 +1,21 @@
1
+ (The MIT License)
2
+
3
+ Copyright (C) 2011-2015 by Vitaly Puzrin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.