Skills › Visual Explainer › visual-explainer › ask.mjs
ask.mjs
51 lines · plugins/visual-explainer/kit/ask.mjs at 9d5f880
1// <ve-ask>: a question the reader answers in the page, with <ve-opt> options. Plans and kit pages share it; ctx:
2// ids Map from an id to where it was first used, shared by the page's asks
3// k, total this ask's number and how many the page has
4// claims plans: the claim numbers an option's removes= may name
5// page pages: default is optional, removes= is an error, and the legend says Question
6import { esc, inner, kids, textOf, words } from "./html.mjs";
8export function expandAsk(n, { errors, warnings, where, ids, k, total, claims = new Set(), page = false }) {
9 const at = where(n);
10 const id = n.attrs.id;
11 if (!id || !/^[\w-]+$/.test(id)) errors.push(`${at}: needs id="word" (letters, digits, - or _)`);
12 else if (id in Object.prototype) errors.push(`${at}: id="${id}" is a reserved name; pick another`);
13 else if (ids.has(id)) errors.push(`${at}: id="${id}" is already used at ${ids.get(id)}`);
14 ids.set(id, at);
15 for (const name of Object.keys(n.attrs)) {
16 if (name === "default") errors.push(`${at}: default goes on the option itself: <ve-opt value="…" default>`);
17 else if (!["id", "q", "multi", "data-if"].includes(name)) errors.push(`${at}: unknown attribute ${name}=; <ve-ask> takes id, q and multi`);
18 }
19 const q = n.attrs.q || "";
20 if (!q) errors.push(`${at}: needs q="the question"`);
21 else if (words(q) > 15) warnings.push(`${at}: the question is ${words(q)} words; keep it to about 15`);
22 const multi = "multi" in n.attrs;
23 const opts = kids(n).filter((k) => k.name === "ve-opt");
24 if (opts.length < 2) errors.push(`${at}: needs at least two <ve-opt>`);
25 const defaults = opts.filter((o) => "default" in o.attrs);
26 if (!multi && !page && defaults.length !== 1) errors.push(`${at}: mark exactly one <ve-opt default>, the option you would build`);
27 if (!multi && page && defaults.length > 1) errors.push(`${at}: mark at most one <ve-opt default>`);
28 const values = new Set();
29 for (const o of opts) {
30 if (!o.attrs.value) errors.push(`${where(o)}: needs value=""`);
31 else if (values.has(o.attrs.value)) errors.push(`${where(o)}: value="${o.attrs.value}" is used twice`);
32 values.add(o.attrs.value);
33 if (page && o.attrs.removes) errors.push(`${where(o)}: removes= belongs to plan pages`);
34 else for (const no of (o.attrs.removes || "").split(/\s+/).filter(Boolean)) if (!claims.has(no)) errors.push(`${where(o)}: removes="${no}", but there is no claim ${no}`);
35 if (o.attrs.note && words(o.attrs.note) > 12) warnings.push(`${where(o)}: note is ${words(o.attrs.note)} words; keep it to about 12`);
36 }
37 const row = opts.every((o) => textOf(o).trim().length <= 18 && !o.attrs.note && !o.attrs.removes);
38 const body = opts.map((o) => {
39 const cost = [];
40 for (const [k, sign, word] of [["new", "+", "new"], ["changed", "~", "changed"], ["deleted", "−", "deleted"]]) {
41 const v = +(o.attrs[k] || 0);
42 if (v) cost.push(`${v > 0 ? sign : "−"}${Math.abs(v)} ${word} file${Math.abs(v) === 1 ? "" : "s"}`);
43 }
44 for (const no of (o.attrs.removes || "").split(/\s+/).filter(Boolean)) cost.push(`<span class="goes">claim ${esc(no)} goes</span>`);
45 const data = ["new", "changed", "deleted", "removes"].filter((k) => o.attrs[k]).map((k) => ` data-${k}="${esc(o.attrs[k])}"`).join("");
46 const def = "default" in o.attrs;
47 return `<label class="opt"><input type="${multi ? "checkbox" : "radio"}" name="${esc(id)}" value="${esc(o.attrs.value ?? "")}"${def ? " checked" : ""}${data}><span class="opt-body"><span class="opt-label">${inner(o).trim()}</span>${def ? '<span class="sug">Suggested</span>' : ""}${o.attrs.note ? `<small>${esc(o.attrs.note)}</small>` : ""}${cost.length ? `<span class="opt-cost">${cost.join(" · ")}</span>` : ""}</span></label>`;
48 }).join("");
49 const legend = page ? (total > 1 ? `Question <b>${k}</b> of ${total}` : "Question") : `Decision <b>${k}</b> of ${total}`;
50 return `<fieldset class="ask${row ? " row" : ""}" id="ask-${esc(id)}" data-ask="${esc(id)}"><legend class="ask-n">${legend}</legend><p class="ask-q">${esc(q)}</p><div class="ask-opts">${body}</div></fieldset>`;
51}