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>';
i += 4;
continue;
}
const htmlResult = tryHtmlPatterns(text, i);
if (htmlResult) {
result += htmlResult.html;
i = htmlResult.newPos;
continue;
}
if (text.startsWith(ESCAPE_CHAR, i) && i + 1 < text.length) {
result += escapeHtml(text[i + 1]);
i += 2;
} else {
result += escapeHtml(text[i]);
i++;
}
}
return result;
}
function findClosing(text: string, start: number, open: string, close: string): number {
let depth = 1;
let i = start + open.length;
while (i < text.length && depth > 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)}${pattern.tag}>`;
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;
}