matrixhasyou-site/js/markdown-parser.js

120 lines
5.6 KiB
JavaScript
Executable file

/**
* Matrix Cycling Blog - Markdown Parser Engine
* Lightweight, zero-dependency Markdown parser with custom extensions for Matrix/FreeBSD alerts and code blocks.
*/
window.MatrixMarkdown = (function () {
function escapeHtml(text) {
return text
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function render(md) {
if (!md) return "";
let html = md;
// 1. Code blocks with headers ```lang:title ... ```
html = html.replace(/```([a-zA-Z0-9_-]*)(?::([^\n]+))?\n([\s\S]*?)```/g, function (match, lang, title, code) {
const headerTitle = title ? title.trim() : (lang ? lang.toUpperCase() : "CODE");
const escapedCode = escapeHtml(code.trim());
return `
<div class="code-block">
<div class="code-header">
<span>${escapeHtml(headerTitle)}</span>
<span>${escapeHtml((lang || "SHELL").toUpperCase())}</span>
</div>
<pre><code>${escapedCode}</code></pre>
</div>`;
});
// 2. Custom Alerts: > [!TIP], > [!WARN], > [!DANGER], > [!NOTE]
html = html.replace(/^>\s*\[!(TIP|WARN|WARNING|DANGER|NOTE|INFO)\](?:\s*([^\n]+))?\n((?:>.*\n?)*)/gim, function (match, type, title, body) {
const alertType = type.toLowerCase() === "warning" ? "warn" : (type.toLowerCase() === "info" ? "tip" : type.toLowerCase());
const alertTitle = title ? title.trim() : (
alertType === "tip" ? "💡 Совет из практики:" :
alertType === "warn" ? "⚠️ Внимание / Грабли:" :
alertType === "danger" ? "🚨 Ошибка / Критично:" : "📌 Примечание:"
);
const cleanBody = body.replace(/^>\s?/gm, "").trim();
const renderedBody = renderInline(cleanBody);
return `
<div class="alert-box ${alertType}">
<span class="alert-title">${escapeHtml(alertTitle)}</span>
<div>${renderedBody}</div>
</div>`;
});
// 3. Blockquotes: > quote text
html = html.replace(/^>\s+(.+)$/gm, function (match, text) {
return `<blockquote>${renderInline(text)}</blockquote>`;
});
// 4. Headers: #, ##, ###, ####
html = html.replace(/^#### (.*?)$/gm, '<h4 style="color: var(--accent-cyan); margin: 12px 0 6px 0;">$1</h4>');
html = html.replace(/^### (.*?)$/gm, '<h3 style="color: var(--accent-cyan); margin: 14px 0 8px 0;">$1</h3>');
html = html.replace(/^## (.*?)$/gm, '<h2 style="color: var(--accent-green); margin: 16px 0 10px 0;">$1</h2>');
html = html.replace(/^# (.*?)$/gm, '<h1 style="color: var(--accent-green); margin: 20px 0 12px 0;">$1</h1>');
// 5. Images: ![alt](url)
html = html.replace(/!\[(.*?)\]\((.*?)\)/g, function (match, alt, url) {
return `<div style="margin: 14px 0; border-radius: 4px; overflow: hidden;"><img src="${escapeHtml(url)}" alt="${escapeHtml(alt)}" style="width: 100%; height: auto; border-radius: 4px; border: 1px solid var(--border-color);" loading="lazy"></div>`;
});
// 6. Links: [text](url)
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
// 7. Unordered lists: - item or * item
html = html.replace(/^(?:-|\*)\s+(.+)$/gm, '<li class="md-li">$1</li>');
html = html.replace(/((?:<li class="md-li">.*?<\/li>\s*)+)/gs, '<ul style="margin: 10px 0 14px 22px; line-height: 1.7;">$1</ul>');
// 8. Ordered lists: 1. item
html = html.replace(/^\d+\.\s+(.+)$/gm, '<li class="md-oli">$1</li>');
html = html.replace(/((?:<li class="md-oli">.*?<\/li>\s*)+)/gs, '<ol style="margin: 10px 0 14px 22px; line-height: 1.7;">$1</ol>');
// 9. Horizontal rules: --- or ***
html = html.replace(/^---|\*\*\*$/gm, '<hr style="border: 0; border-top: 1px dashed var(--border-color); margin: 16px 0;">');
// 10. Split by double newlines for paragraphs (if not already inside custom elements)
const parts = html.split(/\n\n+/);
const processedParts = parts.map(part => {
part = part.trim();
if (!part) return "";
if (part.startsWith("<div") || part.startsWith("<ul") || part.startsWith("<ol") ||
part.startsWith("<h") || part.startsWith("<blockquote") || part.startsWith("<hr")) {
return part;
}
return `<p>${renderInline(part.replace(/\n/g, "<br>"))}</p>`;
});
return processedParts.filter(Boolean).join("\n\n");
}
function renderInline(text) {
if (!text) return "";
let inline = text;
// Bold + Italic: ***text*** or ___text___
inline = inline.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
// Bold: **text**
inline = inline.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Italic: *text* or _text_
inline = inline.replace(/\*([^\*]+)\*/g, '<em>$1</em>');
inline = inline.replace(/_([^_]+)_/g, '<em>$1</em>');
// Strikethrough: ~~text~~
inline = inline.replace(/~~(.*?)~~/g, '<del>$1</del>');
// Inline code: `code`
inline = inline.replace(/`([^`]+)`/g, '<code style="background: var(--bg-code); color: var(--accent-green); padding: 2px 6px; border-radius: 3px; border: 1px solid var(--border-color); font-size: 0.9em;">$1</code>');
return inline;
}
return {
render: render,
renderInline: renderInline
};
})();