refactor(builder): use shared escaper everywhere, drop 26 local copies

Replace divergent, buggy local esc/escapeHtml helpers across 26 files with
imports from src/utils/escape (escapeHtml/escapeAttr). Attribute call sites use
escapeAttr, text-content sites use escapeHtml. Several toHtml outputs now
correctly escape & where old local escapers omitted it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 11:49:10 -07:00
parent cce984508f
commit fad1882117
26 changed files with 101 additions and 119 deletions
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface FeatureItem {
title: string;
@@ -278,23 +279,22 @@ FeaturesGrid.craft = {
/* ---------- HTML export ---------- */
(FeaturesGrid as any).toHtml = (props: FeaturesGridProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const sectionStyle = cssPropsToString({
padding: '80px 20px',
...props.style,
});
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const cards = (props.features || defaultFeatures).map((feat) => {
const media = feat.image
? `<img src="${esc(feat.image)}" alt="${esc(feat.imageAlt || feat.title || '')}" style="max-width:100%;height:auto;margin-bottom:16px;border-radius:8px">`
: `<div style="font-size:36px;margin-bottom:16px">${esc(feat.icon)}</div>`;
? `<img src="${escapeAttr(feat.image)}" alt="${escapeAttr(feat.imageAlt || feat.title || '')}" style="max-width:100%;height:auto;margin-bottom:16px;border-radius:8px">`
: `<div style="font-size:36px;margin-bottom:16px">${escapeHtml(feat.icon)}</div>`;
const button = feat.buttonText
? `\n <a href="${esc(feat.buttonUrl || '#')}" style="display:inline-block;margin-top:16px;padding:10px 24px;background:#3b82f6;color:#fff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600">${esc(feat.buttonText)}</a>`
? `\n <a href="${escapeAttr(feat.buttonUrl || '#')}" style="display:inline-block;margin-top:16px;padding:10px 24px;background:#3b82f6;color:#fff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600">${escapeHtml(feat.buttonText)}</a>`
: '';
return `<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:#f8fafc;border:1px solid #e2e8f0">
${media}
<h3 style="font-size:20px;font-weight:600;color:#18181b;margin-bottom:8px">${esc(feat.title)}</h3>
<p style="font-size:14px;color:#64748b;line-height:1.6">${esc(feat.description)}</p>${button}
<h3 style="font-size:20px;font-weight:600;color:#18181b;margin-bottom:8px">${escapeHtml(feat.title)}</h3>
<p style="font-size:14px;color:#64748b;line-height:1.6">${escapeHtml(feat.description)}</p>${button}
</div>`;
}).join('\n ');