markdownEngine.ts 4.8 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as path from 'path';
8
import { TableOfContentsProvider } from './tableOfContentsProvider';
9
import { MarkdownIt, Token } from 'markdown-it';
10

11
const FrontMatterRegex = /^---\s*[^]*?(-{3}|\.{3})\s*/;
12

13 14 15
export class MarkdownEngine {
	private md: MarkdownIt;

M
Matt Bierner 已提交
16 17
	private firstLine: number;

18 19
	private currentDocument: vscode.Uri;

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
	private plugins: Array<(md: any) => any> = [];

	public addPlugin(factory: (md: any) => any): void {
		if (this.md) {
			this.usePlugin(factory);
		} else {
			this.plugins.push(factory);
		}
	}

	private usePlugin(factory: (md: any) => any): void {
		try {
			this.md = factory(this.md);
		} catch (e) {
			// noop
		}
	}

38
	private async getEngine(): Promise<MarkdownIt> {
39
		if (!this.md) {
40 41 42
			const hljs = await import('highlight.js');
			const mdnh = await import('markdown-it-named-headers');
			this.md = (await import('markdown-it'))({
43 44 45 46 47 48 49
				html: true,
				highlight: (str: string, lang: string) => {
					if (lang && hljs.getLanguage(lang)) {
						try {
							return `<pre class="hljs"><code><div>${hljs.highlight(lang, str, true).value}</div></code></pre>`;
						} catch (error) { }
					}
50
					return `<pre class="hljs"><code><div>${this.md.utils.escapeHtml(str)}</div></code></pre>`;
51
				}
52
			}).use(mdnh, {
53
				slugify: (header: string) => TableOfContentsProvider.slugify(header)
54
			});
55

56 57 58 59 60
			for (const plugin of this.plugins) {
				this.usePlugin(plugin);
			}
			this.plugins = [];

61 62 63
			for (const renderName of ['paragraph_open', 'heading_open', 'image', 'code_block', 'blockquote_open', 'list_item_open']) {
				this.addLineNumberRenderer(this.md, renderName);
			}
64 65 66 67

			this.addLinkNormalizer(this.md);
			this.addLinkValidator(this.md);
		}
68 69 70 71 72 73

		const config = vscode.workspace.getConfiguration('markdown');
		this.md.set({
			breaks: config.get('preview.breaks', false),
			linkify: config.get('preview.linkify', true)
		});
74 75 76
		return this.md;
	}

77 78 79 80 81 82 83 84 85 86 87
	private stripFrontmatter(text: string): { text: string, offset: number } {
		let offset = 0;
		const frontMatterMatch = FrontMatterRegex.exec(text);
		if (frontMatterMatch) {
			const frontMatter = frontMatterMatch[0];
			offset = frontMatter.split(/\r\n|\n|\r/g).length - 1;
			text = text.substr(frontMatter.length);
		}
		return { text, offset };
	}

88
	public async render(document: vscode.Uri, stripFrontmatter: boolean, text: string): Promise<string> {
89 90 91 92 93 94
		let offset = 0;
		if (stripFrontmatter) {
			const markdownContent = this.stripFrontmatter(text);
			offset = markdownContent.offset;
			text = markdownContent.text;
		}
95
		this.currentDocument = document;
96
		this.firstLine = offset;
97 98
		const engine = await this.getEngine();
		return engine.render(text);
99 100
	}

101
	public async parse(document: vscode.Uri, source: string): Promise<Token[]> {
A
Alex Dima 已提交
102
		const { text, offset } = this.stripFrontmatter(source);
103
		this.currentDocument = document;
104 105 106
		const engine = await this.getEngine();

		return engine.parse(text, {}).map(token => {
107 108 109 110 111
			if (token.map) {
				token.map[0] += offset;
			}
			return token;
		});
112 113 114 115 116 117
	}

	private addLineNumberRenderer(md: any, ruleName: string): void {
		const original = md.renderer.rules[ruleName];
		md.renderer.rules[ruleName] = (tokens: any, idx: number, options: any, env: any, self: any) => {
			const token = tokens[idx];
118
			if (token.map && token.map.length) {
M
Matt Bierner 已提交
119
				token.attrSet('data-line', this.firstLine + token.map[0]);
120 121
				token.attrJoin('class', 'code-line');
			}
122

123 124 125 126 127 128 129 130 131 132 133 134 135
			if (original) {
				return original(tokens, idx, options, env, self);
			} else {
				return self.renderToken(tokens, idx, options, env, self);
			}
		};
	}

	private addLinkNormalizer(md: any): void {
		const normalizeLink = md.normalizeLink;
		md.normalizeLink = (link: string) => {
			try {
				let uri = vscode.Uri.parse(link);
136
				if (!uri.scheme && uri.path && !uri.fragment) {
137 138
					// Assume it must be a file
					if (uri.path[0] === '/') {
139 140 141 142
						const root = vscode.workspace.getWorkspaceFolder(this.currentDocument);
						if (root) {
							uri = vscode.Uri.file(path.join(root.uri.fsPath, uri.path));
						}
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
					} else {
						uri = vscode.Uri.file(path.join(path.dirname(this.currentDocument.path), uri.path));
					}
					return normalizeLink(uri.toString(true));
				}
			} catch (e) {
				// noop
			}
			return normalizeLink(link);
		};
	}

	private addLinkValidator(md: any): void {
		const validateLink = md.validateLink;
		md.validateLink = (link: string) => {
			// support file:// links
M
Matt Bierner 已提交
159
			return validateLink(link) || link.indexOf('file:') === 0;
160 161 162
		};
	}
}