// Each template's `layout` is a tree as described in label.js, with leaves whose `type` is one of // label.js's QR_LEAF_TYPES keys or "text", and whose `content` is a function from the resolved // field values (see label.js's buildLabelFields) to what they render - `null`/`undefined` from // that function means the field isn't available yet (see templateIsAvailable below). A template // is only selectable once every leaf's `content` resolves to a value. const GAP = {type: "empty", "min-width": "1mm", "min-height": "1mm"}; // The full "just the code" matrix: every {symbology, error-correction level, [rMQR] size // strategy} label.js's QR_LEAF_TYPES supports, one template each - a plain `id` (no suffix) is // always anyd's own defaults, ecc "M" and (rMQR only) size "balanced". Coverage isn't uniform // (see QR_LEAF_TYPES): full QR gets all four ecc grades L/M/Q/H; Micro QR swaps "L" (QR's actual // lowest) for the even-lower, M1-only, detection-only "Detection" and has no "H" at all; rMQR // only ever supports ecc "M" or "H", each of those crossed with all three size strategies // (balanced/min/max). Generated (rather than hand-writing every near-duplicate entry) so a // symbology/level/size this matrix is missing is one new row here, not a new block to keep in // sync with its neighbors. `id` doubles as the layout's leaf `type`, since that's exactly what // QR_LEAF_TYPES is keyed by. const QR_ONLY_TEMPLATES = [ { id: "qr-l", name: "QR code only (low error correction)", description: "Just the code, at QR's lowest error-correction level - fits more data (or a " + "smaller code) for the same text, but less tolerant of damage.", }, {id: "qr", name: "QR code only", description: "Just the code - smallest label, prints fastest."}, { id: "qr-q", name: "QR code only (quartile error correction)", description: "Just the code, at QR's second-highest (quartile) error-correction level - a " + "middle ground between code size and damage tolerance.", }, { id: "qr-h", name: "QR code only (high error correction)", description: "Just the code, at QR's highest error-correction level - still scans if scuffed " + "or partly obscured, at the cost of a bigger code for the same text.", }, { id: "mqr-d", name: "Micro QR code only (detection only)", description: "Just the code, in the more compact Micro QR format at its lowest, M1-only level - " + "the smallest QR-family code there is, but can only tell a scan is corrupted, not " + "recover from it.", }, { id: "mqr-l", name: "Micro QR code only (low error correction)", description: "Just the code, in the more compact Micro QR format at its low error-correction level.", }, { id: "mqr", name: "Micro QR code only", description: "Just the code, in the more compact Micro QR format - smallest label, prints fastest.", }, { id: "mqr-q", name: "Micro QR code only (quartile error correction)", description: "Just the code, in the more compact Micro QR format at its highest (quartile) " + "error-correction level.", }, { id: "rmqr", name: "rMQR code only", description: "Just the code, in the rectangular rMQR format, sized for the smallest total area " + "that fits the text - smallest label, prints fastest.", }, { id: "rmqr-min", name: "rMQR code only (shortest, widest)", description: "Just the code, in the rectangular rMQR format, preferring the flattest/widest " + "symbol that fits the text - shortest across the tape, longest along it.", }, { id: "rmqr-max", name: "rMQR code only (tallest, narrowest)", description: "Just the code, in the rectangular rMQR format, preferring the tallest/narrowest " + "symbol that fits the text - tallest across the tape, shortest along it.", }, { id: "rmqr-h", name: "rMQR code only (high error correction)", description: "Just the code, in the rectangular rMQR format at its high error-correction level, " + "sized for the smallest total area that fits the text - still scans if scuffed or partly " + "obscured, at the cost of a bigger code for the same text.", }, { id: "rmqr-h-min", name: "rMQR code only (high error correction, shortest/widest)", description: "Just the code, in the rectangular rMQR format at its high error-correction level, " + "preferring the flattest/widest symbol that fits the text.", }, { id: "rmqr-h-max", name: "rMQR code only (high error correction, tallest/narrowest)", description: "Just the code, in the rectangular rMQR format at its high error-correction level, " + "preferring the tallest/narrowest symbol that fits the text.", }, ].map(t => ({...t, required_vars: ["text"], layout: [{type: t.id, content: c => c.text}]})); export const LABEL_TEMPLATES = [ { id: "mqr-token", name: "MQR Token", description: "The code with the encoded text printed next to it.", required_vars: ["shortId"], layout: [{type: "mqr", content: c => c.shortId}] }, { id: "qr-url", name: "MQR Token", description: "The code with the encoded text printed next to it.", required_vars: ["shortUrl"], layout: [{type: "qr-h", content: c => c.shortUrl}] },...QR_ONLY_TEMPLATES, { id: "qr-text", name: "QR code + text", description: "The code with the encoded text printed next to it.", required_vars: ["text"], layout: [{type: "qr", content: c => c.text}, GAP, {type: "text", content: c => c.text?.split("\n")}] }, { id: "qr-text-below", name: "QR code + text below", description: "The code with the encoded text printed below it.", required_vars: ["text"], layout: [[{type: "qr", content: c => c.text}, GAP, {type: "text", content: c => c.text?.split("\n")}]] }, { id: "id-qr-text-vertical", name: "ID + QR code + text below", description: "The code with the encoded text printed below it.", required_vars: ["itemId", "text", "userHandle"], layout: [[{type: "text", content: c => "Item: "+c.itemId}, GAP, { type: "qr", content: c => c.text }, GAP, {type: "text", content: c => c.userHandle}]] }, { id: "text", name: "Text only", description: "No code, just the text itself, as large as it fits.", required_vars: ["text"], layout: [{type: "text", content: c => c.text?.split("\n")}] }, { id: "item-handle", name: "Item handle", description: "The compact owner@domain:id handle - meaningful in-app, not scannable on its own.", required_vars: ["itemHandle"], layout: [{type: "text", content: c => c.itemHandle}] }, { id: "item-url", name: "Item URL", description: "The full item URL as text, with no code - for copying rather than scanning.", required_vars: ["itemUrl"], layout: [{type: "text", content: c => c.itemUrl}] }, { id: "owner-handle", name: "Owner handle", description: "Just the owning user's handle, as text only.", required_vars: ["userHandle"], layout: [{type: "text", content: c => c.userHandle}] }, { id: "item-id", name: "Item ID", description: "Just the bare item id, as text only.", required_vars: ["itemId"], layout: [{type: "text", content: c => c.itemId}] }, { id: "owner-id-text", name: "Owner + item ID", description: "The owner's handle and the item id, as two lines of text - no code.", required_vars: ["userHandle", "itemId"], layout: [{type: "text", content: c => [c.userHandle, c.itemId]}] }, { id: "item-url-qr-handle", name: "Item URL + handle", description: "Scannable item URL, with the item's compact handle printed alongside.", required_vars: ["itemUrl", "itemHandle"], layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemHandle}] }, { id: "item-url-qr-owner", name: "Item URL + owner", description: "Scannable item URL, with the owner's handle printed alongside.", required_vars: ["itemUrl", "userHandle"], layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.userHandle}] }, { id: "item-url-qr-id", name: "Item URL + item ID", description: "Scannable item URL, with the bare item id printed alongside.", required_vars: ["itemUrl", "itemId"], layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemId}] }, { id: "item-url-qr-owner-id", name: "Item URL + owner + ID", description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.", required_vars: ["itemUrl", "userHandle", "itemId"], layout: [{type: "qr", content: c => c.itemUrl}, GAP, {type: "text", content: c => [c.userHandle, c.itemId]}] }, { id: "short-url-qr", name: "Short link (QR code)", description: "Scannable short link for this item or storage location - more compact than " + "the full URL. Available for any element with a resolvable short link, not just items.", required_vars: ["shortUrl"], layout: [{type: "qr", content: c => c.shortUrl}] }, { id: "item-url-qr-owner-id2", name: "Item URL + owner + ID", description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.", required_vars: ["itemUrl", "userHandle", "itemId"], layout: [{type: "qr", content: c => c.itemUrl}, GAP, [{ type: "text", content: c => c.userHandle }, GAP, {type: "text", content: c => c.itemId}]] }, ]; // Every field name any template's required_vars names, in first-seen order. export const KNOWN_VARS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.required_vars))]; // A derived var is a format string calculated from other vars rather than typed directly - it // doesn't get its own input, just a read-only, live-recalculated display next to the ones that // do (see Print.vue and withDerivedVars below). `inputs` names every var (base or, in principle, // another derived one - see itemUrl/itemHandle below, which both read the derived userHandle) // `calc` reads - declared up front rather than inferred from calc's body so BASE_VARS below can // include a var like "webdomain" that only feeds a calculation and that no template ever // references directly. Declaration order matters here: withDerivedVars runs these in a single // pass, so a derived var must be declared after every other derived var it depends on. export const DERIVED_VARS = { // The full owner handle (see federation.md's Unique Handles section / ToolshedUser's // separate username/domain columns) - kept as two base vars (user, domain) rather than one, // since that's how the account itself is actually shaped, with this just the display/URL form. userHandle: { inputs: ["user", "domain"], calc: (f) => `${f.user}@${f.domain}`, }, // The self-contained Item URL (see docs/design-in-progress/items-labels.md) - what a printed // label actually encodes, since scanning it has to resolve the right frontend/backend/item // with no other context, not just this browser's history. `webdomain` defaults to this // browser's own origin (see Print.vue) but is editable, since any frontend can resolve any // handle - the label doesn't have to point back at whichever frontend happened to print it. itemUrl: { inputs: ["webdomain", "userHandle", "itemId"], calc: (f) => `${f.webdomain}/i/${f.userHandle}/${f.itemId}`, }, // The compact "owner handle + id" form from docs/design-in-progress/items-labels.md - // meaningful only where context already makes clear it's a Toolshed item, unlike itemUrl. itemHandle: { inputs: ["userHandle", "itemId"], calc: (f) => `${f.userHandle}:${f.itemId}`, }, }; // What Print.vue's content form offers a plain text input for: every KNOWN_VAR a template // references directly, minus the derived ones, plus every var a DERIVED_VARS calculation itself // needs (like "webdomain", which no template ever names). A template still lights up only once // every one of its own required_vars, base or derived, has a value (see templateIsAvailable // below). export const BASE_VARS = [...new Set([ ...KNOWN_VARS.filter(v => !(v in DERIVED_VARS)), ...Object.values(DERIVED_VARS).flatMap(d => d.inputs).filter(v => !(v in DERIVED_VARS)), ])]; // Runs every DERIVED_VARS calculation against `fields` (already holding the base vars - see // Print.vue), returning a copy with each one's result added wherever all of its own inputs are // present, so a caller never has to know DERIVED_VARS' internal {inputs, calc} shape. export function withDerivedVars(fields) { const result = {...fields}; for (const [name, {inputs, calc}] of Object.entries(DERIVED_VARS)) { if (inputs.every(v => result[v])) { result[name] = calc(result); } } return result; } function walkLeaves(node, fn) { if (Array.isArray(node)) { node.forEach(child => walkLeaves(child, fn)); } else { fn(node); } } function mapTree(node, fn) { return Array.isArray(node) ? node.map(child => mapTree(child, fn)) : fn(node); } // A content leaf's resolved value counts as present only if every part of it is - a single // string for a QR-family leaf (any label.js QR_LEAF_TYPES entry) or plain "text", every line for a // multi-line "text" (see LABEL_TEMPLATES' "owner-id-text" and "item-url-qr-owner-id"). function isResolved(value) { return Array.isArray(value) ? value.every(isResolved) : value !== undefined && value !== null; } // LabelLayoutPreview.vue's thumbnail grid and Print.vue's big preview both resolve a template // through these two functions rather than each re-implementing the leaf-walking/field-resolving // logic itself. export function templateIsAvailable(t, fields) { let available = true; walkLeaves(t.layout, leaf => { if (leaf.type !== "empty" && !isResolved(leaf.content(fields))) { available = false; } }); return available; } /* Resolves a template's `content` functions against actual field values, turning its layout tree into one ready for label.js's drawLabel/drawFallbackLabel - or null if there's nothing to render yet (every leaf's value is still empty, e.g. before the user has typed anything). */ export function templateContent(t, fields) { const tree = mapTree(t.layout, leaf => leaf.type === "empty" ? leaf : {...leaf, value: leaf.content(fields)}); let hasContent = false; walkLeaves(tree, leaf => { if (leaf.type !== "empty" && leaf.value && (!Array.isArray(leaf.value) || leaf.value.some(Boolean))) { hasContent = true; } }); return hasContent ? tree : null; }