MVP
This commit is contained in:
126
lib/courses/lessonMarkdown.ts
Normal file
126
lib/courses/lessonMarkdown.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
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) => `<code>${value}</code>`);
|
||||
rendered = rendered.replace(/\*\*([^*]+)\*\*/g, (_match, value: string) => `<strong>${value}</strong>`);
|
||||
rendered = rendered.replace(/\*([^*]+)\*/g, (_match, value: string) => `<em>${value}</em>`);
|
||||
rendered = rendered.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, label: string, url: string) => {
|
||||
const safeUrl = normalizeHttpUrl(url);
|
||||
if (!safeUrl) return label;
|
||||
return `<a href="${escapeAttribute(safeUrl)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
||||
});
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
export function markdownToSafeHtml(markdown: string): string {
|
||||
const source = markdown.replace(/\r\n/g, "\n").trim();
|
||||
if (!source) return "<p>Sin contenido.</p>";
|
||||
|
||||
const lines = source.split("\n");
|
||||
const output: string[] = [];
|
||||
let listMode: "ol" | "ul" | null = null;
|
||||
|
||||
const closeList = () => {
|
||||
if (!listMode) return;
|
||||
output.push(listMode === "ol" ? "</ol>" : "</ul>");
|
||||
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(`<h3>${renderInline(h3[1])}</h3>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const h2 = line.match(/^##\s+(.+)$/);
|
||||
if (h2) {
|
||||
closeList();
|
||||
output.push(`<h2>${renderInline(h2[1])}</h2>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const h1 = line.match(/^#\s+(.+)$/);
|
||||
if (h1) {
|
||||
closeList();
|
||||
output.push(`<h1>${renderInline(h1[1])}</h1>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ordered = line.match(/^\d+\.\s+(.+)$/);
|
||||
if (ordered) {
|
||||
if (listMode !== "ol") {
|
||||
closeList();
|
||||
output.push("<ol>");
|
||||
listMode = "ol";
|
||||
}
|
||||
output.push(`<li>${renderInline(ordered[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const unordered = line.match(/^[-*]\s+(.+)$/);
|
||||
if (unordered) {
|
||||
if (listMode !== "ul") {
|
||||
closeList();
|
||||
output.push("<ul>");
|
||||
listMode = "ul";
|
||||
}
|
||||
output.push(`<li>${renderInline(unordered[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const quote = line.match(/^>\s+(.+)$/);
|
||||
if (quote) {
|
||||
closeList();
|
||||
output.push(`<blockquote>${renderInline(quote[1])}</blockquote>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
closeList();
|
||||
output.push(`<p>${renderInline(line)}</p>`);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user