UNPKG

@spoolcms/nextjs

Version:

The beautiful headless CMS for Next.js developers

125 lines (124 loc) 4.73 kB
"use strict"; 'use client'; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SpoolContentRenderer = SpoolContentRenderer; exports.SpoolContentSecure = SpoolContentSecure; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = __importDefault(require("react")); /** * Safe renderer for Spool CMS content that prevents XSS attacks * and properly handles code blocks without execution. * * This component safely renders HTML content from Spool CMS by: * 1. Escaping HTML entities inside code blocks to prevent execution * 2. Basic HTML sanitization to remove dangerous elements * 3. Preventing script execution while preserving formatting * 4. Ensuring forms and other HTML in code examples display as text * * @example * ```tsx * import { SpoolContentRenderer } from '@spoolcms/nextjs'; * * export function BlogPost({ post }) { * return ( * <article> * <h1>{post.title}</h1> * <SpoolContentRenderer content={post.body} /> * </article> * ); * } * ``` */ function SpoolContentRenderer({ content, className, as: Component = 'div' }) { const sanitizedContent = react_1.default.useMemo(() => { if (!content) return ''; let processedContent = content; // Fix code blocks: escape HTML entities inside <code> and <pre><code> blocks processedContent = processedContent.replace(/(<pre[^>]*>)?<code[^>]*>([\s\S]*?)<\/code>(<\/pre>)?/gi, (match, preOpen, codeContent, preClose) => { // Escape HTML entities inside code blocks const escapedContent = codeContent .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;'); return `${preOpen || ''}<code>${escapedContent}</code>${preClose || ''}`; }); // Basic HTML sanitization - remove script tags and event handlers processedContent = processedContent .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') .replace(/\son\w+\s*=\s*["'][^"']*["']/gi, '') .replace(/javascript:/gi, ''); return processedContent; }, [content]); return ((0, jsx_runtime_1.jsx)(Component, { className: className, dangerouslySetInnerHTML: { __html: sanitizedContent } })); } /** * Alternative approach using iframe for maximum security * Use this if you need the highest level of security */ function SpoolContentSecure({ content, className }) { const iframeRef = react_1.default.useRef(null); react_1.default.useEffect(() => { if (iframeRef.current && content) { const iframe = iframeRef.current; const doc = iframe.contentDocument || iframe.contentWindow?.document; if (doc) { doc.open(); doc.write(` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> body { font-family: inherit; margin: 0; padding: 16px; line-height: 1.6; } pre { background: #f5f5f5; padding: 12px; border-radius: 4px; overflow-x: auto; } code { background: #f5f5f5; padding: 2px 4px; border-radius: 2px; font-family: 'Monaco', 'Menlo', monospace; } blockquote { border-left: 4px solid #ddd; margin: 0; padding-left: 16px; color: #666; } </style> </head> <body>${content}</body> </html> `); doc.close(); // Auto-resize iframe to content const resizeIframe = () => { if (doc.body) { iframe.style.height = doc.body.scrollHeight + 'px'; } }; setTimeout(resizeIframe, 100); iframe.addEventListener('load', resizeIframe); } } }, [content]); return ((0, jsx_runtime_1.jsx)("iframe", { ref: iframeRef, className: className, style: { width: '100%', border: 'none', minHeight: '100px', }, sandbox: "allow-same-origin", title: "Spool Content" })); }