/** * Parses a limited subset of Markdown and converts it to HTML. * * Supported Markdown elements: * - Headings (#, ##, ###) * - Bold text (**text** or __text__) * - Unordered lists (- or *) * - Ordered lists (1., 2., etc.) * - Paragraphs * * @param {string} markdown - The Markdown-formatted string. * @returns {string} - The resulting HTML string. */ function parseMarkdown(markdown) { const lines = markdown.split('\n'); const html = []; // Stack of open lists: [{ type: "ul"|"ol", indent: number }] const listStack = []; function closeListsToIndent(indent) { while (listStack.length > 0 && listStack[listStack.length - 1].indent >= indent) { const last = listStack.pop(); html.push(``); } } function openList(type, indent, startNumber = null) { listStack.push({ type, indent }); if (type === "ol" && startNumber != null && startNumber !== 1) html.push(`
    `); else html.push(`<${type}>`); } lines.forEach(line => { // Detect indentation level (2 spaces = one indent) const indentSpaces = line.match(/^ */)[0].length; const indent = Math.floor(indentSpaces / 2); const trimmed = line.trim(); if (trimmed === "") { // Close all lists for blank lines, do NOT output

    closeListsToIndent(0); return; } // -------- // Headings // -------- if (/^###\s+/.test(trimmed)) { closeListsToIndent(0); html.push(`

    ${escapeHtml(trimmed.replace(/^###\s+/, ''))}

    `); return; } if (/^##\s+/.test(trimmed)) { closeListsToIndent(0); html.push(`

    ${escapeHtml(trimmed.replace(/^##\s+/, ''))}

    `); return; } if (/^#\s+/.test(trimmed)) { closeListsToIndent(0); html.push(`

    ${escapeHtml(trimmed.replace(/^#\s+/, ''))}

    `); return; } // ------------------------ // Ordered lists: "N. text" // ------------------------ let mOrdered = trimmed.match(/^(\d+)\.\s+(.*)/); if (mOrdered) { const num = parseInt(mOrdered[1], 10); const content = mOrdered[2]; const last = listStack[listStack.length - 1]; if (!last || last.indent < indent || last.type !== "ol") { // NEW ordered list closeListsToIndent(indent); openList("ol", indent, num); } // ELSE: same indent, same list → continue existing OL without closing/opening html.push(`
  1. ${parseInlineMarkdown(escapeHtml(content))}
  2. `); return; } // ------------------------------------- // Unordered lists: "- text" or "* text" // ------------------------------------- let mUnordered = trimmed.match(/^[-*]\s+(.*)/); if (mUnordered) { const content = mUnordered[1]; const last = listStack[listStack.length - 1]; if (!last || last.indent < indent || last.type !== "ul") { closeListsToIndent(indent); openList("ul", indent); } html.push(`
  3. ${parseInlineMarkdown(escapeHtml(content))}
  4. `); return; } // --------- // Paragraph // --------- closeListsToIndent(0); html.push(`

    ${parseInlineMarkdown(escapeHtml(trimmed))}

    `); }); // Close all remaining lists closeListsToIndent(0); return html.join('\n'); } /** * Parses inline Markdown elements like bold text. * * Supported inline elements: * - Bold text (**text** or __text__) * * @param {string} text - The text to parse. * @returns {string} - The text with inline Markdown converted to HTML. */ function parseInlineMarkdown(text) { // Convert **text** and __text__ to text return text .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/__(.+?)__/g, '$1'); } /** * Escapes HTML special characters to prevent XSS attacks. * * @param {string} text - The text to escape. * @returns {string} - The escaped text. */ function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; return text.replace(/[&<>"']/g, function(m) { return map[m]; }); } return L.Class.extend({ parseMarkdown: parseMarkdown, });