mevebu 0.3.1 → 1.0.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 +2 -2
- 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,86 @@
|
|
|
1
|
+
// Switches times between 12 and 24 hour format, one time per line.
|
|
2
|
+
import { bench, stack, panel, textArea, chipRow, notice, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
5
|
+
|
|
6
|
+
function read24(line) {
|
|
7
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(line);
|
|
8
|
+
if (!match) return null;
|
|
9
|
+
const hours = Number(match[1]);
|
|
10
|
+
const minutes = Number(match[2]);
|
|
11
|
+
if (hours > 23 || minutes > 59) return null;
|
|
12
|
+
return { hours, minutes };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function read12(line) {
|
|
16
|
+
const match = /^(\d{1,2}):(\d{2})\s*([ap])\.?m\.?$/i.exec(line);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
let hours = Number(match[1]);
|
|
19
|
+
const minutes = Number(match[2]);
|
|
20
|
+
const half = match[3].toLowerCase();
|
|
21
|
+
if (hours < 1 || hours > 12 || minutes > 59) return null;
|
|
22
|
+
if (half === "p" && hours !== 12) hours += 12;
|
|
23
|
+
if (half === "a" && hours === 12) hours = 0;
|
|
24
|
+
return { hours, minutes };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function to24({ hours, minutes }) {
|
|
28
|
+
return `${pad(hours)}:${pad(minutes)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function to12({ hours, minutes }) {
|
|
32
|
+
const half = hours < 12 ? "AM" : "PM";
|
|
33
|
+
let clock = hours % 12;
|
|
34
|
+
if (clock === 0) clock = 12;
|
|
35
|
+
return `${clock}:${pad(minutes)} ${half}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function mount(slot) {
|
|
39
|
+
const source = textArea({ rows: 8, value: "13:05\n00:30\n9:45 PM", "aria-label": "Times, one per line", placeholder: "13:05" });
|
|
40
|
+
const target = output({ value: "", rows: 8 });
|
|
41
|
+
const error = notice("error");
|
|
42
|
+
let format = "24";
|
|
43
|
+
let text = "";
|
|
44
|
+
|
|
45
|
+
const picker = chipRow(
|
|
46
|
+
[{ value: "24", label: "24 hour input" }, { value: "12", label: "12 hour input" }],
|
|
47
|
+
{ value: format, label: "Input format", onChange: (value) => { format = value; run(); } }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
function run() {
|
|
51
|
+
error.write("");
|
|
52
|
+
const lines = source.value.split("\n");
|
|
53
|
+
const out = [];
|
|
54
|
+
let bad = 0;
|
|
55
|
+
for (const raw of lines) {
|
|
56
|
+
const line = raw.trim();
|
|
57
|
+
if (!line) continue;
|
|
58
|
+
const parsed = format === "24" ? read24(line) : read12(line);
|
|
59
|
+
if (!parsed) {
|
|
60
|
+
bad += 1;
|
|
61
|
+
out.push(line);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
out.push(format === "24" ? to12(parsed) : to24(parsed));
|
|
65
|
+
}
|
|
66
|
+
target.write(out.join("\n"));
|
|
67
|
+
text = out.join("\n");
|
|
68
|
+
if (bad) error.write(`${bad} lines could not be read.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
source.addEventListener("input", run);
|
|
72
|
+
|
|
73
|
+
const card = toolcard({
|
|
74
|
+
body: bench(
|
|
75
|
+
stack(panel("Input format", picker), panel("Times", source), error),
|
|
76
|
+
stack(panel("Converted", target))
|
|
77
|
+
),
|
|
78
|
+
foot: actions(copyButton(() => text, "Copy times", "fill")),
|
|
79
|
+
});
|
|
80
|
+
slot.append(card.node);
|
|
81
|
+
run();
|
|
82
|
+
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default { mount };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Converts between Unix time and dates. Reads seconds or milliseconds.
|
|
2
|
+
import { el, bench, stack, panel, textInput, notice, button, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function relative(date) {
|
|
5
|
+
const diff = Date.now() - date.getTime();
|
|
6
|
+
const abs = Math.abs(diff);
|
|
7
|
+
const units = [
|
|
8
|
+
["year", 31536000000], ["month", 2592000000], ["day", 86400000],
|
|
9
|
+
["hour", 3600000], ["minute", 60000], ["second", 1000],
|
|
10
|
+
];
|
|
11
|
+
for (const [unit, ms] of units) {
|
|
12
|
+
if (abs >= ms) {
|
|
13
|
+
const value = Math.round(abs / ms);
|
|
14
|
+
const word = `${value} ${unit}${value === 1 ? "" : "s"}`;
|
|
15
|
+
return diff >= 0 ? `${word} ago` : `in ${word}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return "now";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parse(text) {
|
|
22
|
+
const value = text.trim();
|
|
23
|
+
if (!value) return null;
|
|
24
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
25
|
+
const num = Number(value);
|
|
26
|
+
const ms = Math.abs(num) < 1e11 ? num * 1000 : num;
|
|
27
|
+
const date = new Date(ms);
|
|
28
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
29
|
+
}
|
|
30
|
+
const date = new Date(value);
|
|
31
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const row = (key, value) =>
|
|
35
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
|
|
36
|
+
|
|
37
|
+
export function mount(slot) {
|
|
38
|
+
const source = textInput({ value: "1700000000", "aria-label": "Timestamp or date", placeholder: "1700000000 or 2026-01-01T00:00:00Z" });
|
|
39
|
+
const kv = el("div", { class: "mvb-kv" });
|
|
40
|
+
const error = notice("error");
|
|
41
|
+
let iso = "";
|
|
42
|
+
|
|
43
|
+
function run() {
|
|
44
|
+
error.write("");
|
|
45
|
+
kv.replaceChildren();
|
|
46
|
+
iso = "";
|
|
47
|
+
if (!source.value.trim()) return;
|
|
48
|
+
const date = parse(source.value);
|
|
49
|
+
if (!date) {
|
|
50
|
+
error.write("That is not a timestamp or a date this browser can read.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
iso = date.toISOString();
|
|
54
|
+
kv.append(
|
|
55
|
+
row("ISO 8601", iso),
|
|
56
|
+
row("Unix seconds", String(Math.floor(date.getTime() / 1000))),
|
|
57
|
+
row("Unix milliseconds", String(date.getTime())),
|
|
58
|
+
row("UTC", date.toUTCString()),
|
|
59
|
+
row("Local", date.toLocaleString()),
|
|
60
|
+
row("Relative", relative(date))
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
source.addEventListener("input", run);
|
|
65
|
+
|
|
66
|
+
const nowButton = button("Use current time", { icon: "clock" });
|
|
67
|
+
nowButton.addEventListener("click", () => {
|
|
68
|
+
source.value = String(Math.floor(Date.now() / 1000));
|
|
69
|
+
run();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const card = toolcard({
|
|
73
|
+
body: bench(
|
|
74
|
+
stack(panel("Timestamp or date", source), error),
|
|
75
|
+
stack(panel("Reading", kv))
|
|
76
|
+
),
|
|
77
|
+
foot: actions(nowButton, copyButton(() => iso, "Copy ISO 8601", "fill")),
|
|
78
|
+
});
|
|
79
|
+
slot.append(card.node);
|
|
80
|
+
run();
|
|
81
|
+
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default { mount };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Converts a wall clock time from one zone to another using the browser's zone rules.
|
|
2
|
+
import { el, bench, stack, panel, textInput, selectInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const FALLBACK_ZONES = [
|
|
5
|
+
"UTC",
|
|
6
|
+
"America/New_York",
|
|
7
|
+
"America/Chicago",
|
|
8
|
+
"America/Denver",
|
|
9
|
+
"America/Los_Angeles",
|
|
10
|
+
"America/Sao_Paulo",
|
|
11
|
+
"Europe/London",
|
|
12
|
+
"Europe/Paris",
|
|
13
|
+
"Europe/Berlin",
|
|
14
|
+
"Europe/Moscow",
|
|
15
|
+
"Africa/Cairo",
|
|
16
|
+
"Asia/Dubai",
|
|
17
|
+
"Asia/Kolkata",
|
|
18
|
+
"Asia/Shanghai",
|
|
19
|
+
"Asia/Tokyo",
|
|
20
|
+
"Asia/Singapore",
|
|
21
|
+
"Australia/Sydney",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function zoneList() {
|
|
25
|
+
let zones = FALLBACK_ZONES;
|
|
26
|
+
if (typeof Intl.supportedValuesOf === "function") {
|
|
27
|
+
try {
|
|
28
|
+
const supported = Intl.supportedValuesOf("timeZone");
|
|
29
|
+
if (Array.isArray(supported) && supported.length) zones = supported;
|
|
30
|
+
} catch {
|
|
31
|
+
zones = FALLBACK_ZONES;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const out = zones.slice();
|
|
35
|
+
if (!out.includes("UTC")) out.unshift("UTC");
|
|
36
|
+
const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
37
|
+
if (local && !out.includes(local)) out.unshift(local);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseDate(value) {
|
|
42
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
43
|
+
if (!match) return null;
|
|
44
|
+
const year = Number(match[1]);
|
|
45
|
+
const month = Number(match[2]) - 1;
|
|
46
|
+
const day = Number(match[3]);
|
|
47
|
+
const date = new Date(year, month, day);
|
|
48
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
|
|
49
|
+
return { year, month, day };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseTime(value) {
|
|
53
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value);
|
|
54
|
+
if (!match) return null;
|
|
55
|
+
const hours = Number(match[1]);
|
|
56
|
+
const minutes = Number(match[2]);
|
|
57
|
+
if (hours > 23 || minutes > 59) return null;
|
|
58
|
+
return { hours, minutes };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function offsetMs(timeZone, date) {
|
|
62
|
+
const parts = {};
|
|
63
|
+
const format = new Intl.DateTimeFormat("en-US", {
|
|
64
|
+
timeZone,
|
|
65
|
+
hourCycle: "h23",
|
|
66
|
+
year: "numeric",
|
|
67
|
+
month: "2-digit",
|
|
68
|
+
day: "2-digit",
|
|
69
|
+
hour: "2-digit",
|
|
70
|
+
minute: "2-digit",
|
|
71
|
+
second: "2-digit",
|
|
72
|
+
});
|
|
73
|
+
for (const part of format.formatToParts(date)) parts[part.type] = part.value;
|
|
74
|
+
const hour = parts.hour === "24" ? 0 : Number(parts.hour);
|
|
75
|
+
const asUTC = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), hour, Number(parts.minute), Number(parts.second));
|
|
76
|
+
return asUTC - date.getTime();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function wallToInstant(day, time, timeZone) {
|
|
80
|
+
const guess = Date.UTC(day.year, day.month, day.day, time.hours, time.minutes);
|
|
81
|
+
const offset = offsetMs(timeZone, new Date(guess));
|
|
82
|
+
return new Date(guess - offset);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
86
|
+
|
|
87
|
+
const kvRow = (key, value) =>
|
|
88
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
|
|
89
|
+
|
|
90
|
+
export function mount(slot) {
|
|
91
|
+
const zones = zoneList();
|
|
92
|
+
const today = new Date();
|
|
93
|
+
const date = textInput({ type: "date", value: `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`, "aria-label": "Date" });
|
|
94
|
+
const time = textInput({ type: "time", value: "12:00", "aria-label": "Time" });
|
|
95
|
+
const fromZone = selectInput(zones, { value: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", "aria-label": "Source zone" });
|
|
96
|
+
const toZone = selectInput(zones, { value: "UTC", "aria-label": "Target zone" });
|
|
97
|
+
|
|
98
|
+
const target = el("span", { class: "mvb-kv__v" }, "-");
|
|
99
|
+
const utc = el("span", { class: "mvb-kv__v" }, "-");
|
|
100
|
+
const diff = el("span", { class: "mvb-kv__v" }, "-");
|
|
101
|
+
const kv = el("div", { class: "mvb-kv" }, kvRow("Target time", target), kvRow("UTC", utc), kvRow("Offset", diff));
|
|
102
|
+
const error = notice("error");
|
|
103
|
+
let text = "";
|
|
104
|
+
|
|
105
|
+
function clear() {
|
|
106
|
+
target.textContent = "-";
|
|
107
|
+
utc.textContent = "-";
|
|
108
|
+
diff.textContent = "-";
|
|
109
|
+
text = "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function run() {
|
|
113
|
+
error.write("");
|
|
114
|
+
const day = parseDate(date.value);
|
|
115
|
+
const clock = parseTime(time.value);
|
|
116
|
+
if (!day || !clock) {
|
|
117
|
+
clear();
|
|
118
|
+
error.write("Pick a valid date and time.");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const instant = wallToInstant(day, clock, fromZone.value);
|
|
123
|
+
const formatted = new Intl.DateTimeFormat(undefined, {
|
|
124
|
+
timeZone: toZone.value,
|
|
125
|
+
weekday: "short",
|
|
126
|
+
day: "numeric",
|
|
127
|
+
month: "short",
|
|
128
|
+
year: "numeric",
|
|
129
|
+
hour: "2-digit",
|
|
130
|
+
minute: "2-digit",
|
|
131
|
+
}).format(instant);
|
|
132
|
+
const utcText = `${instant.getUTCFullYear()}-${pad(instant.getUTCMonth() + 1)}-${pad(instant.getUTCDate())} ${pad(instant.getUTCHours())}:${pad(instant.getUTCMinutes())} UTC`;
|
|
133
|
+
const deltaHours = (offsetMs(toZone.value, instant) - offsetMs(fromZone.value, instant)) / 3600000;
|
|
134
|
+
const sign = deltaHours > 0 ? "+" : deltaHours < 0 ? "-" : "";
|
|
135
|
+
const diffText = deltaHours === 0 ? "Same offset" : `${sign}${Math.abs(deltaHours)} hours`;
|
|
136
|
+
target.textContent = formatted;
|
|
137
|
+
utc.textContent = utcText;
|
|
138
|
+
diff.textContent = diffText;
|
|
139
|
+
text = `${formatted} (${toZone.value})`;
|
|
140
|
+
} catch {
|
|
141
|
+
clear();
|
|
142
|
+
error.write("That zone could not be resolved.");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const input of [date, time]) input.addEventListener("input", run);
|
|
147
|
+
fromZone.addEventListener("change", run);
|
|
148
|
+
toZone.addEventListener("change", run);
|
|
149
|
+
|
|
150
|
+
const card = toolcard({
|
|
151
|
+
body: bench(
|
|
152
|
+
stack(panel("Date", date), panel("Time", time), panel("From", fromZone), panel("To", toZone), error),
|
|
153
|
+
stack(panel("Result", kv))
|
|
154
|
+
),
|
|
155
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
156
|
+
});
|
|
157
|
+
slot.append(card.node);
|
|
158
|
+
run();
|
|
159
|
+
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default { mount };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Several titles compared by length, longest first.
|
|
2
|
+
import { el, blank, bench, stack, panel, textArea, numberInput, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const countWords = (text) => (text.trim() ? text.trim().split(/\s+/).length : 0);
|
|
5
|
+
|
|
6
|
+
function tile(label) {
|
|
7
|
+
const value = el("span", { class: "mvb-stat__v" }, "0");
|
|
8
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
9
|
+
node.set = (text) => { value.textContent = text; };
|
|
10
|
+
return node;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function mount(slot) {
|
|
14
|
+
const source = textArea({ rows: 10, "aria-label": "Titles", placeholder: "One title per line" });
|
|
15
|
+
const limit = numberInput({ min: "0", value: "60", "aria-label": "Character limit" });
|
|
16
|
+
|
|
17
|
+
const titlesTile = tile("Titles");
|
|
18
|
+
const overTile = tile("Over the limit");
|
|
19
|
+
const longestTile = tile("Longest");
|
|
20
|
+
const stats = el("div", { class: "mvb-stats" }, titlesTile, overTile, longestTile);
|
|
21
|
+
|
|
22
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
23
|
+
let listText = "";
|
|
24
|
+
|
|
25
|
+
function run() {
|
|
26
|
+
const cap = parseInt(limit.value, 10) || 0;
|
|
27
|
+
const titles = source.value.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
28
|
+
const items = titles.map((title) => ({ title, chars: title.length, words: countWords(title) }));
|
|
29
|
+
items.sort((a, b) => b.chars - a.chars);
|
|
30
|
+
|
|
31
|
+
titlesTile.set(String(items.length));
|
|
32
|
+
overTile.set(String(cap > 0 ? items.filter((item) => item.chars > cap).length : 0));
|
|
33
|
+
longestTile.set(String(items.length ? items[0].chars : 0));
|
|
34
|
+
|
|
35
|
+
if (!items.length) {
|
|
36
|
+
listWrap.replaceChildren(blank("type", "No titles yet."));
|
|
37
|
+
listText = "";
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rows = items.map((item) => el("tr", {},
|
|
42
|
+
el("th", {}, item.title),
|
|
43
|
+
el("td", {}, cap > 0 && item.chars > cap ? `${item.chars} over` : String(item.chars)),
|
|
44
|
+
el("td", {}, String(item.words))));
|
|
45
|
+
listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
46
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Title"), el("th", {}, "Characters"), el("th", {}, "Words"))),
|
|
47
|
+
el("tbody", {}, ...rows)));
|
|
48
|
+
listText = items.map((item) => `${item.chars} characters, ${item.words} words ${item.title}`).join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
source.addEventListener("input", run);
|
|
52
|
+
limit.addEventListener("input", run);
|
|
53
|
+
|
|
54
|
+
const card = toolcard({
|
|
55
|
+
body: bench(
|
|
56
|
+
stack(panel("Titles", source), panel("Character limit", limit)),
|
|
57
|
+
stack(stats, panel("By length", listWrap))
|
|
58
|
+
),
|
|
59
|
+
foot: actions(copyButton(() => listText, "Copy list", "fill")),
|
|
60
|
+
});
|
|
61
|
+
slot.append(card.node);
|
|
62
|
+
run();
|
|
63
|
+
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default { mount };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Turns characters into Unicode escape sequences and back.
|
|
2
|
+
import { bench, stack, panel, textArea, output, notice, chipRow, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function encode(text) {
|
|
5
|
+
return Array.from(text).map((char) => {
|
|
6
|
+
const point = char.codePointAt(0);
|
|
7
|
+
return point <= 0xffff
|
|
8
|
+
? `\\u${point.toString(16).toUpperCase().padStart(4, "0")}`
|
|
9
|
+
: `\\u{${point.toString(16).toUpperCase()}}`;
|
|
10
|
+
}).join("");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function decode(text) {
|
|
14
|
+
let ok = true;
|
|
15
|
+
const out = text.replace(/\\u\{([0-9a-fA-F]+)\}|\\u([0-9a-fA-F]{4})|\\u/g, (match, braced, plain) => {
|
|
16
|
+
if (match === "\\u") {
|
|
17
|
+
ok = false;
|
|
18
|
+
return match;
|
|
19
|
+
}
|
|
20
|
+
const point = parseInt(braced ?? plain, 16);
|
|
21
|
+
if (point > 0x10ffff) {
|
|
22
|
+
ok = false;
|
|
23
|
+
return match;
|
|
24
|
+
}
|
|
25
|
+
return String.fromCodePoint(point);
|
|
26
|
+
});
|
|
27
|
+
return { ok, text: out };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function mount(slot) {
|
|
31
|
+
const source = textArea({ rows: 7, value: "mevebu", "aria-label": "Input" });
|
|
32
|
+
const result = output({ value: "", rows: 7 });
|
|
33
|
+
const error = notice("error");
|
|
34
|
+
let mode = "encode";
|
|
35
|
+
let text = "";
|
|
36
|
+
|
|
37
|
+
const chips = chipRow(
|
|
38
|
+
[{ value: "encode", label: "Encode" }, { value: "decode", label: "Decode" }],
|
|
39
|
+
{ value: "encode", label: "Direction", onChange: (value) => { mode = value; run(); } }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
function run() {
|
|
43
|
+
error.write("");
|
|
44
|
+
if (!source.value) {
|
|
45
|
+
text = "";
|
|
46
|
+
result.write("");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (mode === "encode") {
|
|
50
|
+
text = encode(source.value);
|
|
51
|
+
result.write(text);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const decoded = decode(source.value);
|
|
55
|
+
if (!decoded.ok) {
|
|
56
|
+
text = "";
|
|
57
|
+
result.write("");
|
|
58
|
+
error.write("That is not a valid escape sequence.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
text = decoded.text;
|
|
62
|
+
result.write(text);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
source.addEventListener("input", run);
|
|
66
|
+
|
|
67
|
+
const card = toolcard({
|
|
68
|
+
body: bench(
|
|
69
|
+
stack(panel("Direction", chips), panel("Input", source)),
|
|
70
|
+
stack(panel("Result", result), error)
|
|
71
|
+
),
|
|
72
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
73
|
+
});
|
|
74
|
+
slot.append(card.node);
|
|
75
|
+
run();
|
|
76
|
+
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default { mount };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Lists each character with its code point, decimal value and UTF-8 bytes.
|
|
2
|
+
import { el, bench, stack, panel, textArea, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const hex = (value, width) => value.toString(16).toUpperCase().padStart(width, "0");
|
|
5
|
+
|
|
6
|
+
function describe(char) {
|
|
7
|
+
const point = char.codePointAt(0);
|
|
8
|
+
const bytes = [...new TextEncoder().encode(char)].map((byte) => hex(byte, 2)).join(" ");
|
|
9
|
+
return { char, point: `U+${hex(point, 4)}`, decimal: String(point), bytes };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function mount(slot) {
|
|
13
|
+
const source = textArea({ rows: 8, value: "mevebu", "aria-label": "Text" });
|
|
14
|
+
const scroll = el("div", { class: "mvb-scroll" });
|
|
15
|
+
let rows = [];
|
|
16
|
+
|
|
17
|
+
function run() {
|
|
18
|
+
rows = Array.from(source.value).map(describe);
|
|
19
|
+
if (!rows.length) {
|
|
20
|
+
scroll.replaceChildren(el("p", { class: "mvb-note" }, "Type or paste text."));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
scroll.replaceChildren(el(
|
|
24
|
+
"table",
|
|
25
|
+
{ class: "mvb-ttable" },
|
|
26
|
+
el("thead", {}, el("tr", {},
|
|
27
|
+
el("th", {}, "Character"),
|
|
28
|
+
el("th", {}, "Code point"),
|
|
29
|
+
el("th", {}, "Decimal"),
|
|
30
|
+
el("th", {}, "UTF-8"))),
|
|
31
|
+
el("tbody", {}, ...rows.map((entry) => el("tr", {},
|
|
32
|
+
el("td", {}, entry.char),
|
|
33
|
+
el("td", {}, entry.point),
|
|
34
|
+
el("td", {}, entry.decimal),
|
|
35
|
+
el("td", {}, entry.bytes))))
|
|
36
|
+
));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
source.addEventListener("input", run);
|
|
40
|
+
|
|
41
|
+
const card = toolcard({
|
|
42
|
+
body: bench(
|
|
43
|
+
stack(panel("Text", source)),
|
|
44
|
+
stack(panel("Characters", scroll))
|
|
45
|
+
),
|
|
46
|
+
foot: actions(copyButton(
|
|
47
|
+
() => rows.map((entry) => `${entry.char}\t${entry.point}\t${entry.decimal}\t${entry.bytes}`).join("\n"),
|
|
48
|
+
"Copy table",
|
|
49
|
+
"fill"
|
|
50
|
+
)),
|
|
51
|
+
});
|
|
52
|
+
slot.append(card.node);
|
|
53
|
+
run();
|
|
54
|
+
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default { mount };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Breaks a link into its parts and flags look-alike hosts before you open it.
|
|
2
|
+
import { el, textInput, notice, copyButton, bench, stack, panel, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
5
|
+
|
|
6
|
+
const kvRow = (key, value) =>
|
|
7
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
|
|
8
|
+
|
|
9
|
+
export function mount(slot) {
|
|
10
|
+
const source = textInput({ value: "https://example.com/login?next=/home", "aria-label": "URL" });
|
|
11
|
+
const scheme = el("span", { class: "mvb-kv__v" }, "-");
|
|
12
|
+
const host = el("span", { class: "mvb-kv__v" }, "-");
|
|
13
|
+
const port = el("span", { class: "mvb-kv__v" }, "-");
|
|
14
|
+
const path = el("span", { class: "mvb-kv__v" }, "-");
|
|
15
|
+
const kv = el(
|
|
16
|
+
"div",
|
|
17
|
+
{ class: "mvb-kv" },
|
|
18
|
+
kvRow("Scheme", scheme),
|
|
19
|
+
kvRow("Host", host),
|
|
20
|
+
kvRow("Port", port),
|
|
21
|
+
kvRow("Path", path)
|
|
22
|
+
);
|
|
23
|
+
const flags = el("div", { class: "mvb-stack" });
|
|
24
|
+
const error = notice("error");
|
|
25
|
+
let summary = "";
|
|
26
|
+
|
|
27
|
+
function flag(kind, text) {
|
|
28
|
+
const node = notice(kind);
|
|
29
|
+
node.write(text);
|
|
30
|
+
flags.append(node);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clear() {
|
|
34
|
+
for (const node of [scheme, host, port, path]) node.textContent = "-";
|
|
35
|
+
flags.replaceChildren();
|
|
36
|
+
summary = "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function run() {
|
|
40
|
+
error.write("");
|
|
41
|
+
const raw = source.value.trim();
|
|
42
|
+
if (!raw) return clear();
|
|
43
|
+
let url;
|
|
44
|
+
try {
|
|
45
|
+
url = new URL(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
clear();
|
|
48
|
+
error.write("That is not a valid URL.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
scheme.textContent = url.protocol.replace(":", "");
|
|
53
|
+
host.textContent = url.hostname;
|
|
54
|
+
port.textContent = url.port || "default";
|
|
55
|
+
path.textContent = url.pathname + url.search;
|
|
56
|
+
summary = url.hostname;
|
|
57
|
+
|
|
58
|
+
flags.replaceChildren();
|
|
59
|
+
if (/[^\x00-\x7F]/.test(raw)) {
|
|
60
|
+
flag("warn", "The address contains non-Latin characters. The host below is the punycode form your browser would use.");
|
|
61
|
+
}
|
|
62
|
+
if (url.hostname.startsWith("xn--") || url.hostname.includes(".xn--")) {
|
|
63
|
+
flag("warn", "The host uses punycode (xn--). It can mix scripts to imitate a familiar name.");
|
|
64
|
+
}
|
|
65
|
+
if (url.username || url.password) {
|
|
66
|
+
flag("warn", `The part before the @ is not the host. The real host is ${url.hostname}.`);
|
|
67
|
+
}
|
|
68
|
+
if (IPV4.test(url.hostname) || url.hostname.startsWith("[")) {
|
|
69
|
+
flag("warn", "The link opens by IP address, not a domain name.");
|
|
70
|
+
}
|
|
71
|
+
if (url.protocol === "http:") {
|
|
72
|
+
flag("warn", "This link uses http, not https. Traffic is not encrypted.");
|
|
73
|
+
}
|
|
74
|
+
if (!flags.childElementCount) {
|
|
75
|
+
flag("good", "No look-alike characters or address tricks found.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
source.addEventListener("input", run);
|
|
80
|
+
|
|
81
|
+
const card = toolcard({
|
|
82
|
+
body: bench(
|
|
83
|
+
stack(
|
|
84
|
+
panel("URL", source),
|
|
85
|
+
error,
|
|
86
|
+
el("p", { class: "mvb-note" }, "The link is read in this tab. Nothing is opened or requested.")
|
|
87
|
+
),
|
|
88
|
+
stack(kv, panel("Checks", flags))
|
|
89
|
+
),
|
|
90
|
+
foot: actions(copyButton(() => summary, "Copy host", "fill")),
|
|
91
|
+
});
|
|
92
|
+
slot.append(card.node);
|
|
93
|
+
run();
|
|
94
|
+
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default { mount };
|