cms-renderer 0.6.11 → 0.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import * as React from 'react';
1
+ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
4
  interface DocsMarkdownProps {
@@ -13,6 +13,6 @@ interface DocsMarkdownProps {
13
13
  }
14
14
  declare function markdownStartsWithHeading(markdown: string): boolean;
15
15
  declare function initMarkdown(): Promise<void>;
16
- declare function DocsMarkdown({ content, className, renderImage, }: DocsMarkdownProps): Promise<React.JSX.Element>;
16
+ declare function DocsMarkdown({ content, className, renderImage, }: DocsMarkdownProps): Promise<react.JSX.Element>;
17
17
 
18
18
  export { DocsMarkdown, type DocsMarkdownProps, initMarkdown, markdownStartsWithHeading };
@@ -45,7 +45,15 @@ function stripScriptTags(content) {
45
45
  if (start === -1) break;
46
46
  const closeStart = lower.indexOf("</script", start);
47
47
  if (closeStart === -1) {
48
- searchFrom = start + 1;
48
+ let end2 = lower.indexOf(">", start);
49
+ if (end2 === -1) {
50
+ end2 = lower.length;
51
+ } else {
52
+ end2 += 1;
53
+ }
54
+ result = result.slice(0, start) + result.slice(end2);
55
+ lower = result.toLowerCase();
56
+ searchFrom = start;
49
57
  continue;
50
58
  }
51
59
  let end = lower.indexOf(">", closeStart);
@@ -96,7 +104,16 @@ function neutralizeMarkdownLinkAndImageDestinations(content) {
96
104
  }
97
105
  const closeParen = result.indexOf(")", openParen);
98
106
  if (closeParen === -1) {
99
- index = markerIndex + marker.length;
107
+ let urlEnd = openParen + 1;
108
+ while (urlEnd < result.length && !isHtmlWhitespace(result.charAt(urlEnd)) && result.charAt(urlEnd) !== "\n" && result.charAt(urlEnd) !== "\r") {
109
+ urlEnd += 1;
110
+ }
111
+ const url = result.slice(openParen + 1, urlEnd);
112
+ const sanitized = neutralizeDangerousSchemesInUrl(url);
113
+ if (sanitized !== url) {
114
+ result = result.slice(0, openParen + 1) + sanitized + result.slice(urlEnd);
115
+ }
116
+ index = openParen + sanitized.length + 1;
100
117
  continue;
101
118
  }
102
119
  const replaced = replaceUrlInParenGroup(result, openParen, closeParen);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/markdown-utils.ts","../../lib/markdown-sanitize.ts","../../lib/result.ts"],"sourcesContent":["/**\n * Markdown Parsing Utilities\n *\n * Three implementations showing the tradeoffs between simple and more\n * defensive parsing:\n * - v1 (Naive): Direct call, crashes on bad input\n * - v2 (Defensive): Try-catch with null fallback\n * - v3 (Robust): Result type, validation, lightweight sanitization\n *\n * The default export points to the v3 implementation.\n */\n\nimport type { MDTree } from 'md4w';\nimport { mdToJSON } from 'md4w';\n\nimport { sanitizeMarkdownContent } from './markdown-sanitize';\nimport { err, ok, type Result } from './result';\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Error codes for markdown parsing failures.\n */\nexport const ParseErrorCode = {\n INVALID_INPUT: 'INVALID_INPUT',\n EMPTY_CONTENT: 'EMPTY_CONTENT',\n PARSE_FAILED: 'PARSE_FAILED',\n CONTENT_TOO_LONG: 'CONTENT_TOO_LONG',\n NESTED_TOO_DEEP: 'NESTED_TOO_DEEP',\n} as const;\n\nexport type ParseErrorCode = (typeof ParseErrorCode)[keyof typeof ParseErrorCode];\n\n/**\n * Structured error for markdown parsing failures.\n */\nexport interface ParseError {\n code: ParseErrorCode;\n message: string;\n cause?: Error;\n}\n\n/**\n * Options for robust markdown parsing.\n */\nexport interface ParseOptions {\n /**\n * Maximum content length in characters.\n * @default 500000 (500KB of text, ~100K words)\n */\n maxLength?: number;\n\n /**\n * Whether to run a lightweight string-based sanitization pass before parsing.\n * @default true\n */\n sanitize?: boolean;\n\n /**\n * Whether to allow empty content.\n * @default false\n */\n allowEmpty?: boolean;\n}\n\n// -----------------------------------------------------------------------------\n// Constants\n// -----------------------------------------------------------------------------\n\nconst DEFAULT_MAX_LENGTH = 500_000; // 500KB of text\n\n/**\n * Patterns that indicate potential XSS attempts in markdown.\n * These are checked in raw content before parsing.\n */\nconst XSS_PATTERNS = [\n /<script\\b/i,\n /javascript:/i,\n /on\\w+\\s*=/i, // onclick=, onerror=, etc.\n /\\bdata:/i, // data: URIs can be dangerous\n /vbscript:/i,\n] as const;\n\n// -----------------------------------------------------------------------------\n// V1: Naive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Naive markdown parser - crashes on bad input.\n *\n * DO NOT USE IN PRODUCTION. This is for demonstration only.\n *\n * Problems:\n * - No input validation\n * - No error handling\n * - Will crash the entire app on malformed input\n * - No protection against XSS\n * - No content limits\n *\n * @example\n * ```ts\n * // This crashes if content is null, undefined, or malformed\n * const ast = parseMarkdownV1(userInput);\n * ```\n */\nexport function parseMarkdownV1(content: string): MDTree {\n return mdToJSON(content);\n}\n\n// -----------------------------------------------------------------------------\n// V2: Defensive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Defensive markdown parser - catches errors but loses context.\n *\n * Better than v1 but still problematic:\n * - Returns null on any error (loses error details)\n * - Caller can't distinguish between empty content and parse failure\n * - Still no XSS protection\n * - Still no content limits\n *\n * @example\n * ```ts\n * const ast = parseMarkdownV2(userInput);\n * if (!ast) {\n * // What went wrong? We don't know.\n * return <ErrorFallback />;\n * }\n * ```\n */\nexport function parseMarkdownV2(content: string): MDTree | null {\n try {\n // Basic null check\n if (content == null) {\n return null;\n }\n return mdToJSON(content);\n } catch {\n return null;\n }\n}\n\n// -----------------------------------------------------------------------------\n// V3: Robust Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a ParseError with the given code and message.\n */\nfunction createParseError(code: ParseErrorCode, message: string, cause?: Error): ParseError {\n return { code, message, cause };\n}\n\n/**\n * Checks content for potential XSS patterns.\n * Returns the first matched pattern or null if clean.\n */\nfunction detectXssPattern(content: string): RegExp | null {\n for (const pattern of XSS_PATTERNS) {\n if (pattern.test(content)) {\n return pattern;\n }\n }\n return null;\n}\n\n/**\n * Sanitizes content by removing dangerous patterns.\n * This is a lightweight, string-based sanitizer and not a full HTML sanitizer.\n */\nfunction sanitizeContent(content: string): string {\n return sanitizeMarkdownContent(content);\n}\n\n/**\n * Robust markdown parser with explicit error handling and lightweight sanitization.\n *\n * Features:\n * - Input validation with specific error codes\n * - Result type for explicit error handling\n * - XSS pattern detection and optional lightweight sanitization\n * - Content length limits\n * - Preserves error context for debugging\n *\n * @param content - Markdown string to parse\n * @param options - Parsing options\n * @returns Result containing the AST or a structured error\n *\n * @example\n * ```ts\n * const result = parseMarkdownV3(userInput);\n *\n * if (!result.ok) {\n * switch (result.error.code) {\n * case 'INVALID_INPUT':\n * return <InvalidInputError message={result.error.message} />;\n * case 'CONTENT_TOO_LONG':\n * return <ContentTooLongError />;\n * case 'PARSE_FAILED':\n * console.error('Parse error:', result.error.cause);\n * return <ParseError />;\n * default:\n * return <GenericError />;\n * }\n * }\n *\n * return <MarkdownRenderer ast={result.value} />;\n * ```\n */\nexport function parseMarkdownV3(\n content: string,\n options: ParseOptions = {}\n): Result<MDTree, ParseError> {\n const { maxLength = DEFAULT_MAX_LENGTH, sanitize = true, allowEmpty = false } = options;\n\n // 1. Validate input type\n if (content === null || content === undefined) {\n return err(\n createParseError(\n ParseErrorCode.INVALID_INPUT,\n `Content must be a string, received ${content === null ? 'null' : 'undefined'}`\n )\n );\n }\n\n if (typeof content !== 'string') {\n return err(\n createParseError(\n ParseErrorCode.INVALID_INPUT,\n `Content must be a string, received ${typeof content}`\n )\n );\n }\n\n // 2. Handle empty content\n const trimmed = content.trim();\n if (trimmed.length === 0 && !allowEmpty) {\n return err(\n createParseError(ParseErrorCode.EMPTY_CONTENT, 'Content is empty or contains only whitespace')\n );\n }\n\n // 3. Check content length\n if (content.length > maxLength) {\n return err(\n createParseError(\n ParseErrorCode.CONTENT_TOO_LONG,\n `Content exceeds maximum length of ${maxLength.toLocaleString()} characters ` +\n `(received ${content.length.toLocaleString()})`\n )\n );\n }\n\n // 4. Check for XSS patterns\n const xssPattern = detectXssPattern(content);\n if (xssPattern) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[parseMarkdownV3] XSS pattern detected: ${xssPattern.toString()}`,\n sanitize ? 'Content will be sanitized.' : 'Sanitization is disabled!'\n );\n }\n }\n\n // 5. Optionally sanitize content\n const processedContent = sanitize ? sanitizeContent(content) : content;\n\n // 6. Parse markdown with error handling\n try {\n const ast = mdToJSON(processedContent);\n return ok(ast);\n } catch (error) {\n const cause = error instanceof Error ? error : new Error(String(error));\n return err(\n createParseError(\n ParseErrorCode.PARSE_FAILED,\n `Failed to parse markdown: ${cause.message}`,\n cause\n )\n );\n }\n}\n\n// -----------------------------------------------------------------------------\n// Default Export\n// -----------------------------------------------------------------------------\n\n/**\n * Default markdown parser.\n *\n * This is an alias for `parseMarkdownV3`, the most defensive implementation\n * in this module.\n *\n * @example\n * ```ts\n * import { parseMarkdown } from '@/lib/markdown-utils';\n *\n * const result = parseMarkdown(content);\n * if (!result.ok) {\n * // Handle error with full context\n * console.error(`[${result.error.code}] ${result.error.message}`);\n * return null;\n * }\n * return result.value;\n * ```\n */\nexport const parseMarkdown = parseMarkdownV3;\n\n// -----------------------------------------------------------------------------\n// Utility Functions\n// -----------------------------------------------------------------------------\n\n/**\n * Checks if content is safe markdown (no XSS patterns detected).\n *\n * @param content - Markdown string to check\n * @returns true if no XSS patterns detected\n */\nexport function isSafeMarkdown(content: string): boolean {\n return detectXssPattern(content) === null;\n}\n\n/**\n * Estimates the word count of markdown content.\n * Useful for content length limits and reading time estimates.\n *\n * @param content - Markdown string to count\n * @returns Estimated word count\n */\nconst MAX_ESTIMATE_LENGTH = 500_000;\n\nfunction stripFencedCodeBlocks(text: string): string {\n let result = text;\n let start = result.indexOf('```');\n while (start !== -1) {\n const end = result.indexOf('```', start + 3);\n if (end === -1) break;\n result = result.slice(0, start) + result.slice(end + 3);\n start = result.indexOf('```');\n }\n return result;\n}\n\nfunction stripInlineCodeSpans(text: string): string {\n let result = text;\n let start = result.indexOf('`');\n while (start !== -1) {\n const end = result.indexOf('`', start + 1);\n if (end === -1) break;\n result = result.slice(0, start) + result.slice(end + 1);\n start = result.indexOf('`');\n }\n return result;\n}\n\nfunction stripMarkdownImages(text: string): string {\n let result = text;\n let index = result.indexOf('![');\n while (index !== -1) {\n const closeBracket = result.indexOf(']', index + 2);\n const openParen = closeBracket !== -1 ? result.indexOf('(', closeBracket) : -1;\n const closeParen = openParen !== -1 ? result.indexOf(')', openParen) : -1;\n if (closeBracket === -1 || openParen === -1 || closeParen === -1) break;\n result = result.slice(0, index) + result.slice(closeParen + 1);\n index = result.indexOf('![');\n }\n return result;\n}\n\nfunction convertMarkdownLinksToText(text: string): string {\n let result = text;\n let index = result.indexOf('[');\n while (index !== -1) {\n const closeBracket = result.indexOf(']', index + 1);\n const openParen = closeBracket !== -1 ? result.indexOf('(', closeBracket) : -1;\n const closeParen = openParen !== -1 ? result.indexOf(')', openParen) : -1;\n if (closeBracket === -1 || openParen === -1 || closeParen === -1) break;\n const label = result.slice(index + 1, closeBracket);\n result = result.slice(0, index) + label + result.slice(closeParen + 1);\n index = result.indexOf('[', index + label.length);\n }\n return result;\n}\n\nexport function estimateWordCount(content: string): number {\n const bounded =\n content.length > MAX_ESTIMATE_LENGTH ? content.slice(0, MAX_ESTIMATE_LENGTH) : content;\n\n let plainText = bounded;\n plainText = plainText.replace(/#+\\s/g, '');\n plainText = stripFencedCodeBlocks(plainText);\n plainText = stripInlineCodeSpans(plainText);\n plainText = stripMarkdownImages(plainText);\n plainText = convertMarkdownLinksToText(plainText);\n plainText = plainText.replace(/\\*\\*|__|~~/g, '');\n\n const words = plainText.trim().split(/\\s+/).filter(Boolean);\n return words.length;\n}\n\n/**\n * Estimates reading time for markdown content.\n *\n * @param content - Markdown string\n * @param wordsPerMinute - Reading speed (default 200 WPM)\n * @returns Reading time in minutes\n */\nexport function estimateReadingTime(content: string, wordsPerMinute = 200): number {\n const wordCount = estimateWordCount(content);\n return Math.ceil(wordCount / wordsPerMinute);\n}\n","/**\n * String-based markdown sanitization helpers (no regex backtracking on user input).\n */\n\nconst DANGEROUS_SCHEMES = ['javascript:', 'vbscript:', 'data:'] as const;\n\nconst HTML_URL_ATTRIBUTES = [\n 'href',\n 'src',\n 'action',\n 'formaction',\n 'cite',\n 'background',\n 'poster',\n 'xlink:href',\n] as const;\n\nfunction isHtmlWhitespace(ch: string): boolean {\n return ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r' || ch === '\\f' || ch === '\\v';\n}\n\nfunction neutralizeDangerousSchemesInUrl(url: string): string {\n let result = url;\n let lower = result.toLowerCase();\n\n for (const scheme of DANGEROUS_SCHEMES) {\n const lowerScheme = scheme.toLowerCase();\n let index = lower.indexOf(lowerScheme);\n\n while (index !== -1) {\n const atUrlStart = index === 0;\n const afterSeparator =\n index > 0 &&\n (isHtmlWhitespace(result.charAt(index - 1)) || result.charAt(index - 1) === '\"');\n\n if (atUrlStart || afterSeparator) {\n result = `${result.slice(0, index)}removed:${result.slice(index + scheme.length)}`;\n lower = result.toLowerCase();\n index = lower.indexOf(lowerScheme, index + 'removed:'.length);\n } else {\n index = lower.indexOf(lowerScheme, index + 1);\n }\n }\n }\n\n return result;\n}\n\nexport function stripScriptTags(content: string): string {\n let result = content;\n let lower = result.toLowerCase();\n let searchFrom = 0;\n\n while (searchFrom < lower.length) {\n const start = lower.indexOf('<script', searchFrom);\n if (start === -1) break;\n\n const closeStart = lower.indexOf('</script', start);\n if (closeStart === -1) {\n searchFrom = start + 1;\n continue;\n }\n\n let end = lower.indexOf('>', closeStart);\n if (end === -1) {\n searchFrom = start + 1;\n continue;\n }\n end += 1;\n\n result = result.slice(0, start) + result.slice(end);\n lower = result.toLowerCase();\n searchFrom = start;\n }\n\n return result;\n}\n\nfunction replaceUrlInParenGroup(\n content: string,\n openParen: number,\n closeParen: number\n): { content: string; nextIndex: number } {\n const url = content.slice(openParen + 1, closeParen);\n const sanitized = neutralizeDangerousSchemesInUrl(url);\n if (sanitized === url) {\n return { content, nextIndex: closeParen + 1 };\n }\n return {\n content: content.slice(0, openParen + 1) + sanitized + content.slice(closeParen),\n nextIndex: openParen + sanitized.length + 1,\n };\n}\n\nfunction neutralizeMarkdownLinkAndImageDestinations(content: string): string {\n let result = content;\n let index = 0;\n\n while (index < result.length) {\n const isImage = result.startsWith('![', index);\n const marker = isImage ? '![' : '[';\n const markerIndex = isImage ? index : result.indexOf('[', index);\n\n if (markerIndex === -1) break;\n if (!isImage && result.startsWith('![', markerIndex)) {\n index = markerIndex + 2;\n continue;\n }\n\n const bracketStart = isImage ? markerIndex + 2 : markerIndex + 1;\n const closeBracket = result.indexOf(']', bracketStart);\n if (closeBracket === -1) {\n index = markerIndex + marker.length;\n continue;\n }\n\n const openParen = result.indexOf('(', closeBracket);\n if (openParen !== closeBracket + 1) {\n index = markerIndex + marker.length;\n continue;\n }\n\n const closeParen = result.indexOf(')', openParen);\n if (closeParen === -1) {\n index = markerIndex + marker.length;\n continue;\n }\n\n const replaced = replaceUrlInParenGroup(result, openParen, closeParen);\n result = replaced.content;\n index = replaced.nextIndex;\n }\n\n return result;\n}\n\nfunction readHtmlAttributeValue(content: string, valueStart: number): number {\n let end = valueStart;\n while (end < content.length && isHtmlWhitespace(content.charAt(end))) {\n end += 1;\n }\n\n const quote = content.charAt(end);\n if (quote === '\"' || quote === \"'\") {\n end += 1;\n while (end < content.length && content.charAt(end) !== quote) {\n end += 1;\n }\n if (end < content.length) end += 1;\n return end;\n }\n\n while (end < content.length) {\n const ch = content.charAt(end);\n if (isHtmlWhitespace(ch) || ch === '>') break;\n end += 1;\n }\n return end;\n}\n\nfunction findHtmlAttribute(\n _content: string,\n lower: string,\n attrName: string,\n from: number\n): number {\n const needle = attrName.toLowerCase();\n let i = from;\n\n while (i < lower.length) {\n const idx = lower.indexOf(needle, i);\n if (idx === -1) return -1;\n\n const before = idx > 0 ? lower.charAt(idx - 1) : '';\n if (idx > 0 && !isHtmlWhitespace(before) && before !== '<' && before !== '/') {\n i = idx + 1;\n continue;\n }\n\n let after = idx + needle.length;\n while (after < lower.length && isHtmlWhitespace(lower.charAt(after))) {\n after += 1;\n }\n\n if (lower.charAt(after) === '=') return idx;\n i = idx + 1;\n }\n\n return -1;\n}\n\nfunction neutralizeHtmlUrlAttributes(content: string): string {\n let result = content;\n let lower = result.toLowerCase();\n\n for (const attr of HTML_URL_ATTRIBUTES) {\n let searchFrom = 0;\n\n while (searchFrom < lower.length) {\n const attrIndex = findHtmlAttribute(result, lower, attr, searchFrom);\n if (attrIndex === -1) break;\n\n let valueStart = attrIndex + attr.length;\n while (valueStart < lower.length && isHtmlWhitespace(lower.charAt(valueStart))) {\n valueStart += 1;\n }\n if (lower.charAt(valueStart) !== '=') {\n searchFrom = attrIndex + 1;\n continue;\n }\n valueStart += 1;\n while (valueStart < result.length && isHtmlWhitespace(result.charAt(valueStart))) {\n valueStart += 1;\n }\n\n const valueEnd = readHtmlAttributeValue(result, valueStart);\n const rawValue = result.slice(valueStart, valueEnd);\n const quote =\n rawValue.charAt(0) === '\"' || rawValue.charAt(0) === \"'\" ? rawValue.charAt(0) : null;\n const innerStart = quote ? 1 : 0;\n const innerEnd =\n quote && rawValue.charAt(rawValue.length - 1) === quote\n ? rawValue.length - 1\n : rawValue.length;\n const url = rawValue.slice(innerStart, innerEnd);\n const sanitized = neutralizeDangerousSchemesInUrl(url);\n const wrapped = quote ? `${quote}${sanitized}${quote}` : sanitized;\n\n if (sanitized !== url) {\n result = result.slice(0, valueStart) + wrapped + result.slice(valueEnd);\n lower = result.toLowerCase();\n }\n\n searchFrom = valueStart + (sanitized !== url ? wrapped.length : rawValue.length);\n }\n }\n\n return result;\n}\n\nfunction stripEventHandlersFromTag(tag: string): string {\n let result = tag;\n let lower = result.toLowerCase();\n let searchFrom = 0;\n\n while (searchFrom < lower.length) {\n let eventStart = -1;\n\n for (let j = searchFrom; j < lower.length; j++) {\n if (lower.charAt(j) !== 'o' || !lower.startsWith('on', j)) continue;\n\n if (j > 0 && !isHtmlWhitespace(lower.charAt(j - 1))) continue;\n\n let nameEnd = j + 2;\n while (nameEnd < lower.length) {\n const ch = lower.charAt(nameEnd);\n if (!((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch === '-')) break;\n nameEnd += 1;\n }\n\n let eqIndex = nameEnd;\n while (eqIndex < lower.length && isHtmlWhitespace(lower.charAt(eqIndex))) {\n eqIndex += 1;\n }\n\n if (lower.charAt(eqIndex) !== '=' || nameEnd - j < 3) continue;\n\n eventStart = j;\n break;\n }\n\n if (eventStart === -1) break;\n\n const valueEnd = readHtmlAttributeValue(result, lower.indexOf('=', eventStart) + 1);\n result = result.slice(0, eventStart) + result.slice(valueEnd);\n lower = result.toLowerCase();\n searchFrom = eventStart;\n }\n\n return result;\n}\n\nexport function stripInlineEventHandlers(content: string): string {\n let result = content;\n let i = 0;\n\n while (i < result.length) {\n const tagStart = result.indexOf('<', i);\n if (tagStart === -1) break;\n\n if (result.startsWith('<!--', tagStart)) {\n const commentEnd = result.indexOf('-->', tagStart);\n i = commentEnd === -1 ? result.length : commentEnd + 3;\n continue;\n }\n\n const tagEnd = result.indexOf('>', tagStart);\n if (tagEnd === -1) break;\n\n const tag = result.slice(tagStart, tagEnd + 1);\n const sanitizedTag = stripEventHandlersFromTag(tag);\n result = result.slice(0, tagStart) + sanitizedTag + result.slice(tagEnd + 1);\n i = tagStart + sanitizedTag.length;\n }\n\n return result;\n}\n\nexport function sanitizeMarkdownContent(content: string): string {\n let sanitized = stripScriptTags(content);\n sanitized = neutralizeMarkdownLinkAndImageDestinations(sanitized);\n sanitized = neutralizeHtmlUrlAttributes(sanitized);\n return stripInlineEventHandlers(sanitized);\n}\n\nexport function containsDangerousScheme(content: string, scheme: string): boolean {\n return content.toLowerCase().includes(scheme.toLowerCase());\n}\n","/**\n * Result Type Utilities\n *\n * Go-style error handling with discriminated union types.\n * Provides type-safe success/error handling without exceptions.\n *\n * @example\n * ```ts\n * const [err, data] = await handle(async () => {\n * const response = await fetch(url);\n * if (!response.ok) throw new Error(`HTTP ${response.status}`);\n * return response.json();\n * });\n *\n * if (err) {\n * console.error('Failed:', err.message);\n * return { error: err.message };\n * }\n * return { data };\n * ```\n */\n\n// -----------------------------------------------------------------------------\n// Result Type\n// -----------------------------------------------------------------------------\n\n/**\n * A discriminated union representing either success or failure.\n *\n * @template T - The success value type\n * @template E - The error type (defaults to Error)\n *\n * @example\n * ```ts\n * function divide(a: number, b: number): Result<number, string> {\n * if (b === 0) return err('Division by zero');\n * return ok(a / b);\n * }\n *\n * const result = divide(10, 2);\n * if (isOk(result)) {\n * console.log('Result:', result.value); // 5\n * } else {\n * console.error('Error:', result.error);\n * }\n * ```\n */\nexport type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };\n\n// -----------------------------------------------------------------------------\n// Constructors\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a success Result.\n *\n * @param value - The success value\n * @returns A success Result containing the value\n *\n * @example\n * ```ts\n * const result = ok(42);\n * // { ok: true, value: 42 }\n * ```\n */\nexport function ok<T>(value: T): Result<T, never> {\n return { ok: true, value };\n}\n\n/**\n * Creates a failure Result.\n *\n * @param error - The error value\n * @returns A failure Result containing the error\n *\n * @example\n * ```ts\n * const result = err(new Error('Something went wrong'));\n * // { ok: false, error: Error('Something went wrong') }\n * ```\n */\nexport function err<E>(error: E): Result<never, E> {\n return { ok: false, error };\n}\n\n// -----------------------------------------------------------------------------\n// Type Guards\n// -----------------------------------------------------------------------------\n\n/**\n * Type guard to check if a Result is successful.\n *\n * @param result - The Result to check\n * @returns true if the Result is a success\n *\n * @example\n * ```ts\n * const result = fetchData();\n * if (isOk(result)) {\n * // TypeScript knows result.value exists here\n * console.log(result.value);\n * }\n * ```\n */\nexport function isOk<T, E>(result: Result<T, E>): result is { ok: true; value: T } {\n return result.ok === true;\n}\n\n/**\n * Type guard to check if a Result is a failure.\n *\n * @param result - The Result to check\n * @returns true if the Result is a failure\n *\n * @example\n * ```ts\n * const result = fetchData();\n * if (isErr(result)) {\n * // TypeScript knows result.error exists here\n * console.error(result.error);\n * }\n * ```\n */\nexport function isErr<T, E>(result: Result<T, E>): result is { ok: false; error: E } {\n return result.ok === false;\n}\n\n// -----------------------------------------------------------------------------\n// Extractors\n// -----------------------------------------------------------------------------\n\n/**\n * Extracts the value from a Result, throwing if it's an error.\n *\n * @param result - The Result to unwrap\n * @returns The success value\n * @throws The error if Result is a failure\n *\n * @example\n * ```ts\n * const result = ok(42);\n * const value = unwrap(result); // 42\n *\n * const errorResult = err(new Error('fail'));\n * const value2 = unwrap(errorResult); // throws Error('fail')\n * ```\n */\nexport function unwrap<T, E>(result: Result<T, E>): T {\n if (isOk(result)) {\n return result.value;\n }\n throw result.error;\n}\n\n/**\n * Extracts the value from a Result, returning a default on error.\n *\n * @param result - The Result to unwrap\n * @param defaultValue - The value to return if Result is an error\n * @returns The success value or the default value\n *\n * @example\n * ```ts\n * const result = err(new Error('fail'));\n * const value = unwrapOr(result, 0); // 0\n *\n * const okResult = ok(42);\n * const value2 = unwrapOr(okResult, 0); // 42\n * ```\n */\nexport function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {\n if (isOk(result)) {\n return result.value;\n }\n return defaultValue;\n}\n\n/**\n * Extracts the value from a Result, computing a default on error.\n *\n * @param result - The Result to unwrap\n * @param fn - Function to compute the default value from the error\n * @returns The success value or the computed default\n *\n * @example\n * ```ts\n * const result = err(new Error('not found'));\n * const value = unwrapOrElse(result, (e) => {\n * console.error('Error:', e.message);\n * return [];\n * });\n * ```\n */\nexport function unwrapOrElse<T, E>(result: Result<T, E>, fn: (error: E) => T): T {\n if (isOk(result)) {\n return result.value;\n }\n return fn(result.error);\n}\n\n// -----------------------------------------------------------------------------\n// Transformers\n// -----------------------------------------------------------------------------\n\n/**\n * Maps a successful Result's value.\n *\n * @param result - The Result to map\n * @param fn - Function to transform the value\n * @returns A new Result with the transformed value, or the original error\n *\n * @example\n * ```ts\n * const result = ok(5);\n * const doubled = map(result, (n) => n * 2); // ok(10)\n *\n * const errorResult = err('fail');\n * const still = map(errorResult, (n) => n * 2); // err('fail')\n * ```\n */\nexport function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {\n if (isOk(result)) {\n return ok(fn(result.value));\n }\n return result;\n}\n\n/**\n * Maps a failed Result's error.\n *\n * @param result - The Result to map\n * @param fn - Function to transform the error\n * @returns A new Result with the transformed error, or the original value\n *\n * @example\n * ```ts\n * const result = err('not found');\n * const mapped = mapErr(result, (e) => new Error(e)); // err(Error('not found'))\n * ```\n */\nexport function mapErr<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> {\n if (isErr(result)) {\n return err(fn(result.error));\n }\n return result;\n}\n\n/**\n * Chains Result-returning operations.\n *\n * @param result - The Result to chain from\n * @param fn - Function that returns a new Result\n * @returns The chained Result\n *\n * @example\n * ```ts\n * function parse(input: string): Result<number, string> {\n * const n = parseInt(input, 10);\n * return isNaN(n) ? err('not a number') : ok(n);\n * }\n *\n * function double(n: number): Result<number, string> {\n * return ok(n * 2);\n * }\n *\n * const result = flatMap(parse('5'), double); // ok(10)\n * const fail = flatMap(parse('abc'), double); // err('not a number')\n * ```\n */\nexport function flatMap<T, U, E>(\n result: Result<T, E>,\n fn: (value: T) => Result<U, E>\n): Result<U, E> {\n if (isOk(result)) {\n return fn(result.value);\n }\n return result;\n}\n\n// -----------------------------------------------------------------------------\n// Async Helpers\n// -----------------------------------------------------------------------------\n\n/**\n * Wraps a Promise in a Result type.\n *\n * @param promise - The Promise to wrap\n * @returns A Promise that resolves to a Result\n *\n * @example\n * ```ts\n * const result = await fromPromise(fetch('/api/data'));\n * if (isErr(result)) {\n * console.error('Fetch failed:', result.error);\n * return;\n * }\n * const response = result.value;\n * ```\n */\nexport async function fromPromise<T>(promise: Promise<T>): Promise<Result<T, Error>> {\n try {\n const value = await promise;\n return ok(value);\n } catch (error) {\n return err(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n/**\n * Wraps a throwing function in a Result type.\n *\n * @param fn - The function to wrap\n * @returns A Result containing the return value or the thrown error\n *\n * @example\n * ```ts\n * const result = tryCatch(() => JSON.parse(input));\n * if (isErr(result)) {\n * console.error('Invalid JSON:', result.error);\n * return null;\n * }\n * return result.value;\n * ```\n */\nexport function tryCatch<T>(fn: () => T): Result<T, Error> {\n try {\n return ok(fn());\n } catch (error) {\n return err(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n/**\n * Wraps an async function in a Result type.\n *\n * @param fn - The async function to wrap\n * @returns A Promise that resolves to a Result\n *\n * @example\n * ```ts\n * const result = await tryCatchAsync(async () => {\n * const response = await fetch('/api/data');\n * if (!response.ok) throw new Error(`HTTP ${response.status}`);\n * return response.json();\n * });\n * ```\n */\nexport async function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {\n try {\n const value = await fn();\n return ok(value);\n } catch (error) {\n return err(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n// -----------------------------------------------------------------------------\n// Tuple Helpers (Go-style)\n// -----------------------------------------------------------------------------\n\n/**\n * Go-style tuple for error handling: [error, value]\n *\n * @example\n * ```ts\n * const [err, data] = await handle(fetchData);\n * if (err) {\n * console.error(err);\n * return;\n * }\n * console.log(data);\n * ```\n */\nexport type GoTuple<T, E = Error> = [E, undefined] | [undefined, T];\n\n/**\n * Converts a Result to a Go-style tuple.\n *\n * @param result - The Result to convert\n * @returns A tuple of [error, value]\n *\n * @example\n * ```ts\n * const result = await fetchData();\n * const [err, data] = toTuple(result);\n * ```\n */\nexport function toTuple<T, E>(result: Result<T, E>): GoTuple<T, E> {\n if (isOk(result)) {\n return [undefined, result.value];\n }\n return [result.error, undefined];\n}\n\n/**\n * Wraps an async function and returns a Go-style tuple.\n *\n * @param fn - The async function to wrap\n * @returns A Promise that resolves to [error, value] tuple\n *\n * @example\n * ```ts\n * const [err, data] = await handle(async () => {\n * const res = await fetch('/api/users');\n * if (!res.ok) throw new Error(`HTTP ${res.status}`);\n * return res.json();\n * });\n *\n * if (err) {\n * console.error('Failed to fetch users:', err.message);\n * return [];\n * }\n * return data;\n * ```\n */\nexport async function handle<T>(fn: () => Promise<T>): Promise<GoTuple<T, Error>> {\n try {\n const value = await fn();\n return [undefined, value];\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n return [err, undefined];\n }\n}\n\n/**\n * Wraps a synchronous function and returns a Go-style tuple.\n *\n * @param fn - The function to wrap\n * @returns A tuple of [error, value]\n *\n * @example\n * ```ts\n * const [err, parsed] = handleSync(() => JSON.parse(input));\n * if (err) {\n * console.error('Invalid JSON:', err.message);\n * return null;\n * }\n * return parsed;\n * ```\n */\nexport function handleSync<T>(fn: () => T): GoTuple<T, Error> {\n try {\n const value = fn();\n return [undefined, value];\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n return [err, undefined];\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../lib/markdown-utils.ts","../../lib/markdown-sanitize.ts","../../lib/result.ts"],"sourcesContent":["/**\n * Markdown Parsing Utilities\n *\n * Three implementations showing the tradeoffs between simple and more\n * defensive parsing:\n * - v1 (Naive): Direct call, crashes on bad input\n * - v2 (Defensive): Try-catch with null fallback\n * - v3 (Robust): Result type, validation, lightweight sanitization\n *\n * The default export points to the v3 implementation.\n */\n\nimport type { MDTree } from 'md4w';\nimport { mdToJSON } from 'md4w';\n\nimport { sanitizeMarkdownContent } from './markdown-sanitize';\nimport { err, ok, type Result } from './result';\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Error codes for markdown parsing failures.\n */\nexport const ParseErrorCode = {\n INVALID_INPUT: 'INVALID_INPUT',\n EMPTY_CONTENT: 'EMPTY_CONTENT',\n PARSE_FAILED: 'PARSE_FAILED',\n CONTENT_TOO_LONG: 'CONTENT_TOO_LONG',\n NESTED_TOO_DEEP: 'NESTED_TOO_DEEP',\n} as const;\n\nexport type ParseErrorCode = (typeof ParseErrorCode)[keyof typeof ParseErrorCode];\n\n/**\n * Structured error for markdown parsing failures.\n */\nexport interface ParseError {\n code: ParseErrorCode;\n message: string;\n cause?: Error;\n}\n\n/**\n * Options for robust markdown parsing.\n */\nexport interface ParseOptions {\n /**\n * Maximum content length in characters.\n * @default 500000 (500KB of text, ~100K words)\n */\n maxLength?: number;\n\n /**\n * Whether to run a lightweight string-based sanitization pass before parsing.\n * @default true\n */\n sanitize?: boolean;\n\n /**\n * Whether to allow empty content.\n * @default false\n */\n allowEmpty?: boolean;\n}\n\n// -----------------------------------------------------------------------------\n// Constants\n// -----------------------------------------------------------------------------\n\nconst DEFAULT_MAX_LENGTH = 500_000; // 500KB of text\n\n/**\n * Patterns that indicate potential XSS attempts in markdown.\n * These are checked in raw content before parsing.\n */\nconst XSS_PATTERNS = [\n /<script\\b/i,\n /javascript:/i,\n /on\\w+\\s*=/i, // onclick=, onerror=, etc.\n /\\bdata:/i, // data: URIs can be dangerous\n /vbscript:/i,\n] as const;\n\n// -----------------------------------------------------------------------------\n// V1: Naive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Naive markdown parser - crashes on bad input.\n *\n * DO NOT USE IN PRODUCTION. This is for demonstration only.\n *\n * Problems:\n * - No input validation\n * - No error handling\n * - Will crash the entire app on malformed input\n * - No protection against XSS\n * - No content limits\n *\n * @example\n * ```ts\n * // This crashes if content is null, undefined, or malformed\n * const ast = parseMarkdownV1(userInput);\n * ```\n */\nexport function parseMarkdownV1(content: string): MDTree {\n return mdToJSON(content);\n}\n\n// -----------------------------------------------------------------------------\n// V2: Defensive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Defensive markdown parser - catches errors but loses context.\n *\n * Better than v1 but still problematic:\n * - Returns null on any error (loses error details)\n * - Caller can't distinguish between empty content and parse failure\n * - Still no XSS protection\n * - Still no content limits\n *\n * @example\n * ```ts\n * const ast = parseMarkdownV2(userInput);\n * if (!ast) {\n * // What went wrong? We don't know.\n * return <ErrorFallback />;\n * }\n * ```\n */\nexport function parseMarkdownV2(content: string): MDTree | null {\n try {\n // Basic null check\n if (content == null) {\n return null;\n }\n return mdToJSON(content);\n } catch {\n return null;\n }\n}\n\n// -----------------------------------------------------------------------------\n// V3: Robust Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a ParseError with the given code and message.\n */\nfunction createParseError(code: ParseErrorCode, message: string, cause?: Error): ParseError {\n return { code, message, cause };\n}\n\n/**\n * Checks content for potential XSS patterns.\n * Returns the first matched pattern or null if clean.\n */\nfunction detectXssPattern(content: string): RegExp | null {\n for (const pattern of XSS_PATTERNS) {\n if (pattern.test(content)) {\n return pattern;\n }\n }\n return null;\n}\n\n/**\n * Sanitizes content by removing dangerous patterns.\n * This is a lightweight, string-based sanitizer and not a full HTML sanitizer.\n */\nfunction sanitizeContent(content: string): string {\n return sanitizeMarkdownContent(content);\n}\n\n/**\n * Robust markdown parser with explicit error handling and lightweight sanitization.\n *\n * Features:\n * - Input validation with specific error codes\n * - Result type for explicit error handling\n * - XSS pattern detection and optional lightweight sanitization\n * - Content length limits\n * - Preserves error context for debugging\n *\n * @param content - Markdown string to parse\n * @param options - Parsing options\n * @returns Result containing the AST or a structured error\n *\n * @example\n * ```ts\n * const result = parseMarkdownV3(userInput);\n *\n * if (!result.ok) {\n * switch (result.error.code) {\n * case 'INVALID_INPUT':\n * return <InvalidInputError message={result.error.message} />;\n * case 'CONTENT_TOO_LONG':\n * return <ContentTooLongError />;\n * case 'PARSE_FAILED':\n * console.error('Parse error:', result.error.cause);\n * return <ParseError />;\n * default:\n * return <GenericError />;\n * }\n * }\n *\n * return <MarkdownRenderer ast={result.value} />;\n * ```\n */\nexport function parseMarkdownV3(\n content: string,\n options: ParseOptions = {}\n): Result<MDTree, ParseError> {\n const { maxLength = DEFAULT_MAX_LENGTH, sanitize = true, allowEmpty = false } = options;\n\n // 1. Validate input type\n if (content === null || content === undefined) {\n return err(\n createParseError(\n ParseErrorCode.INVALID_INPUT,\n `Content must be a string, received ${content === null ? 'null' : 'undefined'}`\n )\n );\n }\n\n if (typeof content !== 'string') {\n return err(\n createParseError(\n ParseErrorCode.INVALID_INPUT,\n `Content must be a string, received ${typeof content}`\n )\n );\n }\n\n // 2. Handle empty content\n const trimmed = content.trim();\n if (trimmed.length === 0 && !allowEmpty) {\n return err(\n createParseError(ParseErrorCode.EMPTY_CONTENT, 'Content is empty or contains only whitespace')\n );\n }\n\n // 3. Check content length\n if (content.length > maxLength) {\n return err(\n createParseError(\n ParseErrorCode.CONTENT_TOO_LONG,\n `Content exceeds maximum length of ${maxLength.toLocaleString()} characters ` +\n `(received ${content.length.toLocaleString()})`\n )\n );\n }\n\n // 4. Check for XSS patterns\n const xssPattern = detectXssPattern(content);\n if (xssPattern) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[parseMarkdownV3] XSS pattern detected: ${xssPattern.toString()}`,\n sanitize ? 'Content will be sanitized.' : 'Sanitization is disabled!'\n );\n }\n }\n\n // 5. Optionally sanitize content\n const processedContent = sanitize ? sanitizeContent(content) : content;\n\n // 6. Parse markdown with error handling\n try {\n const ast = mdToJSON(processedContent);\n return ok(ast);\n } catch (error) {\n const cause = error instanceof Error ? error : new Error(String(error));\n return err(\n createParseError(\n ParseErrorCode.PARSE_FAILED,\n `Failed to parse markdown: ${cause.message}`,\n cause\n )\n );\n }\n}\n\n// -----------------------------------------------------------------------------\n// Default Export\n// -----------------------------------------------------------------------------\n\n/**\n * Default markdown parser.\n *\n * This is an alias for `parseMarkdownV3`, the most defensive implementation\n * in this module.\n *\n * @example\n * ```ts\n * import { parseMarkdown } from '@/lib/markdown-utils';\n *\n * const result = parseMarkdown(content);\n * if (!result.ok) {\n * // Handle error with full context\n * console.error(`[${result.error.code}] ${result.error.message}`);\n * return null;\n * }\n * return result.value;\n * ```\n */\nexport const parseMarkdown = parseMarkdownV3;\n\n// -----------------------------------------------------------------------------\n// Utility Functions\n// -----------------------------------------------------------------------------\n\n/**\n * Checks if content is safe markdown (no XSS patterns detected).\n *\n * @param content - Markdown string to check\n * @returns true if no XSS patterns detected\n */\nexport function isSafeMarkdown(content: string): boolean {\n return detectXssPattern(content) === null;\n}\n\n/**\n * Estimates the word count of markdown content.\n * Useful for content length limits and reading time estimates.\n *\n * @param content - Markdown string to count\n * @returns Estimated word count\n */\nconst MAX_ESTIMATE_LENGTH = 500_000;\n\nfunction stripFencedCodeBlocks(text: string): string {\n let result = text;\n let start = result.indexOf('```');\n while (start !== -1) {\n const end = result.indexOf('```', start + 3);\n if (end === -1) break;\n result = result.slice(0, start) + result.slice(end + 3);\n start = result.indexOf('```');\n }\n return result;\n}\n\nfunction stripInlineCodeSpans(text: string): string {\n let result = text;\n let start = result.indexOf('`');\n while (start !== -1) {\n const end = result.indexOf('`', start + 1);\n if (end === -1) break;\n result = result.slice(0, start) + result.slice(end + 1);\n start = result.indexOf('`');\n }\n return result;\n}\n\nfunction stripMarkdownImages(text: string): string {\n let result = text;\n let index = result.indexOf('![');\n while (index !== -1) {\n const closeBracket = result.indexOf(']', index + 2);\n const openParen = closeBracket !== -1 ? result.indexOf('(', closeBracket) : -1;\n const closeParen = openParen !== -1 ? result.indexOf(')', openParen) : -1;\n if (closeBracket === -1 || openParen === -1 || closeParen === -1) break;\n result = result.slice(0, index) + result.slice(closeParen + 1);\n index = result.indexOf('![');\n }\n return result;\n}\n\nfunction convertMarkdownLinksToText(text: string): string {\n let result = text;\n let index = result.indexOf('[');\n while (index !== -1) {\n const closeBracket = result.indexOf(']', index + 1);\n const openParen = closeBracket !== -1 ? result.indexOf('(', closeBracket) : -1;\n const closeParen = openParen !== -1 ? result.indexOf(')', openParen) : -1;\n if (closeBracket === -1 || openParen === -1 || closeParen === -1) break;\n const label = result.slice(index + 1, closeBracket);\n result = result.slice(0, index) + label + result.slice(closeParen + 1);\n index = result.indexOf('[', index + label.length);\n }\n return result;\n}\n\nexport function estimateWordCount(content: string): number {\n const bounded =\n content.length > MAX_ESTIMATE_LENGTH ? content.slice(0, MAX_ESTIMATE_LENGTH) : content;\n\n let plainText = bounded;\n plainText = plainText.replace(/#+\\s/g, '');\n plainText = stripFencedCodeBlocks(plainText);\n plainText = stripInlineCodeSpans(plainText);\n plainText = stripMarkdownImages(plainText);\n plainText = convertMarkdownLinksToText(plainText);\n plainText = plainText.replace(/\\*\\*|__|~~/g, '');\n\n const words = plainText.trim().split(/\\s+/).filter(Boolean);\n return words.length;\n}\n\n/**\n * Estimates reading time for markdown content.\n *\n * @param content - Markdown string\n * @param wordsPerMinute - Reading speed (default 200 WPM)\n * @returns Reading time in minutes\n */\nexport function estimateReadingTime(content: string, wordsPerMinute = 200): number {\n const wordCount = estimateWordCount(content);\n return Math.ceil(wordCount / wordsPerMinute);\n}\n","/**\n * String-based markdown sanitization helpers (no regex backtracking on user input).\n */\n\nconst DANGEROUS_SCHEMES = ['javascript:', 'vbscript:', 'data:'] as const;\n\nconst HTML_URL_ATTRIBUTES = [\n 'href',\n 'src',\n 'action',\n 'formaction',\n 'cite',\n 'background',\n 'poster',\n 'xlink:href',\n] as const;\n\nfunction isHtmlWhitespace(ch: string): boolean {\n return ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r' || ch === '\\f' || ch === '\\v';\n}\n\nfunction neutralizeDangerousSchemesInUrl(url: string): string {\n let result = url;\n let lower = result.toLowerCase();\n\n for (const scheme of DANGEROUS_SCHEMES) {\n const lowerScheme = scheme.toLowerCase();\n let index = lower.indexOf(lowerScheme);\n\n while (index !== -1) {\n const atUrlStart = index === 0;\n const afterSeparator =\n index > 0 &&\n (isHtmlWhitespace(result.charAt(index - 1)) || result.charAt(index - 1) === '\"');\n\n if (atUrlStart || afterSeparator) {\n result = `${result.slice(0, index)}removed:${result.slice(index + scheme.length)}`;\n lower = result.toLowerCase();\n index = lower.indexOf(lowerScheme, index + 'removed:'.length);\n } else {\n index = lower.indexOf(lowerScheme, index + 1);\n }\n }\n }\n\n return result;\n}\n\nexport function stripScriptTags(content: string): string {\n let result = content;\n let lower = result.toLowerCase();\n let searchFrom = 0;\n\n while (searchFrom < lower.length) {\n const start = lower.indexOf('<script', searchFrom);\n if (start === -1) break;\n\n const closeStart = lower.indexOf('</script', start);\n if (closeStart === -1) {\n // No closing tag — remove the opening <script...> up to the next >\n let end = lower.indexOf('>', start);\n if (end === -1) {\n end = lower.length;\n } else {\n end += 1;\n }\n result = result.slice(0, start) + result.slice(end);\n lower = result.toLowerCase();\n searchFrom = start;\n continue;\n }\n\n let end = lower.indexOf('>', closeStart);\n if (end === -1) {\n searchFrom = start + 1;\n continue;\n }\n end += 1;\n\n result = result.slice(0, start) + result.slice(end);\n lower = result.toLowerCase();\n searchFrom = start;\n }\n\n return result;\n}\n\nfunction replaceUrlInParenGroup(\n content: string,\n openParen: number,\n closeParen: number\n): { content: string; nextIndex: number } {\n const url = content.slice(openParen + 1, closeParen);\n const sanitized = neutralizeDangerousSchemesInUrl(url);\n if (sanitized === url) {\n return { content, nextIndex: closeParen + 1 };\n }\n return {\n content: content.slice(0, openParen + 1) + sanitized + content.slice(closeParen),\n nextIndex: openParen + sanitized.length + 1,\n };\n}\n\nfunction neutralizeMarkdownLinkAndImageDestinations(content: string): string {\n let result = content;\n let index = 0;\n\n while (index < result.length) {\n const isImage = result.startsWith('![', index);\n const marker = isImage ? '![' : '[';\n const markerIndex = isImage ? index : result.indexOf('[', index);\n\n if (markerIndex === -1) break;\n if (!isImage && result.startsWith('![', markerIndex)) {\n index = markerIndex + 2;\n continue;\n }\n\n const bracketStart = isImage ? markerIndex + 2 : markerIndex + 1;\n const closeBracket = result.indexOf(']', bracketStart);\n if (closeBracket === -1) {\n index = markerIndex + marker.length;\n continue;\n }\n\n const openParen = result.indexOf('(', closeBracket);\n if (openParen !== closeBracket + 1) {\n index = markerIndex + marker.length;\n continue;\n }\n\n const closeParen = result.indexOf(')', openParen);\n if (closeParen === -1) {\n // Malformed link with no closing paren — still neutralize dangerous schemes\n let urlEnd = openParen + 1;\n while (\n urlEnd < result.length &&\n !isHtmlWhitespace(result.charAt(urlEnd)) &&\n result.charAt(urlEnd) !== '\\n' &&\n result.charAt(urlEnd) !== '\\r'\n ) {\n urlEnd += 1;\n }\n const url = result.slice(openParen + 1, urlEnd);\n const sanitized = neutralizeDangerousSchemesInUrl(url);\n if (sanitized !== url) {\n result = result.slice(0, openParen + 1) + sanitized + result.slice(urlEnd);\n }\n index = openParen + sanitized.length + 1;\n continue;\n }\n\n const replaced = replaceUrlInParenGroup(result, openParen, closeParen);\n result = replaced.content;\n index = replaced.nextIndex;\n }\n\n return result;\n}\n\nfunction readHtmlAttributeValue(content: string, valueStart: number): number {\n let end = valueStart;\n while (end < content.length && isHtmlWhitespace(content.charAt(end))) {\n end += 1;\n }\n\n const quote = content.charAt(end);\n if (quote === '\"' || quote === \"'\") {\n end += 1;\n while (end < content.length && content.charAt(end) !== quote) {\n end += 1;\n }\n if (end < content.length) end += 1;\n return end;\n }\n\n while (end < content.length) {\n const ch = content.charAt(end);\n if (isHtmlWhitespace(ch) || ch === '>') break;\n end += 1;\n }\n return end;\n}\n\nfunction findHtmlAttribute(\n _content: string,\n lower: string,\n attrName: string,\n from: number\n): number {\n const needle = attrName.toLowerCase();\n let i = from;\n\n while (i < lower.length) {\n const idx = lower.indexOf(needle, i);\n if (idx === -1) return -1;\n\n const before = idx > 0 ? lower.charAt(idx - 1) : '';\n if (idx > 0 && !isHtmlWhitespace(before) && before !== '<' && before !== '/') {\n i = idx + 1;\n continue;\n }\n\n let after = idx + needle.length;\n while (after < lower.length && isHtmlWhitespace(lower.charAt(after))) {\n after += 1;\n }\n\n if (lower.charAt(after) === '=') return idx;\n i = idx + 1;\n }\n\n return -1;\n}\n\nfunction neutralizeHtmlUrlAttributes(content: string): string {\n let result = content;\n let lower = result.toLowerCase();\n\n for (const attr of HTML_URL_ATTRIBUTES) {\n let searchFrom = 0;\n\n while (searchFrom < lower.length) {\n const attrIndex = findHtmlAttribute(result, lower, attr, searchFrom);\n if (attrIndex === -1) break;\n\n let valueStart = attrIndex + attr.length;\n while (valueStart < lower.length && isHtmlWhitespace(lower.charAt(valueStart))) {\n valueStart += 1;\n }\n if (lower.charAt(valueStart) !== '=') {\n searchFrom = attrIndex + 1;\n continue;\n }\n valueStart += 1;\n while (valueStart < result.length && isHtmlWhitespace(result.charAt(valueStart))) {\n valueStart += 1;\n }\n\n const valueEnd = readHtmlAttributeValue(result, valueStart);\n const rawValue = result.slice(valueStart, valueEnd);\n const quote =\n rawValue.charAt(0) === '\"' || rawValue.charAt(0) === \"'\" ? rawValue.charAt(0) : null;\n const innerStart = quote ? 1 : 0;\n const innerEnd =\n quote && rawValue.charAt(rawValue.length - 1) === quote\n ? rawValue.length - 1\n : rawValue.length;\n const url = rawValue.slice(innerStart, innerEnd);\n const sanitized = neutralizeDangerousSchemesInUrl(url);\n const wrapped = quote ? `${quote}${sanitized}${quote}` : sanitized;\n\n if (sanitized !== url) {\n result = result.slice(0, valueStart) + wrapped + result.slice(valueEnd);\n lower = result.toLowerCase();\n }\n\n searchFrom = valueStart + (sanitized !== url ? wrapped.length : rawValue.length);\n }\n }\n\n return result;\n}\n\nfunction stripEventHandlersFromTag(tag: string): string {\n let result = tag;\n let lower = result.toLowerCase();\n let searchFrom = 0;\n\n while (searchFrom < lower.length) {\n let eventStart = -1;\n\n for (let j = searchFrom; j < lower.length; j++) {\n if (lower.charAt(j) !== 'o' || !lower.startsWith('on', j)) continue;\n\n if (j > 0 && !isHtmlWhitespace(lower.charAt(j - 1))) continue;\n\n let nameEnd = j + 2;\n while (nameEnd < lower.length) {\n const ch = lower.charAt(nameEnd);\n if (!((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch === '-')) break;\n nameEnd += 1;\n }\n\n let eqIndex = nameEnd;\n while (eqIndex < lower.length && isHtmlWhitespace(lower.charAt(eqIndex))) {\n eqIndex += 1;\n }\n\n if (lower.charAt(eqIndex) !== '=' || nameEnd - j < 3) continue;\n\n eventStart = j;\n break;\n }\n\n if (eventStart === -1) break;\n\n const valueEnd = readHtmlAttributeValue(result, lower.indexOf('=', eventStart) + 1);\n result = result.slice(0, eventStart) + result.slice(valueEnd);\n lower = result.toLowerCase();\n searchFrom = eventStart;\n }\n\n return result;\n}\n\nexport function stripInlineEventHandlers(content: string): string {\n let result = content;\n let i = 0;\n\n while (i < result.length) {\n const tagStart = result.indexOf('<', i);\n if (tagStart === -1) break;\n\n if (result.startsWith('<!--', tagStart)) {\n const commentEnd = result.indexOf('-->', tagStart);\n i = commentEnd === -1 ? result.length : commentEnd + 3;\n continue;\n }\n\n const tagEnd = result.indexOf('>', tagStart);\n if (tagEnd === -1) break;\n\n const tag = result.slice(tagStart, tagEnd + 1);\n const sanitizedTag = stripEventHandlersFromTag(tag);\n result = result.slice(0, tagStart) + sanitizedTag + result.slice(tagEnd + 1);\n i = tagStart + sanitizedTag.length;\n }\n\n return result;\n}\n\nexport function sanitizeMarkdownContent(content: string): string {\n let sanitized = stripScriptTags(content);\n sanitized = neutralizeMarkdownLinkAndImageDestinations(sanitized);\n sanitized = neutralizeHtmlUrlAttributes(sanitized);\n return stripInlineEventHandlers(sanitized);\n}\n\nexport function containsDangerousScheme(content: string, scheme: string): boolean {\n return content.toLowerCase().includes(scheme.toLowerCase());\n}\n","/**\n * Result Type Utilities\n *\n * Go-style error handling with discriminated union types.\n * Provides type-safe success/error handling without exceptions.\n *\n * @example\n * ```ts\n * const [err, data] = await handle(async () => {\n * const response = await fetch(url);\n * if (!response.ok) throw new Error(`HTTP ${response.status}`);\n * return response.json();\n * });\n *\n * if (err) {\n * console.error('Failed:', err.message);\n * return { error: err.message };\n * }\n * return { data };\n * ```\n */\n\n// -----------------------------------------------------------------------------\n// Result Type\n// -----------------------------------------------------------------------------\n\n/**\n * A discriminated union representing either success or failure.\n *\n * @template T - The success value type\n * @template E - The error type (defaults to Error)\n *\n * @example\n * ```ts\n * function divide(a: number, b: number): Result<number, string> {\n * if (b === 0) return err('Division by zero');\n * return ok(a / b);\n * }\n *\n * const result = divide(10, 2);\n * if (isOk(result)) {\n * console.log('Result:', result.value); // 5\n * } else {\n * console.error('Error:', result.error);\n * }\n * ```\n */\nexport type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };\n\n// -----------------------------------------------------------------------------\n// Constructors\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a success Result.\n *\n * @param value - The success value\n * @returns A success Result containing the value\n *\n * @example\n * ```ts\n * const result = ok(42);\n * // { ok: true, value: 42 }\n * ```\n */\nexport function ok<T>(value: T): Result<T, never> {\n return { ok: true, value };\n}\n\n/**\n * Creates a failure Result.\n *\n * @param error - The error value\n * @returns A failure Result containing the error\n *\n * @example\n * ```ts\n * const result = err(new Error('Something went wrong'));\n * // { ok: false, error: Error('Something went wrong') }\n * ```\n */\nexport function err<E>(error: E): Result<never, E> {\n return { ok: false, error };\n}\n\n// -----------------------------------------------------------------------------\n// Type Guards\n// -----------------------------------------------------------------------------\n\n/**\n * Type guard to check if a Result is successful.\n *\n * @param result - The Result to check\n * @returns true if the Result is a success\n *\n * @example\n * ```ts\n * const result = fetchData();\n * if (isOk(result)) {\n * // TypeScript knows result.value exists here\n * console.log(result.value);\n * }\n * ```\n */\nexport function isOk<T, E>(result: Result<T, E>): result is { ok: true; value: T } {\n return result.ok === true;\n}\n\n/**\n * Type guard to check if a Result is a failure.\n *\n * @param result - The Result to check\n * @returns true if the Result is a failure\n *\n * @example\n * ```ts\n * const result = fetchData();\n * if (isErr(result)) {\n * // TypeScript knows result.error exists here\n * console.error(result.error);\n * }\n * ```\n */\nexport function isErr<T, E>(result: Result<T, E>): result is { ok: false; error: E } {\n return result.ok === false;\n}\n\n// -----------------------------------------------------------------------------\n// Extractors\n// -----------------------------------------------------------------------------\n\n/**\n * Extracts the value from a Result, throwing if it's an error.\n *\n * @param result - The Result to unwrap\n * @returns The success value\n * @throws The error if Result is a failure\n *\n * @example\n * ```ts\n * const result = ok(42);\n * const value = unwrap(result); // 42\n *\n * const errorResult = err(new Error('fail'));\n * const value2 = unwrap(errorResult); // throws Error('fail')\n * ```\n */\nexport function unwrap<T, E>(result: Result<T, E>): T {\n if (isOk(result)) {\n return result.value;\n }\n throw result.error;\n}\n\n/**\n * Extracts the value from a Result, returning a default on error.\n *\n * @param result - The Result to unwrap\n * @param defaultValue - The value to return if Result is an error\n * @returns The success value or the default value\n *\n * @example\n * ```ts\n * const result = err(new Error('fail'));\n * const value = unwrapOr(result, 0); // 0\n *\n * const okResult = ok(42);\n * const value2 = unwrapOr(okResult, 0); // 42\n * ```\n */\nexport function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {\n if (isOk(result)) {\n return result.value;\n }\n return defaultValue;\n}\n\n/**\n * Extracts the value from a Result, computing a default on error.\n *\n * @param result - The Result to unwrap\n * @param fn - Function to compute the default value from the error\n * @returns The success value or the computed default\n *\n * @example\n * ```ts\n * const result = err(new Error('not found'));\n * const value = unwrapOrElse(result, (e) => {\n * console.error('Error:', e.message);\n * return [];\n * });\n * ```\n */\nexport function unwrapOrElse<T, E>(result: Result<T, E>, fn: (error: E) => T): T {\n if (isOk(result)) {\n return result.value;\n }\n return fn(result.error);\n}\n\n// -----------------------------------------------------------------------------\n// Transformers\n// -----------------------------------------------------------------------------\n\n/**\n * Maps a successful Result's value.\n *\n * @param result - The Result to map\n * @param fn - Function to transform the value\n * @returns A new Result with the transformed value, or the original error\n *\n * @example\n * ```ts\n * const result = ok(5);\n * const doubled = map(result, (n) => n * 2); // ok(10)\n *\n * const errorResult = err('fail');\n * const still = map(errorResult, (n) => n * 2); // err('fail')\n * ```\n */\nexport function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {\n if (isOk(result)) {\n return ok(fn(result.value));\n }\n return result;\n}\n\n/**\n * Maps a failed Result's error.\n *\n * @param result - The Result to map\n * @param fn - Function to transform the error\n * @returns A new Result with the transformed error, or the original value\n *\n * @example\n * ```ts\n * const result = err('not found');\n * const mapped = mapErr(result, (e) => new Error(e)); // err(Error('not found'))\n * ```\n */\nexport function mapErr<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> {\n if (isErr(result)) {\n return err(fn(result.error));\n }\n return result;\n}\n\n/**\n * Chains Result-returning operations.\n *\n * @param result - The Result to chain from\n * @param fn - Function that returns a new Result\n * @returns The chained Result\n *\n * @example\n * ```ts\n * function parse(input: string): Result<number, string> {\n * const n = parseInt(input, 10);\n * return isNaN(n) ? err('not a number') : ok(n);\n * }\n *\n * function double(n: number): Result<number, string> {\n * return ok(n * 2);\n * }\n *\n * const result = flatMap(parse('5'), double); // ok(10)\n * const fail = flatMap(parse('abc'), double); // err('not a number')\n * ```\n */\nexport function flatMap<T, U, E>(\n result: Result<T, E>,\n fn: (value: T) => Result<U, E>\n): Result<U, E> {\n if (isOk(result)) {\n return fn(result.value);\n }\n return result;\n}\n\n// -----------------------------------------------------------------------------\n// Async Helpers\n// -----------------------------------------------------------------------------\n\n/**\n * Wraps a Promise in a Result type.\n *\n * @param promise - The Promise to wrap\n * @returns A Promise that resolves to a Result\n *\n * @example\n * ```ts\n * const result = await fromPromise(fetch('/api/data'));\n * if (isErr(result)) {\n * console.error('Fetch failed:', result.error);\n * return;\n * }\n * const response = result.value;\n * ```\n */\nexport async function fromPromise<T>(promise: Promise<T>): Promise<Result<T, Error>> {\n try {\n const value = await promise;\n return ok(value);\n } catch (error) {\n return err(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n/**\n * Wraps a throwing function in a Result type.\n *\n * @param fn - The function to wrap\n * @returns A Result containing the return value or the thrown error\n *\n * @example\n * ```ts\n * const result = tryCatch(() => JSON.parse(input));\n * if (isErr(result)) {\n * console.error('Invalid JSON:', result.error);\n * return null;\n * }\n * return result.value;\n * ```\n */\nexport function tryCatch<T>(fn: () => T): Result<T, Error> {\n try {\n return ok(fn());\n } catch (error) {\n return err(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n/**\n * Wraps an async function in a Result type.\n *\n * @param fn - The async function to wrap\n * @returns A Promise that resolves to a Result\n *\n * @example\n * ```ts\n * const result = await tryCatchAsync(async () => {\n * const response = await fetch('/api/data');\n * if (!response.ok) throw new Error(`HTTP ${response.status}`);\n * return response.json();\n * });\n * ```\n */\nexport async function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {\n try {\n const value = await fn();\n return ok(value);\n } catch (error) {\n return err(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\n// -----------------------------------------------------------------------------\n// Tuple Helpers (Go-style)\n// -----------------------------------------------------------------------------\n\n/**\n * Go-style tuple for error handling: [error, value]\n *\n * @example\n * ```ts\n * const [err, data] = await handle(fetchData);\n * if (err) {\n * console.error(err);\n * return;\n * }\n * console.log(data);\n * ```\n */\nexport type GoTuple<T, E = Error> = [E, undefined] | [undefined, T];\n\n/**\n * Converts a Result to a Go-style tuple.\n *\n * @param result - The Result to convert\n * @returns A tuple of [error, value]\n *\n * @example\n * ```ts\n * const result = await fetchData();\n * const [err, data] = toTuple(result);\n * ```\n */\nexport function toTuple<T, E>(result: Result<T, E>): GoTuple<T, E> {\n if (isOk(result)) {\n return [undefined, result.value];\n }\n return [result.error, undefined];\n}\n\n/**\n * Wraps an async function and returns a Go-style tuple.\n *\n * @param fn - The async function to wrap\n * @returns A Promise that resolves to [error, value] tuple\n *\n * @example\n * ```ts\n * const [err, data] = await handle(async () => {\n * const res = await fetch('/api/users');\n * if (!res.ok) throw new Error(`HTTP ${res.status}`);\n * return res.json();\n * });\n *\n * if (err) {\n * console.error('Failed to fetch users:', err.message);\n * return [];\n * }\n * return data;\n * ```\n */\nexport async function handle<T>(fn: () => Promise<T>): Promise<GoTuple<T, Error>> {\n try {\n const value = await fn();\n return [undefined, value];\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n return [err, undefined];\n }\n}\n\n/**\n * Wraps a synchronous function and returns a Go-style tuple.\n *\n * @param fn - The function to wrap\n * @returns A tuple of [error, value]\n *\n * @example\n * ```ts\n * const [err, parsed] = handleSync(() => JSON.parse(input));\n * if (err) {\n * console.error('Invalid JSON:', err.message);\n * return null;\n * }\n * return parsed;\n * ```\n */\nexport function handleSync<T>(fn: () => T): GoTuple<T, Error> {\n try {\n const value = fn();\n return [undefined, value];\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n return [err, undefined];\n }\n}\n"],"mappings":"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","names":["end"]}
@@ -25,6 +25,19 @@ type ParametricRouteProps = CmsConfig & {
25
25
  };
26
26
  registry?: Partial<BlockComponentRegistry>;
27
27
  };
28
+ /**
29
+ * Render-time options that are NOT part of the page props surface. `preview` is
30
+ * supplied only by the dedicated preview page wrapper (`ParametricRoutePreviewPage`),
31
+ * never by query params, so the production page can never accidentally opt into
32
+ * edit overlays or draft content.
33
+ */
34
+ type RenderParametricRouteOptions = {
35
+ /**
36
+ * Renders the preview experience: forces the edit-mode overlay/hovers and pulls
37
+ * block content from `draft_content` instead of `published_content`.
38
+ */
39
+ preview?: boolean;
40
+ };
28
41
  type ParametricRouteResult = {
29
42
  status: 'ok';
30
43
  node: ReactNode;
@@ -40,6 +53,6 @@ declare function getWebsiteId(providedWebsiteId?: string): string;
40
53
  * Renders CMS-backed blocks for a multi-segment path. Maps NOT_FOUND-style API
41
54
  * outcomes to `{ status: 'not_found' }` instead of throwing.
42
55
  */
43
- declare function renderParametricRoute({ params, searchParams, registry, apiKey, cmsUrl, websiteId: providedWebsiteId, }: ParametricRouteProps): Promise<ParametricRouteResult>;
56
+ declare function renderParametricRoute({ params, searchParams, registry, apiKey, cmsUrl, websiteId: providedWebsiteId, }: ParametricRouteProps, { preview }?: RenderParametricRouteOptions): Promise<ParametricRouteResult>;
44
57
 
45
- export { type ParametricRouteProps, type ParametricRouteResult, getWebsiteId, renderParametricRoute };
58
+ export { type ParametricRouteProps, type ParametricRouteResult, type RenderParametricRouteOptions, getWebsiteId, renderParametricRoute };
@@ -394,11 +394,14 @@ function generateCmsOverlayScript(cmsParentOrigin) {
394
394
  var style = document.createElement('style');
395
395
  style.setAttribute('data-cms-overlay', '');
396
396
  style.textContent = \`
397
+ [data-cms-block],
397
398
  [data-cms-editable] {
398
399
  cursor: pointer;
400
+ }
401
+ [data-cms-editable] {
399
402
  border-radius: 2px;
400
403
  }
401
- [data-cms-editable]:hover {
404
+ [data-cms-editable]:not([data-cms-block]):hover {
402
405
  outline: 2px solid #3b82f6;
403
406
  outline-offset: 2px;
404
407
  }
@@ -411,8 +414,8 @@ function generateCmsOverlayScript(cmsParentOrigin) {
411
414
  pointer-events: none;
412
415
  z-index: 99998;
413
416
  }
414
- #cms-overlay-root > * {
415
- pointer-events: auto;
417
+ #cms-overlay-root > *:not(.cms-block-toolbar) {
418
+ pointer-events: none;
416
419
  }
417
420
  .cms-block-outline {
418
421
  position: fixed;
@@ -508,10 +511,69 @@ function generateCmsOverlayScript(cmsParentOrigin) {
508
511
  blockEl.setAttribute('data-cms-block', '');
509
512
  blockEl.setAttribute('data-block-id', blockId);
510
513
  blockEl.setAttribute('data-block-type', blockType);
511
- blockEl.setAttribute('data-cms-editable', '');
514
+
515
+ injectEditableSpans(
516
+ blockEl,
517
+ blockId,
518
+ blockType,
519
+ sentinel.getAttribute('data-content-entries')
520
+ );
512
521
  });
513
522
  }
514
523
 
524
+ function injectEditableSpans(blockEl, blockId, blockType, rawEntries) {
525
+ if (!rawEntries || blockEl.querySelector('[data-cms-editable][data-content-path]')) return;
526
+
527
+ var entries;
528
+ try {
529
+ entries = JSON.parse(rawEntries);
530
+ } catch (_) {
531
+ return;
532
+ }
533
+ if (!Array.isArray(entries) || entries.length === 0) return;
534
+
535
+ var used = {};
536
+ var walker = document.createTreeWalker(blockEl, NodeFilter.SHOW_TEXT, null);
537
+ var textNodes = [];
538
+ var node = walker.nextNode();
539
+ while (node !== null) {
540
+ if (node.nodeValue && node.nodeValue.trim()) {
541
+ textNodes.push(node);
542
+ }
543
+ node = walker.nextNode();
544
+ }
545
+
546
+ for (var i = 0; i < textNodes.length; i++) {
547
+ var textNode = textNodes[i];
548
+ var parentEl = textNode.parentElement;
549
+ if (!parentEl || parentEl.closest('svg') || parentEl.closest('[data-cms-editable]')) {
550
+ continue;
551
+ }
552
+
553
+ var text = textNode.nodeValue;
554
+ if (!text) continue;
555
+
556
+ for (var j = 0; j < entries.length; j++) {
557
+ var entry = entries[j];
558
+ if (!entry || typeof entry.v !== 'string' || typeof entry.p !== 'string' || used[entry.p]) {
559
+ continue;
560
+ }
561
+ if (text.trim() !== entry.v.trim()) continue;
562
+
563
+ used[entry.p] = true;
564
+ var span = document.createElement('span');
565
+ span.setAttribute('data-cms-editable', '');
566
+ span.setAttribute('data-block-id', blockId);
567
+ span.setAttribute('data-block-type', blockType);
568
+ span.setAttribute('data-content-path', entry.p);
569
+ span.setAttribute('contenteditable', 'true');
570
+ parentEl.insertBefore(span, textNode);
571
+ span.appendChild(textNode);
572
+ break;
573
+ }
574
+ }
575
+ }
576
+
515
577
  stampBlockElements();
516
578
 
517
579
  var stampObserver = new MutationObserver(function(mutations) {
@@ -566,6 +628,38 @@ function generateCmsOverlayScript(cmsParentOrigin) {
566
628
  var toolbarVisible = false;
567
629
  var selectedBlockId = null;
568
630
 
631
+ function decoratePreviewUrl(rawHref) {
632
+ if (!rawHref) return rawHref;
633
+ try {
634
+ var url = new URL(rawHref, window.location.href);
635
+ if (url.origin !== window.location.origin) return rawHref;
636
+ url.searchParams.set('edit_mode', 'true');
637
+ if (CMS_PARENT_ORIGIN) {
638
+ url.searchParams.set('cms_parent_origin', CMS_PARENT_ORIGIN);
639
+ }
640
+ return url.toString();
641
+ } catch (_) {
642
+ return rawHref;
643
+ }
644
+ }
645
+
646
+ function preserveEditParamsOnNavigation(e) {
647
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
648
+ return;
649
+ }
650
+
651
+ var anchor = e.target.closest('a[href]');
652
+ if (!anchor || (anchor.target && anchor.target !== '_self') || anchor.hasAttribute('download')) {
653
+ return;
654
+ }
655
+
656
+ var href = anchor.getAttribute('href');
657
+ var decorated = decoratePreviewUrl(href);
658
+ if (decorated && decorated !== href) {
659
+ anchor.setAttribute('href', decorated);
660
+ }
661
+ }
662
+
569
663
  function postToParent(message) {
570
664
  if (!CMS_PARENT_ORIGIN || !window.parent || window.parent === window) {
571
665
  return;
@@ -684,6 +778,8 @@ function generateCmsOverlayScript(cmsParentOrigin) {
684
778
  });
685
779
 
686
780
  document.addEventListener('click', function(e) {
781
+ preserveEditParamsOnNavigation(e);
782
+
687
783
  if (toolbarVisible && !e.target.closest('[data-cms-toolbar]')) {
688
784
  var block = e.target.closest('[data-cms-block]');
689
785
  if (!block || block.getAttribute('data-block-id') !== currentBlockId) {
@@ -713,23 +809,23 @@ function generateCmsOverlayScript(cmsParentOrigin) {
713
809
  contentPath: null
714
810
  });
715
811
  }
716
- });
812
+ }, true);
717
813
 
718
814
  toolbar.querySelector('.move-up').addEventListener('click', function() {
719
815
  if (!currentBlockId) return;
720
- postToParent({ type: 'cms-block-move', blockId: currentBlockId, direction: 'up' });
816
+ postToParent({ type: 'cms-block-action', action: 'move-up', blockId: currentBlockId });
721
817
  hideToolbar();
722
818
  });
723
819
 
724
820
  toolbar.querySelector('.move-down').addEventListener('click', function() {
725
821
  if (!currentBlockId) return;
726
- postToParent({ type: 'cms-block-move', blockId: currentBlockId, direction: 'down' });
822
+ postToParent({ type: 'cms-block-action', action: 'move-down', blockId: currentBlockId });
727
823
  hideToolbar();
728
824
  });
729
825
 
730
826
  toolbar.querySelector('.delete').addEventListener('click', function() {
731
827
  if (!currentBlockId) return;
732
- postToParent({ type: 'cms-block-delete', blockId: currentBlockId });
828
+ postToParent({ type: 'cms-block-action', action: 'delete', blockId: currentBlockId });
733
829
  hideToolbar();
734
830
  });
735
831
 
@@ -823,6 +919,27 @@ function getCmsParentTargetOrigin(preferredCmsUrl, explicitParentOrigin) {
823
919
 
824
920
  // lib/block-renderer.tsx
825
921
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
922
+ function extractContentValues(content, basePath = []) {
923
+ const map = /* @__PURE__ */ new Map();
924
+ function walk(obj, path) {
925
+ if (typeof obj === "string" && obj.trim() !== "") {
926
+ const contentPath = path.join(".");
927
+ const existing = map.get(obj) || [];
928
+ existing.push({ contentPath, value: obj });
929
+ map.set(obj, existing);
930
+ } else if (Array.isArray(obj)) {
931
+ for (let index = 0; index < obj.length; index++) {
932
+ walk(obj[index], [...path, String(index)]);
933
+ }
934
+ } else if (obj && typeof obj === "object") {
935
+ for (const [key, value] of Object.entries(obj)) {
936
+ walk(value, [...path, key]);
937
+ }
938
+ }
939
+ }
940
+ walk(content, basePath);
941
+ return map;
942
+ }
826
943
  function CmsEditableInit({
827
944
  cmsUrl,
828
945
  cmsParentOrigin
@@ -871,6 +988,7 @@ function BlockRenderer({
871
988
  block,
872
989
  registry,
873
990
  disableEditable,
991
+ enableContentEditable,
874
992
  routeParams,
875
993
  path
876
994
  }) {
@@ -886,6 +1004,7 @@ function BlockRenderer({
886
1004
  if (disableEditable) {
887
1005
  return component;
888
1006
  }
1007
+ const contentEntries = enableContentEditable ? [...extractContentValues(block.content).values()].flat().map(({ value, contentPath }) => ({ v: value, p: contentPath })) : void 0;
889
1008
  return /* @__PURE__ */ jsxs(Fragment, { children: [
890
1009
  /* @__PURE__ */ jsx(
891
1010
  "span",
@@ -893,6 +1012,7 @@ function BlockRenderer({
893
1012
  "data-cms-sentinel": "",
894
1013
  "data-block-id": block.id,
895
1014
  "data-block-type": block.type,
1015
+ "data-content-entries": contentEntries ? JSON.stringify(contentEntries) : void 0,
896
1016
  style: { display: "none" },
897
1017
  "aria-hidden": "true"
898
1018
  }
@@ -974,7 +1094,7 @@ async function renderParametricRoute({
974
1094
  apiKey,
975
1095
  cmsUrl,
976
1096
  websiteId: providedWebsiteId
977
- }) {
1097
+ }, { preview = false } = {}) {
978
1098
  const websiteId = getWebsiteId(providedWebsiteId);
979
1099
  const { slug } = "then" in params ? await params : params;
980
1100
  const resolvedSearchParams = searchParams && "then" in searchParams ? await searchParams : searchParams;
@@ -989,10 +1109,11 @@ async function renderParametricRoute({
989
1109
  }
990
1110
  }
991
1111
  }
992
- const editModeParam = resolvedSearchParams?.edit_mode;
993
- const editMode = editModeParam === true || editModeParam === "true" || editModeParam === "1";
994
1112
  const cmsParentOriginParam = resolvedSearchParams?.cms_parent_origin;
995
1113
  const cmsParentOrigin = typeof cmsParentOriginParam === "boolean" ? void 0 : Array.isArray(cmsParentOriginParam) ? cmsParentOriginParam[0] : cmsParentOriginParam;
1114
+ const previewMode = preview === true;
1115
+ const editModeParam = resolvedSearchParams?.edit_mode;
1116
+ const editMode = previewMode || editModeParam === true || editModeParam === "true" || editModeParam === "1";
996
1117
  const rawPath = `/${slug.join("/")}`;
997
1118
  const path = normalizePath(rawPath);
998
1119
  if (/\.[a-zA-Z0-9]+$/.test(path)) {
@@ -1032,7 +1153,9 @@ async function renderParametricRoute({
1032
1153
  ]);
1033
1154
  const blocks = [];
1034
1155
  for (const block of blockResults) {
1035
- if (!block || block.published_content === null) continue;
1156
+ if (!block) continue;
1157
+ const blockContent = previewMode ? block.draft_content : block.published_content;
1158
+ if (blockContent == null) continue;
1036
1159
  let content = null;
1037
1160
  if (aiPreviewIndex !== null) {
1038
1161
  const generatedBlock = generatedBlocks[block.id];
@@ -1042,7 +1165,7 @@ async function renderParametricRoute({
1042
1165
  content = variants[variantIndex].content;
1043
1166
  }
1044
1167
  }
1045
- content = content ?? block.published_content;
1168
+ content = content ?? blockContent;
1046
1169
  if (!content) continue;
1047
1170
  if (block.schema_name === "article") {
1048
1171
  const article = normalizeArticleContent(content);
@@ -1090,6 +1213,7 @@ async function renderParametricRoute({
1090
1213
  block,
1091
1214
  registry: registry ?? {},
1092
1215
  disableEditable: !editMode,
1216
+ enableContentEditable: previewMode,
1093
1217
  routeParams,
1094
1218
  path
1095
1219
  },