120 lines
5.6 KiB
JavaScript
Executable file
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, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
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: 
|
|
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
|
|
};
|
|
})();
|