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:
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } 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 AccordionItem {
|
||||
title: string;
|
||||
@@ -299,12 +300,11 @@ Accordion.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(Accordion as any).toHtml = (props: AccordionProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
const sectionStyle = cssPropsToString({
|
||||
padding: '60px 20px',
|
||||
...props.style,
|
||||
});
|
||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
||||
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||
const headerBg = props.headerBg || '#f8fafc';
|
||||
const headerColor = props.headerColor || '#18181b';
|
||||
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;`;
|
||||
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">
|
||||
${esc(item.title)}
|
||||
${escapeHtml(item.title)}
|
||||
</summary>
|
||||
<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>
|
||||
</details>`;
|
||||
}).join('\n ');
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface CTASectionProps {
|
||||
heading?: string;
|
||||
@@ -169,7 +170,6 @@ CTASection.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(CTASection as any).toHtml = (props: CTASectionProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>');
|
||||
const sectionStyle = cssPropsToString({
|
||||
background: props.gradient || defaultGradient,
|
||||
padding: '80px 20px',
|
||||
@@ -178,12 +178,12 @@ CTASection.craft = {
|
||||
});
|
||||
const ctas = normalizeCtas(props);
|
||||
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 {
|
||||
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
||||
<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>
|
||||
<p style="font-size:18px;color:rgba(255,255,255,0.85);margin-bottom:28px;line-height:1.6">${esc(props.description || '')}</p>
|
||||
<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">${escapeHtml(props.description || '')}</p>
|
||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
|
||||
</div>
|
||||
</section>`,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface CallToActionProps {
|
||||
heading?: string;
|
||||
@@ -355,8 +356,6 @@ CallToAction.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(CallToAction as any).toHtml = (props: CallToActionProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
const bgType = props.bgType || 'gradient';
|
||||
const bgValue = props.bgValue || defaultGradient;
|
||||
const textColor = props.textColor || '#ffffff';
|
||||
@@ -397,13 +396,13 @@ CallToAction.craft = {
|
||||
|
||||
const ctas = normalizeCtas(props);
|
||||
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 {
|
||||
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
||||
${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>
|
||||
<p style="font-size:18px;color:${textColor};opacity:0.85;margin-bottom:28px;line-height:1.6">${esc(props.description || '')}</p>
|
||||
<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">${escapeHtml(props.description || '')}</p>
|
||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
|
||||
</div>
|
||||
</section>`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties, useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface Slide {
|
||||
type: 'image' | 'content';
|
||||
@@ -443,7 +444,6 @@ ContentSlider.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(ContentSlider as any).toHtml = (props: ContentSliderProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
const {
|
||||
slides = defaultSlides,
|
||||
autoplay = true,
|
||||
@@ -468,20 +468,20 @@ ContentSlider.craft = {
|
||||
const slidesHtml = items.map((slide, i) => {
|
||||
const hasBgImage = slide.imageSrc;
|
||||
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')
|
||||
? `background-image:${slide.bgColor}`
|
||||
: `background-color:${slide.bgColor || '#3b82f6'}`;
|
||||
|
||||
const contentParts: string[] = [];
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { CSSProperties, useEffect, useState, useCallback } 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 CountdownProps {
|
||||
targetDate?: string;
|
||||
@@ -255,7 +256,6 @@ Countdown.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(Countdown as any).toHtml = (props: CountdownProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
const {
|
||||
targetDate = DEFAULT_TARGET,
|
||||
heading = 'Coming Soon',
|
||||
@@ -271,10 +271,10 @@ Countdown.craft = {
|
||||
backgroundColor: bgColor,
|
||||
...style,
|
||||
});
|
||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
||||
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||
|
||||
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';
|
||||
|
||||
@@ -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, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
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 ');
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface GalleryImage {
|
||||
src: string;
|
||||
@@ -277,7 +278,6 @@ Gallery.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(Gallery as any).toHtml = (props: GalleryProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
const sectionStyle = cssPropsToString({
|
||||
padding: '60px 20px',
|
||||
...props.style,
|
||||
@@ -291,11 +291,11 @@ Gallery.craft = {
|
||||
|
||||
const items = images.map((img) => {
|
||||
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}>
|
||||
<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}
|
||||
</div>`;
|
||||
}).join('\n ');
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface HeroProps {
|
||||
heading?: string;
|
||||
@@ -412,7 +413,6 @@ HeroSimple.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(HeroSimple as any).toHtml = (props: HeroProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>');
|
||||
const bg = buildBackground(props);
|
||||
const justifyMap: Record<string, string> = { top: 'flex-start', center: 'center', bottom: 'flex-end' };
|
||||
|
||||
@@ -451,13 +451,13 @@ HeroSimple.craft = {
|
||||
outlineText: props.textColor || '#fff',
|
||||
});
|
||||
|
||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
||||
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||
return {
|
||||
html: `<section${idAttr} style="${sectionStyle}">
|
||||
${videoHtml}${overlayHtml}
|
||||
<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>
|
||||
<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>
|
||||
<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">${escapeHtml(props.subtitle || '')}</p>
|
||||
<div style="display:flex;gap:12px;justify-content:${justifyBtn};flex-wrap:wrap">${buttonsHtml}</div>
|
||||
</div>
|
||||
</section>`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface Counter {
|
||||
number: number;
|
||||
@@ -305,7 +306,6 @@ NumberCounter.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(NumberCounter as any).toHtml = (props: NumberCounterProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
const {
|
||||
counters = defaultCounters,
|
||||
columns = 4,
|
||||
@@ -326,8 +326,8 @@ NumberCounter.craft = {
|
||||
|
||||
const countersHtml = items.map((counter, i) => {
|
||||
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 style="font-size:15px;color:${labelColor};font-family:Inter,sans-serif;font-weight:500">${esc(counter.label)}</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">${escapeHtml(counter.label)}</span>
|
||||
</div>`;
|
||||
}).join('\n ');
|
||||
|
||||
|
||||
@@ -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 PricingPlan {
|
||||
name: string;
|
||||
@@ -404,13 +405,12 @@ PricingTable.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(PricingTable as any).toHtml = (props: PricingTableProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
const bulletType = props.bulletType || 'check';
|
||||
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 plans = props.plans || defaultPlans;
|
||||
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 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 ');
|
||||
|
||||
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}">
|
||||
${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">
|
||||
<span style="font-size:48px;font-weight:700;color:${textColor};line-height:1">${esc(plan.price)}</span>
|
||||
<span style="font-size:16px;color:${subColor}">${esc(plan.period)}</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}">${escapeHtml(plan.period)}</span>
|
||||
</div>
|
||||
<ul style="list-style:none;padding:0;margin:0 0 32px 0;width:100%;display:flex;flex-direction:column;gap:12px">
|
||||
${featuresHtml}
|
||||
</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>`;
|
||||
}).join('\n ');
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } 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 TabItem {
|
||||
label: string;
|
||||
@@ -296,12 +297,11 @@ Tabs.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(Tabs as any).toHtml = (props: TabsProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
const sectionStyle = cssPropsToString({
|
||||
padding: '60px 20px',
|
||||
...props.style,
|
||||
});
|
||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
||||
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||
const tabs = props.tabs || defaultTabs;
|
||||
const activeTabBg = props.activeTabBg || '#3b82f6';
|
||||
const activeTabColor = props.activeTabColor || '#ffffff';
|
||||
@@ -313,11 +313,11 @@ Tabs.craft = {
|
||||
|
||||
const tabButtons = tabs.map((tab, i) => {
|
||||
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 ');
|
||||
|
||||
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 ');
|
||||
|
||||
const switchScript = `<script>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } 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 Testimonial {
|
||||
quote: string;
|
||||
@@ -377,7 +378,6 @@ Testimonials.craft = {
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(Testimonials as any).toHtml = (props: TestimonialsProps, _childrenHtml: string) => {
|
||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
const {
|
||||
testimonials = defaultTestimonials,
|
||||
layout = 'grid',
|
||||
@@ -394,16 +394,16 @@ Testimonials.craft = {
|
||||
backgroundColor: '#ffffff',
|
||||
...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 cards = items.map((t) => {
|
||||
return `<div style="${cardCss}">
|
||||
${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">“${esc(t.quote)}”</p>
|
||||
<div style="font-weight:600;font-size:14px;color:#18181b;font-family:Inter,sans-serif">${esc(t.name)}</div>
|
||||
<div style="font-size:13px;color:#64748b;font-family:Inter,sans-serif">${esc(t.title)}</div>
|
||||
<p style="font-size:15px;color:#374151;line-height:1.7;margin-bottom:16px;font-style:italic;font-family:Inter,sans-serif">“${escapeHtml(t.quote)}”</p>
|
||||
<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">${escapeHtml(t.title)}</div>
|
||||
</div>`;
|
||||
}).join('\n ');
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
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, '<').replace(/>/g, '>');
|
||||
|
||||
export function ctasToHtml(ctas: CtaButton[], defaults: CtaStyleDefaults): string {
|
||||
return ctas.map((c) => {
|
||||
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('');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user