mevebu 0.3.1 → 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 +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,62 @@
|
|
|
1
|
+
// Tags cleaned, de-duplicated and laid out one way or the other.
|
|
2
|
+
import { el, bench, stack, panel, textArea, chipRow, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const clean = (raw) => raw.replace(/#/g, "").replace(/[^\p{L}\p{N}_]/gu, "");
|
|
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: 8, "aria-label": "Tags", placeholder: "One tag per line or space separated" });
|
|
15
|
+
const tagsTile = tile("Tags");
|
|
16
|
+
const removedTile = tile("Duplicates removed");
|
|
17
|
+
const stats = el("div", { class: "mvb-stats" }, tagsTile, removedTile);
|
|
18
|
+
const out = output({ rows: 8 });
|
|
19
|
+
|
|
20
|
+
let mode = "list";
|
|
21
|
+
const modeChips = chipRow(
|
|
22
|
+
[
|
|
23
|
+
{ value: "line", label: "One line" },
|
|
24
|
+
{ value: "list", label: "One per line" },
|
|
25
|
+
],
|
|
26
|
+
{ value: "list", label: "Layout", onChange: (next) => { mode = next; run(); } }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
let result = "";
|
|
30
|
+
|
|
31
|
+
function run() {
|
|
32
|
+
const tokens = source.value.split(/[\s,]+/).map(clean).filter(Boolean);
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
const tags = [];
|
|
35
|
+
for (const token of tokens) {
|
|
36
|
+
const key = token.toLowerCase();
|
|
37
|
+
if (seen.has(key)) continue;
|
|
38
|
+
seen.add(key);
|
|
39
|
+
tags.push(`#${token}`);
|
|
40
|
+
}
|
|
41
|
+
result = tags.join(mode === "line" ? " " : "\n");
|
|
42
|
+
out.write(result);
|
|
43
|
+
tagsTile.set(String(tags.length));
|
|
44
|
+
removedTile.set(String(tokens.length - tags.length));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
source.addEventListener("input", run);
|
|
48
|
+
|
|
49
|
+
const card = toolcard({
|
|
50
|
+
body: bench(
|
|
51
|
+
stack(panel("Tags", source), panel("Layout", modeChips)),
|
|
52
|
+
stack(stats, panel("Result", out))
|
|
53
|
+
),
|
|
54
|
+
foot: actions(copyButton(() => result, "Copy tags", "fill")),
|
|
55
|
+
});
|
|
56
|
+
slot.append(card.node);
|
|
57
|
+
run();
|
|
58
|
+
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default { mount };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Numbers chapter and section headings from a list.
|
|
2
|
+
import { bench, stack, panel, textArea, numberInput, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function levelOf(line) {
|
|
5
|
+
const marks = line.match(/^(#+)\s*/);
|
|
6
|
+
if (marks) return marks[1].length;
|
|
7
|
+
const indent = (line.match(/^[ \t]*/) || [""])[0];
|
|
8
|
+
let spaces = 0;
|
|
9
|
+
for (const ch of indent) spaces += ch === "\t" ? 2 : 1;
|
|
10
|
+
return 1 + Math.floor(spaces / 2);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const strip = (line) => line.replace(/^(#+)\s*/, "").replace(/^[ \t]+/, "").trim();
|
|
14
|
+
|
|
15
|
+
export function mount(slot) {
|
|
16
|
+
const source = textArea({ rows: 12, "aria-label": "Headings", placeholder: "One heading per line" });
|
|
17
|
+
const start = numberInput({ value: "1", min: "1", "aria-label": "Starting number" });
|
|
18
|
+
const result = output({ rows: 12 });
|
|
19
|
+
const note = notice("note");
|
|
20
|
+
note.write("Levels come from leading spaces, or from # marks.");
|
|
21
|
+
let text = "";
|
|
22
|
+
|
|
23
|
+
function run() {
|
|
24
|
+
const lines = source.value.split(/\r?\n/).filter((line) => line.trim());
|
|
25
|
+
const startNumber = Math.max(1, Number(start.value) || 1);
|
|
26
|
+
const counters = [];
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const level = Math.min(6, Math.max(1, levelOf(line)));
|
|
30
|
+
if (counters.length < level) {
|
|
31
|
+
while (counters.length < level) counters.push(0);
|
|
32
|
+
} else {
|
|
33
|
+
counters.length = level;
|
|
34
|
+
}
|
|
35
|
+
if (level === 1 && counters[0] === 0) counters[0] = startNumber - 1;
|
|
36
|
+
counters[level - 1] += 1;
|
|
37
|
+
for (let i = level; i < counters.length; i++) counters[i] = 0;
|
|
38
|
+
const number = counters.slice(0, level).map((n) => (n === 0 ? 1 : n)).join(".");
|
|
39
|
+
out.push(`${number} ${strip(line)}`);
|
|
40
|
+
}
|
|
41
|
+
text = out.join("\n");
|
|
42
|
+
result.write(text);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
source.addEventListener("input", run);
|
|
46
|
+
start.addEventListener("input", run);
|
|
47
|
+
|
|
48
|
+
const card = toolcard({
|
|
49
|
+
body: bench(
|
|
50
|
+
stack(panel("Headings", source), panel("Starting number", start), note),
|
|
51
|
+
stack(panel("Numbered", result))
|
|
52
|
+
),
|
|
53
|
+
foot: actions(copyButton(() => text, "Copy headings", "fill")),
|
|
54
|
+
});
|
|
55
|
+
slot.append(card.node);
|
|
56
|
+
run();
|
|
57
|
+
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default { mount };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// HMAC-SHA signatures for a message and a key, computed in this tab.
|
|
2
|
+
import { el, icon, textArea, passwordInput, selectInput, output, notice, button, copyButton, bench, stack, panel, actions, toolcard, debounce } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const ALGORITHMS = [
|
|
5
|
+
{ value: "SHA-256", label: "SHA-256" },
|
|
6
|
+
{ value: "SHA-384", label: "SHA-384" },
|
|
7
|
+
{ value: "SHA-512", label: "SHA-512" },
|
|
8
|
+
{ value: "SHA-1", label: "SHA-1" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
const toHex = (bytes) => [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const message = textArea({ rows: 5, value: "mevebu", "aria-label": "Message", placeholder: "Type or paste the message" });
|
|
16
|
+
const secret = passwordInput({ value: "your-256-bit-secret", autocomplete: "off", "aria-label": "Secret key" });
|
|
17
|
+
const showButton = button("Show", { icon: "eye" });
|
|
18
|
+
const hashField = selectInput(ALGORITHMS, { value: "SHA-256", "aria-label": "Hash" });
|
|
19
|
+
const result = output();
|
|
20
|
+
const error = notice("error");
|
|
21
|
+
|
|
22
|
+
showButton.addEventListener("click", () => {
|
|
23
|
+
const visible = secret.type === "text";
|
|
24
|
+
secret.type = visible ? "password" : "text";
|
|
25
|
+
showButton.replaceChildren(icon(visible ? "eye" : "eye-off"), el("span", {}, visible ? "Show" : "Hide"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function run() {
|
|
29
|
+
error.write("");
|
|
30
|
+
result.write("");
|
|
31
|
+
if (!message.value || !secret.value) return;
|
|
32
|
+
try {
|
|
33
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret.value), { name: "HMAC", hash: hashField.value }, false, ["sign"]);
|
|
34
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message.value));
|
|
35
|
+
result.write(toHex(new Uint8Array(signature)));
|
|
36
|
+
} catch (caught) {
|
|
37
|
+
error.write(caught.message || "Signing failed.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const live = debounce(run, 150);
|
|
42
|
+
for (const field of [message, secret]) field.addEventListener("input", live);
|
|
43
|
+
hashField.addEventListener("change", run);
|
|
44
|
+
|
|
45
|
+
const card = toolcard({
|
|
46
|
+
body: bench(
|
|
47
|
+
stack(
|
|
48
|
+
panel("Message", message),
|
|
49
|
+
panel("Secret key", el("div", { class: "mvb-secret" }, secret, showButton)),
|
|
50
|
+
panel("Hash", hashField),
|
|
51
|
+
el("p", { class: "mvb-note" }, "The message and key stay in this tab. Do not paste a production secret into a site you do not trust.")
|
|
52
|
+
),
|
|
53
|
+
stack(panel("Signature", result), error)
|
|
54
|
+
),
|
|
55
|
+
foot: actions(copyButton(() => result.textContent, "Copy signature", "fill")),
|
|
56
|
+
});
|
|
57
|
+
slot.append(card.node);
|
|
58
|
+
run();
|
|
59
|
+
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default { mount };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Total production cost and the cost to make one unit.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function fmt(n) {
|
|
5
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 4 });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function tile(label) {
|
|
9
|
+
const value = el("span", { class: "mvb-stat__v" }, "-");
|
|
10
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
11
|
+
node.set = (text) => { value.textContent = text; };
|
|
12
|
+
return node;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function mount(slot) {
|
|
16
|
+
const material = numberInput({ value: "0", min: "0", "aria-label": "Material cost" });
|
|
17
|
+
const labour = numberInput({ value: "0", min: "0", "aria-label": "Labour cost" });
|
|
18
|
+
const overhead = numberInput({ value: "0", min: "0", "aria-label": "Overhead cost" });
|
|
19
|
+
const units = numberInput({ value: "1", min: "1", "aria-label": "Units made" });
|
|
20
|
+
const total = tile("Total cost");
|
|
21
|
+
const perUnit = tile("Cost per unit");
|
|
22
|
+
const stats = el("div", { class: "mvb-stats" }, total, perUnit);
|
|
23
|
+
const error = notice("error");
|
|
24
|
+
let text = "";
|
|
25
|
+
|
|
26
|
+
function clear() {
|
|
27
|
+
total.set("-");
|
|
28
|
+
perUnit.set("-");
|
|
29
|
+
text = "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function run() {
|
|
33
|
+
error.write("");
|
|
34
|
+
const inputs = [material, labour, overhead, units];
|
|
35
|
+
const values = inputs.map((input) => Number(input.value));
|
|
36
|
+
if (inputs.some((input) => input.value.trim() === "") || !values.every(Number.isFinite)) {
|
|
37
|
+
clear();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const [m, l, o, u] = values;
|
|
41
|
+
if (m < 0 || l < 0 || o < 0) { clear(); error.write("The costs cannot be negative."); return; }
|
|
42
|
+
if (u <= 0) { clear(); error.write("The units must be above zero."); return; }
|
|
43
|
+
const sum = m + l + o;
|
|
44
|
+
total.set(fmt(sum));
|
|
45
|
+
perUnit.set(fmt(sum / u));
|
|
46
|
+
text = `Total cost ${fmt(sum)}, cost per unit ${fmt(sum / u)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const input of [material, labour, overhead, units]) input.addEventListener("input", run);
|
|
50
|
+
|
|
51
|
+
const card = toolcard({
|
|
52
|
+
body: bench(
|
|
53
|
+
stack(
|
|
54
|
+
panel("Material cost", material),
|
|
55
|
+
panel("Labour cost", labour),
|
|
56
|
+
panel("Overhead cost", overhead),
|
|
57
|
+
panel("Units made", units),
|
|
58
|
+
error
|
|
59
|
+
),
|
|
60
|
+
stack(stats)
|
|
61
|
+
),
|
|
62
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
63
|
+
});
|
|
64
|
+
slot.append(card.node);
|
|
65
|
+
run();
|
|
66
|
+
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default { mount };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Encodes special characters as HTML entities, or decodes them back.
|
|
2
|
+
import { bench, stack, panel, textArea, notice, chipRow, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const ENCODE = [["&", "&"], ["<", "<"], [">", ">"], ['"', """], ["'", "'"]];
|
|
5
|
+
|
|
6
|
+
const encode = (text) => ENCODE.reduce((out, [char, entity]) => out.split(char).join(entity), text);
|
|
7
|
+
|
|
8
|
+
function decode(text) {
|
|
9
|
+
const area = document.createElement("textarea");
|
|
10
|
+
area.innerHTML = text;
|
|
11
|
+
return area.value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const source = textArea({ rows: 6, value: "<a href=\"/x\">Tom & Jerry</a>", "aria-label": "Input" });
|
|
16
|
+
const result = textArea({ rows: 6, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
|
|
17
|
+
const error = notice("error");
|
|
18
|
+
let mode = "encode";
|
|
19
|
+
|
|
20
|
+
const chips = chipRow(
|
|
21
|
+
[{ value: "encode", label: "Encode" }, { value: "decode", label: "Decode" }],
|
|
22
|
+
{ value: "encode", label: "Direction", onChange: (value) => { mode = value; run(); } }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function run() {
|
|
26
|
+
error.write("");
|
|
27
|
+
if (!source.value) {
|
|
28
|
+
result.value = "";
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
result.value = mode === "encode" ? encode(source.value) : decode(source.value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
source.addEventListener("input", run);
|
|
35
|
+
|
|
36
|
+
const card = toolcard({
|
|
37
|
+
body: bench(
|
|
38
|
+
stack(panel("Direction", chips), panel("Input", source), error),
|
|
39
|
+
stack(panel("Result", result))
|
|
40
|
+
),
|
|
41
|
+
foot: actions(copyButton(() => result.value, "Copy result", "fill")),
|
|
42
|
+
});
|
|
43
|
+
slot.append(card.node);
|
|
44
|
+
run();
|
|
45
|
+
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default { mount };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Reindents HTML by nesting depth and flags tags that never close.
|
|
2
|
+
import { bench, stack, panel, textArea, notice, chipRow, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const VOID = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
5
|
+
|
|
6
|
+
const tagName = (token) => (token.match(/^<\s*\/?\s*([a-zA-Z0-9:-]+)/) || [])[1]?.toLowerCase();
|
|
7
|
+
|
|
8
|
+
export function mount(slot) {
|
|
9
|
+
const source = textArea({
|
|
10
|
+
rows: 12,
|
|
11
|
+
class: "mvb-in mvb-in--area mvb-in--mono",
|
|
12
|
+
"aria-label": "HTML",
|
|
13
|
+
value: "<div class=\"card\"><h1>Title</h1><p>Text <strong>bold</strong></p><br></div>",
|
|
14
|
+
});
|
|
15
|
+
const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Formatted HTML" });
|
|
16
|
+
const warn = notice("warn");
|
|
17
|
+
let size = "2";
|
|
18
|
+
|
|
19
|
+
const chips = chipRow(
|
|
20
|
+
[{ value: "2", label: "2 spaces" }, { value: "4", label: "4 spaces" }, { value: "0", label: "Tabs" }],
|
|
21
|
+
{ value: "2", label: "Indent", onChange: (value) => { size = value; run(); } }
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
function run() {
|
|
25
|
+
warn.write("");
|
|
26
|
+
if (!source.value.trim()) {
|
|
27
|
+
result.value = "";
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const indent = size === "0" ? "\t" : " ".repeat(Number(size));
|
|
31
|
+
const tokens = source.value.replace(/>\s+</g, "><").match(/<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<[^>]+>|[^<]+/g) || [];
|
|
32
|
+
const lines = [];
|
|
33
|
+
const stack = [];
|
|
34
|
+
for (const raw of tokens) {
|
|
35
|
+
const token = raw.trim();
|
|
36
|
+
if (!token) continue;
|
|
37
|
+
if (token.startsWith("</")) {
|
|
38
|
+
const name = tagName(token);
|
|
39
|
+
const at = stack.lastIndexOf(name);
|
|
40
|
+
if (at !== -1) stack.length = at;
|
|
41
|
+
lines.push(indent.repeat(stack.length) + token);
|
|
42
|
+
} else if (token.startsWith("<!")) {
|
|
43
|
+
lines.push(indent.repeat(stack.length) + token);
|
|
44
|
+
} else if (token.startsWith("<")) {
|
|
45
|
+
const name = tagName(token);
|
|
46
|
+
const self = /\/>$/.test(token) || VOID.has(name);
|
|
47
|
+
lines.push(indent.repeat(stack.length) + token);
|
|
48
|
+
if (!self && name) stack.push(name);
|
|
49
|
+
} else {
|
|
50
|
+
lines.push(indent.repeat(stack.length) + token);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
result.value = lines.join("\n");
|
|
54
|
+
if (stack.length) warn.write(`Never closed: ${[...new Set(stack)].join(", ")}.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
source.addEventListener("input", run);
|
|
58
|
+
|
|
59
|
+
const card = toolcard({
|
|
60
|
+
body: bench(
|
|
61
|
+
stack(panel("Indent", chips), panel("HTML", source), warn),
|
|
62
|
+
stack(panel("Formatted", result))
|
|
63
|
+
),
|
|
64
|
+
foot: actions(copyButton(() => result.value, "Copy markup", "fill")),
|
|
65
|
+
});
|
|
66
|
+
slot.append(card.node);
|
|
67
|
+
run();
|
|
68
|
+
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default { mount };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Strips HTML tags with the browser parser and keeps the readable text.
|
|
2
|
+
import { bench, stack, panel, textArea, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function toText(html) {
|
|
5
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
6
|
+
return (doc.body?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function mount(slot) {
|
|
10
|
+
const source = textArea({ rows: 9, value: "<p>Hello <strong>world</strong></p>", "aria-label": "HTML" });
|
|
11
|
+
const result = output({ value: "", rows: 9 });
|
|
12
|
+
const error = notice("error");
|
|
13
|
+
let text = "";
|
|
14
|
+
|
|
15
|
+
function run() {
|
|
16
|
+
error.write("");
|
|
17
|
+
if (!source.value) {
|
|
18
|
+
text = "";
|
|
19
|
+
result.write("");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
text = toText(source.value);
|
|
24
|
+
result.write(text);
|
|
25
|
+
} catch {
|
|
26
|
+
text = "";
|
|
27
|
+
result.write("");
|
|
28
|
+
error.write("That HTML could not be read.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
source.addEventListener("input", run);
|
|
33
|
+
|
|
34
|
+
const card = toolcard({
|
|
35
|
+
body: bench(
|
|
36
|
+
stack(panel("HTML", source)),
|
|
37
|
+
stack(panel("Plain text", result), error)
|
|
38
|
+
),
|
|
39
|
+
foot: actions(copyButton(() => text, "Copy text", "fill")),
|
|
40
|
+
});
|
|
41
|
+
slot.append(card.node);
|
|
42
|
+
run();
|
|
43
|
+
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default { mount };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Reference for the common HTTP status codes, filterable by class and text.
|
|
2
|
+
import { el, bench, stack, panel, textInput, chipRow, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const CODES = [
|
|
5
|
+
[100, "Continue"], [101, "Switching Protocols"], [103, "Early Hints"],
|
|
6
|
+
[200, "OK"], [201, "Created"], [202, "Accepted"], [203, "Non-Authoritative Information"], [204, "No Content"], [205, "Reset Content"], [206, "Partial Content"],
|
|
7
|
+
[300, "Multiple Choices"], [301, "Moved Permanently"], [302, "Found"], [303, "See Other"], [304, "Not Modified"], [307, "Temporary Redirect"], [308, "Permanent Redirect"],
|
|
8
|
+
[400, "Bad Request"], [401, "Unauthorized"], [402, "Payment Required"], [403, "Forbidden"], [404, "Not Found"], [405, "Method Not Allowed"], [406, "Not Acceptable"], [408, "Request Timeout"], [409, "Conflict"], [410, "Gone"], [411, "Length Required"], [412, "Precondition Failed"], [413, "Payload Too Large"], [414, "URI Too Long"], [415, "Unsupported Media Type"], [416, "Range Not Satisfiable"], [418, "I'm a teapot"], [422, "Unprocessable Entity"], [425, "Too Early"], [426, "Upgrade Required"], [428, "Precondition Required"], [429, "Too Many Requests"], [431, "Request Header Fields Too Large"], [451, "Unavailable For Legal Reasons"],
|
|
9
|
+
[500, "Internal Server Error"], [501, "Not Implemented"], [502, "Bad Gateway"], [503, "Service Unavailable"], [504, "Gateway Timeout"], [505, "HTTP Version Not Supported"], [507, "Insufficient Storage"], [508, "Loop Detected"], [511, "Network Authentication Required"],
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const statusClass = (code) => `${Math.floor(code / 100)}xx`;
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const filter = textInput({ type: "search", placeholder: "Code or reason", "aria-label": "Filter" });
|
|
16
|
+
const scroll = el("div", { class: "mvb-scroll" });
|
|
17
|
+
let group = "all";
|
|
18
|
+
let visible = CODES;
|
|
19
|
+
|
|
20
|
+
const chips = chipRow(
|
|
21
|
+
[{ value: "all", label: "All" }, { value: "2xx", label: "2xx" }, { value: "3xx", label: "3xx" }, { value: "4xx", label: "4xx" }, { value: "5xx", label: "5xx" }],
|
|
22
|
+
{ value: "all", label: "Class", onChange: (value) => { group = value; run(); } }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function run() {
|
|
26
|
+
const query = filter.value.trim().toLowerCase();
|
|
27
|
+
visible = CODES.filter(([code, message]) => {
|
|
28
|
+
if (group !== "all" && statusClass(code) !== group) return false;
|
|
29
|
+
if (!query) return true;
|
|
30
|
+
return String(code).includes(query) || message.toLowerCase().includes(query);
|
|
31
|
+
});
|
|
32
|
+
if (!visible.length) {
|
|
33
|
+
scroll.replaceChildren(el("p", { class: "mvb-note" }, "No code matches."));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
scroll.replaceChildren(el(
|
|
37
|
+
"table",
|
|
38
|
+
{ class: "mvb-ttable" },
|
|
39
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Code"), el("th", {}, "Message"), el("th", {}, "Class"))),
|
|
40
|
+
el("tbody", {}, ...visible.map(([code, message]) => el("tr", {},
|
|
41
|
+
el("td", {}, String(code)),
|
|
42
|
+
el("td", {}, message),
|
|
43
|
+
el("td", {}, statusClass(code))
|
|
44
|
+
)))
|
|
45
|
+
));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
filter.addEventListener("input", run);
|
|
49
|
+
|
|
50
|
+
const card = toolcard({
|
|
51
|
+
body: bench(
|
|
52
|
+
stack(panel("Filter", filter), panel("Class", chips)),
|
|
53
|
+
stack(panel("Status codes", scroll))
|
|
54
|
+
),
|
|
55
|
+
foot: actions(copyButton(() => visible.map(([code, message]) => `${code} ${message}`).join("\n"), "Copy list", "fill")),
|
|
56
|
+
});
|
|
57
|
+
slot.append(card.node);
|
|
58
|
+
run();
|
|
59
|
+
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default { mount };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Builds an IEEE numbered reference from source details.
|
|
2
|
+
import { bench, stack, panel, textInput, numberInput, selectInput, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const TYPES = [
|
|
5
|
+
{ value: "journal", label: "Journal article" },
|
|
6
|
+
{ value: "conference", label: "Conference paper" },
|
|
7
|
+
{ value: "book", label: "Book" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const parts = (raw) => raw.split(";").map((item) => item.trim()).filter(Boolean);
|
|
11
|
+
|
|
12
|
+
function flip(author) {
|
|
13
|
+
const bits = author.split(",").map((item) => item.trim());
|
|
14
|
+
return bits.length >= 2 ? `${bits[1]} ${bits[0]}` : author;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function authorList(raw) {
|
|
18
|
+
const list = parts(raw).map(flip);
|
|
19
|
+
if (!list.length) return "";
|
|
20
|
+
if (list.length === 1) return list[0];
|
|
21
|
+
return `${list.slice(0, -1).join(", ")}, and ${list[list.length - 1]}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mount(slot) {
|
|
25
|
+
const number = numberInput({ value: "1", min: "1", "aria-label": "Reference number" });
|
|
26
|
+
const type = selectInput(TYPES, { value: "journal", "aria-label": "Source type" });
|
|
27
|
+
const authors = textInput({ placeholder: "Smith, J. A.; Doe, B.", "aria-label": "Authors" });
|
|
28
|
+
const title = textInput({ placeholder: "Title of the work", "aria-label": "Title" });
|
|
29
|
+
const source = textInput({ placeholder: "Journal, conference or publisher", "aria-label": "Source" });
|
|
30
|
+
const details = textInput({ placeholder: "vol. 1, no. 2, pp. 3-4", "aria-label": "Details" });
|
|
31
|
+
const year = textInput({ placeholder: "2020", "aria-label": "Year" });
|
|
32
|
+
const link = textInput({ placeholder: "https://doi.org/10.0000/abc", "aria-label": "DOI or URL" });
|
|
33
|
+
|
|
34
|
+
const reference = output({ rows: 6 });
|
|
35
|
+
const citation = output({ rows: 1 });
|
|
36
|
+
const note = notice("note");
|
|
37
|
+
note.write("Check the details against the source and the IEEE guide.");
|
|
38
|
+
let refText = "";
|
|
39
|
+
|
|
40
|
+
function build() {
|
|
41
|
+
const n = number.value.trim() || "1";
|
|
42
|
+
const au = authorList(authors.value);
|
|
43
|
+
const ti = title.value.trim();
|
|
44
|
+
const src = source.value.trim();
|
|
45
|
+
const det = details.value.trim();
|
|
46
|
+
const yr = year.value.trim();
|
|
47
|
+
const url = link.value.trim();
|
|
48
|
+
|
|
49
|
+
const body = [];
|
|
50
|
+
if (au) body.push(au);
|
|
51
|
+
if (type.value === "book") {
|
|
52
|
+
if (ti) body.push(ti);
|
|
53
|
+
if (src) body.push(src);
|
|
54
|
+
if (det) body.push(det);
|
|
55
|
+
} else {
|
|
56
|
+
if (ti) body.push(`"${ti}"`);
|
|
57
|
+
if (type.value === "conference") {
|
|
58
|
+
if (src) body.push(`in ${src}`);
|
|
59
|
+
} else if (src) {
|
|
60
|
+
body.push(src);
|
|
61
|
+
}
|
|
62
|
+
if (det) body.push(det);
|
|
63
|
+
}
|
|
64
|
+
if (yr) body.push(yr);
|
|
65
|
+
if (url) body.push(url);
|
|
66
|
+
|
|
67
|
+
refText = `[${n}] ${body.join(", ")}`;
|
|
68
|
+
if (!/[.!?]$/.test(refText)) refText += ".";
|
|
69
|
+
reference.write(refText);
|
|
70
|
+
citation.write(`[${n}]`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const input of [number, authors, title, source, details, year, link]) input.addEventListener("input", build);
|
|
74
|
+
type.addEventListener("change", build);
|
|
75
|
+
|
|
76
|
+
const card = toolcard({
|
|
77
|
+
body: bench(
|
|
78
|
+
stack(
|
|
79
|
+
panel("Number", number),
|
|
80
|
+
panel("Source type", type),
|
|
81
|
+
panel("Authors", authors),
|
|
82
|
+
panel("Title", title),
|
|
83
|
+
panel("Source", source),
|
|
84
|
+
panel("Details", details),
|
|
85
|
+
panel("Year", year),
|
|
86
|
+
panel("DOI or URL", link),
|
|
87
|
+
note
|
|
88
|
+
),
|
|
89
|
+
stack(panel("Reference", reference), panel("Bracketed citation", citation))
|
|
90
|
+
),
|
|
91
|
+
foot: actions(
|
|
92
|
+
copyButton(() => citation.textContent, "Copy number"),
|
|
93
|
+
copyButton(() => refText, "Copy reference", "fill")
|
|
94
|
+
),
|
|
95
|
+
});
|
|
96
|
+
slot.append(card.node);
|
|
97
|
+
build();
|
|
98
|
+
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default { mount };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Formats an in-text citation in APA, Harvard or MLA form.
|
|
2
|
+
import { bench, stack, panel, textInput, selectInput, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const STYLES = [
|
|
5
|
+
{ value: "apa", label: "APA" },
|
|
6
|
+
{ value: "harvard", label: "Harvard" },
|
|
7
|
+
{ value: "mla", label: "MLA" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const FORMS = [
|
|
11
|
+
{ value: "parenthetical", label: "Parenthetical" },
|
|
12
|
+
{ value: "narrative", label: "Narrative" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const parts = (raw) => raw.split(";").map((item) => item.trim()).filter(Boolean);
|
|
16
|
+
const surname = (author) => author.split(",")[0].trim();
|
|
17
|
+
|
|
18
|
+
export function mount(slot) {
|
|
19
|
+
const style = selectInput(STYLES, { value: "apa", "aria-label": "Style" });
|
|
20
|
+
const form = selectInput(FORMS, { value: "parenthetical", "aria-label": "Form" });
|
|
21
|
+
const authors = textInput({ placeholder: "Smith, J. A.; Doe, B.", "aria-label": "Authors" });
|
|
22
|
+
const year = textInput({ placeholder: "2020", "aria-label": "Year" });
|
|
23
|
+
const page = textInput({ placeholder: "5", "aria-label": "Page" });
|
|
24
|
+
|
|
25
|
+
const result = output({ rows: 3 });
|
|
26
|
+
const note = notice("note");
|
|
27
|
+
note.write("Check the details against the source and the style guide.");
|
|
28
|
+
let text = "";
|
|
29
|
+
|
|
30
|
+
function build() {
|
|
31
|
+
const list = parts(authors.value);
|
|
32
|
+
const y = year.value.trim();
|
|
33
|
+
const p = page.value.trim();
|
|
34
|
+
const join = style.value === "harvard" ? " and " : " & ";
|
|
35
|
+
let who = "";
|
|
36
|
+
if (list.length === 1) who = surname(list[0]);
|
|
37
|
+
else if (list.length === 2) who = `${surname(list[0])}${join}${surname(list[1])}`;
|
|
38
|
+
else if (list.length > 2) who = `${surname(list[0])} et al.`;
|
|
39
|
+
|
|
40
|
+
if (style.value === "mla") {
|
|
41
|
+
text = form.value === "narrative" ? who : [who, p].filter(Boolean).length ? `(${[who, p].filter(Boolean).join(" ")})` : "";
|
|
42
|
+
} else {
|
|
43
|
+
const yearPart = [y, p ? `p. ${p}` : ""].filter(Boolean).join(", ");
|
|
44
|
+
text = form.value === "narrative"
|
|
45
|
+
? [who, yearPart ? `(${yearPart})` : ""].filter(Boolean).join(" ")
|
|
46
|
+
: `(${[who, y, p ? `p. ${p}` : ""].filter(Boolean).join(", ")})`;
|
|
47
|
+
if (text === "()") text = "";
|
|
48
|
+
}
|
|
49
|
+
result.write(text);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const input of [authors, year, page]) input.addEventListener("input", build);
|
|
53
|
+
for (const select of [style, form]) select.addEventListener("change", build);
|
|
54
|
+
|
|
55
|
+
const card = toolcard({
|
|
56
|
+
body: bench(
|
|
57
|
+
stack(
|
|
58
|
+
panel("Style", style),
|
|
59
|
+
panel("Form", form),
|
|
60
|
+
panel("Authors", authors),
|
|
61
|
+
panel("Year", year),
|
|
62
|
+
panel("Page", page),
|
|
63
|
+
note
|
|
64
|
+
),
|
|
65
|
+
stack(panel("Citation", result))
|
|
66
|
+
),
|
|
67
|
+
foot: actions(copyButton(() => text, "Copy citation", "fill")),
|
|
68
|
+
});
|
|
69
|
+
slot.append(card.node);
|
|
70
|
+
build();
|
|
71
|
+
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default { mount };
|