"use client"; /** * Rich-composition component overrides: the embed layer (Video SSOT, * Reddit/Twitter/LinkedIn embeds, OG link previews, Figma, MarkdownImage). * * These imports are deliberately isolated in this file — keeping them out * of the engine and the simple composition keeps the CHAT bundle free of * embed code (a runtime `variant` prop could not be tree-shaken; a * separate composition module can). */ import React from 'react'; import type { Components } from 'react-markdown'; import { RedditEmbedClient } from '../../../embeds/reddit-embed-client'; import { TwitterEmbedClient } from '../../../embeds/twitter-embed-client'; import { LinkedInEmbedClient } from '../../../embeds/linkedin-embed-client'; import { Video } from '../../../features/video'; import { OGLinkPreview, OGLinkErrorBoundary } from '../../../embeds/og-link-preview'; import { FigmaEmbed } from '../../../embeds/figma-embed'; import { ClaudeEmbed } from '../../../embeds/claude-embed'; import { MarkdownImage } from '../../../embeds/markdown-image'; import { buildStandardLeafRenderers, hasRenderableSrc } from '../base-components'; import type { TextSizeElement } from '../text-size'; /** Depth-first search of a hast node for the first `` matching `hostRe`. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function findFirstHref(node: any, hostRe: RegExp): string | null { if (!node) return null; if (node.tagName === 'a') { const href = node.properties?.href; if (typeof href === 'string' && hostRe.test(href)) return href; } for (const child of node.children ?? []) { const found = findFirstHref(child, hostRe); if (found) return found; } return null; } export interface BuildRichEmbedOverridesOptions { ogApiBaseUrl: string; ogEndpointPath: string; textSizes: Record; } /** * Overrides spread LAST onto the engine's base map: `code` (fence-language * embeds), `div` (shortcode-expanded embeds), `img` (MarkdownImage with * runtime transformImageSrc), `video` (raw tags → Video SSOT). */ export function buildRichEmbedOverrides({ ogApiBaseUrl, ogEndpointPath, textSizes, }: BuildRichEmbedOverridesOptions): Partial { // Standard `code` / `blockquote` / `div` come from the base SSOT — this // module owns ONLY the embed special cases and delegates every // fall-through, so the shared markup can never drift between the two // compositions. // // The delegations below CALL those renderers as plain functions rather // than rendering them as components (that is what the `as any` casts are // for). That is only sound because `buildStandardLeafRenderers` is // documented HOOK-FREE — if a hook ever appears in one of them, these // call sites must become real elements (``) // first. const standard = buildStandardLeafRenderers({ textSizes }); return { // Fence-language embeds (```youtube-embed etc.); everything else // (mermaid, highlighted blocks, inline code) falls through to `standard`. // eslint-disable-next-line @typescript-eslint/no-explicit-any code: (codeProps: any) => { const { inline, className: codeClassName, children } = codeProps; const match = /language-(\w+)/.exec(codeClassName || ''); const language = match ? match[1] : ''; const fenceText = () => String(children).replace(/\n$/, '').trim(); if (!inline && language === 'youtube-embed') { return
` (58 blocks // across 9 posts) — and Twitter's equivalent is // `
`. Each pair // ships a `widgets.js` loader script, which `processShortcodes` strips // from the source text (Step 1.5). The composition rehydrates the // blockquote itself: extract the post URL from the first matching link // and render the platform's embed-client SSOT. Other blockquotes fall // through to the engine's base blockquote. // eslint-disable-next-line @typescript-eslint/no-explicit-any blockquote: (bqProps: any) => { const { node, className } = bqProps; const classNames: string = Array.isArray(className) ? className.join(' ') : (className ?? ''); if (classNames.includes('reddit-embed-bq')) { const postUrl = findFirstHref(node, /reddit\.com/); if (postUrl) return ; } if (classNames.includes('twitter-tweet')) { const tweetUrl = findFirstHref(node, /(?:twitter\.com|x\.com)\/[^/]+\/status\//); if (tweetUrl) return ; } // eslint-disable-next-line @typescript-eslint/no-explicit-any return (standard.blockquote as any)(bqProps); }, // In-article images: MarkdownImage reads `transformImageSrc` from the // rich-markdown runtime (Supabase optimization on the hub, identity // elsewhere). Guard against empty `![]()`. // eslint-disable-next-line @typescript-eslint/no-explicit-any img: ({ src, alt }: any) => { // Empty-`![]()` guard shared with the base `img` renderer. if (!hasRenderableSrc(src)) return null; return ; }, // Raw