function escapeHtml(value: string): string {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function escapeAttribute(value: string): string {
return value.replaceAll("&", "&").replaceAll('"', """);
}
function normalizeHttpUrl(rawUrl: string): string | null {
try {
const parsed = new URL(rawUrl);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
return parsed.toString();
} catch {
return null;
}
}
function renderInline(markdown: string): string {
let rendered = escapeHtml(markdown);
rendered = rendered.replace(/`([^`]+)`/g, (_match, value: string) => `${value}`);
rendered = rendered.replace(/\*\*([^*]+)\*\*/g, (_match, value: string) => `${value}`);
rendered = rendered.replace(/\*([^*]+)\*/g, (_match, value: string) => `${value}`);
rendered = rendered.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, label: string, url: string) => {
const safeUrl = normalizeHttpUrl(url);
if (!safeUrl) return label;
return `${label}`;
});
return rendered;
}
export function markdownToSafeHtml(markdown: string): string {
const source = markdown.replace(/\r\n/g, "\n").trim();
if (!source) return "
Sin contenido.
"; const lines = source.split("\n"); const output: string[] = []; let listMode: "ol" | "ul" | null = null; const closeList = () => { if (!listMode) return; output.push(listMode === "ol" ? "" : ""); listMode = null; }; for (const rawLine of lines) { const line = rawLine.trim(); if (!line) { closeList(); continue; } const h3 = line.match(/^###\s+(.+)$/); if (h3) { closeList(); output.push(`${renderInline(quote[1])}`); continue; } closeList(); output.push(`
${renderInline(line)}
`); } closeList(); return output.join("\n"); } export function markdownToPlainText(markdown: string): string { return markdown .replace(/\r\n/g, "\n") .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1") .replace(/[`*_>#-]/g, "") .replace(/\n{2,}/g, "\n") .trim(); }