← back to Small Business Builder

src/lib/escape.js

48 lines

// Tiny HTML escapes for safe template rendering
export function esc(s) {
  if (s === null || s === undefined) return '';
  return String(s)
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}
export function attr(s) { return esc(s); }

// Escape a stringified JSON blob for safe embedding inside
// <script type="application/ld+json">. Without this, content containing
// "</script>" (or other angle brackets / ampersands) can break out of the
// script tag (XSS). Use this before interpolating ANY ld+json blob.
// (Refactor: was duplicated in profile.js, corridor.js, corridor-map.js,
// near.js, fresh.js, category.js, proprietors.js — single source of truth.)
export function escJsonLd(json) {
  return String(json)
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026');
}

// Reject href values that aren't a real http/https URL. Returns the
// trimmed URL string when safe, null otherwise. Use to neutralize
// stored-XSS via `javascript:` / `data:` href values that snuck into the DB.
export function safeHref(raw) {
  if (typeof raw !== 'string') return null;
  const u = raw.trim();
  if (!u) return null;
  try {
    const p = new URL(u);
    if (p.protocol === 'http:' || p.protocol === 'https:') return p.toString();
  } catch {}
  return null;
}

// Validate user-supplied hex colors before they hit a <style> block.
// Returns the color if it matches #rgb / #rgba / #rrggbb / #rrggbbaa, else fallback.
// This blocks stored-XSS payloads of the form `</style><script>...</script>`
// being interpolated as CSS variable values.
export function safeColor(c, fallback = '#1f2937') {
  if (typeof c !== 'string') return fallback;
  return /^#[0-9a-f]{3,8}$/i.test(c.trim()) ? c.trim() : fallback;
}