html.js (2080B)
1 // src/conductor/ui/html.js - HTML templating 2 // 3 // A tagged template that escapes every interpolated value unless it is 4 // explicitly marked safe. Repository names, commit subjects, job output and 5 // user names all reach these pages, so escaping is the default and opting 6 // out has to be written down. 7 // 8 // html`<td>${project.name}</td>` escaped 9 // html`<tbody>${rows.map(renderRow)}</tbody>` nested templates kept 10 // html`<div>${raw(trustedMarkup)}</div>` passed through 11 // 12 // There is no dependency here on purpose: the interface is a few pages of 13 // tables, which does not justify a template engine or a build step. 14 15 const RAW = Symbol('raw'); 16 17 export function raw(value) { 18 return { [RAW]: String(value) }; 19 } 20 21 export function isRaw(value) { 22 return value !== null && typeof value === 'object' && Object.hasOwn(value, RAW); 23 } 24 25 const ESCAPES = { 26 '&': '&', 27 '<': '<', 28 '>': '>', 29 '"': '"', 30 "'": ''', 31 }; 32 33 export function esc(value) { 34 if (value === null || value === undefined || value === false) return ''; 35 return String(value).replace(/[&<>"']/g, (c) => ESCAPES[c]); 36 } 37 38 function render(value) { 39 if (value === null || value === undefined || value === false || value === true) return ''; 40 if (isRaw(value)) return value[RAW]; 41 if (Array.isArray(value)) return value.map(render).join(''); 42 return esc(value); 43 } 44 45 export function html(strings, ...values) { 46 let out = strings[0]; 47 for (let i = 0; i < values.length; i += 1) { 48 out += render(values[i]) + strings[i + 1]; 49 } 50 return raw(out); 51 } 52 53 // Renders a completed template to a string for sending. 54 export function toHtml(value) { 55 return render(value); 56 } 57 58 // Attribute helper for optional attributes, so a page does not have to 59 // build strings by hand. 60 export function attrs(map) { 61 const parts = []; 62 for (const [key, value] of Object.entries(map)) { 63 if (value === null || value === undefined || value === false) continue; 64 if (value === true) parts.push(esc(key)); 65 else parts.push(`${esc(key)}="${esc(value)}"`); 66 } 67 return raw(parts.join(' ')); 68 }