mevebu 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -8
- package/mevebu/app/mevebu.js +3 -3
- package/mevebu/libs/css/tools.css +103 -1
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/icons.js +115 -1
- package/mevebu/libs/js/registry.js +3102 -0
- package/mevebu/libs/js/tools/abstract-checklist.js +54 -0
- package/mevebu/libs/js/tools/add-subtract-days.js +105 -0
- package/mevebu/libs/js/tools/age-calculator.js +108 -0
- package/mevebu/libs/js/tools/apa-reference.js +98 -0
- package/mevebu/libs/js/tools/api-request-builder.js +76 -0
- package/mevebu/libs/js/tools/area-converter.js +87 -0
- package/mevebu/libs/js/tools/aspect-ratio.js +81 -0
- package/mevebu/libs/js/tools/average-calculator.js +83 -0
- package/mevebu/libs/js/tools/bibliography-sorter.js +43 -0
- package/mevebu/libs/js/tools/bibtex-generator.js +98 -0
- package/mevebu/libs/js/tools/binary-calculator.js +58 -0
- package/mevebu/libs/js/tools/border-radius.js +88 -0
- package/mevebu/libs/js/tools/box-shadow.js +71 -0
- package/mevebu/libs/js/tools/branch-name.js +44 -0
- package/mevebu/libs/js/tools/break-even.js +68 -0
- package/mevebu/libs/js/tools/business-days.js +120 -0
- package/mevebu/libs/js/tools/canonical-tag.js +63 -0
- package/mevebu/libs/js/tools/caption-counter.js +66 -0
- package/mevebu/libs/js/tools/case-inspector.js +114 -0
- package/mevebu/libs/js/tools/cash-denomination.js +74 -0
- package/mevebu/libs/js/tools/cash-flow.js +124 -0
- package/mevebu/libs/js/tools/cert-expiry.js +83 -0
- package/mevebu/libs/js/tools/changelog-generator.js +48 -0
- package/mevebu/libs/js/tools/character-counter.js +80 -0
- package/mevebu/libs/js/tools/checklist.js +86 -0
- package/mevebu/libs/js/tools/citation-metadata.js +89 -0
- package/mevebu/libs/js/tools/citation-number-check.js +107 -0
- package/mevebu/libs/js/tools/color-palette.js +110 -0
- package/mevebu/libs/js/tools/color-picker.js +93 -0
- package/mevebu/libs/js/tools/commit-message.js +62 -0
- package/mevebu/libs/js/tools/content-calendar.js +111 -0
- package/mevebu/libs/js/tools/contrast-checker.js +93 -0
- package/mevebu/libs/js/tools/countdown-timer.js +174 -0
- package/mevebu/libs/js/tools/cron-builder.js +91 -0
- package/mevebu/libs/js/tools/css-clamp.js +66 -0
- package/mevebu/libs/js/tools/css-formatter.js +84 -0
- package/mevebu/libs/js/tools/css-grid.js +75 -0
- package/mevebu/libs/js/tools/css-minifier.js +98 -0
- package/mevebu/libs/js/tools/csv-to-json.js +114 -0
- package/mevebu/libs/js/tools/csv-viewer.js +132 -0
- package/mevebu/libs/js/tools/currency-format.js +69 -0
- package/mevebu/libs/js/tools/daily-planner.js +124 -0
- package/mevebu/libs/js/tools/data-masker.js +71 -0
- package/mevebu/libs/js/tools/data-storage.js +89 -0
- package/mevebu/libs/js/tools/data-validator.js +111 -0
- package/mevebu/libs/js/tools/date-difference.js +100 -0
- package/mevebu/libs/js/tools/date-format.js +107 -0
- package/mevebu/libs/js/tools/delimiter-converter.js +109 -0
- package/mevebu/libs/js/tools/discount-calculator.js +83 -0
- package/mevebu/libs/js/tools/doi-resolver.js +67 -0
- package/mevebu/libs/js/tools/env-formatter.js +72 -0
- package/mevebu/libs/js/tools/equation-formatter.js +71 -0
- package/mevebu/libs/js/tools/expense-split.js +90 -0
- package/mevebu/libs/js/tools/favicon-markup.js +53 -0
- package/mevebu/libs/js/tools/find-replace.js +88 -0
- package/mevebu/libs/js/tools/flashcard-creator.js +117 -0
- package/mevebu/libs/js/tools/font-pairing.js +88 -0
- package/mevebu/libs/js/tools/fraction-calculator.js +130 -0
- package/mevebu/libs/js/tools/git-command-builder.js +100 -0
- package/mevebu/libs/js/tools/gitignore-generator.js +60 -0
- package/mevebu/libs/js/tools/gpa-calculator.js +100 -0
- package/mevebu/libs/js/tools/grade-calculator.js +101 -0
- package/mevebu/libs/js/tools/gradient-generator.js +99 -0
- package/mevebu/libs/js/tools/habit-tracker.js +120 -0
- package/mevebu/libs/js/tools/harvard-citation.js +90 -0
- package/mevebu/libs/js/tools/hashtag-formatter.js +62 -0
- package/mevebu/libs/js/tools/heading-formatter.js +61 -0
- package/mevebu/libs/js/tools/hmac-generator.js +63 -0
- package/mevebu/libs/js/tools/hpp-calculator.js +70 -0
- package/mevebu/libs/js/tools/html-entity.js +49 -0
- package/mevebu/libs/js/tools/html-formatter.js +72 -0
- package/mevebu/libs/js/tools/html-to-text.js +47 -0
- package/mevebu/libs/js/tools/http-status.js +63 -0
- package/mevebu/libs/js/tools/ieee-reference.js +102 -0
- package/mevebu/libs/js/tools/intext-citation.js +75 -0
- package/mevebu/libs/js/tools/inventory-quantity.js +103 -0
- package/mevebu/libs/js/tools/invoice-number.js +64 -0
- package/mevebu/libs/js/tools/json-escape.js +76 -0
- package/mevebu/libs/js/tools/json-formatter.js +51 -0
- package/mevebu/libs/js/tools/json-minifier.js +58 -0
- package/mevebu/libs/js/tools/json-to-csv.js +72 -0
- package/mevebu/libs/js/tools/json-to-typescript.js +102 -0
- package/mevebu/libs/js/tools/json-validator.js +49 -0
- package/mevebu/libs/js/tools/jwt-decoder.js +74 -0
- package/mevebu/libs/js/tools/jwt-expiry.js +97 -0
- package/mevebu/libs/js/tools/length-converter.js +86 -0
- package/mevebu/libs/js/tools/license-reference.js +58 -0
- package/mevebu/libs/js/tools/line-break-formatter.js +48 -0
- package/mevebu/libs/js/tools/line-counter.js +55 -0
- package/mevebu/libs/js/tools/list-difference.js +86 -0
- package/mevebu/libs/js/tools/list-merger.js +67 -0
- package/mevebu/libs/js/tools/locale-number.js +79 -0
- package/mevebu/libs/js/tools/markdown-preview.js +143 -0
- package/mevebu/libs/js/tools/markdown-table.js +111 -0
- package/mevebu/libs/js/tools/markdown-to-text.js +48 -0
- package/mevebu/libs/js/tools/markup-calculator.js +69 -0
- package/mevebu/libs/js/tools/meeting-agenda.js +137 -0
- package/mevebu/libs/js/tools/meta-preview.js +51 -0
- package/mevebu/libs/js/tools/meta-tags.js +62 -0
- package/mevebu/libs/js/tools/mime-types.js +54 -0
- package/mevebu/libs/js/tools/mla-citation.js +91 -0
- package/mevebu/libs/js/tools/number-base-converter.js +97 -0
- package/mevebu/libs/js/tools/open-graph.js +62 -0
- package/mevebu/libs/js/tools/password-strength.js +118 -0
- package/mevebu/libs/js/tools/percent-to-fraction.js +96 -0
- package/mevebu/libs/js/tools/percentage-calculator.js +86 -0
- package/mevebu/libs/js/tools/percentage-change.js +70 -0
- package/mevebu/libs/js/tools/permutation-combination.js +106 -0
- package/mevebu/libs/js/tools/prefix-suffix.js +46 -0
- package/mevebu/libs/js/tools/priority-matrix.js +100 -0
- package/mevebu/libs/js/tools/profit-margin.js +69 -0
- package/mevebu/libs/js/tools/quadratic-solver.js +93 -0
- package/mevebu/libs/js/tools/query-builder.js +71 -0
- package/mevebu/libs/js/tools/quiz-formatter.js +70 -0
- package/mevebu/libs/js/tools/quotation-formatter.js +77 -0
- package/mevebu/libs/js/tools/ratio-calculator.js +103 -0
- package/mevebu/libs/js/tools/readability.js +112 -0
- package/mevebu/libs/js/tools/reading-time.js +76 -0
- package/mevebu/libs/js/tools/readme-generator.js +50 -0
- package/mevebu/libs/js/tools/reference-completeness.js +62 -0
- package/mevebu/libs/js/tools/regex-tester.js +86 -0
- package/mevebu/libs/js/tools/remove-duplicate-lines.js +46 -0
- package/mevebu/libs/js/tools/remove-extra-spaces.js +41 -0
- package/mevebu/libs/js/tools/robots-txt.js +60 -0
- package/mevebu/libs/js/tools/roman-numeral-converter.js +106 -0
- package/mevebu/libs/js/tools/sales-commission.js +96 -0
- package/mevebu/libs/js/tools/scientific-calculator.js +247 -0
- package/mevebu/libs/js/tools/scientific-notation.js +83 -0
- package/mevebu/libs/js/tools/script-planner.js +72 -0
- package/mevebu/libs/js/tools/secure-token.js +60 -0
- package/mevebu/libs/js/tools/selling-price.js +74 -0
- package/mevebu/libs/js/tools/semver-helper.js +54 -0
- package/mevebu/libs/js/tools/sentence-length.js +80 -0
- package/mevebu/libs/js/tools/sequence-analyzer.js +90 -0
- package/mevebu/libs/js/tools/sitemap-xml.js +82 -0
- package/mevebu/libs/js/tools/slug-converter.js +48 -0
- package/mevebu/libs/js/tools/slug-preview.js +83 -0
- package/mevebu/libs/js/tools/sort-lines.js +54 -0
- package/mevebu/libs/js/tools/speed-converter.js +83 -0
- package/mevebu/libs/js/tools/sql-formatter.js +78 -0
- package/mevebu/libs/js/tools/sql-minifier.js +102 -0
- package/mevebu/libs/js/tools/standard-deviation.js +96 -0
- package/mevebu/libs/js/tools/stopwatch.js +133 -0
- package/mevebu/libs/js/tools/study-schedule.js +88 -0
- package/mevebu/libs/js/tools/subtitle-formatter.js +74 -0
- package/mevebu/libs/js/tools/table-to-markdown.js +137 -0
- package/mevebu/libs/js/tools/tax-calculator.js +84 -0
- package/mevebu/libs/js/tools/temperature-converter.js +94 -0
- package/mevebu/libs/js/tools/text-diff.js +104 -0
- package/mevebu/libs/js/tools/text-direction.js +55 -0
- package/mevebu/libs/js/tools/text-reverser.js +51 -0
- package/mevebu/libs/js/tools/time-duration.js +83 -0
- package/mevebu/libs/js/tools/time-format-converter.js +86 -0
- package/mevebu/libs/js/tools/timestamp-converter.js +85 -0
- package/mevebu/libs/js/tools/timezone-converter.js +163 -0
- package/mevebu/libs/js/tools/title-length.js +67 -0
- package/mevebu/libs/js/tools/unicode-escape.js +80 -0
- package/mevebu/libs/js/tools/unicode-inspector.js +58 -0
- package/mevebu/libs/js/tools/url-inspector.js +98 -0
- package/mevebu/libs/js/tools/url-parser.js +83 -0
- package/mevebu/libs/js/tools/utf8-bytes.js +67 -0
- package/mevebu/libs/js/tools/version-compare.js +70 -0
- package/mevebu/libs/js/tools/video-duration.js +72 -0
- package/mevebu/libs/js/tools/volume-converter.js +88 -0
- package/mevebu/libs/js/tools/weekly-planner.js +101 -0
- package/mevebu/libs/js/tools/weight-converter.js +85 -0
- package/mevebu/libs/js/tools/weighted-average.js +86 -0
- package/mevebu/libs/js/tools/word-counter.js +90 -0
- package/mevebu/libs/js/tools/word-limit.js +60 -0
- package/mevebu/libs/js/tools/xml-formatter.js +85 -0
- package/mevebu/libs/js/tools/yaml-formatter.js +71 -0
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Closing stock per item from opening stock, stock in and stock out.
|
|
2
|
+
import { el, toast, blank, bench, stack, row, panel, textInput, numberInput, button, copyButton, notice, actions, toolcard, readPin, writePin } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
5
|
+
|
|
6
|
+
function fmt(n) {
|
|
7
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function mount(slot) {
|
|
11
|
+
const PIN = "inventory-quantity";
|
|
12
|
+
const saved = readPin(PIN, []);
|
|
13
|
+
let list = Array.isArray(saved) ? saved : [];
|
|
14
|
+
|
|
15
|
+
const name = textInput({ placeholder: "Item", "aria-label": "Item" });
|
|
16
|
+
const opening = numberInput({ value: "0", min: "0", "aria-label": "Opening stock" });
|
|
17
|
+
const stockIn = numberInput({ value: "0", min: "0", "aria-label": "Stock in" });
|
|
18
|
+
const stockOut = numberInput({ value: "0", min: "0", "aria-label": "Stock out" });
|
|
19
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
20
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
21
|
+
const warn = notice("warn");
|
|
22
|
+
|
|
23
|
+
const addButton = button("Add", { icon: "plus", kind: "fill" });
|
|
24
|
+
|
|
25
|
+
const save = () => writePin(PIN, list);
|
|
26
|
+
const tile = (key, value) => el("div", { class: "mvb-stat" },
|
|
27
|
+
el("span", { class: "mvb-stat__v" }, String(value)),
|
|
28
|
+
el("span", { class: "mvb-stat__k" }, key));
|
|
29
|
+
|
|
30
|
+
const closing = (item) => item.opening + item.in - item.out;
|
|
31
|
+
|
|
32
|
+
function removeButton(item) {
|
|
33
|
+
const node = button("", { icon: "trash", "aria-label": `Remove ${item.name}` });
|
|
34
|
+
node.addEventListener("click", () => {
|
|
35
|
+
list = list.filter((entry) => entry.id !== item.id);
|
|
36
|
+
save();
|
|
37
|
+
render();
|
|
38
|
+
});
|
|
39
|
+
return node;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function render() {
|
|
43
|
+
if (!list.length) {
|
|
44
|
+
listWrap.replaceChildren(blank("package", "No items yet."));
|
|
45
|
+
} else {
|
|
46
|
+
listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
47
|
+
el("thead", {}, el("tr", {},
|
|
48
|
+
el("th", {}, "Item"), el("th", {}, "Opening"), el("th", {}, "In"),
|
|
49
|
+
el("th", {}, "Out"), el("th", {}, "Closing"), el("th", {}, ""))),
|
|
50
|
+
el("tbody", {}, ...list.map((item) => el("tr", {},
|
|
51
|
+
el("th", {}, item.name),
|
|
52
|
+
el("td", {}, fmt(item.opening)),
|
|
53
|
+
el("td", {}, fmt(item.in)),
|
|
54
|
+
el("td", {}, fmt(item.out)),
|
|
55
|
+
el("td", {}, fmt(closing(item))),
|
|
56
|
+
el("td", {}, removeButton(item)))))));
|
|
57
|
+
}
|
|
58
|
+
const negative = list.filter((item) => closing(item) < 0).length;
|
|
59
|
+
const totalClosing = list.reduce((sum, item) => sum + closing(item), 0);
|
|
60
|
+
stats.replaceChildren(tile("Items", String(list.length)), tile("Closing", fmt(totalClosing)));
|
|
61
|
+
warn.write(negative ? `${negative} ${negative === 1 ? "item closes" : "items close"} below zero.` : "");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function add() {
|
|
65
|
+
const text = name.value.trim();
|
|
66
|
+
if (!text) return toast("Name the item first");
|
|
67
|
+
const o = Number(opening.value);
|
|
68
|
+
const i = Number(stockIn.value);
|
|
69
|
+
const out = Number(stockOut.value);
|
|
70
|
+
if ([opening, stockIn, stockOut].some((input) => input.value.trim() === "") || ![o, i, out].every(Number.isFinite)) {
|
|
71
|
+
return toast("Add the opening, in and out amounts");
|
|
72
|
+
}
|
|
73
|
+
if (o < 0 || i < 0 || out < 0) return toast("The amounts cannot be negative");
|
|
74
|
+
list.push({ id: uid(), name: text, opening: o, in: i, out });
|
|
75
|
+
name.value = "";
|
|
76
|
+
opening.value = "0";
|
|
77
|
+
stockIn.value = "0";
|
|
78
|
+
stockOut.value = "0";
|
|
79
|
+
name.focus();
|
|
80
|
+
save();
|
|
81
|
+
render();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
name.addEventListener("keydown", (event) => { if (event.key === "Enter") add(); });
|
|
85
|
+
addButton.addEventListener("click", add);
|
|
86
|
+
|
|
87
|
+
const card = toolcard({
|
|
88
|
+
body: bench(
|
|
89
|
+
stack(panel("Item", name), row(panel("Opening", opening), panel("In", stockIn), panel("Out", stockOut)), warn),
|
|
90
|
+
stack(stats, panel("Stock", listWrap))
|
|
91
|
+
),
|
|
92
|
+
foot: actions(
|
|
93
|
+
copyButton(() => list.map((item) => `${item.name}: opening ${fmt(item.opening)}, in ${fmt(item.in)}, out ${fmt(item.out)}, closing ${fmt(closing(item))}`).join("\n"), "Copy stock"),
|
|
94
|
+
addButton
|
|
95
|
+
),
|
|
96
|
+
});
|
|
97
|
+
slot.append(card.node);
|
|
98
|
+
render();
|
|
99
|
+
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default { mount };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Builds invoice numbers from a prefix, a date and a padded counter.
|
|
2
|
+
import { bench, stack, row, panel, textInput, numberInput, switchControl, notice, output, copyButton, downloadButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function today() {
|
|
5
|
+
const now = new Date();
|
|
6
|
+
return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function mount(slot) {
|
|
10
|
+
const prefix = textInput({ value: "INV-", "aria-label": "Prefix" });
|
|
11
|
+
const includeDate = switchControl("Include date", { checked: true });
|
|
12
|
+
const digits = numberInput({ value: "4", min: "1", max: "10", "aria-label": "Counter digits" });
|
|
13
|
+
const start = numberInput({ value: "1", min: "0", "aria-label": "Starting number" });
|
|
14
|
+
const count = numberInput({ value: "10", min: "1", max: "500", "aria-label": "How many" });
|
|
15
|
+
const result = output({ rows: 12 });
|
|
16
|
+
const error = notice("error");
|
|
17
|
+
let text = "";
|
|
18
|
+
|
|
19
|
+
function run() {
|
|
20
|
+
error.write("");
|
|
21
|
+
const d = parseInt(digits.value, 10);
|
|
22
|
+
const s = parseInt(start.value, 10);
|
|
23
|
+
const n = parseInt(count.value, 10);
|
|
24
|
+
if ([digits, start, count].some((input) => input.value.trim() === "") || ![d, s, n].every(Number.isFinite)) {
|
|
25
|
+
result.write("");
|
|
26
|
+
text = "";
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (d < 1 || d > 10) { result.write(""); text = ""; error.write("Use between 1 and 10 counter digits."); return; }
|
|
30
|
+
if (s < 0) { result.write(""); text = ""; error.write("The starting number cannot be negative."); return; }
|
|
31
|
+
const total = Math.max(1, Math.min(500, n));
|
|
32
|
+
const date = includeDate.input.checked ? today() : "";
|
|
33
|
+
const lines = Array.from({ length: total }, (_, i) =>
|
|
34
|
+
`${prefix.value}${date}${String(s + i).padStart(d, "0")}`);
|
|
35
|
+
text = lines.join("\n");
|
|
36
|
+
result.write(text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const input of [prefix, digits, start, count]) input.addEventListener("input", run);
|
|
40
|
+
includeDate.input.addEventListener("change", run);
|
|
41
|
+
|
|
42
|
+
const card = toolcard({
|
|
43
|
+
body: bench(
|
|
44
|
+
stack(
|
|
45
|
+
panel("Prefix", prefix),
|
|
46
|
+
panel("Counter digits", digits),
|
|
47
|
+
row(panel("Start at", start), panel("How many", count)),
|
|
48
|
+
includeDate,
|
|
49
|
+
error
|
|
50
|
+
),
|
|
51
|
+
stack(panel("Numbers", result))
|
|
52
|
+
),
|
|
53
|
+
foot: actions(
|
|
54
|
+
downloadButton(() => text, "invoice-numbers.txt"),
|
|
55
|
+
copyButton(() => text, "Copy numbers", "fill")
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
slot.append(card.node);
|
|
59
|
+
run();
|
|
60
|
+
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default { mount };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Escapes text for a JSON string and unescapes it back.
|
|
2
|
+
import { bench, stack, panel, textArea, output, notice, chipRow, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function escapeJson(text) {
|
|
5
|
+
return JSON.stringify(text).slice(1, -1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function unescapeJson(text) {
|
|
9
|
+
if (/\\(u(?![0-9a-fA-F]{4})|[^"\\/bfnrtu]|$)/.test(text)) return { ok: false, text: "" };
|
|
10
|
+
const out = text.replace(/\\(u[0-9a-fA-F]{4}|["\\/bfnrt])/g, (match, seq) => {
|
|
11
|
+
switch (seq) {
|
|
12
|
+
case "n": return "\n";
|
|
13
|
+
case "t": return "\t";
|
|
14
|
+
case "r": return "\r";
|
|
15
|
+
case "b": return "\b";
|
|
16
|
+
case "f": return "\f";
|
|
17
|
+
case "/": return "/";
|
|
18
|
+
case '"': return '"';
|
|
19
|
+
case "\\": return "\\";
|
|
20
|
+
default: return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return { ok: true, text: out };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mount(slot) {
|
|
27
|
+
const source = textArea({ rows: 7, value: 'He said "hi".', "aria-label": "Input" });
|
|
28
|
+
const result = output({ value: "", rows: 7 });
|
|
29
|
+
const error = notice("error");
|
|
30
|
+
let mode = "escape";
|
|
31
|
+
let text = "";
|
|
32
|
+
|
|
33
|
+
const chips = chipRow(
|
|
34
|
+
[{ value: "escape", label: "Escape" }, { value: "unescape", label: "Unescape" }],
|
|
35
|
+
{ value: "escape", label: "Direction", onChange: (value) => { mode = value; run(); } }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function run() {
|
|
39
|
+
error.write("");
|
|
40
|
+
if (!source.value) {
|
|
41
|
+
text = "";
|
|
42
|
+
result.write("");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (mode === "escape") {
|
|
46
|
+
text = escapeJson(source.value);
|
|
47
|
+
result.write(text);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const decoded = unescapeJson(source.value);
|
|
51
|
+
if (!decoded.ok) {
|
|
52
|
+
text = "";
|
|
53
|
+
result.write("");
|
|
54
|
+
error.write("That is not a valid escape sequence.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
text = decoded.text;
|
|
58
|
+
result.write(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
source.addEventListener("input", run);
|
|
62
|
+
|
|
63
|
+
const card = toolcard({
|
|
64
|
+
body: bench(
|
|
65
|
+
stack(panel("Direction", chips), panel("Input", source)),
|
|
66
|
+
stack(panel("Result", result), error)
|
|
67
|
+
),
|
|
68
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
69
|
+
});
|
|
70
|
+
slot.append(card.node);
|
|
71
|
+
run();
|
|
72
|
+
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default { mount };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Pretty-prints or compacts JSON and points at the error when it is invalid.
|
|
2
|
+
import { bench, stack, panel, textArea, notice, chipRow, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
export function mount(slot) {
|
|
5
|
+
const source = textArea({
|
|
6
|
+
rows: 12,
|
|
7
|
+
class: "mvb-in mvb-in--area mvb-in--mono",
|
|
8
|
+
"aria-label": "JSON",
|
|
9
|
+
placeholder: "Paste JSON",
|
|
10
|
+
value: '{"hello":"world","count":3,"items":[1,2,3]}',
|
|
11
|
+
});
|
|
12
|
+
const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
|
|
13
|
+
const error = notice("error");
|
|
14
|
+
let indent = "2";
|
|
15
|
+
|
|
16
|
+
const chips = chipRow(
|
|
17
|
+
[{ value: "2", label: "2 spaces" }, { value: "4", label: "4 spaces" }, { value: "0", label: "Compact" }],
|
|
18
|
+
{ value: "2", label: "Indent", onChange: (value) => { indent = value; run(); } }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function run() {
|
|
22
|
+
error.write("");
|
|
23
|
+
if (!source.value.trim()) {
|
|
24
|
+
result.value = "";
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(source.value);
|
|
29
|
+
result.value = indent === "0" ? JSON.stringify(data) : JSON.stringify(data, null, Number(indent));
|
|
30
|
+
} catch (caught) {
|
|
31
|
+
result.value = "";
|
|
32
|
+
error.write(caught.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
source.addEventListener("input", run);
|
|
37
|
+
|
|
38
|
+
const card = toolcard({
|
|
39
|
+
body: bench(
|
|
40
|
+
stack(panel("Indent", chips), panel("JSON", source), error),
|
|
41
|
+
stack(panel("Result", result))
|
|
42
|
+
),
|
|
43
|
+
foot: actions(copyButton(() => result.value, "Copy result", "fill")),
|
|
44
|
+
});
|
|
45
|
+
slot.append(card.node);
|
|
46
|
+
run();
|
|
47
|
+
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default { mount };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Compacts valid JSON and reports the size saved.
|
|
2
|
+
import { el, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const bytes = (text) => new TextEncoder().encode(text).length;
|
|
5
|
+
|
|
6
|
+
const tile = (key, value) =>
|
|
7
|
+
el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__v" }, value), el("span", { class: "mvb-stat__k" }, key));
|
|
8
|
+
|
|
9
|
+
export function mount(slot) {
|
|
10
|
+
const source = textArea({
|
|
11
|
+
rows: 12,
|
|
12
|
+
class: "mvb-in mvb-in--area mvb-in--mono",
|
|
13
|
+
"aria-label": "JSON",
|
|
14
|
+
placeholder: "Paste JSON",
|
|
15
|
+
value: '{\n "hello": "world",\n "count": 3\n}',
|
|
16
|
+
});
|
|
17
|
+
const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Minified JSON" });
|
|
18
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
19
|
+
const error = notice("error");
|
|
20
|
+
|
|
21
|
+
function run() {
|
|
22
|
+
error.write("");
|
|
23
|
+
stats.replaceChildren();
|
|
24
|
+
if (!source.value.trim()) {
|
|
25
|
+
result.value = "";
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let compact;
|
|
29
|
+
try {
|
|
30
|
+
compact = JSON.stringify(JSON.parse(source.value));
|
|
31
|
+
} catch (caught) {
|
|
32
|
+
result.value = "";
|
|
33
|
+
error.write(caught.message);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const before = bytes(source.value);
|
|
37
|
+
const after = bytes(compact);
|
|
38
|
+
const saved = before ? Math.max(0, Math.round((1 - after / before) * 100)) : 0;
|
|
39
|
+
result.value = compact;
|
|
40
|
+
stats.append(tile("Before", `${before} B`), tile("After", `${after} B`), tile("Saved", `${saved}%`));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
source.addEventListener("input", run);
|
|
44
|
+
|
|
45
|
+
const card = toolcard({
|
|
46
|
+
body: bench(
|
|
47
|
+
stack(panel("JSON", source), error),
|
|
48
|
+
stack(panel("Minified", result), stats)
|
|
49
|
+
),
|
|
50
|
+
foot: actions(copyButton(() => result.value, "Copy result", "fill")),
|
|
51
|
+
});
|
|
52
|
+
slot.append(card.node);
|
|
53
|
+
run();
|
|
54
|
+
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default { mount };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Turns JSON records into CSV, using the object keys as the column names.
|
|
2
|
+
import { el, bench, stack, panel, textArea, notice, output, copyButton, downloadButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function stringifyCSV(rows, delimiter) {
|
|
5
|
+
return rows.map((row) => row.map((cell) => {
|
|
6
|
+
const value = cell == null ? "" : String(cell);
|
|
7
|
+
const needsQuote = value.includes(delimiter) || /["\n\r]/.test(value) || value !== value.trim();
|
|
8
|
+
return needsQuote ? `"${value.replace(/"/g, '""')}"` : value;
|
|
9
|
+
}).join(delimiter)).join("\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function mount(slot) {
|
|
13
|
+
const source = textArea({ rows: 12, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "JSON", placeholder: '[{"name":"Ada","role":"engineer"}]' });
|
|
14
|
+
const result = output({ rows: 12 });
|
|
15
|
+
const error = notice("error");
|
|
16
|
+
let current = "";
|
|
17
|
+
|
|
18
|
+
function run() {
|
|
19
|
+
error.write("");
|
|
20
|
+
const text = source.value;
|
|
21
|
+
if (!text.trim()) {
|
|
22
|
+
current = "";
|
|
23
|
+
result.write("");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let data;
|
|
27
|
+
try {
|
|
28
|
+
data = JSON.parse(text);
|
|
29
|
+
} catch (caught) {
|
|
30
|
+
current = "";
|
|
31
|
+
result.write("");
|
|
32
|
+
error.write(caught.message);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const records = Array.isArray(data) ? data : [data];
|
|
36
|
+
if (!records.length || records.some((record) => !record || typeof record !== "object" || Array.isArray(record))) {
|
|
37
|
+
current = "";
|
|
38
|
+
result.write("");
|
|
39
|
+
error.write("Paste an array of objects.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const header = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
43
|
+
const rows = [
|
|
44
|
+
header,
|
|
45
|
+
...records.map((record) => header.map((key) => {
|
|
46
|
+
const value = record[key];
|
|
47
|
+
return value && typeof value === "object" ? JSON.stringify(value) : value;
|
|
48
|
+
})),
|
|
49
|
+
];
|
|
50
|
+
current = stringifyCSV(rows, ",");
|
|
51
|
+
result.write(current);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
source.addEventListener("input", run);
|
|
55
|
+
|
|
56
|
+
const card = toolcard({
|
|
57
|
+
body: bench(
|
|
58
|
+
stack(panel("JSON", source), error),
|
|
59
|
+
stack(panel("CSV", result))
|
|
60
|
+
),
|
|
61
|
+
foot: actions(
|
|
62
|
+
downloadButton(() => current, "data.csv", "Download CSV", "text/csv"),
|
|
63
|
+
copyButton(() => current, "Copy CSV", "fill")
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
slot.append(card.node);
|
|
67
|
+
run();
|
|
68
|
+
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default { mount };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Generates TypeScript interfaces from a JSON sample, nested objects included.
|
|
2
|
+
import { bench, stack, panel, textArea, textInput, notice, output, copyButton, debounce, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const pascal = (text) => {
|
|
5
|
+
const words = String(text).replace(/[^a-zA-Z0-9]+/g, " ").trim().split(" ").filter(Boolean);
|
|
6
|
+
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("") || "Value";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const singular = (name) => {
|
|
10
|
+
if (/ies$/.test(name)) return name.slice(0, -3) + "y";
|
|
11
|
+
if (/ss$/.test(name)) return name;
|
|
12
|
+
if (/s$/.test(name)) return name.slice(0, -1);
|
|
13
|
+
return `${name}Item`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function generate(data, rootName) {
|
|
17
|
+
const interfaces = [];
|
|
18
|
+
const used = new Set();
|
|
19
|
+
|
|
20
|
+
const unique = (base) => {
|
|
21
|
+
let name = base;
|
|
22
|
+
let index = 2;
|
|
23
|
+
while (used.has(name)) name = `${base}${index++}`;
|
|
24
|
+
used.add(name);
|
|
25
|
+
return name;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const walk = (value, name) => {
|
|
29
|
+
if (value === null) return "null";
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
if (!value.length) return "unknown[]";
|
|
32
|
+
const types = [...new Set(value.map((item) => walk(item, singular(name))))];
|
|
33
|
+
return types.length === 1 ? `${types[0]}[]` : `Array<${types.join(" | ")}>`;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "object") {
|
|
36
|
+
const typeName = unique(name);
|
|
37
|
+
const lines = Object.entries(value).map(([key, item]) => {
|
|
38
|
+
const optional = item === null ? "?" : "";
|
|
39
|
+
const property = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
40
|
+
return ` ${property}${optional}: ${walk(item, pascal(key))};`;
|
|
41
|
+
});
|
|
42
|
+
interfaces.push(`export interface ${typeName} {\n${lines.join("\n")}\n}`);
|
|
43
|
+
return typeName;
|
|
44
|
+
}
|
|
45
|
+
return typeof value;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const rootIsObject = data !== null && typeof data === "object" && !Array.isArray(data);
|
|
49
|
+
const root = walk(data, rootName);
|
|
50
|
+
if (rootIsObject) return interfaces.join("\n\n");
|
|
51
|
+
if (!interfaces.length) return `export type ${rootName} = ${root};`;
|
|
52
|
+
return `${interfaces.join("\n\n")}\n\nexport type ${rootName} = ${root};`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function mount(slot) {
|
|
56
|
+
const source = textArea({
|
|
57
|
+
rows: 12,
|
|
58
|
+
class: "mvb-in mvb-in--area mvb-in--mono",
|
|
59
|
+
"aria-label": "JSON",
|
|
60
|
+
placeholder: "Paste a JSON object or array",
|
|
61
|
+
value: '{\n "id": 1,\n "name": "Ada",\n "tags": ["dev"],\n "address": { "city": "London" }\n}',
|
|
62
|
+
});
|
|
63
|
+
const nameInput = textInput({ value: "Root", "aria-label": "Root type name" });
|
|
64
|
+
const result = output({ rows: 16 });
|
|
65
|
+
const error = notice("error");
|
|
66
|
+
|
|
67
|
+
function run() {
|
|
68
|
+
error.write("");
|
|
69
|
+
if (!source.value.trim()) {
|
|
70
|
+
result.write("");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const rootName = pascal(nameInput.value) || "Root";
|
|
74
|
+
let data;
|
|
75
|
+
try {
|
|
76
|
+
data = JSON.parse(source.value);
|
|
77
|
+
} catch (caught) {
|
|
78
|
+
result.write("");
|
|
79
|
+
error.write(caught.message);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
result.write(generate(data, rootName));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const live = debounce(run, 200);
|
|
86
|
+
source.addEventListener("input", live);
|
|
87
|
+
nameInput.addEventListener("input", live);
|
|
88
|
+
|
|
89
|
+
const card = toolcard({
|
|
90
|
+
body: bench(
|
|
91
|
+
stack(panel("Root type name", nameInput), panel("JSON", source), error),
|
|
92
|
+
stack(panel("TypeScript", result))
|
|
93
|
+
),
|
|
94
|
+
foot: actions(copyButton(() => result.textContent, "Copy types", "fill")),
|
|
95
|
+
});
|
|
96
|
+
slot.append(card.node);
|
|
97
|
+
run();
|
|
98
|
+
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default { mount };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Checks whether JSON parses and names the first error when it does not.
|
|
2
|
+
import { bench, stack, panel, textArea, notice, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function describe(data) {
|
|
5
|
+
if (Array.isArray(data)) return `an array of ${data.length} item${data.length === 1 ? "" : "s"}`;
|
|
6
|
+
if (data === null) return "null";
|
|
7
|
+
if (typeof data === "object") {
|
|
8
|
+
const count = Object.keys(data).length;
|
|
9
|
+
return `an object with ${count} key${count === 1 ? "" : "s"}`;
|
|
10
|
+
}
|
|
11
|
+
return `a ${typeof data}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const source = textArea({ rows: 12, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "JSON", placeholder: "Paste JSON" });
|
|
16
|
+
const report = output({ rows: 4 });
|
|
17
|
+
const error = notice("error");
|
|
18
|
+
|
|
19
|
+
function run() {
|
|
20
|
+
error.write("");
|
|
21
|
+
if (!source.value.trim()) {
|
|
22
|
+
report.write("");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const data = JSON.parse(source.value);
|
|
27
|
+
report.write(`Valid JSON: ${describe(data)}.`);
|
|
28
|
+
} catch (caught) {
|
|
29
|
+
report.write("");
|
|
30
|
+
error.write(caught.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
source.addEventListener("input", run);
|
|
35
|
+
|
|
36
|
+
const card = toolcard({
|
|
37
|
+
body: bench(
|
|
38
|
+
stack(panel("JSON", source), error),
|
|
39
|
+
stack(panel("Result", report))
|
|
40
|
+
),
|
|
41
|
+
foot: actions(copyButton(() => report.textContent, "Copy result", "fill")),
|
|
42
|
+
});
|
|
43
|
+
slot.append(card.node);
|
|
44
|
+
run();
|
|
45
|
+
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default { mount };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Reads a JWT header and payload. Decoding is not signature validation.
|
|
2
|
+
import { el, bench, stack, panel, textArea, notice, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const TIME_CLAIMS = [["iat", "Issued at"], ["nbf", "Not before"], ["exp", "Expires"]];
|
|
5
|
+
|
|
6
|
+
function decodeSegment(segment) {
|
|
7
|
+
const base = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
8
|
+
const padded = base.padEnd(base.length + ((4 - (base.length % 4)) % 4), "=");
|
|
9
|
+
const binary = atob(padded);
|
|
10
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
11
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const source = textArea({ rows: 4, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Token", placeholder: "Paste a JWT" });
|
|
16
|
+
const headerOut = output({ rows: 4 });
|
|
17
|
+
const payloadOut = output({ rows: 8 });
|
|
18
|
+
const claims = el("div", { class: "mvb-kv" });
|
|
19
|
+
const error = notice("error");
|
|
20
|
+
|
|
21
|
+
function render() {
|
|
22
|
+
headerOut.write("");
|
|
23
|
+
payloadOut.write("");
|
|
24
|
+
claims.replaceChildren();
|
|
25
|
+
error.write("");
|
|
26
|
+
const token = source.value.trim();
|
|
27
|
+
if (!token) return;
|
|
28
|
+
const parts = token.split(".");
|
|
29
|
+
if (parts.length < 2) {
|
|
30
|
+
error.write("A JWT has three parts separated by dots.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
headerOut.write(JSON.stringify(decodeSegment(parts[0]), null, 2));
|
|
35
|
+
} catch {
|
|
36
|
+
error.write("The header is not readable base64url JSON.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let payload;
|
|
40
|
+
try {
|
|
41
|
+
payload = decodeSegment(parts[1]);
|
|
42
|
+
payloadOut.write(JSON.stringify(payload, null, 2));
|
|
43
|
+
} catch {
|
|
44
|
+
error.write("The payload is not readable base64url JSON.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const [key, label] of TIME_CLAIMS) {
|
|
48
|
+
if (payload[key] === undefined) continue;
|
|
49
|
+
const value = Number(payload[key]);
|
|
50
|
+
const when = Number.isFinite(value) ? new Date(value * 1000).toLocaleString() : String(payload[key]);
|
|
51
|
+
claims.append(
|
|
52
|
+
el("div", { class: "mvb-kv__row" },
|
|
53
|
+
el("span", { class: "mvb-kv__k" }, label),
|
|
54
|
+
el("span", { class: "mvb-kv__v" }, when))
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
source.addEventListener("input", render);
|
|
60
|
+
|
|
61
|
+
const card = toolcard({
|
|
62
|
+
body: bench(
|
|
63
|
+
stack(panel("Token", source), error),
|
|
64
|
+
stack(panel("Header", headerOut), panel("Payload", payloadOut), claims)
|
|
65
|
+
),
|
|
66
|
+
foot: actions(copyButton(() => payloadOut.textContent, "Copy payload", "fill")),
|
|
67
|
+
});
|
|
68
|
+
slot.append(card.node);
|
|
69
|
+
render();
|
|
70
|
+
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default { mount };
|