// Default renderers for react-native-hyper-markdown
import React from 'react'
import { Text, View, Image as RNImage, Pressable } from 'react-native'
import type { MarkdownNode } from '../types/ast'
import type { RenderContext, RendererFn } from '../types/renderers'
// Text node renderer
export function renderText(
node: MarkdownNode,
_key: string | number,
_ctx: RenderContext
): React.ReactNode {
return node.content ?? ''
}
// Strong (bold) renderer
export function renderStrong(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Emphasis (italic) renderer
export function renderEmphasis(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Strikethrough renderer
export function renderStrikethrough(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Heading renderer
export function renderHeading(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
const level = node.level ?? 1
const styleKey = `heading${level}` as keyof typeof ctx.theme.textStyles
const style = ctx.theme.textStyles[styleKey] ?? ctx.theme.textStyles.heading1
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Paragraph renderer
export function renderParagraph(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
// Check if paragraph contains block-level elements (like images)
// that can't be nested inside Text
const hasBlockChildren = node.children?.some(
(child) => child.type === 'image'
)
if (hasBlockChildren) {
// For paragraphs with images, render as View with Text wrapper for inline content
return (
{node.children?.map((child, i) => {
if (child.type === 'image') {
return ctx.renderNode(child, `${key}-${i}`)
}
// Wrap non-image children in Text
return (
{ctx.renderNode(child, `${key}-${i}-inner`)}
)
})}
)
}
// Standard text-only paragraph
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Link renderer
export function renderLink(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
const handlePress = () => {
if (ctx.onLinkPress && node.href) {
ctx.onLinkPress(node.href, node.title)
}
}
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Image renderer
export function renderImage(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
const handlePress = () => {
if (ctx.onImagePress && node.src) {
ctx.onImagePress(node.src, node.alt, node.title)
}
}
if (!node.src) {
return null
}
return (
{node.alt && (
{node.alt}
)}
)
}
// Code inline renderer
export function renderCodeInline(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return (
{node.content ??
node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Code block renderer with optional syntax highlighting
export function renderCodeBlock(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
const code =
node.content ??
node.children?.map((child) => child.content ?? '').join('') ??
''
const language = node.language ?? 'text'
// Try to use syntax highlighting if enabled
const syntaxOptions = ctx.theme.syntaxHighlighting
const isEnabled = syntaxOptions?.enabled !== false
if (isEnabled) {
try {
// Dynamic import to handle optional dependency
const {
SyntaxHighlighter,
isSyntaxHighlightingAvailable,
} = require('../components/SyntaxHighlighter')
if (isSyntaxHighlightingAvailable()) {
return (
{code}
)
}
} catch {
// Fallback to plain text if SyntaxHighlighter is not available
}
}
// Fallback: plain text rendering
return (
{code}
)
}
// Blockquote renderer
export function renderBlockquote(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// List renderer
export function renderList(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// List item renderer
export function renderListItem(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return (
•
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Task list item renderer
export function renderTaskListItem(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
const handleToggle = () => {
if (ctx.onCheckboxToggle) {
ctx.onCheckboxToggle(!node.checked, node)
}
}
return (
{node.checked && ✓}
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Table renderer
export function renderTable(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Table row renderer
export function renderTableRow(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Table cell renderer
export function renderTableCell(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
const textStyle = node.isHeader
? ctx.theme.textStyles.tableHeader
: ctx.theme.textStyles.tableCell
const alignStyle = node.align
? { textAlign: node.align as 'left' | 'center' | 'right' }
: {}
return (
{node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
// Thematic break (hr) renderer
export function renderThematicBreak(
_node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
return
}
// Soft break renderer
export function renderSoftbreak(
_node: MarkdownNode,
key: string | number,
_ctx: RenderContext
): React.ReactNode {
return
}
// Hard break renderer
export function renderHardbreak(
_node: MarkdownNode,
key: string | number,
_ctx: RenderContext
): React.ReactNode {
return {'\n'}
}
// Default renderer for unknown types
export function renderDefault(
node: MarkdownNode,
key: string | number,
ctx: RenderContext
): React.ReactNode {
if (node.children && node.children.length > 0) {
return (
{node.children.map((child, i) => ctx.renderNode(child, `${key}-${i}`))}
)
}
return node.content ? {node.content} : null
}
/**
* Map of all default renderers by node type
*/
export const defaultRenderers: Record = {
text: renderText,
strong: renderStrong,
emphasis: renderEmphasis,
strikethrough: renderStrikethrough,
heading: renderHeading,
paragraph: renderParagraph,
link: renderLink,
image: renderImage,
code_inline: renderCodeInline,
code_block: renderCodeBlock,
blockquote: renderBlockquote,
list: renderList,
list_item: renderListItem,
task_list_item: renderTaskListItem,
table: renderTable,
table_row: renderTableRow,
table_cell: renderTableCell,
thematic_break: renderThematicBreak,
softbreak: renderSoftbreak,
hardbreak: renderHardbreak,
}
/**
* Get default renderer by node type
* Returns renderDefault for unknown types
*/
export function getDefaultRenderer(nodeType: string): RendererFn {
return defaultRenderers[nodeType] ?? renderDefault
}
// Get renderer for node type (backward compatibility)
export function getRenderer(
nodeType: string
): (
node: MarkdownNode,
key: string | number,
ctx: RenderContext
) => React.ReactNode {
switch (nodeType) {
case 'text':
return renderText
case 'strong':
return renderStrong
case 'emphasis':
return renderEmphasis
case 'strikethrough':
return renderStrikethrough
case 'heading':
return renderHeading
case 'paragraph':
return renderParagraph
case 'link':
return renderLink
case 'image':
return renderImage
case 'code_inline':
return renderCodeInline
case 'code_block':
return renderCodeBlock
case 'blockquote':
return renderBlockquote
case 'list':
return renderList
case 'list_item':
return renderListItem
case 'task_list_item':
return renderTaskListItem
case 'table':
return renderTable
case 'table_head':
case 'table_body':
return renderDefault
case 'table_row':
return renderTableRow
case 'table_cell':
return renderTableCell
case 'thematic_break':
return renderThematicBreak
case 'softbreak':
return renderSoftbreak
case 'hardbreak':
return renderHardbreak
default:
return renderDefault
}
}