import React, { CSSProperties } from 'react'; import { useNode, Element, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { Container } from './Container'; import { escapeAttr, cssValue } from '../../utils/escape'; /* ---------- Shape Divider SVG Paths ---------- */ type DividerShape = 'none' | 'wave' | 'angle' | 'curve' | 'triangle' | 'zigzag'; const DIVIDER_PATHS: Record, string> = { wave: 'M0,0 C150,120 350,0 600,60 C850,120 1050,0 1200,60 L1200,120 L0,120 Z', angle: 'M0,0 L1200,120 L0,120 Z', curve: 'M0,0 Q600,140 1200,0 L1200,120 L0,120 Z', triangle: 'M0,120 L600,0 L1200,120 Z', zigzag: 'M0,120 L100,40 L200,120 L300,40 L400,120 L500,40 L600,120 L700,40 L800,120 L900,40 L1000,120 L1100,40 L1200,120 Z', }; interface SectionProps { style?: CSSProperties; innerMaxWidth?: string; children?: React.ReactNode; topDivider?: DividerShape; topDividerColor?: string; topDividerHeight?: string; bottomDivider?: DividerShape; bottomDividerColor?: string; bottomDividerHeight?: string; anchorId?: string; } /* ---------- Divider renderer ---------- */ const ShapeDivider: React.FC<{ shape: DividerShape; color: string; height: string; position: 'top' | 'bottom'; }> = ({ shape, color, height, position }) => { if (!shape || shape === 'none') return null; // `shape` is attacker-controlled (AI update_props / deserialized state) and // not runtime-type-checked. A plain-object index lookup with a string key // like '__proto__', 'toString', or 'constructor' returns an INHERITED // Object.prototype value (not undefined), which would otherwise leak // "[object Object]" / a function's source text into the SVG `d` attribute // below. hasOwnProperty restricts the lookup to the real allowlisted keys. if (!Object.prototype.hasOwnProperty.call(DIVIDER_PATHS, shape)) return null; const path = DIVIDER_PATHS[shape as Exclude]; if (!path) return null; const isTop = position === 'top'; return (
); }; /* ---------- Component ---------- */ export const Section: UserComponent = ({ style = {}, innerMaxWidth = '1200px', children, topDivider = 'none', topDividerColor = '#ffffff', topDividerHeight = '50px', bottomDivider = 'none', bottomDividerColor = '#ffffff', bottomDividerHeight = '50px', anchorId, }) => { const { connectors: { connect, drag } } = useNode(); const hasTopDivider = topDivider && topDivider !== 'none'; const hasBottomDivider = bottomDivider && bottomDivider !== 'none'; return (
{ if (ref) connect(drag(ref)); }} id={anchorId || undefined} style={{ width: '100%', position: (hasTopDivider || hasBottomDivider) ? 'relative' : undefined, ...style, }} > {hasTopDivider && ( )} {children} {hasBottomDivider && ( )}
); }; /* ---------- Craft config ---------- */ Section.craft = { displayName: 'Section', props: { style: { padding: '40px 0', backgroundColor: '#ffffff' }, innerMaxWidth: '1200px', topDivider: 'none', topDividerColor: '#ffffff', topDividerHeight: '50px', bottomDivider: 'none', bottomDividerColor: '#ffffff', bottomDividerHeight: '50px', anchorId: '', }, rules: { canDrag: () => true, canMoveIn: () => true, canMoveOut: () => true, }, }; /* ---------- HTML export ---------- */ function buildDividerHtml( shape: DividerShape | undefined, color: string | undefined, height: string | undefined, position: 'top' | 'bottom', ): string { if (!shape || shape === 'none') return ''; // See the matching hasOwnProperty guard in above -- same // prototype-pollution-shaped lookup, same fix. if (!Object.prototype.hasOwnProperty.call(DIVIDER_PATHS, shape)) return ''; const path = DIVIDER_PATHS[shape as Exclude]; if (!path) return ''; const isTop = position === 'top'; const h = height || '50px'; // Sanitized -- raw string-interpolation sink in the SVG `fill:${c}` below. const c = cssValue(color) || '#ffffff'; const wrapperStyle = cssPropsToString({ position: 'absolute', [position]: '0', left: '0', right: '0', height: h, overflow: 'hidden', lineHeight: '0', pointerEvents: 'none', } as CSSProperties); const svgTransform = isTop ? ' transform:rotate(180deg);' : ''; return `
`; } (Section as any).toHtml = (props: SectionProps, childrenHtml: string) => { const hasTopDivider = props.topDivider && props.topDivider !== 'none'; const hasBottomDivider = props.bottomDivider && props.bottomDivider !== 'none'; const outerStyle = cssPropsToString({ width: '100%', position: (hasTopDivider || hasBottomDivider) ? 'relative' : undefined, ...props.style, }); const innerStyle = cssPropsToString({ maxWidth: props.innerMaxWidth || '1200px', margin: '0 auto', position: (hasTopDivider || hasBottomDivider) ? 'relative' : undefined, zIndex: (hasTopDivider || hasBottomDivider) ? 1 : undefined, } as CSSProperties); const topHtml = buildDividerHtml(props.topDivider, props.topDividerColor, props.topDividerHeight, 'top'); const bottomHtml = buildDividerHtml(props.bottomDivider, props.bottomDividerColor, props.bottomDividerHeight, 'bottom'); const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : ''; return { html: `${topHtml}${childrenHtml}${bottomHtml}`, }; };