`)
// Header
lines.push(``)
// Signatures
const signatures = symbol.nodes
.slice(0, hasOverloads ? MAX_OVERLOAD_SIGNATURES : 1)
.map(n => getNodeSignature(n))
.filter(Boolean) as string[]
if (signatures.length > 0) {
const signatureCode = signatures.join('\n')
const highlightedSignature = await highlightCodeBlock(signatureCode, 'typescript')
lines.push(`${highlightedSignature}
`)
if (symbol.nodes.length > MAX_OVERLOAD_SIGNATURES) {
const remaining = symbol.nodes.length - MAX_OVERLOAD_SIGNATURES
lines.push(`+ ${remaining} more overloads
`)
}
}
// Description
if (symbol.jsDoc?.doc) {
const description = symbol.jsDoc.doc.trim()
lines.push(
`${await renderMarkdown(description, symbolLookup)}
`,
)
}
// JSDoc tags
if (symbol.jsDoc?.tags && symbol.jsDoc.tags.length > 0) {
lines.push(await renderJsDocTags(symbol.jsDoc.tags, symbolLookup))
}
// Type-specific members
if (symbol.kind === 'class' && primaryNode.classDef) {
lines.push(renderClassMembers(primaryNode.classDef))
} else if (symbol.kind === 'interface' && primaryNode.interfaceDef) {
lines.push(renderInterfaceMembers(primaryNode.interfaceDef))
} else if (symbol.kind === 'enum' && primaryNode.enumDef) {
lines.push(renderEnumMembers(primaryNode.enumDef))
}
lines.push(` `)
return lines.join('\n')
}
/**
* Render JSDoc tags (params, returns, examples, etc.)
*/
async function renderJsDocTags(tags: JsDocTag[], symbolLookup: SymbolLookup): Promise {
const lines: string[] = []
const params = tags.filter(t => t.kind === 'param')
const returns = tags.find(t => t.kind === 'return')
const examples = tags.filter(t => t.kind === 'example')
const deprecated = tags.find(t => t.kind === 'deprecated')
const see = tags.filter(t => t.kind === 'see')
// Deprecated warning
if (deprecated) {
lines.push(``)
lines.push(`
Deprecated `)
if (deprecated.doc) {
// We remove new lines because they look weird when rendered into the deprecated block
// I think markdown is actually supposed to collapse single new lines automatically but this function doesn't do that so if that changes remove this
const renderedMessage = await renderMarkdown(deprecated.doc.replace(/\n/g, ' '), symbolLookup)
lines.push(`
${renderedMessage}
`)
}
lines.push(`
`)
}
// Parameters
if (params.length > 0) {
lines.push(``)
lines.push(`
Parameters `)
lines.push(`
`)
for (const param of params) {
lines.push(
`${escapeHtml(param.name || '')}${param.optional ? '?' : ''} `,
)
if (param.doc) {
lines.push(`${parseJsDocLinks(param.doc, symbolLookup)} `)
}
}
lines.push(` `)
lines.push(`
`)
}
// Returns
if (returns?.doc) {
lines.push(``)
lines.push(`
Returns `)
lines.push(`
${parseJsDocLinks(returns.doc, symbolLookup)}
`)
lines.push(`
`)
}
// Examples (with syntax highlighting)
if (examples.length > 0) {
lines.push(``)
lines.push(`
Example${examples.length > 1 ? 's' : ''} `)
for (const example of examples) {
if (example.doc) {
const langMatch = example.doc.match(/```(\w+)?/)
const lang = langMatch?.[1] || 'typescript'
const code = example.doc.replace(/```\w*\n?/g, '').trim()
const highlighted = await highlightCodeBlock(code, lang)
lines.push(highlighted)
}
}
lines.push(``)
}
// See also
if (see.length > 0) {
lines.push(``)
lines.push(`
See Also `)
lines.push(`
`)
for (const s of see) {
if (s.doc) {
lines.push(`${parseJsDocLinks(s.doc, symbolLookup)} `)
}
}
lines.push(` `)
lines.push(`
`)
}
return lines.join('\n')
}
// =============================================================================
// Member Rendering
// =============================================================================
/**
* Render class members (constructor, properties, methods).
*/
function renderClassMembers(def: NonNullable): string {
const lines: string[] = []
const { constructors, properties, methods } = def
if (constructors && constructors.length > 0) {
lines.push(``)
lines.push(`
Constructor `)
for (const ctor of constructors) {
const params = ctor.params?.map(p => formatParam(p)).join(', ') || ''
lines.push(`
constructor(${escapeHtml(params)})`)
}
lines.push(`
`)
}
if (properties && properties.length > 0) {
lines.push(``)
lines.push(`
Properties `)
lines.push(`
`)
for (const prop of properties) {
const modifiers: string[] = []
if (prop.isStatic) modifiers.push('static')
if (prop.readonly) modifiers.push('readonly')
const modStr = modifiers.length > 0 ? `${modifiers.join(' ')} ` : ''
const type = formatType(prop.tsType)
const opt = prop.optional ? '?' : ''
lines.push(
`${escapeHtml(modStr)}${escapeHtml(prop.name)}${opt}: ${escapeHtml(type)} `,
)
if (prop.jsDoc?.doc) {
lines.push(`${escapeHtml(prop.jsDoc.doc.split('\n')[0] ?? '')} `)
}
}
lines.push(` `)
lines.push(`
`)
}
if (methods && methods.length > 0) {
lines.push(``)
lines.push(`
Methods `)
lines.push(`
`)
for (const method of methods) {
const params = method.functionDef?.params?.map(p => formatParam(p)).join(', ') || ''
const ret = formatType(method.functionDef?.returnType) || 'void'
const staticStr = method.isStatic ? 'static ' : ''
lines.push(
`${escapeHtml(staticStr)}${escapeHtml(method.name)}(${escapeHtml(params)}): ${escapeHtml(ret)} `,
)
if (method.jsDoc?.doc) {
lines.push(`${escapeHtml(method.jsDoc.doc.split('\n')[0] ?? '')} `)
}
}
lines.push(` `)
lines.push(`
`)
}
return lines.join('\n')
}
/**
* Render interface members (properties, methods).
*/
function renderInterfaceMembers(def: NonNullable): string {
const lines: string[] = []
const { properties, methods } = def
if (properties && properties.length > 0) {
lines.push(``)
lines.push(`
Properties `)
lines.push(`
`)
for (const prop of properties) {
const type = formatType(prop.tsType)
const opt = prop.optional ? '?' : ''
const ro = prop.readonly ? 'readonly ' : ''
lines.push(
`${escapeHtml(ro)}${escapeHtml(prop.name)}${opt}: ${escapeHtml(type)} `,
)
if (prop.jsDoc?.doc) {
lines.push(`${escapeHtml(prop.jsDoc.doc.split('\n')[0] ?? '')} `)
}
}
lines.push(` `)
lines.push(`
`)
}
if (methods && methods.length > 0) {
lines.push(``)
lines.push(`
Methods `)
lines.push(`
`)
for (const method of methods) {
const params = method.params?.map(p => formatParam(p)).join(', ') || ''
const ret = formatType(method.returnType) || 'void'
lines.push(
`${escapeHtml(method.name)}(${escapeHtml(params)}): ${escapeHtml(ret)} `,
)
if (method.jsDoc?.doc) {
lines.push(`${escapeHtml(method.jsDoc.doc.split('\n')[0] ?? '')} `)
}
}
lines.push(` `)
lines.push(`
`)
}
return lines.join('\n')
}
/**
* Render enum members.
*/
function renderEnumMembers(def: NonNullable): string {
const lines: string[] = []
const { members } = def
if (members && members.length > 0) {
lines.push(``)
lines.push(`
Members `)
lines.push(`
`)
for (const member of members) {
lines.push(`${escapeHtml(member.name)} `)
}
lines.push(` `)
lines.push(`
`)
}
return lines.join('\n')
}
// =============================================================================
// Table of Contents
// =============================================================================
/**
* Render table of contents.
*/
export function renderToc(symbols: MergedSymbol[]): string {
const grouped = groupMergedByKind(symbols)
const lines: string[] = []
lines.push(``)
lines.push(``)
for (const kind of KIND_DISPLAY_ORDER) {
const kindSymbols = grouped[kind]
if (!kindSymbols || kindSymbols.length === 0) continue
const title = KIND_TITLES[kind] || kind
lines.push(``)
lines.push(
`${title} (${kindSymbols.length}) `,
)
const showSymbols = kindSymbols.slice(0, MAX_TOC_ITEMS_PER_KIND)
lines.push(``)
for (const symbol of showSymbols) {
const id = createSymbolId(symbol.kind, symbol.name)
lines.push(
`${escapeHtml(symbol.name)} `,
)
}
if (kindSymbols.length > MAX_TOC_ITEMS_PER_KIND) {
const remaining = kindSymbols.length - MAX_TOC_ITEMS_PER_KIND
lines.push(`... and ${remaining} more `)
}
lines.push(` `)
lines.push(` `)
}
lines.push(` `)
lines.push(` `)
return lines.join('\n')
}