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
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user