import { escapeHtml } from './utils'; const ESCAPE_CHAR = '\uE000'; const SIZE_MULTIPLIER_POSITIVE = 20; const SIZE_MULTIPLIER_NEGATIVE = 15; const MIN_FONT_SIZE = 50; const MAX_FONT_SIZE = 300; interface MarkdownPattern { start: string; end: string; tag: string; className?: string; } const MARKDOWN_PATTERNS: MarkdownPattern[] = [ { start: '***', end: '***', tag: 'strong> 0) { if (text.startsWith(open, i)) { depth++; i += open.length; } else if (text.startsWith(close, i)) { depth--; if (depth === 0) return i; i += close.length; } else { i++; } } return -1; } function processFormatBlock(content: string): string { content = content.trim(); if (ESCAPE_PATTERNS.some(pattern => pattern.test(content))) { return escapeHtml(content); } const sizeMatch = content.match(/^([+-]\d)\s+(.+)$/s); if (sizeMatch) { const size = parseInt(sizeMatch[1]); const multiplier = size > 0 ? SIZE_MULTIPLIER_POSITIVE : SIZE_MULTIPLIER_NEGATIVE; const fontSize = 100 + size * multiplier; const clampedSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, fontSize)); return `${formatText(sizeMatch[2])}`; } const colorMatch = content.match(/^(#[0-9a-fA-F]{3,6}(?:,#[0-9a-fA-F]{3,6})?)\s+(.+)$/s); if (colorMatch) { const colors = colorMatch[1].split(','); if (colors.length === 2) { return `${formatText(colorMatch[2])}`; } return `${formatText(colorMatch[2])}`; } return formatText(content); } function processWikiLink(content: string): string { const [target, display] = content.split('|').map(s => s.trim()); return renderLink(target, display || target, 'bogam-link'); } function tryMarkdownPatterns(text: string, pos: number): { html: string; newPos: number } | null { for (const pattern of MARKDOWN_PATTERNS) { if (!text.startsWith(pattern.start, pos)) continue; if (pattern.start === '*' && text.startsWith('**', pos)) continue; const end = findDelimiter(text, pattern.end, pos + pattern.start.length); if (end === -1) continue; const content = text.slice(pos + pattern.start.length, end); const className = pattern.className ? ` class="${pattern.className}"` : ''; const html = pattern.tag.includes('><') ? `<${pattern.tag}>${formatText(content)}` : `<${pattern.tag}${className}>${formatText(content)}`; return { html, newPos: end + pattern.end.length }; } return null; } function tryHtmlPatterns(text: string, pos: number): { html: string; newPos: number } | null { const brMatch = text.slice(pos).match(/^/); if (brMatch) { return { html: '
', newPos: pos + brMatch[0].length }; } const subMatch = text.slice(pos).match(/^(.+?)<\/sub>/); if (subMatch) { return { html: `${escapeHtml(subMatch[1])}`, newPos: pos + subMatch[0].length, }; } const supMatch = text.slice(pos).match(/^(.+?)<\/sup>/); if (supMatch) { return { html: `${escapeHtml(supMatch[1])}`, newPos: pos + supMatch[0].length, }; } return null; } export function resolveHref(target: string): { href: string; isExternal: boolean } { if (target.match(/^[a-zA-Z]+:\/\//) || target.startsWith('mailto:')) { return { href: target, isExternal: true }; } if (target.includes(':')) { const [prefix, routeTarget] = target.split(':', 2); return { href: `/${encodeURIComponent(prefix)}/${encodeURIComponent(routeTarget)}`, isExternal: false, }; } return { href: `/t/${encodeURIComponent(target)}`, isExternal: false }; } export function renderLink(target: string, display: string, className: string): string { const { href, isExternal } = resolveHref(target); const linkText = escapeHtml(display); const externalAttrs = isExternal ? ' target="_blank" rel="noopener noreferrer"' : ''; return `${linkText}`; } function findDelimiter(text: string, delimiter: string, start: number): number { let pos = start; while (pos < text.length) { const idx = text.indexOf(delimiter, pos); if (idx === -1) return -1; if (idx > 0 && text[idx - 1] === ESCAPE_CHAR) { pos = idx + 1; continue; } return idx; } return -1; }