import { useMemo, type ReactElement, type ReactNode } from 'react' import { citationLink, numberKey, type Citation } from './citations' import { parseMarkdown, type Block, type Inline } from './markdown' // An analyst's Markdown as React elements. Nothing here becomes HTML from a string: the parser reads // the text into a tree and each node is an element, so an answer cannot inject markup. Links go out // through the bridge, since the frame cannot open them. A citation tag renders as its sentence and a // numbered marker that opens the source on sec.gov, on the quoted words, once the room has checked it. /** The message's citations, as the markers need them: a number per passage, and what is known about each. */ export interface CiteLookup { numbers: Map resolved: Map } interface Render { onLink: (href: string) => void cite: CiteLookup | null } export function Markdown({ source, onLink, cite = null }: { source: string; onLink: (href: string) => void; cite?: CiteLookup | null }): ReactElement { const blocks = useMemo(() => parseMarkdown(source), [source]) const render: Render = { onLink, cite } return
{blocks.map((block, i) => renderBlock(block, i, render))}
} function renderBlock(block: Block, key: number, render: Render): ReactNode { const onLink = render.onLink switch (block.kind) { case 'heading': return (
{inline(block.children, render)}
) case 'paragraph': return

{inline(block.children, render)}

case 'list': { const items = block.items.map((item, i) =>
  • {inline(item, render)}
  • ) return block.ordered ?
      {items}
    : } case 'code': return (
              {block.text}
            
    ) case 'table': return (
    {block.header.map((cell, i) => ( ))} {block.rows.map((row, r) => ( {row.map((cell, i) => ( ))} ))}
    {inline(cell, render)}
    {inline(cell, render)}
    ) case 'quote': return
    {inline(block.children, render)}
    case 'rule': return
    } return null } function inline(nodes: Inline[], render: Render): ReactNode[] { const { onLink, cite } = render return nodes.map((node, i) => { switch (node.kind) { case 'text': return node.text case 'code': return {node.text} case 'strong': return {inline(node.children, render)} case 'em': return {inline(node.children, render)} case 'link': return ( { event.preventDefault() onLink(node.href) }} > {inline(node.children, render)} ) case 'cite': { const slot = numberKey(node.attrs) const n = slot === null ? undefined : cite?.numbers.get(slot) const citation = n === undefined ? undefined : cite?.resolved.get(n) return ( {inline(node.children, render)} {n !== undefined && } ) } } return null }) } /** The [n] after a cited sentence: a link to the source once the room has one, a plain mark before. */ export function CiteMark({ n, citation, onLink }: { n: number; citation: Citation | undefined; onLink: (href: string) => void }): ReactElement { const state = citation?.verified === true ? 'rs-cite-ok' : citation?.verified === false ? 'rs-cite-bad' : 'rs-cite-pending' const url = citation ? citationLink(citation) : null if (!url) return [{n}] return ( { event.preventDefault() onLink(url) }} > [{n}] ) }