cms-renderer 0.1.2 → 0.1.4

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 +1 @@
1
- {"version":3,"sources":["../../lib/data-utils.ts"],"sourcesContent":["/**\n * Data Fetching Utilities\n *\n * Three implementations showing the progression from naive to production-ready:\n * - v1 (Naive): Direct await, crashes on any error\n * - v2 (Defensive): Try-catch with null fallback\n * - v3 (Robust): Result type, retry logic, timeout support\n *\n * The robust version (v3) is exported as the default `fetchPage`.\n */\n\nimport { err, ok, type Result } from './result';\nimport type { BlockData } from './types';\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Page structure returned by the tRPC API.\n */\nexport interface Page {\n slug: string;\n title: string;\n blocks: BlockData[];\n}\n\n/**\n * Error codes for data fetching failures.\n */\nexport const FetchErrorCode = {\n INVALID_SLUG: 'INVALID_SLUG',\n NOT_FOUND: 'NOT_FOUND',\n NETWORK_ERROR: 'NETWORK_ERROR',\n TIMEOUT: 'TIMEOUT',\n PARSE_ERROR: 'PARSE_ERROR',\n SERVER_ERROR: 'SERVER_ERROR',\n RETRY_EXHAUSTED: 'RETRY_EXHAUSTED',\n} as const;\n\nexport type FetchErrorCode = (typeof FetchErrorCode)[keyof typeof FetchErrorCode];\n\n/**\n * Structured error for data fetching failures.\n */\nexport interface FetchError {\n code: FetchErrorCode;\n message: string;\n cause?: Error;\n retryCount?: number;\n}\n\n/**\n * Options for robust data fetching.\n */\nexport interface FetchOptions {\n /**\n * Timeout in milliseconds.\n * @default 10000 (10 seconds)\n */\n timeout?: number;\n\n /**\n * Number of retry attempts for transient failures.\n * @default 3\n */\n retries?: number;\n\n /**\n * Base delay between retries in milliseconds.\n * Uses exponential backoff: delay * 2^attempt\n * @default 1000 (1 second)\n */\n retryDelay?: number;\n\n /**\n * AbortSignal for cancellation support.\n */\n signal?: AbortSignal;\n}\n\n// -----------------------------------------------------------------------------\n// Constants\n// -----------------------------------------------------------------------------\n\nconst DEFAULT_TIMEOUT = 10_000; // 10 seconds\nconst DEFAULT_RETRIES = 3;\nconst DEFAULT_RETRY_DELAY = 1_000; // 1 second\n\n/**\n * HTTP status codes that indicate transient failures (should retry).\n */\nconst TRANSIENT_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const;\n\n// -----------------------------------------------------------------------------\n// Mock Data Store (for demonstration)\n// -----------------------------------------------------------------------------\n\n/**\n * Simulated data store.\n * In production, this would be replaced with actual tRPC/API calls.\n */\nconst mockPages: Record<string, Page> = {\n demo: {\n slug: 'demo',\n title: 'Demo Page',\n blocks: [\n {\n id: 'mock-header-1',\n type: 'header',\n content: {\n headline: 'Welcome',\n alignment: 'center',\n },\n },\n {\n id: 'mock-article-1',\n type: 'article',\n content: {\n headline: 'Getting Started',\n body: '## Introduction\\n\\nThis is a demo article.',\n },\n },\n ],\n },\n about: {\n slug: 'about',\n title: 'About Us',\n blocks: [\n {\n id: 'mock-header-2',\n type: 'header',\n content: {\n headline: 'About Our Company',\n alignment: 'left',\n },\n },\n ],\n },\n};\n\n// -----------------------------------------------------------------------------\n// Helper Functions\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a FetchError with the given code and message.\n */\nfunction createFetchError(\n code: FetchErrorCode,\n message: string,\n options: { cause?: Error; retryCount?: number } = {}\n): FetchError {\n return { code, message, ...options };\n}\n\n/**\n * Delays execution for the specified duration.\n */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Calculates exponential backoff delay.\n */\nfunction exponentialBackoff(baseDelay: number, attempt: number): number {\n // Add jitter to prevent thundering herd\n const jitter = Math.random() * 100;\n return baseDelay * 2 ** attempt + jitter;\n}\n\n/**\n * Checks if an error represents a transient failure that should be retried.\n */\nfunction isTransientError(error: unknown): boolean {\n if (error instanceof Error) {\n // Network errors\n if (error.name === 'TypeError' && error.message.includes('fetch')) {\n return true;\n }\n // Check for HTTP status codes in error message\n for (const code of TRANSIENT_STATUS_CODES) {\n if (error.message.includes(String(code))) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Simulates a network request with configurable behavior.\n * In production, this would be an actual fetch/tRPC call.\n */\nasync function simulateFetch(slug: string, signal?: AbortSignal): Promise<Page> {\n // Simulate network latency\n await delay(50 + Math.random() * 100);\n\n // Check for cancellation\n if (signal?.aborted) {\n throw new Error('Request aborted');\n }\n\n const page = mockPages[slug];\n if (!page) {\n const error = new Error(`Page not found: ${slug}`);\n error.name = 'NotFoundError';\n throw error;\n }\n\n return page;\n}\n\n// -----------------------------------------------------------------------------\n// V1: Naive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Naive data fetcher - crashes on any error.\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 network failure\n * - No timeout protection\n * - No retry logic for transient failures\n *\n * @example\n * ```ts\n * // This throws if slug is invalid or network fails\n * const page = await fetchPageV1('demo');\n * ```\n */\nexport async function fetchPageV1(slug: string): Promise<Page> {\n // Direct await - any error crashes the caller\n return simulateFetch(slug);\n}\n\n// -----------------------------------------------------------------------------\n// V2: Defensive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Defensive data fetcher - 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 404 and network failure\n * - No retry logic for transient failures\n * - No timeout protection\n *\n * @example\n * ```ts\n * const page = await fetchPageV2(slug);\n * if (!page) {\n * // What went wrong? 404? Network? Timeout? We don't know.\n * return <NotFound />;\n * }\n * ```\n */\nexport async function fetchPageV2(slug: string): Promise<Page | null> {\n try {\n // Basic validation\n if (!slug || typeof slug !== 'string') {\n return null;\n }\n\n return await simulateFetch(slug);\n } catch {\n // All errors become null - we lose valuable context\n return null;\n }\n}\n\n// -----------------------------------------------------------------------------\n// V3: Robust Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Robust data fetcher - production-ready with full error context.\n *\n * Features:\n * - Input validation with specific error codes\n * - Result type for explicit error handling\n * - Automatic retry with exponential backoff\n * - Timeout support via AbortController\n * - Distinguishes between error types (404 vs network vs timeout)\n * - Preserves error context for debugging\n *\n * @param slug - Page slug to fetch\n * @param options - Fetch options (timeout, retries, etc.)\n * @returns Result containing the page data or a structured error\n *\n * @example\n * ```ts\n * const result = await fetchPageV3('demo', {\n * timeout: 5000,\n * retries: 3,\n * });\n *\n * if (!result.ok) {\n * switch (result.error.code) {\n * case 'NOT_FOUND':\n * return <NotFoundPage slug={slug} />;\n * case 'TIMEOUT':\n * return <TimeoutError onRetry={handleRetry} />;\n * case 'NETWORK_ERROR':\n * return <NetworkError message={result.error.message} />;\n * case 'RETRY_EXHAUSTED':\n * return <RetryExhausted attempts={result.error.retryCount} />;\n * default:\n * return <GenericError />;\n * }\n * }\n *\n * return <PageRenderer page={result.value} />;\n * ```\n */\nexport async function fetchPageV3(\n slug: string,\n options: FetchOptions = {}\n): Promise<Result<Page, FetchError>> {\n const {\n timeout = DEFAULT_TIMEOUT,\n retries = DEFAULT_RETRIES,\n retryDelay = DEFAULT_RETRY_DELAY,\n signal: externalSignal,\n } = options;\n\n // 1. Validate input\n if (slug === null || slug === undefined) {\n return err(\n createFetchError(\n FetchErrorCode.INVALID_SLUG,\n `Slug must be a string, received ${slug === null ? 'null' : 'undefined'}`\n )\n );\n }\n\n if (typeof slug !== 'string') {\n return err(\n createFetchError(\n FetchErrorCode.INVALID_SLUG,\n `Slug must be a string, received ${typeof slug}`\n )\n );\n }\n\n const trimmedSlug = slug.trim();\n if (trimmedSlug.length === 0) {\n return err(createFetchError(FetchErrorCode.INVALID_SLUG, 'Slug cannot be empty'));\n }\n\n // 2. Create abort controller for timeout\n const controller = new AbortController();\n const { signal } = controller;\n\n // Link external signal if provided\n if (externalSignal) {\n externalSignal.addEventListener('abort', () => controller.abort());\n }\n\n // 3. Set up timeout\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n // 4. Attempt fetch with retry logic\n let lastError: Error | undefined;\n let attempt = 0;\n\n try {\n while (attempt <= retries) {\n try {\n const page = await simulateFetch(trimmedSlug, signal);\n clearTimeout(timeoutId);\n return ok(page);\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Check for abort/timeout\n if (signal.aborted) {\n clearTimeout(timeoutId);\n return err(\n createFetchError(FetchErrorCode.TIMEOUT, `Request timed out after ${timeout}ms`, {\n cause: lastError,\n })\n );\n }\n\n // Check for 404 (not retryable)\n if (lastError.name === 'NotFoundError') {\n clearTimeout(timeoutId);\n return err(\n createFetchError(FetchErrorCode.NOT_FOUND, `Page \"${trimmedSlug}\" not found`, {\n cause: lastError,\n })\n );\n }\n\n // Check if we should retry\n if (attempt < retries && isTransientError(lastError)) {\n attempt++;\n const backoff = exponentialBackoff(retryDelay, attempt);\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[fetchPageV3] Retry ${attempt}/${retries} for \"${trimmedSlug}\" after ${Math.round(backoff)}ms`\n );\n }\n await delay(backoff);\n continue;\n }\n\n // No more retries or non-transient error\n break;\n }\n }\n\n // 5. All retries exhausted\n clearTimeout(timeoutId);\n\n if (attempt >= retries) {\n return err(\n createFetchError(FetchErrorCode.RETRY_EXHAUSTED, `Failed after ${retries} retry attempts`, {\n cause: lastError,\n retryCount: attempt,\n })\n );\n }\n\n // Non-retryable error\n return err(\n createFetchError(\n FetchErrorCode.NETWORK_ERROR,\n lastError?.message ?? 'Unknown network error',\n { cause: lastError }\n )\n );\n } catch (error) {\n clearTimeout(timeoutId);\n const cause = error instanceof Error ? error : new Error(String(error));\n return err(\n createFetchError(FetchErrorCode.SERVER_ERROR, `Unexpected error: ${cause.message}`, { cause })\n );\n }\n}\n\n// -----------------------------------------------------------------------------\n// Default Export\n// -----------------------------------------------------------------------------\n\n/**\n * Production-ready page fetcher.\n *\n * This is an alias for `fetchPageV3` - the robust implementation\n * with validation, retry logic, and Result-based error handling.\n *\n * @example\n * ```ts\n * import { fetchPage } from '@/lib/data-utils';\n *\n * const result = await fetchPage('demo');\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 fetchPage = fetchPageV3;\n\n// -----------------------------------------------------------------------------\n// Utility Functions\n// -----------------------------------------------------------------------------\n\n/**\n * Validates a page slug format.\n *\n * @param slug - Slug to validate\n * @returns true if slug is valid format\n */\nexport function isValidSlug(slug: string): boolean {\n // Slugs should be lowercase, alphanumeric with hyphens\n return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);\n}\n\n/**\n * Normalizes a slug (lowercase, trim, replace spaces with hyphens).\n *\n * @param input - Raw input to normalize\n * @returns Normalized slug\n */\nexport function normalizeSlug(input: string): string {\n return input\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\n/**\n * Prefetches multiple pages in parallel with Result types.\n *\n * @param slugs - Array of page slugs to fetch\n * @param options - Fetch options applied to all requests\n * @returns Array of Results for each page\n *\n * @example\n * ```ts\n * const results = await prefetchPages(['home', 'about', 'blog']);\n * const errors = results.filter(r => !r.ok);\n * if (errors.length > 0) {\n * console.warn(`${errors.length} pages failed to load`);\n * }\n * ```\n */\nexport async function prefetchPages(\n slugs: string[],\n options?: FetchOptions\n): Promise<Result<Page, FetchError>[]> {\n return Promise.all(slugs.map((slug) => fetchPageV3(slug, options)));\n}\n\n/**\n * Fetches a page with stale-while-revalidate semantics.\n * Returns cached data immediately if available, then updates in background.\n *\n * @param slug - Page slug to fetch\n * @param cache - Cache storage (e.g., Map, localStorage wrapper)\n * @param options - Fetch options\n * @returns Cached data or fresh fetch result\n */\nexport async function fetchPageWithSWR(\n slug: string,\n cache: Map<string, { data: Page; timestamp: number }>,\n options: FetchOptions & { maxAge?: number } = {}\n): Promise<Result<Page, FetchError>> {\n const { maxAge = 60_000 } = options; // 1 minute default\n\n const cached = cache.get(slug);\n const now = Date.now();\n\n // Return fresh cache immediately\n if (cached && now - cached.timestamp < maxAge) {\n return ok(cached.data);\n }\n\n // Fetch fresh data\n const result = await fetchPageV3(slug, options);\n\n // Update cache on success\n if (result.ok) {\n cache.set(slug, { data: result.value, timestamp: now });\n }\n\n // If fetch failed but we have stale cache, return it with warning\n if (!result.ok && cached) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[fetchPageWithSWR] Returning stale cache for \"${slug}\" after fetch failure:`,\n result.error.message\n );\n }\n return ok(cached.data);\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../lib/result.ts","../../lib/data-utils.ts"],"sourcesContent":["/**\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","/**\n * Data Fetching Utilities\n *\n * Three implementations showing the progression from naive to production-ready:\n * - v1 (Naive): Direct await, crashes on any error\n * - v2 (Defensive): Try-catch with null fallback\n * - v3 (Robust): Result type, retry logic, timeout support\n *\n * The robust version (v3) is exported as the default `fetchPage`.\n */\n\nimport { err, ok, type Result } from './result';\nimport type { BlockData } from './types';\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Page structure returned by the tRPC API.\n */\nexport interface Page {\n slug: string;\n title: string;\n blocks: BlockData[];\n}\n\n/**\n * Error codes for data fetching failures.\n */\nexport const FetchErrorCode = {\n INVALID_SLUG: 'INVALID_SLUG',\n NOT_FOUND: 'NOT_FOUND',\n NETWORK_ERROR: 'NETWORK_ERROR',\n TIMEOUT: 'TIMEOUT',\n PARSE_ERROR: 'PARSE_ERROR',\n SERVER_ERROR: 'SERVER_ERROR',\n RETRY_EXHAUSTED: 'RETRY_EXHAUSTED',\n} as const;\n\nexport type FetchErrorCode = (typeof FetchErrorCode)[keyof typeof FetchErrorCode];\n\n/**\n * Structured error for data fetching failures.\n */\nexport interface FetchError {\n code: FetchErrorCode;\n message: string;\n cause?: Error;\n retryCount?: number;\n}\n\n/**\n * Options for robust data fetching.\n */\nexport interface FetchOptions {\n /**\n * Timeout in milliseconds.\n * @default 10000 (10 seconds)\n */\n timeout?: number;\n\n /**\n * Number of retry attempts for transient failures.\n * @default 3\n */\n retries?: number;\n\n /**\n * Base delay between retries in milliseconds.\n * Uses exponential backoff: delay * 2^attempt\n * @default 1000 (1 second)\n */\n retryDelay?: number;\n\n /**\n * AbortSignal for cancellation support.\n */\n signal?: AbortSignal;\n}\n\n// -----------------------------------------------------------------------------\n// Constants\n// -----------------------------------------------------------------------------\n\nconst DEFAULT_TIMEOUT = 10_000; // 10 seconds\nconst DEFAULT_RETRIES = 3;\nconst DEFAULT_RETRY_DELAY = 1_000; // 1 second\n\n/**\n * HTTP status codes that indicate transient failures (should retry).\n */\nconst TRANSIENT_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const;\n\n// -----------------------------------------------------------------------------\n// Mock Data Store (for demonstration)\n// -----------------------------------------------------------------------------\n\n/**\n * Simulated data store.\n * In production, this would be replaced with actual tRPC/API calls.\n */\nconst mockPages: Record<string, Page> = {\n demo: {\n slug: 'demo',\n title: 'Demo Page',\n blocks: [\n {\n id: 'mock-header-1',\n type: 'header',\n content: {\n headline: 'Welcome',\n alignment: 'center',\n },\n },\n {\n id: 'mock-article-1',\n type: 'article',\n content: {\n headline: 'Getting Started',\n body: '## Introduction\\n\\nThis is a demo article.',\n },\n },\n ],\n },\n about: {\n slug: 'about',\n title: 'About Us',\n blocks: [\n {\n id: 'mock-header-2',\n type: 'header',\n content: {\n headline: 'About Our Company',\n alignment: 'left',\n },\n },\n ],\n },\n};\n\n// -----------------------------------------------------------------------------\n// Helper Functions\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a FetchError with the given code and message.\n */\nfunction createFetchError(\n code: FetchErrorCode,\n message: string,\n options: { cause?: Error; retryCount?: number } = {}\n): FetchError {\n return { code, message, ...options };\n}\n\n/**\n * Delays execution for the specified duration.\n */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Calculates exponential backoff delay.\n */\nfunction exponentialBackoff(baseDelay: number, attempt: number): number {\n // Add jitter to prevent thundering herd\n const jitter = Math.random() * 100;\n return baseDelay * 2 ** attempt + jitter;\n}\n\n/**\n * Checks if an error represents a transient failure that should be retried.\n */\nfunction isTransientError(error: unknown): boolean {\n if (error instanceof Error) {\n // Network errors\n if (error.name === 'TypeError' && error.message.includes('fetch')) {\n return true;\n }\n // Check for HTTP status codes in error message\n for (const code of TRANSIENT_STATUS_CODES) {\n if (error.message.includes(String(code))) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Simulates a network request with configurable behavior.\n * In production, this would be an actual fetch/tRPC call.\n */\nasync function simulateFetch(slug: string, signal?: AbortSignal): Promise<Page> {\n // Simulate network latency\n await delay(50 + Math.random() * 100);\n\n // Check for cancellation\n if (signal?.aborted) {\n throw new Error('Request aborted');\n }\n\n const page = mockPages[slug];\n if (!page) {\n const error = new Error(`Page not found: ${slug}`);\n error.name = 'NotFoundError';\n throw error;\n }\n\n return page;\n}\n\n// -----------------------------------------------------------------------------\n// V1: Naive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Naive data fetcher - crashes on any error.\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 network failure\n * - No timeout protection\n * - No retry logic for transient failures\n *\n * @example\n * ```ts\n * // This throws if slug is invalid or network fails\n * const page = await fetchPageV1('demo');\n * ```\n */\nexport async function fetchPageV1(slug: string): Promise<Page> {\n // Direct await - any error crashes the caller\n return simulateFetch(slug);\n}\n\n// -----------------------------------------------------------------------------\n// V2: Defensive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Defensive data fetcher - 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 404 and network failure\n * - No retry logic for transient failures\n * - No timeout protection\n *\n * @example\n * ```ts\n * const page = await fetchPageV2(slug);\n * if (!page) {\n * // What went wrong? 404? Network? Timeout? We don't know.\n * return <NotFound />;\n * }\n * ```\n */\nexport async function fetchPageV2(slug: string): Promise<Page | null> {\n try {\n // Basic validation\n if (!slug || typeof slug !== 'string') {\n return null;\n }\n\n return await simulateFetch(slug);\n } catch {\n // All errors become null - we lose valuable context\n return null;\n }\n}\n\n// -----------------------------------------------------------------------------\n// V3: Robust Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Robust data fetcher - production-ready with full error context.\n *\n * Features:\n * - Input validation with specific error codes\n * - Result type for explicit error handling\n * - Automatic retry with exponential backoff\n * - Timeout support via AbortController\n * - Distinguishes between error types (404 vs network vs timeout)\n * - Preserves error context for debugging\n *\n * @param slug - Page slug to fetch\n * @param options - Fetch options (timeout, retries, etc.)\n * @returns Result containing the page data or a structured error\n *\n * @example\n * ```ts\n * const result = await fetchPageV3('demo', {\n * timeout: 5000,\n * retries: 3,\n * });\n *\n * if (!result.ok) {\n * switch (result.error.code) {\n * case 'NOT_FOUND':\n * return <NotFoundPage slug={slug} />;\n * case 'TIMEOUT':\n * return <TimeoutError onRetry={handleRetry} />;\n * case 'NETWORK_ERROR':\n * return <NetworkError message={result.error.message} />;\n * case 'RETRY_EXHAUSTED':\n * return <RetryExhausted attempts={result.error.retryCount} />;\n * default:\n * return <GenericError />;\n * }\n * }\n *\n * return <PageRenderer page={result.value} />;\n * ```\n */\nexport async function fetchPageV3(\n slug: string,\n options: FetchOptions = {}\n): Promise<Result<Page, FetchError>> {\n const {\n timeout = DEFAULT_TIMEOUT,\n retries = DEFAULT_RETRIES,\n retryDelay = DEFAULT_RETRY_DELAY,\n signal: externalSignal,\n } = options;\n\n // 1. Validate input\n if (slug === null || slug === undefined) {\n return err(\n createFetchError(\n FetchErrorCode.INVALID_SLUG,\n `Slug must be a string, received ${slug === null ? 'null' : 'undefined'}`\n )\n );\n }\n\n if (typeof slug !== 'string') {\n return err(\n createFetchError(\n FetchErrorCode.INVALID_SLUG,\n `Slug must be a string, received ${typeof slug}`\n )\n );\n }\n\n const trimmedSlug = slug.trim();\n if (trimmedSlug.length === 0) {\n return err(createFetchError(FetchErrorCode.INVALID_SLUG, 'Slug cannot be empty'));\n }\n\n // 2. Create abort controller for timeout\n const controller = new AbortController();\n const { signal } = controller;\n\n // Link external signal if provided\n if (externalSignal) {\n externalSignal.addEventListener('abort', () => controller.abort());\n }\n\n // 3. Set up timeout\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n // 4. Attempt fetch with retry logic\n let lastError: Error | undefined;\n let attempt = 0;\n\n try {\n while (attempt <= retries) {\n try {\n const page = await simulateFetch(trimmedSlug, signal);\n clearTimeout(timeoutId);\n return ok(page);\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Check for abort/timeout\n if (signal.aborted) {\n clearTimeout(timeoutId);\n return err(\n createFetchError(FetchErrorCode.TIMEOUT, `Request timed out after ${timeout}ms`, {\n cause: lastError,\n })\n );\n }\n\n // Check for 404 (not retryable)\n if (lastError.name === 'NotFoundError') {\n clearTimeout(timeoutId);\n return err(\n createFetchError(FetchErrorCode.NOT_FOUND, `Page \"${trimmedSlug}\" not found`, {\n cause: lastError,\n })\n );\n }\n\n // Check if we should retry\n if (attempt < retries && isTransientError(lastError)) {\n attempt++;\n const backoff = exponentialBackoff(retryDelay, attempt);\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[fetchPageV3] Retry ${attempt}/${retries} for \"${trimmedSlug}\" after ${Math.round(backoff)}ms`\n );\n }\n await delay(backoff);\n continue;\n }\n\n // No more retries or non-transient error\n break;\n }\n }\n\n // 5. All retries exhausted\n clearTimeout(timeoutId);\n\n if (attempt >= retries) {\n return err(\n createFetchError(FetchErrorCode.RETRY_EXHAUSTED, `Failed after ${retries} retry attempts`, {\n cause: lastError,\n retryCount: attempt,\n })\n );\n }\n\n // Non-retryable error\n return err(\n createFetchError(\n FetchErrorCode.NETWORK_ERROR,\n lastError?.message ?? 'Unknown network error',\n { cause: lastError }\n )\n );\n } catch (error) {\n clearTimeout(timeoutId);\n const cause = error instanceof Error ? error : new Error(String(error));\n return err(\n createFetchError(FetchErrorCode.SERVER_ERROR, `Unexpected error: ${cause.message}`, { cause })\n );\n }\n}\n\n// -----------------------------------------------------------------------------\n// Default Export\n// -----------------------------------------------------------------------------\n\n/**\n * Production-ready page fetcher.\n *\n * This is an alias for `fetchPageV3` - the robust implementation\n * with validation, retry logic, and Result-based error handling.\n *\n * @example\n * ```ts\n * import { fetchPage } from '@/lib/data-utils';\n *\n * const result = await fetchPage('demo');\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 fetchPage = fetchPageV3;\n\n// -----------------------------------------------------------------------------\n// Utility Functions\n// -----------------------------------------------------------------------------\n\n/**\n * Validates a page slug format.\n *\n * @param slug - Slug to validate\n * @returns true if slug is valid format\n */\nexport function isValidSlug(slug: string): boolean {\n // Slugs should be lowercase, alphanumeric with hyphens\n return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);\n}\n\n/**\n * Normalizes a slug (lowercase, trim, replace spaces with hyphens).\n *\n * @param input - Raw input to normalize\n * @returns Normalized slug\n */\nexport function normalizeSlug(input: string): string {\n return input\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\n/**\n * Prefetches multiple pages in parallel with Result types.\n *\n * @param slugs - Array of page slugs to fetch\n * @param options - Fetch options applied to all requests\n * @returns Array of Results for each page\n *\n * @example\n * ```ts\n * const results = await prefetchPages(['home', 'about', 'blog']);\n * const errors = results.filter(r => !r.ok);\n * if (errors.length > 0) {\n * console.warn(`${errors.length} pages failed to load`);\n * }\n * ```\n */\nexport async function prefetchPages(\n slugs: string[],\n options?: FetchOptions\n): Promise<Result<Page, FetchError>[]> {\n return Promise.all(slugs.map((slug) => fetchPageV3(slug, options)));\n}\n\n/**\n * Fetches a page with stale-while-revalidate semantics.\n * Returns cached data immediately if available, then updates in background.\n *\n * @param slug - Page slug to fetch\n * @param cache - Cache storage (e.g., Map, localStorage wrapper)\n * @param options - Fetch options\n * @returns Cached data or fresh fetch result\n */\nexport async function fetchPageWithSWR(\n slug: string,\n cache: Map<string, { data: Page; timestamp: number }>,\n options: FetchOptions & { maxAge?: number } = {}\n): Promise<Result<Page, FetchError>> {\n const { maxAge = 60_000 } = options; // 1 minute default\n\n const cached = cache.get(slug);\n const now = Date.now();\n\n // Return fresh cache immediately\n if (cached && now - cached.timestamp < maxAge) {\n return ok(cached.data);\n }\n\n // Fetch fresh data\n const result = await fetchPageV3(slug, options);\n\n // Update cache on success\n if (result.ok) {\n cache.set(slug, { data: result.value, timestamp: now });\n }\n\n // If fetch failed but we have stale cache, return it with warning\n if (!result.ok && cached) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[fetchPageWithSWR] Returning stale cache for \"${slug}\" after fetch failure:`,\n result.error.message\n );\n }\n return ok(cached.data);\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ "use client";
2
3
 
3
4
  // lib/image/lazy-load.ts
4
5
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../lib/image/lazy-load.ts"],"sourcesContent":["/**\n * Lazy Loading Utilities\n *\n * Reusable IntersectionObserver-based lazy loading with\n * connection-aware quality adjustment.\n *\n * This module provides:\n * - useLazyLoad: Hook for deferred loading when elements enter viewport\n * - getConnectionAwareQuality: Adjusts image quality based on network speed\n */\n\n'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface LazyLoadOptions {\n /** Root margin for early loading (default: \"200px\") */\n rootMargin?: string;\n /** Threshold for intersection (default: 0) */\n threshold?: number;\n /** Callback when element enters viewport */\n onEnter?: () => void;\n}\n\nexport interface LazyLoadResult {\n /** Callback ref to attach to the element */\n ref: (node: HTMLElement | null) => void;\n /** Whether the element has entered the viewport */\n inView: boolean;\n}\n\n// =============================================================================\n// useLazyLoad Hook\n// =============================================================================\n\n/**\n * IntersectionObserver-based lazy loading hook.\n *\n * Triggers when an element approaches the viewport. Once triggered,\n * the observer disconnects (one-shot behavior).\n *\n * @param options - Configuration options\n * @returns Object with ref callback and inView state\n *\n * @example\n * ```tsx\n * function LazyComponent() {\n * const { ref, inView } = useLazyLoad({ rootMargin: '200px' });\n *\n * return (\n * <div ref={ref}>\n * {inView && <ExpensiveContent />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useLazyLoad(options: LazyLoadOptions = {}): LazyLoadResult {\n const { rootMargin = '200px', threshold = 0, onEnter } = options;\n const [inView, setInView] = useState(false);\n const observerRef = useRef<IntersectionObserver | null>(null);\n\n const ref = useCallback(\n (node: HTMLElement | null) => {\n // Disconnect previous observer\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n // No node to observe\n if (!node) return;\n\n // Create new observer\n observerRef.current = new IntersectionObserver(\n ([entry]) => {\n if (entry?.isIntersecting) {\n setInView(true);\n onEnter?.();\n // Disconnect after first intersection (one-shot)\n observerRef.current?.disconnect();\n observerRef.current = null;\n }\n },\n { rootMargin, threshold }\n );\n\n observerRef.current.observe(node);\n },\n [rootMargin, threshold, onEnter]\n );\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n }, []);\n\n return { ref, inView };\n}\n\n// =============================================================================\n// Connection-Aware Quality\n// =============================================================================\n\n/**\n * Network Information API types (not in standard TypeScript lib).\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API\n */\ninterface NetworkInformation {\n effectiveType?: 'slow-2g' | '2g' | '3g' | '4g';\n downlink?: number;\n saveData?: boolean;\n}\n\ninterface NavigatorWithConnection extends Navigator {\n connection?: NetworkInformation;\n}\n\n/**\n * Quality presets based on connection type.\n */\nconst QUALITY_PRESETS = {\n 'slow-2g': 30,\n '2g': 50,\n '3g': 65,\n '4g': 80,\n saveData: 40,\n default: 80,\n} as const;\n\n/**\n * Returns appropriate image quality based on network conditions.\n *\n * Uses the Network Information API when available to detect:\n * - Data saver mode (returns lowest quality)\n * - Effective connection type (slow-2g, 2g, 3g, 4g)\n *\n * Falls back to default quality (80) when:\n * - Running on server (SSR)\n * - Network Information API not supported\n *\n * @returns Quality value between 30-80\n *\n * @example\n * ```typescript\n * const quality = getConnectionAwareQuality();\n * const imageUrl = `${baseUrl}?q=${quality}`;\n * ```\n */\nexport function getConnectionAwareQuality(): number {\n // SSR fallback\n if (typeof navigator === 'undefined') {\n return QUALITY_PRESETS.default;\n }\n\n const nav = navigator as NavigatorWithConnection;\n const connection = nav.connection;\n\n // No Network Information API support\n if (!connection) {\n return QUALITY_PRESETS.default;\n }\n\n // User has enabled data saver - respect their preference\n if (connection.saveData) {\n return QUALITY_PRESETS.saveData;\n }\n\n // Adjust based on effective connection type\n const effectiveType = connection.effectiveType;\n if (effectiveType && effectiveType in QUALITY_PRESETS) {\n return QUALITY_PRESETS[effectiveType];\n }\n\n return QUALITY_PRESETS.default;\n}\n\n/**\n * Check if the user prefers reduced data usage.\n *\n * @returns true if Save-Data is enabled or connection is slow\n */\nexport function prefersReducedData(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n\n const nav = navigator as NavigatorWithConnection;\n const connection = nav.connection;\n\n if (!connection) {\n return false;\n }\n\n return (\n connection.saveData === true ||\n connection.effectiveType === 'slow-2g' ||\n connection.effectiveType === '2g'\n );\n}\n"],"mappings":";;;AAaA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAgDlD,SAAS,YAAY,UAA2B,CAAC,GAAmB;AACzE,QAAM,EAAE,aAAa,SAAS,YAAY,GAAG,QAAQ,IAAI;AACzD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,cAAc,OAAoC,IAAI;AAE5D,QAAM,MAAM;AAAA,IACV,CAAC,SAA6B;AAE5B,UAAI,YAAY,SAAS;AACvB,oBAAY,QAAQ,WAAW;AAC/B,oBAAY,UAAU;AAAA,MACxB;AAGA,UAAI,CAAC,KAAM;AAGX,kBAAY,UAAU,IAAI;AAAA,QACxB,CAAC,CAAC,KAAK,MAAM;AACX,cAAI,OAAO,gBAAgB;AACzB,sBAAU,IAAI;AACd,sBAAU;AAEV,wBAAY,SAAS,WAAW;AAChC,wBAAY,UAAU;AAAA,UACxB;AAAA,QACF;AAAA,QACA,EAAE,YAAY,UAAU;AAAA,MAC1B;AAEA,kBAAY,QAAQ,QAAQ,IAAI;AAAA,IAClC;AAAA,IACA,CAAC,YAAY,WAAW,OAAO;AAAA,EACjC;AAGA,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,YAAY,SAAS;AACvB,oBAAY,QAAQ,WAAW;AAC/B,oBAAY,UAAU;AAAA,MACxB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,KAAK,OAAO;AACvB;AAuBA,IAAM,kBAAkB;AAAA,EACtB,WAAW;AAAA,EACX,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,UAAU;AAAA,EACV,SAAS;AACX;AAqBO,SAAS,4BAAoC;AAElD,MAAI,OAAO,cAAc,aAAa;AACpC,WAAO,gBAAgB;AAAA,EACzB;AAEA,QAAM,MAAM;AACZ,QAAM,aAAa,IAAI;AAGvB,MAAI,CAAC,YAAY;AACf,WAAO,gBAAgB;AAAA,EACzB;AAGA,MAAI,WAAW,UAAU;AACvB,WAAO,gBAAgB;AAAA,EACzB;AAGA,QAAM,gBAAgB,WAAW;AACjC,MAAI,iBAAiB,iBAAiB,iBAAiB;AACrD,WAAO,gBAAgB,aAAa;AAAA,EACtC;AAEA,SAAO,gBAAgB;AACzB;AAOO,SAAS,qBAA8B;AAC5C,MAAI,OAAO,cAAc,aAAa;AACpC,WAAO;AAAA,EACT;AAEA,QAAM,MAAM;AACZ,QAAM,aAAa,IAAI;AAEvB,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AAEA,SACE,WAAW,aAAa,QACxB,WAAW,kBAAkB,aAC7B,WAAW,kBAAkB;AAEjC;","names":[]}
1
+ {"version":3,"sources":["../../../lib/image/lazy-load.ts"],"sourcesContent":["/**\n * Lazy Loading Utilities\n *\n * Reusable IntersectionObserver-based lazy loading with\n * connection-aware quality adjustment.\n *\n * This module provides:\n * - useLazyLoad: Hook for deferred loading when elements enter viewport\n * - getConnectionAwareQuality: Adjusts image quality based on network speed\n */\n\n'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface LazyLoadOptions {\n /** Root margin for early loading (default: \"200px\") */\n rootMargin?: string;\n /** Threshold for intersection (default: 0) */\n threshold?: number;\n /** Callback when element enters viewport */\n onEnter?: () => void;\n}\n\nexport interface LazyLoadResult {\n /** Callback ref to attach to the element */\n ref: (node: HTMLElement | null) => void;\n /** Whether the element has entered the viewport */\n inView: boolean;\n}\n\n// =============================================================================\n// useLazyLoad Hook\n// =============================================================================\n\n/**\n * IntersectionObserver-based lazy loading hook.\n *\n * Triggers when an element approaches the viewport. Once triggered,\n * the observer disconnects (one-shot behavior).\n *\n * @param options - Configuration options\n * @returns Object with ref callback and inView state\n *\n * @example\n * ```tsx\n * function LazyComponent() {\n * const { ref, inView } = useLazyLoad({ rootMargin: '200px' });\n *\n * return (\n * <div ref={ref}>\n * {inView && <ExpensiveContent />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useLazyLoad(options: LazyLoadOptions = {}): LazyLoadResult {\n const { rootMargin = '200px', threshold = 0, onEnter } = options;\n const [inView, setInView] = useState(false);\n const observerRef = useRef<IntersectionObserver | null>(null);\n\n const ref = useCallback(\n (node: HTMLElement | null) => {\n // Disconnect previous observer\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n // No node to observe\n if (!node) return;\n\n // Create new observer\n observerRef.current = new IntersectionObserver(\n ([entry]) => {\n if (entry?.isIntersecting) {\n setInView(true);\n onEnter?.();\n // Disconnect after first intersection (one-shot)\n observerRef.current?.disconnect();\n observerRef.current = null;\n }\n },\n { rootMargin, threshold }\n );\n\n observerRef.current.observe(node);\n },\n [rootMargin, threshold, onEnter]\n );\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n }, []);\n\n return { ref, inView };\n}\n\n// =============================================================================\n// Connection-Aware Quality\n// =============================================================================\n\n/**\n * Network Information API types (not in standard TypeScript lib).\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API\n */\ninterface NetworkInformation {\n effectiveType?: 'slow-2g' | '2g' | '3g' | '4g';\n downlink?: number;\n saveData?: boolean;\n}\n\ninterface NavigatorWithConnection extends Navigator {\n connection?: NetworkInformation;\n}\n\n/**\n * Quality presets based on connection type.\n */\nconst QUALITY_PRESETS = {\n 'slow-2g': 30,\n '2g': 50,\n '3g': 65,\n '4g': 80,\n saveData: 40,\n default: 80,\n} as const;\n\n/**\n * Returns appropriate image quality based on network conditions.\n *\n * Uses the Network Information API when available to detect:\n * - Data saver mode (returns lowest quality)\n * - Effective connection type (slow-2g, 2g, 3g, 4g)\n *\n * Falls back to default quality (80) when:\n * - Running on server (SSR)\n * - Network Information API not supported\n *\n * @returns Quality value between 30-80\n *\n * @example\n * ```typescript\n * const quality = getConnectionAwareQuality();\n * const imageUrl = `${baseUrl}?q=${quality}`;\n * ```\n */\nexport function getConnectionAwareQuality(): number {\n // SSR fallback\n if (typeof navigator === 'undefined') {\n return QUALITY_PRESETS.default;\n }\n\n const nav = navigator as NavigatorWithConnection;\n const connection = nav.connection;\n\n // No Network Information API support\n if (!connection) {\n return QUALITY_PRESETS.default;\n }\n\n // User has enabled data saver - respect their preference\n if (connection.saveData) {\n return QUALITY_PRESETS.saveData;\n }\n\n // Adjust based on effective connection type\n const effectiveType = connection.effectiveType;\n if (effectiveType && effectiveType in QUALITY_PRESETS) {\n return QUALITY_PRESETS[effectiveType];\n }\n\n return QUALITY_PRESETS.default;\n}\n\n/**\n * Check if the user prefers reduced data usage.\n *\n * @returns true if Save-Data is enabled or connection is slow\n */\nexport function prefersReducedData(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n\n const nav = navigator as NavigatorWithConnection;\n const connection = nav.connection;\n\n if (!connection) {\n return false;\n }\n\n return (\n connection.saveData === true ||\n connection.effectiveType === 'slow-2g' ||\n connection.effectiveType === '2g'\n );\n}\n"],"mappings":";;;;AAaA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAgDlD,SAAS,YAAY,UAA2B,CAAC,GAAmB;AACzE,QAAM,EAAE,aAAa,SAAS,YAAY,GAAG,QAAQ,IAAI;AACzD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,cAAc,OAAoC,IAAI;AAE5D,QAAM,MAAM;AAAA,IACV,CAAC,SAA6B;AAE5B,UAAI,YAAY,SAAS;AACvB,oBAAY,QAAQ,WAAW;AAC/B,oBAAY,UAAU;AAAA,MACxB;AAGA,UAAI,CAAC,KAAM;AAGX,kBAAY,UAAU,IAAI;AAAA,QACxB,CAAC,CAAC,KAAK,MAAM;AACX,cAAI,OAAO,gBAAgB;AACzB,sBAAU,IAAI;AACd,sBAAU;AAEV,wBAAY,SAAS,WAAW;AAChC,wBAAY,UAAU;AAAA,UACxB;AAAA,QACF;AAAA,QACA,EAAE,YAAY,UAAU;AAAA,MAC1B;AAEA,kBAAY,QAAQ,QAAQ,IAAI;AAAA,IAClC;AAAA,IACA,CAAC,YAAY,WAAW,OAAO;AAAA,EACjC;AAGA,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,YAAY,SAAS;AACvB,oBAAY,QAAQ,WAAW;AAC/B,oBAAY,UAAU;AAAA,MACxB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,KAAK,OAAO;AACvB;AAuBA,IAAM,kBAAkB;AAAA,EACtB,WAAW;AAAA,EACX,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,UAAU;AAAA,EACV,SAAS;AACX;AAqBO,SAAS,4BAAoC;AAElD,MAAI,OAAO,cAAc,aAAa;AACpC,WAAO,gBAAgB;AAAA,EACzB;AAEA,QAAM,MAAM;AACZ,QAAM,aAAa,IAAI;AAGvB,MAAI,CAAC,YAAY;AACf,WAAO,gBAAgB;AAAA,EACzB;AAGA,MAAI,WAAW,UAAU;AACvB,WAAO,gBAAgB;AAAA,EACzB;AAGA,QAAM,gBAAgB,WAAW;AACjC,MAAI,iBAAiB,iBAAiB,iBAAiB;AACrD,WAAO,gBAAgB,aAAa;AAAA,EACtC;AAEA,SAAO,gBAAgB;AACzB;AAOO,SAAS,qBAA8B;AAC5C,MAAI,OAAO,cAAc,aAAa;AACpC,WAAO;AAAA,EACT;AAEA,QAAM,MAAM;AACZ,QAAM,aAAa,IAAI;AAEvB,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AAEA,SACE,WAAW,aAAa,QACxB,WAAW,kBAAkB,aAC7B,WAAW,kBAAkB;AAEjC;","names":[]}
@@ -1,10 +1,15 @@
1
- import {
2
- err,
3
- ok
4
- } from "../chunk-HVKFEZBT.js";
5
-
6
1
  // lib/markdown-utils.ts
7
2
  import { mdToJSON } from "md4w";
3
+
4
+ // lib/result.ts
5
+ function ok(value) {
6
+ return { ok: true, value };
7
+ }
8
+ function err(error) {
9
+ return { ok: false, error };
10
+ }
11
+
12
+ // lib/markdown-utils.ts
8
13
  var ParseErrorCode = {
9
14
  INVALID_INPUT: "INVALID_INPUT",
10
15
  EMPTY_CONTENT: "EMPTY_CONTENT",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/markdown-utils.ts"],"sourcesContent":["/**\n * Markdown Parsing Utilities\n *\n * Three implementations showing the progression from naive to production-ready:\n * - v1 (Naive): Direct call, crashes on bad input\n * - v2 (Defensive): Try-catch with null fallback\n * - v3 (Robust): Result type, validation, sanitization\n *\n * The robust version (v3) is exported as the default `parseMarkdown`.\n */\n\nimport type { MDTree } from 'md4w';\nimport { mdToJSON } from 'md4w';\n\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 sanitize XSS attempts in the output.\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 /data:/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 basic sanitizer; production should use DOMPurify.\n */\nfunction sanitizeContent(content: string): string {\n let sanitized = content;\n\n // Remove script tags\n sanitized = sanitized.replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '');\n\n // Remove javascript: and vbscript: URLs\n sanitized = sanitized.replace(/javascript:/gi, 'removed:');\n sanitized = sanitized.replace(/vbscript:/gi, 'removed:');\n\n // Remove event handlers (onclick, onerror, etc.)\n sanitized = sanitized.replace(/\\bon\\w+\\s*=\\s*[\"'][^\"']*[\"']/gi, '');\n sanitized = sanitized.replace(/\\bon\\w+\\s*=\\s*\\S+/gi, '');\n\n return sanitized;\n}\n\n/**\n * Robust markdown parser - production-ready with full error context.\n *\n * Features:\n * - Input validation with specific error codes\n * - Result type for explicit error handling\n * - XSS pattern detection and optional 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 * Production-ready markdown parser.\n *\n * This is an alias for `parseMarkdownV3` - the robust implementation\n * with validation, sanitization, and Result-based error handling.\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 */\nexport function estimateWordCount(content: string): number {\n // Remove markdown syntax for more accurate count\n const plainText = content\n .replace(/#+\\s/g, '') // Remove headings\n .replace(/\\*\\*|__|~~|`/g, '') // Remove formatting\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1') // Convert links to text\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, '') // Remove images\n .replace(/```[\\s\\S]*?```/g, '') // Remove code blocks\n .replace(/`[^`]+`/g, ''); // Remove inline code\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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../lib/markdown-utils.ts","../../lib/result.ts"],"sourcesContent":["/**\n * Markdown Parsing Utilities\n *\n * Three implementations showing the progression from naive to production-ready:\n * - v1 (Naive): Direct call, crashes on bad input\n * - v2 (Defensive): Try-catch with null fallback\n * - v3 (Robust): Result type, validation, sanitization\n *\n * The robust version (v3) is exported as the default `parseMarkdown`.\n */\n\nimport type { MDTree } from 'md4w';\nimport { mdToJSON } from 'md4w';\n\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 sanitize XSS attempts in the output.\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 /data:/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 basic sanitizer; production should use DOMPurify.\n */\nfunction sanitizeContent(content: string): string {\n let sanitized = content;\n\n // Remove script tags\n sanitized = sanitized.replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '');\n\n // Remove javascript: and vbscript: URLs\n sanitized = sanitized.replace(/javascript:/gi, 'removed:');\n sanitized = sanitized.replace(/vbscript:/gi, 'removed:');\n\n // Remove event handlers (onclick, onerror, etc.)\n sanitized = sanitized.replace(/\\bon\\w+\\s*=\\s*[\"'][^\"']*[\"']/gi, '');\n sanitized = sanitized.replace(/\\bon\\w+\\s*=\\s*\\S+/gi, '');\n\n return sanitized;\n}\n\n/**\n * Robust markdown parser - production-ready with full error context.\n *\n * Features:\n * - Input validation with specific error codes\n * - Result type for explicit error handling\n * - XSS pattern detection and optional 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 * Production-ready markdown parser.\n *\n * This is an alias for `parseMarkdownV3` - the robust implementation\n * with validation, sanitization, and Result-based error handling.\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 */\nexport function estimateWordCount(content: string): number {\n // Remove markdown syntax for more accurate count\n const plainText = content\n .replace(/#+\\s/g, '') // Remove headings\n .replace(/\\*\\*|__|~~|`/g, '') // Remove formatting\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1') // Convert links to text\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, '') // Remove images\n .replace(/```[\\s\\S]*?```/g, '') // Remove code blocks\n .replace(/`[^`]+`/g, ''); // Remove inline code\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 * 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,11 +1,3 @@
1
- import {
2
- BlockRenderer
3
- } from "../chunk-SJZTIW2I.js";
4
- import "../chunk-TIR3RJMY.js";
5
- import {
6
- getCmsClient
7
- } from "../chunk-JHKDRASN.js";
8
-
9
1
  // ../../packages/cms-schema/src/blocks/article.ts
10
2
  function normalizeArticleContent(payload) {
11
3
  if (!payload || typeof payload !== "object") {
@@ -249,7 +241,326 @@ function isValidBlockSchemaName(name) {
249
241
  // lib/renderer.tsx
250
242
  import { unstable_noStore } from "next/cache";
251
243
  import { notFound } from "next/navigation";
252
- import { jsx } from "react/jsx-runtime";
244
+
245
+ // lib/block-renderer.tsx
246
+ import React from "react";
247
+ import { BlockToolbar } from "./block-toolbar.js";
248
+ import { jsx, jsxs } from "react/jsx-runtime";
249
+ function walkReactNode(node, visitors, ctx = {}) {
250
+ const path = ctx.path ?? [];
251
+ if (node == null || typeof node === "boolean") return node;
252
+ if (typeof node === "string" || typeof node === "number") {
253
+ const value = String(node);
254
+ return visitors.onText ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key }) : node;
255
+ }
256
+ if (Array.isArray(node)) {
257
+ return node.map((child, i) => {
258
+ const childKey = child?.key ?? null;
259
+ const result = walkReactNode(child, visitors, {
260
+ path: [...path, i],
261
+ parentType: ctx.parentType,
262
+ key: childKey
263
+ });
264
+ if (React.isValidElement(result) && result.key == null) {
265
+ return React.cloneElement(result, { key: childKey ?? `arr-${path.join("-")}-${i}` });
266
+ }
267
+ return result;
268
+ });
269
+ }
270
+ if (React.isValidElement(node)) {
271
+ const el = node;
272
+ const elProps = el.props;
273
+ const hasChildren = elProps && "children" in elProps;
274
+ const nextChildren = hasChildren ? React.Children.map(elProps.children, (child, i) => {
275
+ const childKey = child?.key ?? null;
276
+ const result = walkReactNode(child, visitors, {
277
+ path: [...path, "children", i],
278
+ parentType: el.type,
279
+ key: childKey
280
+ });
281
+ if (React.isValidElement(result) && result.key == null) {
282
+ return React.cloneElement(result, { key: childKey ?? `child-${path.join("-")}-${i}` });
283
+ }
284
+ return result;
285
+ }) : elProps?.children;
286
+ const cloned = hasChildren ? React.cloneElement(el, void 0, nextChildren) : el;
287
+ return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;
288
+ }
289
+ return node;
290
+ }
291
+ function extractContentValues(content, basePath = []) {
292
+ const map = /* @__PURE__ */ new Map();
293
+ function walk(obj, path) {
294
+ if (typeof obj === "string" && obj.trim() !== "") {
295
+ const contentPath = path.join(".");
296
+ const existing = map.get(obj) || [];
297
+ existing.push({ contentPath, value: obj });
298
+ map.set(obj, existing);
299
+ } else if (Array.isArray(obj)) {
300
+ for (let index = 0; index < obj.length; index++) {
301
+ walk(obj[index], [...path, String(index)]);
302
+ }
303
+ } else if (obj && typeof obj === "object") {
304
+ for (const [key, value] of Object.entries(obj)) {
305
+ walk(value, [...path, key]);
306
+ }
307
+ }
308
+ }
309
+ walk(content, basePath);
310
+ return map;
311
+ }
312
+ function renderToWalkableTree(node, keyPrefix = "") {
313
+ if (node == null || typeof node === "boolean") return node;
314
+ if (typeof node === "string" || typeof node === "number") return node;
315
+ if (Array.isArray(node)) {
316
+ return node.map((child, i) => {
317
+ const result = renderToWalkableTree(child, `${keyPrefix}${i}-`);
318
+ if (React.isValidElement(result) && result.key == null) {
319
+ const existingKey = child?.key;
320
+ return React.cloneElement(result, { key: existingKey ?? `${keyPrefix}${i}` });
321
+ }
322
+ return result;
323
+ });
324
+ }
325
+ if (React.isValidElement(node)) {
326
+ const el = node;
327
+ const elProps = el.props;
328
+ if (typeof el.type === "function") {
329
+ try {
330
+ const rendered = el.type(el.props);
331
+ return renderToWalkableTree(rendered, keyPrefix);
332
+ } catch {
333
+ return node;
334
+ }
335
+ }
336
+ if (elProps && "children" in elProps) {
337
+ const newChildren = renderToWalkableTree(elProps.children, keyPrefix);
338
+ return React.cloneElement(el, void 0, newChildren);
339
+ }
340
+ return node;
341
+ }
342
+ return node;
343
+ }
344
+ function BlockRenderer({ block, registry, disableEditable }) {
345
+ const Component = registry[block.type];
346
+ if (!Component) {
347
+ if (process.env.NODE_ENV === "development") {
348
+ console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);
349
+ }
350
+ return null;
351
+ }
352
+ const component = /* @__PURE__ */ jsx(Component, { content: block.content });
353
+ if (disableEditable) {
354
+ return component;
355
+ }
356
+ const renderedTree = renderToWalkableTree(component);
357
+ const contentValueMap = extractContentValues(block.content);
358
+ const usedPaths = /* @__PURE__ */ new Set();
359
+ let isRoot = true;
360
+ const wrappedComponent = walkReactNode(renderedTree, {
361
+ onText: ({ value, key, path }) => {
362
+ const matches = contentValueMap.get(value);
363
+ if (!matches || matches.length === 0) {
364
+ return value;
365
+ }
366
+ const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];
367
+ if (!match) return value;
368
+ usedPaths.add(match.contentPath);
369
+ const spanKey = key ?? `${block.id}-${match.contentPath}-${path.join("-")}`;
370
+ return /* @__PURE__ */ jsx(
371
+ "span",
372
+ {
373
+ "data-cms-editable": true,
374
+ "data-block-id": block.id,
375
+ "data-block-type": block.type,
376
+ "data-content-path": match.contentPath,
377
+ children: value
378
+ },
379
+ spanKey
380
+ );
381
+ },
382
+ onElement: ({ element, path }) => {
383
+ if (isRoot && path.length === 0) {
384
+ isRoot = false;
385
+ const elProps = element.props;
386
+ const existingChildren = elProps?.children;
387
+ return React.cloneElement(
388
+ element,
389
+ {
390
+ "data-cms-block": true,
391
+ "data-block-id": block.id,
392
+ "data-block-type": block.type
393
+ },
394
+ existingChildren,
395
+ /* @__PURE__ */ jsx(BlockToolbar, { blockId: block.id }, "cms-toolbar")
396
+ );
397
+ }
398
+ return element;
399
+ }
400
+ });
401
+ return /* @__PURE__ */ jsxs("div", { style: { display: "contents" }, children: [
402
+ /* @__PURE__ */ jsx("style", { children: `
403
+ [data-cms-block] {
404
+ position: relative;
405
+ }
406
+ [data-cms-block]:hover {
407
+ outline: 2px solid #3b82f6;
408
+ outline-offset: 4px;
409
+ }
410
+ [data-cms-editable] {
411
+ cursor: pointer;
412
+ border-radius: 2px;
413
+ }
414
+ [data-cms-editable]:hover {
415
+ outline: 2px solid #3b82f6;
416
+ outline-offset: 2px;
417
+ }
418
+ .cms-block-toolbar {
419
+ position: absolute;
420
+ bottom: -16px;
421
+ left: 50%;
422
+ transform: translateX(-50%);
423
+ display: flex;
424
+ gap: 4px;
425
+ background: #1f2937;
426
+ border-radius: 6px;
427
+ padding: 4px;
428
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
429
+ opacity: 0;
430
+ pointer-events: none;
431
+ transition: opacity 0.15s ease;
432
+ z-index: 1000;
433
+ }
434
+ [data-cms-block]:hover .cms-block-toolbar {
435
+ opacity: 1;
436
+ pointer-events: auto;
437
+ }
438
+ .cms-block-toolbar button {
439
+ display: flex;
440
+ align-items: center;
441
+ justify-content: center;
442
+ width: 28px;
443
+ height: 28px;
444
+ border: none;
445
+ background: transparent;
446
+ color: #9ca3af;
447
+ border-radius: 4px;
448
+ cursor: pointer;
449
+ transition: background 0.15s ease, color 0.15s ease;
450
+ }
451
+ .cms-block-toolbar button:hover {
452
+ background: #374151;
453
+ color: #fff;
454
+ }
455
+ .cms-block-toolbar button.delete:hover {
456
+ background: #dc2626;
457
+ color: #fff;
458
+ }
459
+ .cms-block-toolbar button:disabled {
460
+ opacity: 0.4;
461
+ cursor: not-allowed;
462
+ }
463
+ .cms-block-toolbar button:disabled:hover {
464
+ background: transparent;
465
+ color: #9ca3af;
466
+ }
467
+ .cms-block-toolbar svg {
468
+ width: 16px;
469
+ height: 16px;
470
+ }
471
+ ` }),
472
+ /* @__PURE__ */ jsx(
473
+ "script",
474
+ {
475
+ dangerouslySetInnerHTML: {
476
+ __html: `
477
+ (function() {
478
+ if (window.__cmsEditableInitialized) return;
479
+ window.__cmsEditableInitialized = true;
480
+
481
+ document.addEventListener('click', function(e) {
482
+ // Ignore toolbar clicks
483
+ if (e.target.closest('.cms-block-toolbar')) {
484
+ return;
485
+ }
486
+
487
+ // Check for editable text click first (more specific)
488
+ var editableTarget = e.target.closest('[data-cms-editable]');
489
+ if (editableTarget) {
490
+ var message = {
491
+ type: 'cms-editable-click',
492
+ blockId: editableTarget.getAttribute('data-block-id'),
493
+ blockType: editableTarget.getAttribute('data-block-type'),
494
+ contentPath: editableTarget.getAttribute('data-content-path')
495
+ };
496
+
497
+ if (window.parent && window.parent !== window) {
498
+ window.parent.postMessage(message, '*');
499
+ }
500
+ return;
501
+ }
502
+
503
+ // Check for block-level click
504
+ var blockTarget = e.target.closest('[data-cms-block]');
505
+ if (blockTarget) {
506
+ var message = {
507
+ type: 'cms-editable-click',
508
+ blockId: blockTarget.getAttribute('data-block-id'),
509
+ blockType: blockTarget.getAttribute('data-block-type'),
510
+ contentPath: null
511
+ };
512
+
513
+ if (window.parent && window.parent !== window) {
514
+ window.parent.postMessage(message, '*');
515
+ }
516
+ }
517
+ });
518
+ })();
519
+ `
520
+ }
521
+ }
522
+ ),
523
+ wrappedComponent
524
+ ] }, block.id);
525
+ }
526
+
527
+ // lib/cms-api.ts
528
+ import { createTRPCClient, httpBatchLink } from "@trpc/client";
529
+ import superjson from "superjson";
530
+ function getCmsApiUrl(cmsUrl) {
531
+ return `${cmsUrl}/api/trpc`;
532
+ }
533
+ function createFetchWithApiKey(apiKey) {
534
+ return async (url, options) => {
535
+ let finalUrl = url;
536
+ if (apiKey) {
537
+ const urlObj = new URL(url.toString());
538
+ urlObj.searchParams.set("api_key", apiKey);
539
+ finalUrl = urlObj.toString();
540
+ }
541
+ const response = await fetch(finalUrl, options);
542
+ return response;
543
+ };
544
+ }
545
+ function createCmsClient(options) {
546
+ const url = getCmsApiUrl(options.cmsUrl);
547
+ console.log("[CMS API] Creating client with URL:", url);
548
+ return createTRPCClient({
549
+ links: [
550
+ httpBatchLink({
551
+ url,
552
+ transformer: superjson,
553
+ fetch: createFetchWithApiKey(options.apiKey)
554
+ })
555
+ ]
556
+ });
557
+ }
558
+ function getCmsClient(options) {
559
+ return createCmsClient(options);
560
+ }
561
+
562
+ // lib/renderer.tsx
563
+ import { jsx as jsx2 } from "react/jsx-runtime";
253
564
  function getWebsiteId(providedWebsiteId) {
254
565
  const websiteId = providedWebsiteId ?? process.env.NEXT_PUBLIC_WEBSITE_ID ?? process.env.WEBSITE_ID ?? process.env.CMS_WEBSITE_ID;
255
566
  if (!websiteId) {
@@ -348,7 +659,7 @@ async function ParametricRoutePage({
348
659
  content
349
660
  });
350
661
  }
351
- return /* @__PURE__ */ jsx("main", { children: blocks.map((block) => /* @__PURE__ */ jsx(
662
+ return /* @__PURE__ */ jsx2("main", { children: blocks.map((block) => /* @__PURE__ */ jsx2(
352
663
  BlockRenderer,
353
664
  {
354
665
  block,