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,103 @@
|
|
|
1
|
+
// Simplifies a ratio and solves the missing term in a proportion.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, textInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function gcd(a, b) {
|
|
5
|
+
a = Math.abs(a);
|
|
6
|
+
b = Math.abs(b);
|
|
7
|
+
while (b) {
|
|
8
|
+
const next = a % b;
|
|
9
|
+
a = b;
|
|
10
|
+
b = next;
|
|
11
|
+
}
|
|
12
|
+
return a;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fmt(n) {
|
|
16
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const kvRow = (key, value) =>
|
|
20
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
|
|
21
|
+
|
|
22
|
+
export function mount(slot) {
|
|
23
|
+
const ratioInput = textInput({ value: "12:18:24", "aria-label": "Ratio" });
|
|
24
|
+
const a = numberInput({ placeholder: "A", "aria-label": "First term" });
|
|
25
|
+
const b = numberInput({ placeholder: "B", "aria-label": "Second term" });
|
|
26
|
+
const c = numberInput({ placeholder: "C", "aria-label": "Third term" });
|
|
27
|
+
const d = numberInput({ placeholder: "D", "aria-label": "Fourth term" });
|
|
28
|
+
const simplified = el("span", { class: "mvb-kv__v" }, "-");
|
|
29
|
+
const decimal = el("span", { class: "mvb-kv__v" }, "-");
|
|
30
|
+
const missing = el("span", { class: "mvb-kv__v" }, "-");
|
|
31
|
+
const kv = el(
|
|
32
|
+
"div",
|
|
33
|
+
{ class: "mvb-kv" },
|
|
34
|
+
kvRow("Simplified ratio", simplified),
|
|
35
|
+
kvRow("Decimal", decimal),
|
|
36
|
+
kvRow("Missing term", missing)
|
|
37
|
+
);
|
|
38
|
+
const error = notice("error");
|
|
39
|
+
let text = "";
|
|
40
|
+
|
|
41
|
+
function simplify() {
|
|
42
|
+
const tokens = ratioInput.value.split(/[^0-9.]+/).filter(Boolean);
|
|
43
|
+
const nums = tokens.map(Number);
|
|
44
|
+
simplified.textContent = "-";
|
|
45
|
+
decimal.textContent = "-";
|
|
46
|
+
if (!ratioInput.value.trim()) return "";
|
|
47
|
+
if (tokens.length < 2 || !nums.every(Number.isFinite)) {
|
|
48
|
+
error.write("Use numbers separated by colons, for example 12:18:24.");
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
const divisor = nums.reduce((acc, n) => gcd(acc, n), 0);
|
|
52
|
+
if (!divisor) return "";
|
|
53
|
+
const out = nums.map((n) => n / divisor);
|
|
54
|
+
simplified.textContent = out.join(" : ");
|
|
55
|
+
if (nums.length === 2 && nums[1] !== 0) decimal.textContent = fmt(nums[0] / nums[1]);
|
|
56
|
+
return out.join(":");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function solve() {
|
|
60
|
+
const terms = [a, b, c, d].map((input) => input.value.trim());
|
|
61
|
+
missing.textContent = "-";
|
|
62
|
+
if (terms.filter((term) => term !== "").length !== 3) return "";
|
|
63
|
+
const nums = terms.map((term) => (term === "" ? null : Number(term)));
|
|
64
|
+
if (nums.some((n) => n !== null && !Number.isFinite(n))) return "";
|
|
65
|
+
const [A, B, C, D] = nums;
|
|
66
|
+
let value = null;
|
|
67
|
+
if (D === null) value = A === 0 ? null : (B * C) / A;
|
|
68
|
+
else if (A === null) value = D === 0 ? null : (B * C) / D;
|
|
69
|
+
else if (B === null) value = C === 0 ? null : (A * D) / C;
|
|
70
|
+
else if (C === null) value = A === 0 ? null : (B * D) / A;
|
|
71
|
+
if (value === null || !Number.isFinite(value)) return "";
|
|
72
|
+
missing.textContent = fmt(value);
|
|
73
|
+
return fmt(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function run() {
|
|
77
|
+
error.write("");
|
|
78
|
+
const ratio = simplify();
|
|
79
|
+
const term = solve();
|
|
80
|
+
text = ratio || term;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ratioInput.addEventListener("input", run);
|
|
84
|
+
for (const input of [a, b, c, d]) input.addEventListener("input", run);
|
|
85
|
+
|
|
86
|
+
const card = toolcard({
|
|
87
|
+
body: bench(
|
|
88
|
+
stack(
|
|
89
|
+
panel("Ratio", ratioInput),
|
|
90
|
+
panel("Proportion", el("div", { class: "mvb-row" }, a, b, c, d)),
|
|
91
|
+
error
|
|
92
|
+
),
|
|
93
|
+
stack(panel("Result", kv))
|
|
94
|
+
),
|
|
95
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
96
|
+
});
|
|
97
|
+
slot.append(card.node);
|
|
98
|
+
run();
|
|
99
|
+
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default { mount };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Common readability formulas over a pasted text, with the counts behind them.
|
|
2
|
+
import { el, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function wordsOf(text) {
|
|
5
|
+
const trimmed = text.trim();
|
|
6
|
+
if (!trimmed) return [];
|
|
7
|
+
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
8
|
+
try {
|
|
9
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "word" });
|
|
10
|
+
return [...segmenter.segment(trimmed)].filter((part) => part.isWordLike).map((part) => part.segment);
|
|
11
|
+
} catch {
|
|
12
|
+
/* fall back to the whitespace split below */
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return trimmed.split(/\s+/);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sentencesOf = (text) => text.trim()
|
|
19
|
+
? text.trim().split(/[.!?\u2026]+/).map((part) => part.trim()).filter(Boolean)
|
|
20
|
+
: [];
|
|
21
|
+
|
|
22
|
+
function syllablesOf(word) {
|
|
23
|
+
const clean = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
24
|
+
if (!clean) return 0;
|
|
25
|
+
const groups = clean.replace(/e$/, "").match(/[aeiouy]+/g);
|
|
26
|
+
return Math.max(1, groups ? groups.length : 1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tile(label, initial = "-") {
|
|
30
|
+
const value = el("span", { class: "mvb-stat__v" }, initial);
|
|
31
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
32
|
+
node.set = (text) => { value.textContent = text; };
|
|
33
|
+
return node;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const kvRow = (key, value) => el("div", { class: "mvb-kv__row" },
|
|
37
|
+
el("span", { class: "mvb-kv__k" }, key),
|
|
38
|
+
el("span", { class: "mvb-kv__v" }, value));
|
|
39
|
+
|
|
40
|
+
export function mount(slot) {
|
|
41
|
+
const source = textArea({ rows: 12, "aria-label": "Text", placeholder: "Type or paste text" });
|
|
42
|
+
|
|
43
|
+
const easeTile = tile("Reading ease");
|
|
44
|
+
const gradeTile = tile("Grade level");
|
|
45
|
+
const fogTile = tile("Gunning Fog");
|
|
46
|
+
const stats = el("div", { class: "mvb-stats" }, easeTile, gradeTile, fogTile);
|
|
47
|
+
|
|
48
|
+
const counts = {
|
|
49
|
+
words: el("span", { class: "mvb-kv__v" }, "0"),
|
|
50
|
+
sentences: el("span", { class: "mvb-kv__v" }, "0"),
|
|
51
|
+
syllables: el("span", { class: "mvb-kv__v" }, "0"),
|
|
52
|
+
};
|
|
53
|
+
const countList = el("div", { class: "mvb-kv" },
|
|
54
|
+
kvRow("Words", counts.words),
|
|
55
|
+
kvRow("Sentences", counts.sentences),
|
|
56
|
+
kvRow("Syllables", counts.syllables));
|
|
57
|
+
|
|
58
|
+
const note = notice("note");
|
|
59
|
+
note.write("These formulas suit English best, so read the result with care.");
|
|
60
|
+
|
|
61
|
+
let summary = "";
|
|
62
|
+
|
|
63
|
+
function run() {
|
|
64
|
+
const words = wordsOf(source.value);
|
|
65
|
+
const sentences = sentencesOf(source.value);
|
|
66
|
+
const syllableCount = words.reduce((sum, word) => sum + syllablesOf(word), 0);
|
|
67
|
+
counts.words.textContent = String(words.length);
|
|
68
|
+
counts.sentences.textContent = String(sentences.length);
|
|
69
|
+
counts.syllables.textContent = String(syllableCount);
|
|
70
|
+
|
|
71
|
+
if (!words.length || !sentences.length) {
|
|
72
|
+
easeTile.set("-");
|
|
73
|
+
gradeTile.set("-");
|
|
74
|
+
fogTile.set("-");
|
|
75
|
+
summary = "";
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const perSentence = words.length / sentences.length;
|
|
80
|
+
const perWord = syllableCount / words.length;
|
|
81
|
+
const ease = 206.835 - 1.015 * perSentence - 84.6 * perWord;
|
|
82
|
+
const grade = 0.39 * perSentence + 11.8 * perWord - 15.59;
|
|
83
|
+
const complex = words.filter((word) => syllablesOf(word) >= 3).length;
|
|
84
|
+
const fog = 0.4 * (perSentence + 100 * (complex / words.length));
|
|
85
|
+
|
|
86
|
+
easeTile.set(ease.toFixed(1));
|
|
87
|
+
gradeTile.set(grade.toFixed(1));
|
|
88
|
+
fogTile.set(fog.toFixed(1));
|
|
89
|
+
summary = [
|
|
90
|
+
`Reading ease ${ease.toFixed(1)}`,
|
|
91
|
+
`Grade level ${grade.toFixed(1)}`,
|
|
92
|
+
`Gunning Fog ${fog.toFixed(1)}`,
|
|
93
|
+
`${words.length} words, ${sentences.length} sentences, ${syllableCount} syllables`,
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
source.addEventListener("input", run);
|
|
98
|
+
|
|
99
|
+
const card = toolcard({
|
|
100
|
+
body: bench(
|
|
101
|
+
stack(panel("Text", source)),
|
|
102
|
+
stack(stats, countList, note)
|
|
103
|
+
),
|
|
104
|
+
foot: actions(copyButton(() => summary, "Copy scores", "fill")),
|
|
105
|
+
});
|
|
106
|
+
slot.append(card.node);
|
|
107
|
+
run();
|
|
108
|
+
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default { mount };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Reading and speaking time for a text, at a speed the user sets.
|
|
2
|
+
import { el, bench, stack, panel, textArea, numberInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const countWords = (text) => {
|
|
5
|
+
const trimmed = text.trim();
|
|
6
|
+
if (!trimmed) return 0;
|
|
7
|
+
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
8
|
+
try {
|
|
9
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "word" });
|
|
10
|
+
let count = 0;
|
|
11
|
+
for (const part of segmenter.segment(trimmed)) if (part.isWordLike) count++;
|
|
12
|
+
return count;
|
|
13
|
+
} catch {
|
|
14
|
+
/* fall back to the whitespace split below */
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return trimmed.split(/\s+/).length;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const clock = (minutes) => {
|
|
21
|
+
const total = Math.round(minutes * 60);
|
|
22
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function tile(label) {
|
|
26
|
+
const value = el("span", { class: "mvb-stat__v" }, "0:00");
|
|
27
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
28
|
+
node.set = (text) => { value.textContent = text; };
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function mount(slot) {
|
|
33
|
+
const source = textArea({ rows: 10, "aria-label": "Text", placeholder: "Type or paste text" });
|
|
34
|
+
const speed = numberInput({ min: "50", max: "1000", step: "10", value: "200", "aria-label": "Reading speed in words per minute" });
|
|
35
|
+
|
|
36
|
+
const wordsTile = tile("Words");
|
|
37
|
+
const readTile = tile("Reading time");
|
|
38
|
+
const speakTile = tile("Speaking time");
|
|
39
|
+
const stats = el("div", { class: "mvb-stats" }, wordsTile, readTile, speakTile);
|
|
40
|
+
const note = notice("note");
|
|
41
|
+
note.write("Reading and speaking speeds vary by person and text.");
|
|
42
|
+
|
|
43
|
+
let summary = "";
|
|
44
|
+
|
|
45
|
+
function run() {
|
|
46
|
+
const words = countWords(source.value);
|
|
47
|
+
const wpm = Math.max(1, parseInt(speed.value, 10) || 200);
|
|
48
|
+
const readMinutes = words / wpm;
|
|
49
|
+
const speakMinutes = words / 130;
|
|
50
|
+
wordsTile.set(String(words));
|
|
51
|
+
readTile.set(words ? clock(readMinutes) : "0:00");
|
|
52
|
+
speakTile.set(words ? clock(speakMinutes) : "0:00");
|
|
53
|
+
summary = [
|
|
54
|
+
`${words} words`,
|
|
55
|
+
`Reading ${words ? clock(readMinutes) : "0:00"} at ${wpm} words per minute`,
|
|
56
|
+
`Speaking ${words ? clock(speakMinutes) : "0:00"} at 130 words per minute`,
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
source.addEventListener("input", run);
|
|
61
|
+
speed.addEventListener("input", run);
|
|
62
|
+
|
|
63
|
+
const card = toolcard({
|
|
64
|
+
body: bench(
|
|
65
|
+
stack(panel("Text", source), panel("Reading speed", speed, "Words per minute")),
|
|
66
|
+
stack(stats, note)
|
|
67
|
+
),
|
|
68
|
+
foot: actions(copyButton(() => summary, "Copy estimate", "fill")),
|
|
69
|
+
});
|
|
70
|
+
slot.append(card.node);
|
|
71
|
+
run();
|
|
72
|
+
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default { mount };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Drafts a README from a project name, description and the sections you pick.
|
|
2
|
+
import { bench, stack, panel, textInput, toggleRow, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
export function mount(slot) {
|
|
5
|
+
const name = textInput({ value: "my-project", "aria-label": "Project name" });
|
|
6
|
+
const description = textInput({ value: "A short description of the project.", "aria-label": "Description" });
|
|
7
|
+
const license = textInput({ value: "MIT", "aria-label": "License" });
|
|
8
|
+
const sections = toggleRow([
|
|
9
|
+
{ value: "features", label: "Features" },
|
|
10
|
+
{ value: "install", label: "Installation" },
|
|
11
|
+
{ value: "usage", label: "Usage" },
|
|
12
|
+
{ value: "license", label: "License" },
|
|
13
|
+
{ value: "contributing", label: "Contributing", checked: false },
|
|
14
|
+
]);
|
|
15
|
+
const out = output({ rows: 18 });
|
|
16
|
+
let text = "";
|
|
17
|
+
|
|
18
|
+
function run() {
|
|
19
|
+
const picked = new Set(sections.values());
|
|
20
|
+
const project = name.value.trim() || "Project";
|
|
21
|
+
const lines = [`# ${project}`, ""];
|
|
22
|
+
if (description.value.trim()) lines.push(description.value.trim(), "");
|
|
23
|
+
if (picked.has("features")) lines.push("## Features", "", "- First feature", "- Second feature", "");
|
|
24
|
+
if (picked.has("install")) lines.push("## Installation", "", "```bash", `npm install ${project}`, "```", "");
|
|
25
|
+
if (picked.has("usage")) lines.push("## Usage", "", "```js", "// Example", "```", "");
|
|
26
|
+
if (picked.has("license")) lines.push("## License", "", license.value.trim() || "MIT", "");
|
|
27
|
+
if (picked.has("contributing")) lines.push("## Contributing", "", "Issues and pull requests are welcome.", "");
|
|
28
|
+
text = `${lines.join("\n").replace(/\n+$/, "")}\n`;
|
|
29
|
+
out.write(text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
name.addEventListener("input", run);
|
|
33
|
+
description.addEventListener("input", run);
|
|
34
|
+
license.addEventListener("input", run);
|
|
35
|
+
sections.addEventListener("change", run);
|
|
36
|
+
|
|
37
|
+
const card = toolcard({
|
|
38
|
+
body: bench(
|
|
39
|
+
stack(panel("Project name", name), panel("Description", description), panel("License", license), panel("Sections", sections)),
|
|
40
|
+
stack(panel("README", out))
|
|
41
|
+
),
|
|
42
|
+
foot: actions(copyButton(() => text, "Copy Markdown", "fill")),
|
|
43
|
+
});
|
|
44
|
+
slot.append(card.node);
|
|
45
|
+
run();
|
|
46
|
+
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default { mount };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Checks a reference for the usual parts.
|
|
2
|
+
import { el, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const PARTS = ["Author", "Year", "Title", "Source"];
|
|
5
|
+
|
|
6
|
+
function kvRow(key) {
|
|
7
|
+
const value = el("span", { class: "mvb-kv__v" }, "-");
|
|
8
|
+
const row = el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), value);
|
|
9
|
+
return { row, value };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function check(text) {
|
|
13
|
+
const t = text.trim();
|
|
14
|
+
return {
|
|
15
|
+
Author: /^[A-Z][A-Za-z'’.-]+,/.test(t) || /\b(and|&)\s+[A-Z]/.test(t),
|
|
16
|
+
Year: /\b(1[5-9]\d{2}|20\d{2})\b/.test(t),
|
|
17
|
+
Title: /["“][^"”]{2,}["”]/.test(t) || t.replace(/^[^.]*\./, "").trim().length > 8,
|
|
18
|
+
Source: /https?:\/\/|www\.|\bIn\b|\b(Journal|Press|Publishing|Publishers|University|vol\.|pp\.|ed\.)\b/i.test(t),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mount(slot) {
|
|
23
|
+
const source = textArea({ rows: 6, "aria-label": "Reference", placeholder: "Paste one reference" });
|
|
24
|
+
const rows = {};
|
|
25
|
+
const kv = el("div", { class: "mvb-kv" });
|
|
26
|
+
for (const key of PARTS) {
|
|
27
|
+
rows[key] = kvRow(key);
|
|
28
|
+
kv.append(rows[key].row);
|
|
29
|
+
}
|
|
30
|
+
const status = notice("note");
|
|
31
|
+
const note = notice("note");
|
|
32
|
+
note.write("A complete reference is not proof the source is real.");
|
|
33
|
+
let summary = "";
|
|
34
|
+
|
|
35
|
+
function run() {
|
|
36
|
+
const hasText = source.value.trim() !== "";
|
|
37
|
+
const found = check(source.value);
|
|
38
|
+
const missing = [];
|
|
39
|
+
for (const key of PARTS) {
|
|
40
|
+
rows[key].value.textContent = hasText ? (found[key] ? "Found" : "Missing") : "-";
|
|
41
|
+
if (hasText && !found[key]) missing.push(key);
|
|
42
|
+
}
|
|
43
|
+
status.write(hasText ? (missing.length ? `Missing: ${missing.join(", ")}.` : "All four parts found.") : "");
|
|
44
|
+
summary = hasText ? PARTS.map((key) => `${key}: ${found[key] ? "found" : "missing"}`).join("\n") : "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
source.addEventListener("input", run);
|
|
48
|
+
|
|
49
|
+
const card = toolcard({
|
|
50
|
+
body: bench(
|
|
51
|
+
stack(panel("Reference", source), note),
|
|
52
|
+
stack(panel("Parts", kv), status)
|
|
53
|
+
),
|
|
54
|
+
foot: actions(copyButton(() => summary, "Copy check", "fill")),
|
|
55
|
+
});
|
|
56
|
+
slot.append(card.node);
|
|
57
|
+
run();
|
|
58
|
+
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default { mount };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Tests a pattern against sample text, highlighting matches and listing groups.
|
|
2
|
+
import { el, bench, stack, panel, textInput, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const MAX_MATCHES = 1000;
|
|
5
|
+
|
|
6
|
+
export function mount(slot) {
|
|
7
|
+
const patternInput = textInput({ value: "\\b(\\w+)@(\\w+)\\.com\\b", "aria-label": "Pattern", spellcheck: "false" });
|
|
8
|
+
const flagsInput = textInput({ value: "g", "aria-label": "Flags", spellcheck: "false", placeholder: "g i m s u y" });
|
|
9
|
+
const source = textArea({
|
|
10
|
+
rows: 8,
|
|
11
|
+
class: "mvb-in mvb-in--area mvb-in--mono",
|
|
12
|
+
"aria-label": "Text to test",
|
|
13
|
+
value: "Contact ada@example.com or grace@navy.com for details.",
|
|
14
|
+
});
|
|
15
|
+
const pane = el("div", { class: "mvb-pane" });
|
|
16
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
17
|
+
const error = notice("error");
|
|
18
|
+
let lines = [];
|
|
19
|
+
|
|
20
|
+
function run() {
|
|
21
|
+
error.write("");
|
|
22
|
+
pane.replaceChildren();
|
|
23
|
+
listWrap.replaceChildren();
|
|
24
|
+
lines = [];
|
|
25
|
+
const pattern = patternInput.value;
|
|
26
|
+
if (!pattern) return;
|
|
27
|
+
let flags = flagsInput.value.replace(/[^gimsuy]/g, "");
|
|
28
|
+
if (!flags.includes("g")) flags += "g";
|
|
29
|
+
let re;
|
|
30
|
+
try {
|
|
31
|
+
re = new RegExp(pattern, flags);
|
|
32
|
+
} catch (caught) {
|
|
33
|
+
error.write(caught.message);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const text = source.value;
|
|
37
|
+
const nodes = [];
|
|
38
|
+
const rows = [];
|
|
39
|
+
let last = 0;
|
|
40
|
+
let count = 0;
|
|
41
|
+
let match;
|
|
42
|
+
while ((match = re.exec(text)) && count < MAX_MATCHES) {
|
|
43
|
+
if (match.index === re.lastIndex) re.lastIndex++;
|
|
44
|
+
nodes.push(document.createTextNode(text.slice(last, match.index)));
|
|
45
|
+
nodes.push(el("span", { class: "mvb-hl" }, match[0] || "\u200b"));
|
|
46
|
+
last = match.index + match[0].length;
|
|
47
|
+
const groups = match.slice(1).map((group) => group ?? "").join(", ");
|
|
48
|
+
rows.push(el("tr", {},
|
|
49
|
+
el("th", {}, String(count + 1)),
|
|
50
|
+
el("td", {}, match[0] || "(empty)"),
|
|
51
|
+
el("td", {}, groups)
|
|
52
|
+
));
|
|
53
|
+
lines.push(match[0]);
|
|
54
|
+
count++;
|
|
55
|
+
}
|
|
56
|
+
nodes.push(document.createTextNode(text.slice(last)));
|
|
57
|
+
pane.append(...nodes);
|
|
58
|
+
if (!count) {
|
|
59
|
+
listWrap.append(el("p", { class: "mvb-note" }, "No matches."));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const table = el("table", { class: "mvb-ttable" },
|
|
63
|
+
el("thead", {}, el("tr", {}, el("th", {}, "#"), el("th", {}, "Match"), el("th", {}, "Groups"))),
|
|
64
|
+
el("tbody", {}, ...rows)
|
|
65
|
+
);
|
|
66
|
+
listWrap.append(table);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
patternInput.addEventListener("input", run);
|
|
70
|
+
flagsInput.addEventListener("input", run);
|
|
71
|
+
source.addEventListener("input", run);
|
|
72
|
+
|
|
73
|
+
const card = toolcard({
|
|
74
|
+
body: bench(
|
|
75
|
+
stack(panel("Pattern", patternInput), panel("Flags", flagsInput), panel("Text", source), error),
|
|
76
|
+
stack(panel("Matches", pane), panel("Groups", listWrap))
|
|
77
|
+
),
|
|
78
|
+
foot: actions(copyButton(() => lines.join("\n"), "Copy matches", "fill")),
|
|
79
|
+
});
|
|
80
|
+
slot.append(card.node);
|
|
81
|
+
run();
|
|
82
|
+
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default { mount };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Drops repeated lines and keeps the first time each one appears.
|
|
2
|
+
import { bench, stack, panel, textArea, checkbox, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
export function mount(slot) {
|
|
5
|
+
const source = textArea({ rows: 12, "aria-label": "Text", placeholder: "Paste your list, one entry per line" });
|
|
6
|
+
const ignoreCase = checkbox("Ignore case", { checked: false });
|
|
7
|
+
const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
|
|
8
|
+
const note = notice("note");
|
|
9
|
+
|
|
10
|
+
function run() {
|
|
11
|
+
const text = source.value;
|
|
12
|
+
if (!text) {
|
|
13
|
+
result.value = "";
|
|
14
|
+
note.write("");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const lines = text.split("\n");
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const kept = [];
|
|
20
|
+
let removed = 0;
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const key = ignoreCase.input.checked ? line.toLowerCase() : line;
|
|
23
|
+
if (seen.has(key)) removed++;
|
|
24
|
+
else { seen.add(key); kept.push(line); }
|
|
25
|
+
}
|
|
26
|
+
result.value = kept.join("\n");
|
|
27
|
+
note.write(removed ? `${removed} lines removed.` : "No duplicate lines found.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
source.addEventListener("input", run);
|
|
31
|
+
ignoreCase.input.addEventListener("change", run);
|
|
32
|
+
|
|
33
|
+
const card = toolcard({
|
|
34
|
+
body: bench(
|
|
35
|
+
stack(panel("Text", source), ignoreCase, note),
|
|
36
|
+
stack(panel("Result", result))
|
|
37
|
+
),
|
|
38
|
+
foot: actions(copyButton(() => result.value, "Copy result", "fill")),
|
|
39
|
+
});
|
|
40
|
+
slot.append(card.node);
|
|
41
|
+
run();
|
|
42
|
+
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default { mount };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Tidies spacing in pasted text: collapses runs of spaces, trims each line and
|
|
2
|
+
// optionally drops blank lines.
|
|
3
|
+
import { bench, stack, panel, textArea, toggleRow, copyButton, actions, toolcard } from "../core.js";
|
|
4
|
+
|
|
5
|
+
export function mount(slot) {
|
|
6
|
+
const source = textArea({ rows: 12, "aria-label": "Text", placeholder: "Paste your text" });
|
|
7
|
+
const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
|
|
8
|
+
const options = toggleRow([
|
|
9
|
+
{ value: "collapse", label: "Collapse spaces", checked: true },
|
|
10
|
+
{ value: "trim", label: "Trim lines", checked: true },
|
|
11
|
+
{ value: "blank", label: "Remove blank lines", checked: false },
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function run() {
|
|
15
|
+
const text = source.value;
|
|
16
|
+
if (!text) { result.value = ""; return; }
|
|
17
|
+
const opts = options.values();
|
|
18
|
+
let lines = text.split("\n");
|
|
19
|
+
if (opts.includes("trim")) lines = lines.map((line) => line.trim());
|
|
20
|
+
if (opts.includes("collapse")) lines = lines.map((line) => line.replace(/[ \t]+/g, " "));
|
|
21
|
+
if (opts.includes("blank")) lines = lines.filter((line) => line.trim());
|
|
22
|
+
result.value = lines.join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
source.addEventListener("input", run);
|
|
26
|
+
options.addEventListener("change", run);
|
|
27
|
+
|
|
28
|
+
const card = toolcard({
|
|
29
|
+
body: bench(
|
|
30
|
+
stack(panel("Text", source), options),
|
|
31
|
+
stack(panel("Result", result))
|
|
32
|
+
),
|
|
33
|
+
foot: actions(copyButton(() => result.value, "Copy result", "fill")),
|
|
34
|
+
});
|
|
35
|
+
slot.append(card.node);
|
|
36
|
+
run();
|
|
37
|
+
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default { mount };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Builds a robots.txt from a user agent and the allow and disallow paths, one
|
|
2
|
+
// per line.
|
|
3
|
+
import { bench, stack, panel, textInput, textArea, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
4
|
+
|
|
5
|
+
const isUrl = (value) => {
|
|
6
|
+
try {
|
|
7
|
+
new URL(value);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const agent = textInput({ value: "*", "aria-label": "User agent" });
|
|
16
|
+
const disallow = textArea({ rows: 5, class: "mvb-in mvb-in--area mvb-in--mono", placeholder: "/admin\n/private", "aria-label": "Disallow paths" });
|
|
17
|
+
const allow = textArea({ rows: 3, class: "mvb-in mvb-in--area mvb-in--mono", placeholder: "/public", "aria-label": "Allow paths" });
|
|
18
|
+
const sitemap = textInput({ value: "https://example.com/sitemap.xml", "aria-label": "Sitemap URL" });
|
|
19
|
+
const result = output({ rows: 8 });
|
|
20
|
+
const error = notice("error");
|
|
21
|
+
|
|
22
|
+
const list = (area) => area.value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
23
|
+
|
|
24
|
+
function build() {
|
|
25
|
+
error.write("");
|
|
26
|
+
const lines = [`User-agent: ${agent.value.trim() || "*"}`];
|
|
27
|
+
const blocked = list(disallow);
|
|
28
|
+
if (blocked.length) for (const path of blocked) lines.push(`Disallow: ${path}`);
|
|
29
|
+
else lines.push("Disallow:");
|
|
30
|
+
for (const path of list(allow)) lines.push(`Allow: ${path}`);
|
|
31
|
+
const site = sitemap.value.trim();
|
|
32
|
+
if (site) {
|
|
33
|
+
if (isUrl(site)) lines.push(`Sitemap: ${site}`);
|
|
34
|
+
else error.write("The sitemap address is not a valid URL.");
|
|
35
|
+
}
|
|
36
|
+
result.write(lines.join("\n"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const input of [agent, disallow, allow, sitemap]) input.addEventListener("input", build);
|
|
40
|
+
|
|
41
|
+
const card = toolcard({
|
|
42
|
+
body: bench(
|
|
43
|
+
stack(
|
|
44
|
+
panel("User-agent", agent),
|
|
45
|
+
panel("Disallow", disallow),
|
|
46
|
+
panel("Allow", allow),
|
|
47
|
+
panel("Sitemap", sitemap),
|
|
48
|
+
error
|
|
49
|
+
),
|
|
50
|
+
stack(panel("robots.txt", result))
|
|
51
|
+
),
|
|
52
|
+
foot: actions(copyButton(() => result.textContent, "Copy robots.txt", "fill")),
|
|
53
|
+
});
|
|
54
|
+
slot.append(card.node);
|
|
55
|
+
build();
|
|
56
|
+
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default { mount };
|