conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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   '&': '&amp;',
     27   '<': '&lt;',
     28   '>': '&gt;',
     29   '"': '&quot;',
     30   "'": '&#39;',
     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 }