- Editorial web platform built with Node.js, Express, EJS, and Vanilla CSS - Markdown content engine with YAML frontmatter, bilingual variants, and hot-reload watcher - Unified SQLite with FTS5 search indexing, query logging, and schema filters - Centered editorial post layout with margin-docked left TOC and mobile bottom sheet - WordPress-grade comment system with moderation dashboard and bulk operations under /guanli - Complete SEO suite (JSON-LD structured data, RSS/Atom feeds, XML sitemap, robots.txt) - CI/CD workflows for linting, security audits (Gitleaks), automated tests, and Docker releases
89 lines
2.6 KiB
JavaScript
89 lines
2.6 KiB
JavaScript
const MarkdownIt = require('markdown-it');
|
|
const markdownItAnchor = require('markdown-it-anchor');
|
|
const markdownItFootnote = require('markdown-it-footnote');
|
|
const markdownItAttrs = require('markdown-it-attrs');
|
|
const Prism = require('prismjs');
|
|
require('prismjs/components/prism-bash');
|
|
require('prismjs/components/prism-json');
|
|
require('prismjs/components/prism-yaml');
|
|
require('prismjs/components/prism-python');
|
|
require('prismjs/components/prism-go');
|
|
require('prismjs/components/prism-docker');
|
|
require('prismjs/components/prism-sql');
|
|
require('prismjs/components/prism-typescript');
|
|
require('prismjs/components/prism-markdown');
|
|
|
|
function slugifyHeading(s) {
|
|
return encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-'));
|
|
}
|
|
|
|
function createMarkdownRenderer(pluginManager) {
|
|
const md = new MarkdownIt({
|
|
html: true,
|
|
linkify: true,
|
|
typographer: true,
|
|
highlight: function (str, lang) {
|
|
if (lang && Prism.languages[lang]) {
|
|
try {
|
|
return `<pre class="language-${lang}"><code class="language-${lang}">${Prism.highlight(str, Prism.languages[lang], lang)}</code></pre>`;
|
|
} catch (__) {}
|
|
}
|
|
const escaped = md.utils.escapeHtml(str);
|
|
return `<pre class="language-text"><code class="language-text">${escaped}</code></pre>`;
|
|
}
|
|
});
|
|
|
|
md.use(markdownItAnchor, {
|
|
slugify: slugifyHeading,
|
|
permalink: markdownItAnchor.permalink.linkInsideHeader({
|
|
symbol: '#',
|
|
placement: 'after',
|
|
ariaHidden: true
|
|
})
|
|
});
|
|
|
|
md.use(markdownItFootnote);
|
|
md.use(markdownItAttrs);
|
|
|
|
// Disable indented code blocks (4 spaces) so indented HTML elements don't get converted to <pre><code>
|
|
md.disable('code');
|
|
|
|
if (pluginManager) {
|
|
pluginManager.applyMarkdownHooks(md);
|
|
}
|
|
|
|
return md;
|
|
}
|
|
|
|
function extractToc(markdownContent) {
|
|
const headings = [];
|
|
const lines = markdownContent.split(/\r?\n/);
|
|
let inCodeBlock = false;
|
|
|
|
for (const line of lines) {
|
|
if (line.trim().startsWith('```')) {
|
|
inCodeBlock = !inCodeBlock;
|
|
continue;
|
|
}
|
|
if (inCodeBlock) continue;
|
|
|
|
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
if (match) {
|
|
const level = match[1].length;
|
|
// Strip markdown links/formatting from heading text for TOC display
|
|
const title = match[2].replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/[*_`]/g, '').trim();
|
|
const id = slugifyHeading(title);
|
|
headings.push({ level, title, id });
|
|
}
|
|
}
|
|
|
|
// Build hierarchical tree if needed or flat list with levels
|
|
return headings;
|
|
}
|
|
|
|
module.exports = {
|
|
createMarkdownRenderer,
|
|
extractToc,
|
|
slugifyHeading
|
|
};
|