UNPKG

gitsite-cli

Version:

Tools for generate static web site from Markdown files in git repository.

180 lines (166 loc) 6.45 kB
import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { readdir } from 'node:fs/promises'; import MarkdownIt from 'markdown-it'; import MarkdownItContainer from 'markdown-it-container'; import footnote_plugin from 'markdown-it-footnote'; import hljs from 'highlight.js'; import { katex } from '@mdit/plugin-katex'; function getAttr(tokens, idx, attrName) { let index = tokens[idx].attrIndex(attrName); if (index >= 0) { return tokens[idx].attrs[index][1]; } return null; } function setAttr(tokens, idx, attrName, attrValue) { let index = tokens[idx].attrIndex(attrName); if (index < 0) { tokens[idx].attrPush([attrName, attrValue]); } else { tokens[idx].attrs[index][1] = attrValue; } } const unescapeAll = MarkdownIt().utils.unescapeAll; const escapeHtml = MarkdownIt().utils.escapeHtml; async function createMarkdown(opt) { if (opt === undefined) { // default options: opt = { html: true, // enable HTML tags in source linkify: false, // autoconvert URL-like text to links external_link: true, // add target="_blank" for external link lazy_image: true, // add loading="lazy" for image math: true, // enable mathematical expressions footnote: true, // enable footnate }; } const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const plugin_dir = path.join(__dirname, 'markdown-plugin'); const plugin_names = await readdir(plugin_dir); plugin_names.sort(); const codeBlockPlugins = new Map(); for (let name of plugin_names) { if (name.endsWith('.js')) { console.debug(`auto import markdown plugin: ${name}`); const mod = await import(`./markdown-plugin/${name}`); const pName = name.substring(0, name.length - 3); codeBlockPlugins.set(pName, mod.default); } } let md = new MarkdownIt({ html: opt.html, linkify: opt.linkify, highlight: (str, lang) => { if (!lang) { lang = 'text'; } lang = lang.toLowerCase(); if (hljs.getLanguage(lang)) { try { return `<pre class="hljs"><code class="language-${lang}">` + hljs.highlight(str, { language: lang, ignoreIllegals: true }).value + '</code></pre>'; } catch (e) { } } return `<pre class="hljs"><code class="language-${lang}">${escapeHtml(str)}</code></pre>`; } }); if (codeBlockPlugins.size > 0) { const defaultFence = md.renderer.rules.fence; md.renderer.rules.fence = function (tokens, idx, options, env, self) { let token = tokens[idx], info = token.info ? unescapeAll(token.info).trim() : ''; if (info) { let arr = info.match(/(?:[^\s"]+|"[^"]*")+/g).map(s => { if (s.startsWith('"') && s.endsWith('"')) { s = s.substring(1, s.length - 1); } return s; }); let type = arr.shift().toLowerCase(); if (codeBlockPlugins.has(type)) { console.log(`use markdown plugin ${type}.`); let plugin = codeBlockPlugins.get(type); let result = plugin(md, arr, token.content); if (result) { return result; } } } // pass token to default renderer: return defaultFence(tokens, idx, options, env, self); }; } if (opt.external_link) { // add target="_blank" for external link: const defaultLinkRender = md.renderer.rules.link_open || function (tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; md.renderer.rules.link_open = function (tokens, idx, options, env, self) { // check if relative url: let isRelative = false; let href = getAttr(tokens, idx, 'href'); if (href) { try { new URL(href); } catch (e) { // relative url: isRelative = true; } } if (!isRelative) { setAttr(tokens, idx, 'target', '_blank'); } // pass token to default renderer: return defaultLinkRender(tokens, idx, options, env, self); }; } if (opt.lazy_image) { // add loading="lazy" for image: const defaultImgRender = md.renderer.rules.image || function (tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; md.renderer.rules.image = function (tokens, idx, options, env, self) { // get src: let src = getAttr(tokens, idx, 'src'); if (src && !src.trim().startsWith('data:')) { setAttr(tokens, idx, 'loading', 'lazy'); } // pass token to default renderer: return defaultImgRender(tokens, idx, options, env, self); }; } if (opt.math) { md.use(katex, { mhchem: true }); } if (opt.footnote) { md.use(footnote_plugin); } return { instance: md, render: (str) => { return md.render(str); }, addContainer: (type) => { console.debug(`add container support: type = ${type}.`); let regex = RegExp(`^\\s*${type}\\s+(.*)\\s*$`); md.use(MarkdownItContainer, type, { validate: (params) => { return params.trim().match(regex); }, render: (tokens, idx) => { let m = tokens[idx].info.trim().match(regex); if (tokens[idx].nesting === 1) { let subtype = m[1].trim(); return subtype ? `<div class="${type} ${type}-${subtype}">` : `<div class="${type}">`; } else { return '</div>\n'; } } }); } }; } export default createMarkdown;