/* oxlint-disable regexp/no-super-linear-backtracking */
/**
* Text Processing Utilities
*
* Functions for escaping HTML, parsing JSDoc links, and rendering markdown.
*
* @module server/utils/docs/text
*/
import { highlightCodeBlock } from '../shiki'
import type { SymbolLookup } from './types'
/**
* Strip ANSI escape codes from text.
* Deno doc output may contain terminal color codes that need to be removed.
*/
const ESC = String.fromCharCode(27)
const ANSI_PATTERN = new RegExp(`${ESC}\\[[0-9;]*m`, 'g')
export function stripAnsi(text: string): string {
return text.replace(ANSI_PATTERN, '')
}
/**
* Escape HTML special characters.
*
* @internal Exported for testing
*/
export function escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
/**
* Clean up symbol names by stripping esm.sh prefixes.
*
* Packages using @types/* definitions get "default." or "default_" prefixes
* from esm.sh that we need to remove for clean display.
*/
export function cleanSymbolName(name: string): string {
if (name.startsWith('default.')) {
return name.slice(8)
}
if (name.startsWith('default_')) {
return name.slice(8)
}
return name
}
/**
* Create a URL-safe HTML anchor ID for a symbol.
*/
export function createSymbolId(kind: string, name: string): string {
return `${kind}-${name}`.replace(/[^a-z0-9-]/gi, '_')
}
/**
* Parse JSDoc {@link} tags into HTML links.
*
* Handles:
* - {@link https://example.com} - external URL
* - {@link https://example.com Link Text} - external URL with label
* - {@link SomeSymbol} - internal cross-reference
*
* @internal Exported for testing
*/
export function parseJsDocLinks(text: string, symbolLookup: SymbolLookup): string {
let result = escapeHtml(text)
result = result.replace(/\{@link\s+([^\s}]+)(?:\s+([^}]+))?\}/g, (_, target, label) => {
const displayText = label || target
// External URL
if (target.startsWith('http://') || target.startsWith('https://')) {
return `${displayText}`
}
// Internal symbol reference
const symbolId = symbolLookup.get(target)
if (symbolId) {
return `${displayText}`
}
// Unknown symbol
return `${displayText}`
})
return result
}
/**
* Render simple markdown-like formatting.
* Uses
for line breaks to avoid nesting issues with inline elements.
* Fenced code blocks (```) are syntax-highlighted with Shiki.
*
* @internal Exported for testing
*/
export async function renderMarkdown(text: string, symbolLookup: SymbolLookup): Promise {
// Extract fenced code blocks FIRST (before any HTML escaping)
// Pattern handles:
// - Optional whitespace before/after language identifier
// - \r\n, \n, or \r line endings
const codeBlockData: Array<{ lang: string; code: string }> = []
let result = text.replace(
/```[ \t]*(\w*)[ \t]*(?:\r\n|\r|\n)([\s\S]*?)(?:\r\n|\r|\n)?```/g,
(_, lang, code) => {
const index = codeBlockData.length
codeBlockData.push({ lang: lang || 'text', code: code.trim() })
return `__CODE_BLOCK_${index}__`
},
)
// Now process the rest (JSDoc links, HTML escaping, etc.)
result = parseJsDocLinks(result, symbolLookup)
// Markdown links - i.e. [text](url)
result = result.replace(
/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g,
'$1',
)
// Handle inline code (single backticks) - won't interfere with fenced blocks
result = result
.replace(/`([^`]+)`/g, '$1')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\n{2,}/g, '
')
.replace(/\n/g, '
')
// Highlight and restore code blocks
for (let i = 0; i < codeBlockData.length; i++) {
const { lang, code } = codeBlockData[i]!
const highlighted = await highlightCodeBlock(code, lang)
result = result.replace(`__CODE_BLOCK_${i}__`, highlighted)
}
return result
}