//// import { extensions, workspace, WorkspaceConfiguration } from 'vscode'; import { slugify } from './util'; import { MarkdownContribution, MarkdownContributionProvider, getMarkdownContributionProvider } from './markdownExtensions'; import MarkdownIt = require('markdown-it'); // extensions that treat specially export const extensionBlacklist = new Set(["vscode.markdown-language-features", "yzhang.markdown-all-in-one"]); interface IMarkdownEngine { } /** * A strict CommonMark only engine powered by `markdown-it`. */ class CommonmarkEngine implements IMarkdownEngine { readonly #engine: MarkdownIt; public get engine(): MarkdownIt { return this.#engine; } constructor() { this.#engine = new MarkdownIt('commonmark'); } } class MarkdownEngine { public cacheMd: MarkdownIt; public async getEngine() { if (!this.cacheMd) { this.cacheMd = await this.newEngine(); } return this.cacheMd; } public contributionsProvider = getMarkdownContributionProvider(); private _slugCount = new Map(); constructor() { this.cacheMd = null; this.contributionsProvider.onContributionsChanged(() => { this.newEngine().then((engine) => { this.cacheMd = engine; }) }); this.newEngine().then((engine) => { this.cacheMd = engine; }) } private async newEngine() { let md: MarkdownIt; const hljs = await import('highlight.js'); const mdtl = await import('markdown-it-task-lists'); const mdkt = await import('@neilsustc/markdown-it-katex'); //// Make a deep copy as `macros` will be modified by KaTeX during initialization let userMacros = JSON.parse(JSON.stringify(workspace.getConfiguration('markdown.extension.katex').get('macros'))); let katexOptions = { throwOnError: false }; if (Object.keys(userMacros).length !== 0) { katexOptions['macros'] = userMacros; } md = new MarkdownIt({ html: true, highlight: (str: string, lang?: string) => { lang = normalizeHighlightLang(lang); if (lang && hljs.getLanguage(lang)) { try { return `
${hljs.highlight(lang, str, true).value}
`; } catch (error) { } } return `
${md.utils.escapeHtml(str)}
`; } }); // contributions provided by this extension must be processed specially, // since this extension may not finish activing when a engine is needed to be created. md.use(mdtl).use(mdkt, katexOptions); if (!workspace.getConfiguration('markdown.extension.print').get('validateUrls', true)) { md.validateLink = () => true; } this.addNamedHeaders(md); for (const contribute of this.contributionsProvider.contributions) { if (extensionBlacklist.has(contribute.extensionId)) { continue; } md = await contribute.extendMarkdownIt(md); } return md; } public async render(text: string, config: WorkspaceConfiguration): Promise { const md: MarkdownIt = await this.getEngine(); md.set({ breaks: config.get('breaks', false), linkify: config.get('linkify', true) }); this._slugCount = new Map(); return md.render(text); } private addNamedHeaders(md: MarkdownIt): void { const originalHeadingOpen = md.renderer.rules.heading_open; md.renderer.rules.heading_open = function (tokens, idx, options, env, self) { const title = tokens[idx + 1].children.reduce((acc: string, t: any) => acc + t.content, ''); let slug = slugify(title); if (mdEngine._slugCount.has(slug)) { mdEngine._slugCount.set(slug, mdEngine._slugCount.get(slug) + 1); slug += '-' + mdEngine._slugCount.get(slug); } else { mdEngine._slugCount.set(slug, 0); } tokens[idx].attrs = tokens[idx].attrs || []; tokens[idx].attrs.push(['id', slug]); if (originalHeadingOpen) { return originalHeadingOpen(tokens, idx, options, env, self); } else { return self.renderToken(tokens, idx, options); } }; } } function normalizeHighlightLang(lang: string | undefined) { switch (lang && lang.toLowerCase()) { case 'tsx': case 'typescriptreact': return 'jsx'; case 'json5': case 'jsonc': return 'json'; case 'c#': case 'csharp': return 'cs'; default: return lang; } } export const mdEngine = new MarkdownEngine(); /** * A strict CommonMark only engine instance. */ export const commonmarkEngine = new CommonmarkEngine();