Files
pengzhan.dev/server/services/markdown.js
T
haopengzhan 8c0b04db0e feat: initial release of pengzhan.dev personal editorial blog platform
- 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
2026-09-16 23:08:53 -07:00

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
};