svg_metadata.js

const GROUP_RE = (id) => new RegExp(`<g id="${id}"[^>]*>([\\s\\S]*?)</g>`);
const ATTR_RE = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)="([^"]*)"/g;

function extractAttrs(tagOpenSource) {
  const attrs = {};
  ATTR_RE.lastIndex = 0;
  let m;
  while ((m = ATTR_RE.exec(tagOpenSource))) {
    attrs[m[1]] = m[2];
  }
  return attrs;
}

function extractTags(groupContent, tagName) {
  const re = new RegExp(`<${tagName}\\b([^>]*)>`, "g");
  const tags = [];
  let m;
  while ((m = re.exec(groupContent))) {
    tags.push(extractAttrs(m[1]));
  }
  return tags;
}

/**
 * Extracts the handful of attributes createMap() needs from a topo-map SVG's raw text, with
 * no DOM/DOMParser involved — this must run in a Web Worker or Node worker_threads thread,
 * neither of which has DOMParser. #polygons is parsed (so its escaped data-points JSON
 * attribute is exercised) but intentionally discarded — see
 * notes/Programming/specs/2026-08-03-createmap-topo-refactor-design.md.
 */
export function parseSvgMetadata(svgText) {
  const rootMatch = svgText.match(/<svg\b([^>]*)>/);
  const rootAttrs = rootMatch ? extractAttrs(rootMatch[1]) : {};

  let width, height;
  if (rootAttrs.viewBox) {
    const parts = rootAttrs.viewBox.trim().split(/[\s,]+/).map(Number);
    width = parts[2];
    height = parts[3];
  } else {
    width = parseFloat(rootAttrs.width) || 800;
    height = parseFloat(rootAttrs.height) || 600;
  }

  const sitesMatch = svgText.match(GROUP_RE("sites"));
  const sites = sitesMatch
    ? extractTags(sitesMatch[1], "image")
        .filter((attrs) => attrs["data-types"])
        .map((attrs) => ({
          id: attrs["data-id"],
          types: attrs["data-types"].split(",").filter(Boolean),
        }))
    : [];

  const labelsMatch = svgText.match(GROUP_RE("labels"));
  const labels = labelsMatch
    ? extractTags(labelsMatch[1], "text")
        .filter((attrs) => attrs["data-type"])
        .map((attrs) => ({
          id: attrs["data-id"],
          type: attrs["data-type"],
          group: attrs["data-group"] ?? null,
        }))
    : [];

  // #polygons isn't turned into Locations yet — parsed via the same extractTags/extractAttrs
  // path used for #sites/#labels (so its escaped data-points JSON attribute is genuinely
  // exercised) and discarded. See notes/Programming/specs/2026-08-03-createmap-topo-refactor-design.md.
  const polygonsMatch = svgText.match(GROUP_RE("polygons"));
  if (polygonsMatch) extractTags(polygonsMatch[1], "path");

  return { name: rootAttrs["data-topo-name"], width, height, sites, labels };
}