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
+4 -9
View File
@@ -2,6 +2,7 @@ import React, { CSSProperties, useCallback, useRef, useState } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { useSiteDesign } from '../../state/SiteDesignContext'; import { useSiteDesign } from '../../state/SiteDesignContext';
import { escapeHtml, escapeAttr } from '../../utils/escape';
/* ---------- Types ---------- */ /* ---------- Types ---------- */
@@ -37,12 +38,6 @@ async function uploadToWhp(file: File): Promise<string | null> {
} catch { return null; } } catch { return null; }
} }
/* ---------- Helper: escape HTML ---------- */
function esc(str: any): string {
str = String(str ?? "");
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* ---------- Component ---------- */ /* ---------- Component ---------- */
export const Logo: UserComponent<LogoProps> = ({ export const Logo: UserComponent<LogoProps> = ({
@@ -394,7 +389,7 @@ Logo.craft = {
let innerHtml: string; let innerHtml: string;
if (props.type === 'image' && props.imageSrc) { if (props.type === 'image' && props.imageSrc) {
const imgStyle = cssPropsToString({ width: props.imageWidth || '120px', height: 'auto', display: 'block' }); const imgStyle = cssPropsToString({ width: props.imageWidth || '120px', height: 'auto', display: 'block' });
innerHtml = `<img src="${esc(props.imageSrc)}" alt="${esc(props.text || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} />`; innerHtml = `<img src="${escapeAttr(props.imageSrc)}" alt="${escapeAttr(props.text || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} />`;
} else { } else {
const spanStyle = cssPropsToString({ const spanStyle = cssPropsToString({
fontWeight: props.fontWeight || '700', fontWeight: props.fontWeight || '700',
@@ -402,7 +397,7 @@ Logo.craft = {
fontFamily: props.fontFamily || 'Inter, sans-serif', fontFamily: props.fontFamily || 'Inter, sans-serif',
color: props.color || '#1f2937', color: props.color || '#1f2937',
}); });
innerHtml = `<span${spanStyle ? ` style="${spanStyle}"` : ''}>${esc(props.text || 'MySite')}</span>`; innerHtml = `<span${spanStyle ? ` style="${spanStyle}"` : ''}>${escapeHtml(props.text || 'MySite')}</span>`;
} }
const aStyle = cssPropsToString({ const aStyle = cssPropsToString({
@@ -414,6 +409,6 @@ Logo.craft = {
}); });
return { return {
html: `<a href="${esc(href)}"${aStyle ? ` style="${aStyle}"` : ''}>${innerHtml}</a>`, html: `<a href="${escapeAttr(href)}"${aStyle ? ` style="${aStyle}"` : ''}>${innerHtml}</a>`,
}; };
}; };
+2 -7
View File
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { usePages } from '../../state/PageContext'; import { usePages } from '../../state/PageContext';
import { escapeHtml, escapeAttr } from '../../utils/escape';
/* ---------- Types ---------- */ /* ---------- Types ---------- */
@@ -34,12 +35,6 @@ const defaultLinks: MenuLink[] = [
{ text: 'Contact', href: '#contact', isCta: true }, { text: 'Contact', href: '#contact', isCta: true },
]; ];
/* ---------- Helper: escape HTML ---------- */
function esc(str: any): string {
str = String(str ?? "");
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* ---------- Component ---------- */ /* ---------- Component ---------- */
export const Menu: UserComponent<MenuProps> = ({ export const Menu: UserComponent<MenuProps> = ({
@@ -494,7 +489,7 @@ Menu.craft = {
borderRadius: link.isCta ? '6px' : '0', borderRadius: link.isCta ? '6px' : '0',
transition: 'color 0.15s, background-color 0.15s', transition: 'color 0.15s, background-color 0.15s',
}); });
return `<a href="${esc(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${esc(link.text)}</a>`; return `<a href="${escapeAttr(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
}).join('\n '); }).join('\n ');
const hoverCss = `<style> const hoverCss = `<style>
+8 -13
View File
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { usePages } from '../../state/PageContext'; import { usePages } from '../../state/PageContext';
import { useSiteDesign } from '../../state/SiteDesignContext'; import { useSiteDesign } from '../../state/SiteDesignContext';
import { escapeHtml, escapeAttr } from '../../utils/escape';
/* ---------- Types ---------- */ /* ---------- Types ---------- */
@@ -70,12 +71,6 @@ async function uploadToWhp(file: File): Promise<string | null> {
} catch { return null; } } catch { return null; }
} }
/* ---------- Helper: escape HTML ---------- */
function esc(str: any): string {
str = String(str ?? "");
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* ---------- Component ---------- */ /* ---------- Component ---------- */
export const Navbar: UserComponent<NavbarProps> = ({ export const Navbar: UserComponent<NavbarProps> = ({
@@ -853,7 +848,7 @@ Navbar.craft = {
let logoHtml: string; let logoHtml: string;
if (props.logoType === 'image' && props.logoImage) { if (props.logoType === 'image' && props.logoImage) {
const imgStyle = cssPropsToString({ width: props.logoWidth || '120px', height: 'auto', display: 'block' }); const imgStyle = cssPropsToString({ width: props.logoWidth || '120px', height: 'auto', display: 'block' });
logoHtml = `<a href="${esc(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><img src="${esc(props.logoImage)}" alt="${esc(props.logoText || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} /></a>`; logoHtml = `<a href="${escapeAttr(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><img src="${escapeAttr(props.logoImage)}" alt="${escapeAttr(props.logoText || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} /></a>`;
} else { } else {
const logoStyle = cssPropsToString({ const logoStyle = cssPropsToString({
fontWeight: '700', fontWeight: '700',
@@ -861,7 +856,7 @@ Navbar.craft = {
fontFamily: props.logoFontFamily || 'Inter, sans-serif', fontFamily: props.logoFontFamily || 'Inter, sans-serif',
color: props.logoColor || textCol, color: props.logoColor || textCol,
}); });
logoHtml = `<a href="${esc(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><span${logoStyle ? ` style="${logoStyle}"` : ''}>${esc(props.logoText || 'MySite')}</span></a>`; logoHtml = `<a href="${escapeAttr(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><span${logoStyle ? ` style="${logoStyle}"` : ''}>${escapeHtml(props.logoText || 'MySite')}</span></a>`;
} }
// Links HTML // Links HTML
@@ -878,15 +873,15 @@ Navbar.craft = {
borderRadius: link.isCta ? '6px' : '0', borderRadius: link.isCta ? '6px' : '0',
transition: 'color 0.15s, background-color 0.15s', transition: 'color 0.15s, background-color 0.15s',
}); });
return `<a href="${esc(link.href)}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${esc(link.text)}</a>`; return `<a href="${escapeAttr(link.href)}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
}).join('\n '); }).join('\n ');
// Hamburger HTML for mobile // Hamburger HTML for mobile
const hamburgerHtml = mobile const hamburgerHtml = mobile
? `\n <button class="navbar-hamburger" onclick="this.parentElement.querySelector('.navbar-links').classList.toggle('navbar-open')" style="display:none;background:none;border:none;cursor:pointer;padding:4px;flex-direction:column;gap:4px"> ? `\n <button class="navbar-hamburger" onclick="this.parentElement.querySelector('.navbar-links').classList.toggle('navbar-open')" style="display:none;background:none;border:none;cursor:pointer;padding:4px;flex-direction:column;gap:4px">
<span style="display:block;width:24px;height:2px;background-color:${esc(textCol)}"></span> <span style="display:block;width:24px;height:2px;background-color:${escapeAttr(textCol)}"></span>
<span style="display:block;width:24px;height:2px;background-color:${esc(textCol)}"></span> <span style="display:block;width:24px;height:2px;background-color:${escapeAttr(textCol)}"></span>
<span style="display:block;width:24px;height:2px;background-color:${esc(textCol)}"></span> <span style="display:block;width:24px;height:2px;background-color:${escapeAttr(textCol)}"></span>
</button>` </button>`
: ''; : '';
@@ -915,7 +910,7 @@ Navbar.craft = {
borderRadius: link.isCta ? '6px' : '0', borderRadius: link.isCta ? '6px' : '0',
transition: 'color 0.15s, background-color 0.15s', transition: 'color 0.15s, background-color 0.15s',
}); });
return `<a href="${esc(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${esc(link.text)}</a>`; return `<a href="${escapeAttr(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
}).join('\n '); }).join('\n ');
return { return {
+3 -3
View File
@@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface SearchBarProps { interface SearchBarProps {
placeholder?: string; placeholder?: string;
@@ -171,7 +172,6 @@ SearchBar.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(SearchBar as any).toHtml = (props: SearchBarProps, _childrenHtml: string) => { (SearchBar as any).toHtml = (props: SearchBarProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const { const {
placeholder = 'Search...', placeholder = 'Search...',
buttonText = 'Search', buttonText = 'Search',
@@ -189,14 +189,14 @@ SearchBar.craft = {
const inputStyleStr = `width:100%;padding:12px 16px 12px 40px;font-size:15px;font-family:Inter,sans-serif;border:1px solid #d1d5db;border-radius:${showButton ? '8px 0 0 8px' : '8px'};background-color:#ffffff;color:#1f2937;outline:none;box-sizing:border-box`; const inputStyleStr = `width:100%;padding:12px 16px 12px 40px;font-size:15px;font-family:Inter,sans-serif;border:1px solid #d1d5db;border-radius:${showButton ? '8px 0 0 8px' : '8px'};background-color:#ffffff;color:#1f2937;outline:none;box-sizing:border-box`;
const btnHtml = showButton const btnHtml = showButton
? `<button type="submit" style="padding:12px 20px;font-size:15px;font-weight:600;font-family:Inter,sans-serif;color:#ffffff;background-color:#3b82f6;border:none;border-radius:0 8px 8px 0;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:6px"><i class="fa fa-search" style="font-size:13px"></i>${esc(buttonText)}</button>` ? `<button type="submit" style="padding:12px 20px;font-size:15px;font-weight:600;font-family:Inter,sans-serif;color:#ffffff;background-color:#3b82f6;border:none;border-radius:0 8px 8px 0;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:6px"><i class="fa fa-search" style="font-size:13px"></i>${escapeHtml(buttonText)}</button>`
: ''; : '';
return { return {
html: `<form role="search"${formStyle ? ` style="${formStyle}"` : ''}> html: `<form role="search"${formStyle ? ` style="${formStyle}"` : ''}>
<div style="position:relative;flex:1"> <div style="position:relative;flex:1">
<i class="fa fa-search" style="position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#9ca3af;font-size:14px;pointer-events:none"></i> <i class="fa fa-search" style="position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#9ca3af;font-size:14px;pointer-events:none"></i>
<input type="search" placeholder="${esc(placeholder)}" style="${inputStyleStr}" /> <input type="search" placeholder="${escapeAttr(placeholder)}" style="${inputStyleStr}" />
</div> </div>
${btnHtml} ${btnHtml}
</form>`, </form>`,
+7 -7
View File
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { relayFormWiring } from '../../utils/form-relay-wiring'; import { relayFormWiring } from '../../utils/form-relay-wiring';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface ContactFormField { interface ContactFormField {
type: 'text' | 'email' | 'tel' | 'textarea' | 'select'; type: 'text' | 'email' | 'tel' | 'textarea' | 'select';
@@ -394,7 +395,6 @@ ContactForm.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(ContactForm as any).toHtml = (props: ContactFormProps, _childrenHtml: string) => { (ContactForm as any).toHtml = (props: ContactFormProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const formStyle = cssPropsToString({ const formStyle = cssPropsToString({
padding: '32px', padding: '32px',
display: 'flex', display: 'flex',
@@ -409,16 +409,16 @@ ContactForm.craft = {
const fieldsHtml = (props.fields || defaultFields).map((field) => { const fieldsHtml = (props.fields || defaultFields).map((field) => {
const reqStar = field.required ? '<span style="color:#ef4444;margin-left:2px">*</span>' : ''; const reqStar = field.required ? '<span style="color:#ef4444;margin-left:2px">*</span>' : '';
const labelHtml = `<label style="font-size:14px;font-weight:500;color:${labelColor}">${esc(field.label)}${reqStar}</label>`; const labelHtml = `<label style="font-size:14px;font-weight:500;color:${labelColor}">${escapeHtml(field.label)}${reqStar}</label>`;
const reqAttr = field.required ? ' required' : ''; const reqAttr = field.required ? ' required' : '';
let inputHtml = ''; let inputHtml = '';
if (field.type === 'textarea') { if (field.type === 'textarea') {
inputHtml = `<textarea name="${esc(field.name)}" placeholder="${esc(field.placeholder)}" rows="4" style="${inputStyleStr};resize:vertical"${reqAttr}></textarea>`; inputHtml = `<textarea name="${escapeAttr(field.name)}" placeholder="${escapeAttr(field.placeholder)}" rows="4" style="${inputStyleStr};resize:vertical"${reqAttr}></textarea>`;
} else if (field.type === 'select') { } else if (field.type === 'select') {
const opts = (field.options || []).map((o) => `<option value="${esc(o)}">${esc(o)}</option>`).join(''); const opts = (field.options || []).map((o) => `<option value="${escapeAttr(o)}">${escapeHtml(o)}</option>`).join('');
inputHtml = `<select name="${esc(field.name)}" style="${inputStyleStr};cursor:pointer"${reqAttr}><option value="">${esc(field.placeholder || 'Select...')}</option>${opts}</select>`; inputHtml = `<select name="${escapeAttr(field.name)}" style="${inputStyleStr};cursor:pointer"${reqAttr}><option value="">${escapeHtml(field.placeholder || 'Select...')}</option>${opts}</select>`;
} else { } else {
inputHtml = `<input type="${field.type}" name="${esc(field.name)}" placeholder="${esc(field.placeholder)}" style="${inputStyleStr}"${reqAttr} />`; inputHtml = `<input type="${field.type}" name="${escapeAttr(field.name)}" placeholder="${escapeAttr(field.placeholder)}" style="${inputStyleStr}"${reqAttr} />`;
} }
return `<div style="display:flex;flex-direction:column;gap:6px">${labelHtml}${inputHtml}</div>`; return `<div style="display:flex;flex-direction:column;gap:6px">${labelHtml}${inputHtml}</div>`;
}).join('\n '); }).join('\n ');
@@ -440,7 +440,7 @@ ContactForm.craft = {
return { return {
html: `${marker}<form action="${actionAttr}" method="POST"${formStyle ? ` style="${formStyle}"` : ''}> html: `${marker}<form action="${actionAttr}" method="POST"${formStyle ? ` style="${formStyle}"` : ''}>
${honeypot ? ` ${honeypot}\n` : ''}${fieldsHtml ? ` ${fieldsHtml}\n` : ''} <button type="submit"${btnStyle ? ` style="${btnStyle}"` : ''}>${esc(props.submitText || 'Send Message')}</button> ${honeypot ? ` ${honeypot}\n` : ''}${fieldsHtml ? ` ${fieldsHtml}\n` : ''} <button type="submit"${btnStyle ? ` style="${btnStyle}"` : ''}>${escapeHtml(props.submitText || 'Send Message')}</button>
</form>`, </form>`,
}; };
}; };
+3 -3
View File
@@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface InputFieldProps { interface InputFieldProps {
label?: string; label?: string;
@@ -165,7 +166,6 @@ InputField.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(InputField as any).toHtml = (props: InputFieldProps, _childrenHtml: string) => { (InputField as any).toHtml = (props: InputFieldProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const wrapStyle = cssPropsToString({ const wrapStyle = cssPropsToString({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -174,12 +174,12 @@ InputField.craft = {
}); });
const reqAttr = props.required ? ' required' : ''; const reqAttr = props.required ? ' required' : '';
const labelHtml = props.label const labelHtml = props.label
? `<label style="font-size:14px;font-weight:500;color:#18181b">${esc(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>` ? `<label style="font-size:14px;font-weight:500;color:#18181b">${escapeHtml(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>`
: ''; : '';
return { return {
html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}> html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}>
${labelHtml} ${labelHtml}
<input type="${props.type || 'text'}" name="${esc(props.name || 'field')}" placeholder="${esc(props.placeholder || '')}"${reqAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box" /> <input type="${props.type || 'text'}" name="${escapeAttr(props.name || 'field')}" placeholder="${escapeAttr(props.placeholder || '')}"${reqAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box" />
</div>`, </div>`,
}; };
}; };
+4 -4
View File
@@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface SubscribeFormProps { interface SubscribeFormProps {
heading?: string; heading?: string;
@@ -249,7 +250,6 @@ SubscribeForm.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(SubscribeForm as any).toHtml = (props: SubscribeFormProps, _childrenHtml: string) => { (SubscribeForm as any).toHtml = (props: SubscribeFormProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const { const {
heading = 'Subscribe to our newsletter', heading = 'Subscribe to our newsletter',
placeholder = 'Enter your email', placeholder = 'Enter your email',
@@ -268,7 +268,7 @@ SubscribeForm.craft = {
}); });
const headingHtml = heading const headingHtml = heading
? `<h3 style="font-size:22px;font-weight:600;color:#1f2937;margin-bottom:20px;font-family:Inter,sans-serif">${esc(heading)}</h3>` ? `<h3 style="font-size:22px;font-weight:600;color:#1f2937;margin-bottom:20px;font-family:Inter,sans-serif">${escapeHtml(heading)}</h3>`
: ''; : '';
const formStyle = cssPropsToString({ const formStyle = cssPropsToString({
@@ -299,8 +299,8 @@ SubscribeForm.craft = {
html: `<div${wrapperStyle ? ` style="${wrapperStyle}"` : ''}> html: `<div${wrapperStyle ? ` style="${wrapperStyle}"` : ''}>
${headingHtml} ${headingHtml}
<form method="POST"${formStyle ? ` style="${formStyle}"` : ''}> <form method="POST"${formStyle ? ` style="${formStyle}"` : ''}>
<input type="email" name="email" placeholder="${esc(placeholder)}" required style="${inputStyleStr}" /> <input type="email" name="email" placeholder="${escapeAttr(placeholder)}" required style="${inputStyleStr}" />
<button type="submit"${btnStyle ? ` style="${btnStyle}"` : ''}>${esc(buttonText)}</button> <button type="submit"${btnStyle ? ` style="${btnStyle}"` : ''}>${escapeHtml(buttonText)}</button>
</form> </form>
</div>`, </div>`,
}; };
+3 -3
View File
@@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface TextareaFieldProps { interface TextareaFieldProps {
label?: string; label?: string;
@@ -167,7 +168,6 @@ TextareaField.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(TextareaField as any).toHtml = (props: TextareaFieldProps, _childrenHtml: string) => { (TextareaField as any).toHtml = (props: TextareaFieldProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const wrapStyle = cssPropsToString({ const wrapStyle = cssPropsToString({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -176,12 +176,12 @@ TextareaField.craft = {
}); });
const reqAttr = props.required ? ' required' : ''; const reqAttr = props.required ? ' required' : '';
const labelHtml = props.label const labelHtml = props.label
? `<label style="font-size:14px;font-weight:500;color:#18181b">${esc(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>` ? `<label style="font-size:14px;font-weight:500;color:#18181b">${escapeHtml(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>`
: ''; : '';
return { return {
html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}> html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}>
${labelHtml} ${labelHtml}
<textarea name="${esc(props.name || 'message')}" placeholder="${esc(props.placeholder || '')}" rows="${props.rows || 4}"${reqAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box;resize:vertical;font-family:inherit"></textarea> <textarea name="${escapeAttr(props.name || 'message')}" placeholder="${escapeAttr(props.placeholder || '')}" rows="${props.rows || 4}"${reqAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box;resize:vertical;font-family:inherit"></textarea>
</div>`, </div>`,
}; };
}; };
@@ -3,6 +3,7 @@ import { useNode, Element, UserComponent } from '@craftjs/core';
import { Container } from './Container'; import { Container } from './Container';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeAttr } from '../../utils/escape';
interface BackgroundSectionProps { interface BackgroundSectionProps {
bgImage?: string; bgImage?: string;
@@ -182,7 +183,6 @@ BackgroundSection.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(BackgroundSection as any).toHtml = (props: BackgroundSectionProps, childrenHtml: string) => { (BackgroundSection as any).toHtml = (props: BackgroundSectionProps, childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const outerStyle = cssPropsToString({ const outerStyle = cssPropsToString({
position: 'relative', position: 'relative',
width: '100%', width: '100%',
@@ -207,7 +207,7 @@ BackgroundSection.craft = {
margin: '0 auto', margin: '0 auto',
padding: '60px 20px', padding: '60px 20px',
}); });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
return { return {
html: `<section${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}><div${overlayStyle ? ` style="${overlayStyle}"` : ''}></div><div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div></section>`, html: `<section${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}><div${overlayStyle ? ` style="${overlayStyle}"` : ''}></div><div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div></section>`,
}; };
+2 -2
View File
@@ -3,6 +3,7 @@ import { useNode, Element, UserComponent } from '@craftjs/core';
import { Container } from './Container'; import { Container } from './Container';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeAttr } from '../../utils/escape';
type SplitOption = type SplitOption =
| '100' | '100'
@@ -290,7 +291,6 @@ ColumnLayout.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => { (ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const gap = props.gap || '16px'; const gap = props.gap || '16px';
const outerStyle = cssPropsToString({ const outerStyle = cssPropsToString({
display: 'flex', display: 'flex',
@@ -299,7 +299,7 @@ ColumnLayout.craft = {
width: '100%', width: '100%',
...props.style, ...props.style,
}); });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
return { return {
html: `<div${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`, html: `<div${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
}; };
+2 -2
View File
@@ -5,6 +5,7 @@ import { SettingsTabs } from '../../ui/SettingsTabs';
import { BorderControl } from '../../ui/BorderControl'; import { BorderControl } from '../../ui/BorderControl';
import { AdvancedTab } from '../../ui/AdvancedTab'; import { AdvancedTab } from '../../ui/AdvancedTab';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeAttr } from '../../utils/escape';
interface ContainerProps { interface ContainerProps {
style?: CSSProperties; style?: CSSProperties;
@@ -324,7 +325,6 @@ Container.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(Container as any).toHtml = (props: ContainerProps, childrenHtml: string) => { (Container as any).toHtml = (props: ContainerProps, childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const tag = props.tag || 'div'; const tag = props.tag || 'div';
const isBoxed = props.contentWidth === 'boxed'; const isBoxed = props.contentWidth === 'boxed';
const flexStyles = flexAlignFromTextAlign(props.style?.textAlign); const flexStyles = flexAlignFromTextAlign(props.style?.textAlign);
@@ -340,7 +340,7 @@ Container.craft = {
} }
const styleStr = cssPropsToString(outerCss); const styleStr = cssPropsToString(outerCss);
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
if (isBoxed) { if (isBoxed) {
const innerStyle = cssPropsToString({ maxWidth: '1200px', margin: '0 auto', ...flexStyles }); const innerStyle = cssPropsToString({ maxWidth: '1200px', margin: '0 auto', ...flexStyles });
+2 -2
View File
@@ -3,6 +3,7 @@ import { useNode, Element, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { Container } from './Container'; import { Container } from './Container';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeAttr } from '../../utils/escape';
/* ---------- Shape Divider SVG Paths ---------- */ /* ---------- Shape Divider SVG Paths ---------- */
@@ -383,7 +384,6 @@ function buildDividerHtml(
} }
(Section as any).toHtml = (props: SectionProps, childrenHtml: string) => { (Section as any).toHtml = (props: SectionProps, childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const hasTopDivider = props.topDivider && props.topDivider !== 'none'; const hasTopDivider = props.topDivider && props.topDivider !== 'none';
const hasBottomDivider = props.bottomDivider && props.bottomDivider !== 'none'; const hasBottomDivider = props.bottomDivider && props.bottomDivider !== 'none';
@@ -401,7 +401,7 @@ function buildDividerHtml(
const topHtml = buildDividerHtml(props.topDivider, props.topDividerColor, props.topDividerHeight, 'top'); const topHtml = buildDividerHtml(props.topDivider, props.topDividerColor, props.topDividerHeight, 'top');
const bottomHtml = buildDividerHtml(props.bottomDivider, props.bottomDividerColor, props.bottomDividerHeight, 'bottom'); const bottomHtml = buildDividerHtml(props.bottomDivider, props.bottomDividerColor, props.bottomDividerHeight, 'bottom');
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
return { return {
html: `<section${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${topHtml}<div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div>${bottomHtml}</section>`, html: `<section${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${topHtml}<div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div>${bottomHtml}</section>`,
+4 -4
View File
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface AccordionItem { interface AccordionItem {
title: string; title: string;
@@ -299,12 +300,11 @@ Accordion.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(Accordion as any).toHtml = (props: AccordionProps, _childrenHtml: string) => { (Accordion as any).toHtml = (props: AccordionProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const sectionStyle = cssPropsToString({ const sectionStyle = cssPropsToString({
padding: '60px 20px', padding: '60px 20px',
...props.style, ...props.style,
}); });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const headerBg = props.headerBg || '#f8fafc'; const headerBg = props.headerBg || '#f8fafc';
const headerColor = props.headerColor || '#18181b'; const headerColor = props.headerColor || '#18181b';
const contentBg = props.contentBg || '#ffffff'; const contentBg = props.contentBg || '#ffffff';
@@ -318,10 +318,10 @@ Accordion.craft = {
const borderBottom = i === items.length - 1 ? `border:1px solid ${borderColor};` : `border:1px solid ${borderColor};border-bottom:none;`; const borderBottom = i === items.length - 1 ? `border:1px solid ${borderColor};` : `border:1px solid ${borderColor};border-bottom:none;`;
return `<details${openAttr} style="${borderBottom}${topRadius}${bottomRadius}"> return `<details${openAttr} style="${borderBottom}${topRadius}${bottomRadius}">
<summary style="padding:16px 20px;background-color:${headerBg};color:${headerColor};cursor:pointer;font-weight:600;font-size:16px;list-style:none;display:flex;justify-content:space-between;align-items:center"> <summary style="padding:16px 20px;background-color:${headerBg};color:${headerColor};cursor:pointer;font-weight:600;font-size:16px;list-style:none;display:flex;justify-content:space-between;align-items:center">
${esc(item.title)} ${escapeHtml(item.title)}
</summary> </summary>
<div style="padding:16px 20px;background-color:${contentBg};color:#4b5563;font-size:14px;line-height:1.6;border-top:1px solid ${borderColor}"> <div style="padding:16px 20px;background-color:${contentBg};color:#4b5563;font-size:14px;line-height:1.6;border-top:1px solid ${borderColor}">
${esc(item.content)} ${escapeHtml(item.content)}
</div> </div>
</details>`; </details>`;
}).join('\n '); }).join('\n ');
+4 -4
View File
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers'; import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface CTASectionProps { interface CTASectionProps {
heading?: string; heading?: string;
@@ -169,7 +170,6 @@ CTASection.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(CTASection as any).toHtml = (props: CTASectionProps, _childrenHtml: string) => { (CTASection as any).toHtml = (props: CTASectionProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;');
const sectionStyle = cssPropsToString({ const sectionStyle = cssPropsToString({
background: props.gradient || defaultGradient, background: props.gradient || defaultGradient,
padding: '80px 20px', padding: '80px 20px',
@@ -178,12 +178,12 @@ CTASection.craft = {
}); });
const ctas = normalizeCtas(props); const ctas = normalizeCtas(props);
const buttonsHtml = ctasToHtml(ctas, { primaryBg: '#ffffff', primaryText: '#18181b', outlineText: '#ffffff' }); const buttonsHtml = ctasToHtml(ctas, { primaryBg: '#ffffff', primaryText: '#18181b', outlineText: '#ffffff' });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
return { return {
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}> html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
<div style="max-width:700px;margin:0 auto"> <div style="max-width:700px;margin:0 auto">
<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px">${esc(props.heading || '')}</h2> <h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px">${escapeHtml(props.heading || '')}</h2>
<p style="font-size:18px;color:rgba(255,255,255,0.85);margin-bottom:28px;line-height:1.6">${esc(props.description || '')}</p> <p style="font-size:18px;color:rgba(255,255,255,0.85);margin-bottom:28px;line-height:1.6">${escapeHtml(props.description || '')}</p>
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div> <div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
</div> </div>
</section>`, </section>`,
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers'; import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface CallToActionProps { interface CallToActionProps {
heading?: string; heading?: string;
@@ -355,8 +356,6 @@ CallToAction.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(CallToAction as any).toHtml = (props: CallToActionProps, _childrenHtml: string) => { (CallToAction as any).toHtml = (props: CallToActionProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;');
const bgType = props.bgType || 'gradient'; const bgType = props.bgType || 'gradient';
const bgValue = props.bgValue || defaultGradient; const bgValue = props.bgValue || defaultGradient;
const textColor = props.textColor || '#ffffff'; const textColor = props.textColor || '#ffffff';
@@ -397,13 +396,13 @@ CallToAction.craft = {
const ctas = normalizeCtas(props); const ctas = normalizeCtas(props);
const buttonsHtml = ctasToHtml(ctas, { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor }); const buttonsHtml = ctasToHtml(ctas, { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
return { return {
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}> html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
${overlayHtml}<div style="max-width:700px;margin:0 auto;position:relative;z-index:1"> ${overlayHtml}<div style="max-width:700px;margin:0 auto;position:relative;z-index:1">
<h2 style="font-size:36px;font-weight:700;color:${textColor};margin-bottom:12px">${esc(props.heading || '')}</h2> <h2 style="font-size:36px;font-weight:700;color:${textColor};margin-bottom:12px">${escapeHtml(props.heading || '')}</h2>
<p style="font-size:18px;color:${textColor};opacity:0.85;margin-bottom:28px;line-height:1.6">${esc(props.description || '')}</p> <p style="font-size:18px;color:${textColor};opacity:0.85;margin-bottom:28px;line-height:1.6">${escapeHtml(props.description || '')}</p>
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div> <div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
</div> </div>
</section>`, </section>`,
@@ -1,6 +1,7 @@
import React, { CSSProperties, useState, useEffect, useRef, useCallback } from 'react'; import React, { CSSProperties, useState, useEffect, useRef, useCallback } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface Slide { interface Slide {
type: 'image' | 'content'; type: 'image' | 'content';
@@ -443,7 +444,6 @@ ContentSlider.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(ContentSlider as any).toHtml = (props: ContentSliderProps, _childrenHtml: string) => { (ContentSlider as any).toHtml = (props: ContentSliderProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const { const {
slides = defaultSlides, slides = defaultSlides,
autoplay = true, autoplay = true,
@@ -468,20 +468,20 @@ ContentSlider.craft = {
const slidesHtml = items.map((slide, i) => { const slidesHtml = items.map((slide, i) => {
const hasBgImage = slide.imageSrc; const hasBgImage = slide.imageSrc;
const bgStyle = hasBgImage const bgStyle = hasBgImage
? `background-image:url(${esc(slide.imageSrc!)});background-size:cover;background-position:center` ? `background-image:url(${escapeAttr(slide.imageSrc!)});background-size:cover;background-position:center`
: slide.bgColor?.startsWith('linear-gradient') : slide.bgColor?.startsWith('linear-gradient')
? `background-image:${slide.bgColor}` ? `background-image:${slide.bgColor}`
: `background-color:${slide.bgColor || '#3b82f6'}`; : `background-color:${slide.bgColor || '#3b82f6'}`;
const contentParts: string[] = []; const contentParts: string[] = [];
if (slide.heading) { if (slide.heading) {
contentParts.push(`<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px;font-family:Inter,sans-serif;text-shadow:0 2px 8px rgba(0,0,0,0.3)">${esc(slide.heading)}</h2>`); contentParts.push(`<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px;font-family:Inter,sans-serif;text-shadow:0 2px 8px rgba(0,0,0,0.3)">${escapeHtml(slide.heading)}</h2>`);
} }
if (slide.text) { if (slide.text) {
contentParts.push(`<p style="font-size:18px;color:rgba(255,255,255,0.9);margin-bottom:20px;font-family:Inter,sans-serif;text-shadow:0 1px 4px rgba(0,0,0,0.3)">${esc(slide.text)}</p>`); contentParts.push(`<p style="font-size:18px;color:rgba(255,255,255,0.9);margin-bottom:20px;font-family:Inter,sans-serif;text-shadow:0 1px 4px rgba(0,0,0,0.3)">${escapeHtml(slide.text)}</p>`);
} }
if (slide.buttonText) { if (slide.buttonText) {
contentParts.push(`<a href="${esc(slide.buttonHref || '#')}" style="display:inline-block;padding:12px 28px;background:#ffffff;color:#18181b;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;font-family:Inter,sans-serif">${esc(slide.buttonText)}</a>`); contentParts.push(`<a href="${escapeAttr(slide.buttonHref || '#')}" style="display:inline-block;padding:12px 28px;background:#ffffff;color:#18181b;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;font-family:Inter,sans-serif">${escapeHtml(slide.buttonText)}</a>`);
} }
const innerHtml = contentParts.length > 0 const innerHtml = contentParts.length > 0
+3 -3
View File
@@ -2,6 +2,7 @@ import React, { CSSProperties, useEffect, useState, useCallback } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface CountdownProps { interface CountdownProps {
targetDate?: string; targetDate?: string;
@@ -255,7 +256,6 @@ Countdown.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(Countdown as any).toHtml = (props: CountdownProps, _childrenHtml: string) => { (Countdown as any).toHtml = (props: CountdownProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const { const {
targetDate = DEFAULT_TARGET, targetDate = DEFAULT_TARGET,
heading = 'Coming Soon', heading = 'Coming Soon',
@@ -271,10 +271,10 @@ Countdown.craft = {
backgroundColor: bgColor, backgroundColor: bgColor,
...style, ...style,
}); });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const headingHtml = heading const headingHtml = heading
? `<h2 style="font-size:32px;font-weight:700;color:${digitColor};margin-bottom:32px;font-family:Inter,sans-serif">${esc(heading)}</h2>` ? `<h2 style="font-size:32px;font-weight:700;color:${digitColor};margin-bottom:32px;font-family:Inter,sans-serif">${escapeHtml(heading)}</h2>`
: ''; : '';
const boxStyle = 'display:flex;flex-direction:column;align-items:center;gap:4px;min-width:80px'; const boxStyle = 'display:flex;flex-direction:column;align-items:center;gap:4px;min-width:80px';
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface FeatureItem { interface FeatureItem {
title: string; title: string;
@@ -278,23 +279,22 @@ FeaturesGrid.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(FeaturesGrid as any).toHtml = (props: FeaturesGridProps, _childrenHtml: string) => { (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({ const sectionStyle = cssPropsToString({
padding: '80px 20px', padding: '80px 20px',
...props.style, ...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 cards = (props.features || defaultFeatures).map((feat) => {
const media = feat.image 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">` ? `<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">${esc(feat.icon)}</div>`; : `<div style="font-size:36px;margin-bottom:16px">${escapeHtml(feat.icon)}</div>`;
const button = feat.buttonText 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"> return `<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:#f8fafc;border:1px solid #e2e8f0">
${media} ${media}
<h3 style="font-size:20px;font-weight:600;color:#18181b;margin-bottom:8px">${esc(feat.title)}</h3> <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">${esc(feat.description)}</p>${button} <p style="font-size:14px;color:#64748b;line-height:1.6">${escapeHtml(feat.description)}</p>${button}
</div>`; </div>`;
}).join('\n '); }).join('\n ');
+4 -4
View File
@@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface GalleryImage { interface GalleryImage {
src: string; src: string;
@@ -277,7 +278,6 @@ Gallery.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(Gallery as any).toHtml = (props: GalleryProps, _childrenHtml: string) => { (Gallery as any).toHtml = (props: GalleryProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const sectionStyle = cssPropsToString({ const sectionStyle = cssPropsToString({
padding: '60px 20px', padding: '60px 20px',
...props.style, ...props.style,
@@ -291,11 +291,11 @@ Gallery.craft = {
const items = images.map((img) => { const items = images.map((img) => {
const caption = img.caption const caption = img.caption
? `<div style="position:absolute;bottom:0;left:0;right:0;padding:8px 12px;background:linear-gradient(transparent,rgba(0,0,0,0.7));color:#ffffff;font-size:12px;border-bottom-left-radius:8px;border-bottom-right-radius:8px">${esc(img.caption)}</div>` ? `<div style="position:absolute;bottom:0;left:0;right:0;padding:8px 12px;background:linear-gradient(transparent,rgba(0,0,0,0.7));color:#ffffff;font-size:12px;border-bottom-left-radius:8px;border-bottom-right-radius:8px">${escapeHtml(img.caption)}</div>`
: ''; : '';
const clickAttr = lightbox ? ` onclick="${galleryId}_open('${esc(img.src)}')" style="cursor:pointer;position:relative;overflow:hidden;border-radius:8px"` : ' style="position:relative;overflow:hidden;border-radius:8px"'; const clickAttr = lightbox ? ` onclick="${galleryId}_open('${escapeAttr(img.src)}')" style="cursor:pointer;position:relative;overflow:hidden;border-radius:8px"` : ' style="position:relative;overflow:hidden;border-radius:8px"';
return `<div${clickAttr}> return `<div${clickAttr}>
<img src="${esc(img.src)}" alt="${esc(img.alt)}" style="width:100%;height:200px;object-fit:cover;display:block;border-radius:8px;background-color:#f1f5f9" /> <img src="${escapeAttr(img.src)}" alt="${escapeAttr(img.alt)}" style="width:100%;height:200px;object-fit:cover;display:block;border-radius:8px;background-color:#f1f5f9" />
${caption} ${caption}
</div>`; </div>`;
}).join('\n '); }).join('\n ');
+4 -4
View File
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers'; import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface HeroProps { interface HeroProps {
heading?: string; heading?: string;
@@ -412,7 +413,6 @@ HeroSimple.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(HeroSimple as any).toHtml = (props: HeroProps, _childrenHtml: string) => { (HeroSimple as any).toHtml = (props: HeroProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;');
const bg = buildBackground(props); const bg = buildBackground(props);
const justifyMap: Record<string, string> = { top: 'flex-start', center: 'center', bottom: 'flex-end' }; const justifyMap: Record<string, string> = { top: 'flex-start', center: 'center', bottom: 'flex-end' };
@@ -451,13 +451,13 @@ HeroSimple.craft = {
outlineText: props.textColor || '#fff', outlineText: props.textColor || '#fff',
}); });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
return { return {
html: `<section${idAttr} style="${sectionStyle}"> html: `<section${idAttr} style="${sectionStyle}">
${videoHtml}${overlayHtml} ${videoHtml}${overlayHtml}
<div style="max-width:800px;width:100%;position:relative;z-index:2;text-align:${textAlign}"> <div style="max-width:800px;width:100%;position:relative;z-index:2;text-align:${textAlign}">
<h1 style="font-size:48px;font-weight:700;color:${props.textColor || '#fff'};margin-bottom:16px;line-height:1.2">${esc(props.heading || '')}</h1> <h1 style="font-size:48px;font-weight:700;color:${props.textColor || '#fff'};margin-bottom:16px;line-height:1.2">${escapeHtml(props.heading || '')}</h1>
<p style="font-size:20px;color:${props.textColor || '#fff'};opacity:0.85;margin-bottom:32px;line-height:1.6;white-space:pre-line">${esc(props.subtitle || '')}</p> <p style="font-size:20px;color:${props.textColor || '#fff'};opacity:0.85;margin-bottom:32px;line-height:1.6;white-space:pre-line">${escapeHtml(props.subtitle || '')}</p>
<div style="display:flex;gap:12px;justify-content:${justifyBtn};flex-wrap:wrap">${buttonsHtml}</div> <div style="display:flex;gap:12px;justify-content:${justifyBtn};flex-wrap:wrap">${buttonsHtml}</div>
</div> </div>
</section>`, </section>`,
@@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface Counter { interface Counter {
number: number; number: number;
@@ -305,7 +306,6 @@ NumberCounter.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(NumberCounter as any).toHtml = (props: NumberCounterProps, _childrenHtml: string) => { (NumberCounter as any).toHtml = (props: NumberCounterProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const { const {
counters = defaultCounters, counters = defaultCounters,
columns = 4, columns = 4,
@@ -326,8 +326,8 @@ NumberCounter.craft = {
const countersHtml = items.map((counter, i) => { const countersHtml = items.map((counter, i) => {
return `<div style="display:flex;flex-direction:column;align-items:center;gap:8px"> return `<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
<span id="${uid}_n${i}" data-target="${counter.number}" data-suffix="${esc(counter.suffix)}" style="font-size:${numberSize};font-weight:700;color:${numberColor};line-height:1.1;font-family:Inter,sans-serif">0${esc(counter.suffix)}</span> <span id="${uid}_n${i}" data-target="${counter.number}" data-suffix="${escapeAttr(counter.suffix)}" style="font-size:${numberSize};font-weight:700;color:${numberColor};line-height:1.1;font-family:Inter,sans-serif">0${escapeHtml(counter.suffix)}</span>
<span style="font-size:15px;color:${labelColor};font-family:Inter,sans-serif;font-weight:500">${esc(counter.label)}</span> <span style="font-size:15px;color:${labelColor};font-family:Inter,sans-serif;font-weight:500">${escapeHtml(counter.label)}</span>
</div>`; </div>`;
}).join('\n '); }).join('\n ');
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface PricingPlan { interface PricingPlan {
name: string; name: string;
@@ -404,13 +405,12 @@ PricingTable.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(PricingTable as any).toHtml = (props: PricingTableProps, _childrenHtml: string) => { (PricingTable as any).toHtml = (props: PricingTableProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const bulletType = props.bulletType || 'check'; const bulletType = props.bulletType || 'check';
const sectionStyle = cssPropsToString({ const sectionStyle = cssPropsToString({
padding: '80px 20px', padding: '80px 20px',
...props.style, ...props.style,
}); });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const plans = props.plans || defaultPlans; const plans = props.plans || defaultPlans;
const featuredBg = props.featuredBg || '#3b82f6'; const featuredBg = props.featuredBg || '#3b82f6';
@@ -427,7 +427,7 @@ PricingTable.craft = {
const shadow = plan.isFeatured ? 'box-shadow:0 20px 60px rgba(59,130,246,0.3);' : 'box-shadow:0 1px 3px rgba(0,0,0,0.06);'; const shadow = plan.isFeatured ? 'box-shadow:0 20px 60px rgba(59,130,246,0.3);' : 'box-shadow:0 1px 3px rgba(0,0,0,0.06);';
const featuresHtml = (Array.isArray(plan.features) ? plan.features : []).map((f) => const featuresHtml = (Array.isArray(plan.features) ? plan.features : []).map((f) =>
`<li style="font-size:14px;color:${featColor};display:flex;align-items:center;gap:8px"><span style="color:${checkColor};font-weight:700">${bulletChars[bulletType] || '✓'}</span>${esc(f)}</li>` `<li style="font-size:14px;color:${featColor};display:flex;align-items:center;gap:8px"><span style="color:${checkColor};font-weight:700">${bulletChars[bulletType] || '✓'}</span>${escapeHtml(f)}</li>`
).join('\n '); ).join('\n ');
const badge = plan.isFeatured const badge = plan.isFeatured
@@ -436,15 +436,15 @@ PricingTable.craft = {
return `<div style="flex:1 1 280px;max-width:360px;background-color:${cardBg};${cardBorder}border-radius:16px;padding:40px 32px;display:flex;flex-direction:column;align-items:center;text-align:center;position:relative;${scale}${shadow}"> return `<div style="flex:1 1 280px;max-width:360px;background-color:${cardBg};${cardBorder}border-radius:16px;padding:40px 32px;display:flex;flex-direction:column;align-items:center;text-align:center;position:relative;${scale}${shadow}">
${badge} ${badge}
<h3 style="font-size:20px;font-weight:600;color:${textColor};margin-bottom:8px;${plan.isFeatured ? 'margin-top:8px;' : ''}">${esc(plan.name)}</h3> <h3 style="font-size:20px;font-weight:600;color:${textColor};margin-bottom:8px;${plan.isFeatured ? 'margin-top:8px;' : ''}">${escapeHtml(plan.name)}</h3>
<div style="margin-bottom:24px"> <div style="margin-bottom:24px">
<span style="font-size:48px;font-weight:700;color:${textColor};line-height:1">${esc(plan.price)}</span> <span style="font-size:48px;font-weight:700;color:${textColor};line-height:1">${escapeHtml(plan.price)}</span>
<span style="font-size:16px;color:${subColor}">${esc(plan.period)}</span> <span style="font-size:16px;color:${subColor}">${escapeHtml(plan.period)}</span>
</div> </div>
<ul style="list-style:none;padding:0;margin:0 0 32px 0;width:100%;display:flex;flex-direction:column;gap:12px"> <ul style="list-style:none;padding:0;margin:0 0 32px 0;width:100%;display:flex;flex-direction:column;gap:12px">
${featuresHtml} ${featuresHtml}
</ul> </ul>
<a href="${plan.buttonHref || '#'}" style="margin-top:auto;display:inline-block;padding:14px 32px;background-color:${btnBg};color:${btnColor};text-decoration:none;border-radius:8px;font-weight:600;font-size:14px;width:100%;text-align:center">${esc(plan.buttonText)}</a> <a href="${plan.buttonHref || '#'}" style="margin-top:auto;display:inline-block;padding:14px 32px;background-color:${btnBg};color:${btnColor};text-decoration:none;border-radius:8px;font-weight:600;font-size:14px;width:100%;text-align:center">${escapeHtml(plan.buttonText)}</a>
</div>`; </div>`;
}).join('\n '); }).join('\n ');
+4 -4
View File
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface TabItem { interface TabItem {
label: string; label: string;
@@ -296,12 +297,11 @@ Tabs.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(Tabs as any).toHtml = (props: TabsProps, _childrenHtml: string) => { (Tabs as any).toHtml = (props: TabsProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const sectionStyle = cssPropsToString({ const sectionStyle = cssPropsToString({
padding: '60px 20px', padding: '60px 20px',
...props.style, ...props.style,
}); });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const tabs = props.tabs || defaultTabs; const tabs = props.tabs || defaultTabs;
const activeTabBg = props.activeTabBg || '#3b82f6'; const activeTabBg = props.activeTabBg || '#3b82f6';
const activeTabColor = props.activeTabColor || '#ffffff'; const activeTabColor = props.activeTabColor || '#ffffff';
@@ -313,11 +313,11 @@ Tabs.craft = {
const tabButtons = tabs.map((tab, i) => { const tabButtons = tabs.map((tab, i) => {
const isActive = i === 0; const isActive = i === 0;
return `<button onclick="${tabId}_switch(${i})" id="${tabId}_btn_${i}" style="padding:12px 24px;font-size:14px;font-weight:600;border:none;border-top-left-radius:8px;border-top-right-radius:8px;cursor:pointer;background-color:${isActive ? activeTabBg : inactiveTabBg};color:${isActive ? activeTabColor : inactiveTabColor}">${esc(tab.label)}</button>`; return `<button onclick="${tabId}_switch(${i})" id="${tabId}_btn_${i}" style="padding:12px 24px;font-size:14px;font-weight:600;border:none;border-top-left-radius:8px;border-top-right-radius:8px;cursor:pointer;background-color:${isActive ? activeTabBg : inactiveTabBg};color:${isActive ? activeTabColor : inactiveTabColor}">${escapeHtml(tab.label)}</button>`;
}).join('\n '); }).join('\n ');
const tabPanels = tabs.map((tab, i) => { const tabPanels = tabs.map((tab, i) => {
return `<div id="${tabId}_panel_${i}" style="padding:24px;background-color:${contentBg};border:1px solid #e2e8f0;border-top:none;border-bottom-left-radius:8px;border-bottom-right-radius:8px;font-size:14px;line-height:1.7;color:#4b5563;min-height:100px;${i !== 0 ? 'display:none' : ''}">${esc(tab.content)}</div>`; return `<div id="${tabId}_panel_${i}" style="padding:24px;background-color:${contentBg};border:1px solid #e2e8f0;border-top:none;border-bottom-left-radius:8px;border-bottom-right-radius:8px;font-size:14px;line-height:1.7;color:#4b5563;min-height:100px;${i !== 0 ? 'display:none' : ''}">${escapeHtml(tab.content)}</div>`;
}).join('\n '); }).join('\n ');
const switchScript = `<script> const switchScript = `<script>
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react';
import { useNode, UserComponent } from '@craftjs/core'; import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers'; import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField'; import { AnchorIdField } from '../../ui/AnchorIdField';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface Testimonial { interface Testimonial {
quote: string; quote: string;
@@ -377,7 +378,6 @@ Testimonials.craft = {
/* ---------- HTML export ---------- */ /* ---------- HTML export ---------- */
(Testimonials as any).toHtml = (props: TestimonialsProps, _childrenHtml: string) => { (Testimonials as any).toHtml = (props: TestimonialsProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const { const {
testimonials = defaultTestimonials, testimonials = defaultTestimonials,
layout = 'grid', layout = 'grid',
@@ -394,16 +394,16 @@ Testimonials.craft = {
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
...style, ...style,
}); });
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const cardCss = `background-color:${cardBg};border-radius:12px;padding:32px 24px;text-align:center;border:1px solid #e2e8f0`; const cardCss = `background-color:${cardBg};border-radius:12px;padding:32px 24px;text-align:center;border:1px solid #e2e8f0`;
const cards = items.map((t) => { const cards = items.map((t) => {
return `<div style="${cardCss}"> return `<div style="${cardCss}">
${starsHtml(t.rating, starColor)} ${starsHtml(t.rating, starColor)}
<p style="font-size:15px;color:#374151;line-height:1.7;margin-bottom:16px;font-style:italic;font-family:Inter,sans-serif">&ldquo;${esc(t.quote)}&rdquo;</p> <p style="font-size:15px;color:#374151;line-height:1.7;margin-bottom:16px;font-style:italic;font-family:Inter,sans-serif">&ldquo;${escapeHtml(t.quote)}&rdquo;</p>
<div style="font-weight:600;font-size:14px;color:#18181b;font-family:Inter,sans-serif">${esc(t.name)}</div> <div style="font-weight:600;font-size:14px;color:#18181b;font-family:Inter,sans-serif">${escapeHtml(t.name)}</div>
<div style="font-size:13px;color:#64748b;font-family:Inter,sans-serif">${esc(t.title)}</div> <div style="font-size:13px;color:#64748b;font-family:Inter,sans-serif">${escapeHtml(t.title)}</div>
</div>`; </div>`;
}).join('\n '); }).join('\n ');
@@ -1,4 +1,5 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { escapeHtml, escapeAttr } from '../../utils/escape';
export type CtaVariant = 'primary' | 'outline' | 'ghost'; export type CtaVariant = 'primary' | 'outline' | 'ghost';
@@ -78,12 +79,10 @@ export function ctaCssString(cta: CtaButton, defaults: CtaStyleDefaults): string
} }
} }
const esc = (s: any) => String(s ?? '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
export function ctasToHtml(ctas: CtaButton[], defaults: CtaStyleDefaults): string { export function ctasToHtml(ctas: CtaButton[], defaults: CtaStyleDefaults): string {
return ctas.map((c) => { return ctas.map((c) => {
const target = c.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : ''; const target = c.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
return `<a href="${esc(c.href || '#')}"${target} style="${ctaCssString(c, defaults)}">${esc(c.text || '')}</a>`; return `<a href="${escapeAttr(c.href || '#')}"${target} style="${ctaCssString(c, defaults)}">${escapeHtml(c.text || '')}</a>`;
}).join(''); }).join('');
} }
+3 -4
View File
@@ -9,8 +9,7 @@
* HTML — see PR #47 review). * HTML — see PR #47 review).
*/ */
const esc = (s: unknown): string => import { escapeAttr } from './escape';
String(s ?? '').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
export interface RelayWiring { export interface RelayWiring {
/** true when a recipient is set (relay path); false = legacy formAction fallback */ /** true when a recipient is set (relay path); false = legacy formAction fallback */
@@ -34,12 +33,12 @@ export function relayFormWiring(
fallbackAction: string | undefined, fallbackAction: string | undefined,
): RelayWiring { ): RelayWiring {
if (!recipientEmail) { if (!recipientEmail) {
return { useRelay: false, marker: '', actionAttr: esc(fallbackAction || '#'), honeypot: '' }; return { useRelay: false, marker: '', actionAttr: escapeAttr(fallbackAction || '#'), honeypot: '' };
} }
const fid = 'F' + Math.random().toString(36).slice(2, 8); const fid = 'F' + Math.random().toString(36).slice(2, 8);
return { return {
useRelay: true, useRelay: true,
marker: `<!--WHP-FORM id="${fid}" recipient="${esc(recipientEmail)}" thankyou="${esc(thankYouUrl || '')}"-->`, marker: `<!--WHP-FORM id="${fid}" recipient="${escapeAttr(recipientEmail)}" thankyou="${escapeAttr(thankYouUrl || '')}"-->`,
actionAttr: `__WHP_FORM_ACTION__${fid}__`, actionAttr: `__WHP_FORM_ACTION__${fid}__`,
honeypot: `<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true">`, honeypot: `<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true">`,
}; };