import type { ReactNode } from "react"; import type { ImageFit, ImageRenderProtocol, ImageSource } from "@opentui/core"; import type { CommandContext } from "@tooee/commands"; import { formatTableCell } from "@tooee/renderers"; import type { ColumnDef, SourceLineRow, TableRow } from "@tooee/renderers"; import type { DocumentController } from "@tooee/shell"; import type { MarkSet } from "@tooee/marks"; // === Built-in content types === export type Content = | MarkdownContent | CodeContent | TextContent | ImageContent | TableContent | DiffContent; export type ContentFormat = Content["format"]; /** Handles an inline Markdown link with the View's live command context. */ // oxlint-disable-next-line anti-slop/no-unknown-returns -- public callback contract preserves the renderer's exact-true link-consumption result and permits legacy return values export type MarkdownLinkActivateHandler = (href: string, context: CommandContext) => unknown; interface BaseContent { title?: string; getTextContent?: () => string; } export interface MarkdownContent extends BaseContent { format: "markdown"; markdown: string; /** Directory used to resolve relative standard Markdown and Obsidian image embeds. */ imageBasePath?: string; } export interface CodeContent extends BaseContent { format: "code"; code: string; language?: string; } export interface TextContent extends BaseContent { format: "text"; text: string; } export interface ImageContent extends BaseContent { format: "image"; src: ImageSource; fit?: ImageFit; protocol?: ImageRenderProtocol; } export interface TableContent extends BaseContent { format: "table"; columns: ColumnDef[]; rows: TableRow[]; } export interface DiffContent extends BaseContent { format: "diff"; /** Unified patch text, single- or multi-file. */ patch: string; /** Initial layout; defaults to stacked (unified). */ layout?: "split" | "stack"; } // === Custom content === export interface CustomContent { format: string; data: T; title?: string; getTextContent?: () => string; } /** Content or custom content — used where custom formats are accepted */ export type AnyContent = Content | CustomContent; // === Custom renderers === export interface ContentRendererProps { content: CustomContent; /** * The host's document controller. Its rows are the content's source lines * (`SourceLineRow`), which is also the unit navigation, search and copy work * in; `row.text` is the plain-text line and `row.source` its provenance. * * A renderer that owns a `` binds the controller directly — it * satisfies `DocumentBindings`, so `ref`, `decorations` and `onMouseDown` * spread onto the element the way the built-in views wire them. A renderer * with its own markup reads `activeIndex` and `navigation.selection` for * cursor state and calls `selectRow(index)` from its mouse handlers; * `selectRow` stands down while a modal overlay is open, so it can be wired * unconditionally. */ document: DocumentController; } export type ContentRenderer = (props: ContentRendererProps) => ReactNode; // === Streaming === export type AppendableFormat = Extract; export type ContentChunk = | { type: "append"; format: AppendableFormat; data: string; language?: string; } | { type: "replace"; content: AnyContent; } | { type: "patch"; apply: (current: AnyContent | null) => AnyContent; } | { type: "marks"; set: MarkSet; }; // === Provider === export interface ContentProvider { load: () => AnyContent | Promise | AsyncIterable; format?: string; title?: string; /** Static marks to apply to the content. Merged with nav marks in View. */ marks?: MarkSet[]; } // === Compat aliases === export type ViewContent = Content; export type ViewContentProvider = ContentProvider; export type { ColumnDef, TableRow } from "@tooee/renderers"; // === Utilities === const BUILTIN_FORMATS = new Set(["markdown", "code", "text", "image", "table", "diff"]); export const isBuiltinContent = function isBuiltinContent(content: AnyContent): content is Content { return BUILTIN_FORMATS.has(content.format); }; export const isCustomContent = function isCustomContent( content: AnyContent, ): content is CustomContent { return !BUILTIN_FORMATS.has(content.format); }; export const getTextContent = function getTextContent(content: AnyContent): string { if (content.getTextContent) { return content.getTextContent(); } if (isCustomContent(content)) { return ""; } switch (content.format) { case "markdown": { return content.markdown; } case "code": { return content.code; } case "diff": { return content.patch; } case "text": { return content.text; } case "image": { // oxlint-disable-next-line anti-slop/no-runtime-typeof -- ImageSource is OpenTUI's public string-or-binary union; only its string and URL variants have text content if (typeof content.src === "string") { return content.src; } return content.src instanceof URL ? content.src.href : ""; } case "table": { const headers = content.columns.map((column) => column.header ?? column.key); const rowLines = content.rows.map((row) => content.columns.map((column) => formatTableCell(row[column.key])).join("\t"), ); return [headers.join("\t"), ...rowLines].join("\n"); } default: { return ""; } } };