master
noah Publish 1.0.0 a09b9ee 2mo ago
/*
 * render-svg.js  —  draw list -> SVG string (live on-screen preview).
 * Consumes exactly the ops produced by harness-layout.js.
 */
(function (global) {
  'use strict';

  function esc(s) {
    return String(s == null ? '' : s)
      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }
  function fontFamily(f) {
    return f === 'mono'
      ? "'JetBrains Mono','SFMono-Regular',Consolas,monospace"
      : "'Helvetica Neue',Helvetica,Arial,sans-serif";
  }
  function anchor(a) { return a === 'middle' ? 'middle' : a === 'end' ? 'end' : 'start'; }

  function dashAttr(d) { return d ? ' stroke-dasharray="' + d.join(',') + '"' : ''; }
  function pickAttr(o) {
    if (!o.pick) return '';
    return ' data-pick-type="' + o.pick.type + '" data-pick-id="' + esc(o.pick.id) + '" class="pickable"';
  }

  function segToD(d) {
    var out = '';
    for (var i = 0; i < d.length; i++) {
      var c = d[i];
      if (c[0] === 'M') out += 'M' + c[1] + ',' + c[2] + ' ';
      else if (c[0] === 'L') out += 'L' + c[1] + ',' + c[2] + ' ';
      else if (c[0] === 'C') out += 'C' + c[1] + ',' + c[2] + ' ' + c[3] + ',' + c[4] + ' ' + c[5] + ',' + c[6] + ' ';
      else if (c[0] === 'Q') out += 'Q' + c[1] + ',' + c[2] + ' ' + c[3] + ',' + c[4] + ' ';
      else if (c[0] === 'Z') out += 'Z ';
    }
    return out.trim();
  }

  function opToSvg(o) {
    if (o.op === 'rect') {
      return '<rect x="' + o.x + '" y="' + o.y + '" width="' + o.w + '" height="' + o.h + '"' +
        (o.rx ? ' rx="' + o.rx + '"' : '') +
        ' fill="' + (o.fill || 'none') + '"' +
        (o.stroke ? ' stroke="' + o.stroke + '" stroke-width="' + (o.lw || 1) + '"' : ' stroke="none"') +
        dashAttr(o.dash) + pickAttr(o) + '/>';
    }
    if (o.op === 'line') {
      return '<line x1="' + o.x1 + '" y1="' + o.y1 + '" x2="' + o.x2 + '" y2="' + o.y2 + '"' +
        ' stroke="' + (o.stroke || '#000') + '" stroke-width="' + (o.lw || 1) + '"' +
        (o.cap ? ' stroke-linecap="' + o.cap + '"' : '') + dashAttr(o.dash) + '/>';
    }
    if (o.op === 'path') {
      if (o.svgOnly) { // invisible, wide, clickable hit-area
        return '<path d="' + segToD(o.d) + '" fill="none" stroke="#000" stroke-opacity="0" stroke-width="' + (o.lw || 1) + '"' +
          (o.cap ? ' stroke-linecap="' + o.cap + '"' : '') + ' pointer-events="stroke"' + pickAttr(o) + '/>';
      }
      return '<path d="' + segToD(o.d) + '"' +
        ' fill="' + (o.fill || 'none') + '"' +
        (o.stroke ? ' stroke="' + o.stroke + '" stroke-width="' + (o.lw || 1) + '"' : ' stroke="none"') +
        (o.cap ? ' stroke-linecap="' + o.cap + '"' : '') +
        (o.join ? ' stroke-linejoin="' + o.join + '"' : '') + dashAttr(o.dash) + '/>';
    }
    if (o.op === 'circle') {
      return '<circle cx="' + o.x + '" cy="' + o.y + '" r="' + o.r + '"' +
        ' fill="' + (o.fill || 'none') + '"' +
        (o.stroke ? ' stroke="' + o.stroke + '" stroke-width="' + (o.lw || 1) + '"' : '') + '/>';
    }
    if (o.op === 'text') {
      var t = '<text x="' + o.x + '" y="' + o.y + '"' +
        ' font-family="' + fontFamily(o.font) + '"' +
        ' font-size="' + (o.size || 8) + '"' +
        (o.weight === 'bold' ? ' font-weight="700"' : '') +
        ' fill="' + (o.color || '#000') + '"' +
        ' text-anchor="' + anchor(o.align) + '"';
      if (o.angle) t += ' transform="rotate(' + o.angle + ' ' + o.x + ' ' + o.y + ')"';
      t += '>' + esc(o.text) + '</text>';
      return t;
    }
    return '';
  }

  // Render one page object {w,h,ops} to an <svg> string.
  function renderPage(page) {
    var body = '';
    for (var i = 0; i < page.ops.length; i++) body += opToSvg(page.ops[i]);
    return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + page.w + ' ' + page.h + '" ' +
      'preserveAspectRatio="xMidYMid meet" class="harness-sheet">' +
      '<rect x="0" y="0" width="' + page.w + '" height="' + page.h + '" fill="#ffffff"/>' +
      body + '</svg>';
  }

  global.RenderSVG = { renderPage: renderPage };
})(typeof window !== 'undefined' ? window : globalThis);