import React from 'react'; import type { ComponentProps, ComponentType } from 'react'; import type { Components } from 'react-markdown'; import type { LinkRule } from './types'; // react-markdown's `Components['a']` is `keyof IntrinsicElements | ComponentType<…>`, // so it isn't necessarily a function we can directly call. This is the // component-shaped variant — what callers practically pass when they // override `a`. type AComponent = ComponentType>; /** Run every rule's `preprocess` hook in order. Errors in a single * rule are logged and skipped — the other rules still run. */ export function applyPreprocess( source: string, rules: readonly LinkRule[] | undefined, ): string { if (!rules || rules.length === 0) return source; let s = source; for (const rule of rules) { if (!rule.preprocess) continue; try { s = rule.preprocess(s); } catch (err) { // eslint-disable-next-line no-console console.warn( `[MarkdownMessage] linkRule "${rule.name ?? '(anonymous)'}" preprocess threw; skipping`, err, ); } } return s; } /** Union of `extraHrefProtocols` and any `protocols` declared by rules. */ export function collectProtocols( extraHrefProtocols: readonly string[] | undefined, rules: readonly LinkRule[] | undefined, ): readonly string[] | undefined { const set = new Set(); if (extraHrefProtocols) for (const p of extraHrefProtocols) set.add(p); if (rules) { for (const r of rules) { if (r.protocols) for (const p of r.protocols) set.add(p); } } return set.size === 0 ? undefined : Array.from(set); } /** Build a custom `a` renderer that dispatches to the first matching * rule, falling through to `callerA` (or the built-in chat anchor) * for anything no rule claims. */ export function buildLinkRulesComponent( rules: readonly LinkRule[], isUser: boolean, callerA: NonNullable | undefined, ): NonNullable { const Renderer: AComponent = (props) => { const { href, children } = props; if (typeof href === 'string') { for (const rule of rules) { if (rule.match(href)) { return React.createElement( React.Fragment, null, rule.render({ href, children, isUser }), ); } } } // Defer to the caller's `a` override if any. We only call it when // it's a function/component — string-form intrinsic overrides // (`'a' | 'span' | …`) aren't a thing react-markdown supports for // tag overrides, but the type still admits the union, so guard. if (callerA && typeof callerA === 'function') { const Caller = callerA as AComponent; return React.createElement(Caller, props); } // Fall through to the built-in anchor by rendering a plain ``. return React.createElement('a', props, children); }; return Renderer as NonNullable; }