diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts
index ddf32163..f78abc43 100644
--- a/src/leetCodeSolutionProvider.ts
+++ b/src/leetCodeSolutionProvider.ts
@@ -1,41 +1,20 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.
-import * as hljs from "highlight.js";
-import * as MarkdownIt from "markdown-it";
-import * as path from "path";
-import * as vscode from "vscode";
import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode";
-import { leetCodeChannel } from "./leetCodeChannel";
import { IProblem } from "./shared";
+import { MarkdownEngine } from "./webview/MarkdownEngine";
class LeetCodeSolutionProvider implements Disposable {
private context: ExtensionContext;
private panel: WebviewPanel | undefined;
- private markdown: MarkdownIt;
- private markdownPath: string; // path of vscode built-in markdown extension
+ private mdEngine: MarkdownEngine;
private solution: Solution;
public initialize(context: ExtensionContext): void {
this.context = context;
- this.markdown = new MarkdownIt({
- linkify: true,
- typographer: true,
- highlight: this.codeHighlighter.bind(this),
- });
- this.markdownPath = path.join(vscode.env.appRoot, "extensions", "markdown-language-features");
-
- // Override code_block rule for highlighting in solution language
- // tslint:disable-next-line:typedef
- this.markdown.renderer.rules["code_block"] = (tokens, idx, options, _, self) => {
- const highlight: string = options.highlight(tokens[idx].content, undefined);
- return [
- `
`,
- highlight || this.markdown.utils.escapeHtml(tokens[idx].content),
- "
",
- ].join("\n");
- };
+ this.mdEngine = new MarkdownEngine();
}
public async show(solutionString: string, problem: IProblem): Promise {
@@ -43,7 +22,7 @@ class LeetCodeSolutionProvider implements Disposable {
this.panel = window.createWebviewPanel("leetCode.solution", "Top Voted Solution", ViewColumn.Active, {
retainContextWhenHidden: true,
enableFindWidget: true,
- localResourceRoots: [vscode.Uri.file(path.join(this.markdownPath, "media"))],
+ localResourceRoots: this.mdEngine.localResourceRoots,
});
this.panel.onDidDispose(() => {
@@ -76,41 +55,20 @@ class LeetCodeSolutionProvider implements Disposable {
return solution;
}
- private codeHighlighter(code: string, lang: string | undefined): string {
- if (!lang) {
- lang = this.solution.lang;
- }
- if (hljs.getLanguage(lang)) {
- try {
- return hljs.highlight(lang, code, true).value;
- } catch (error) { /* do not highlight */ }
- }
- return ""; // use external default escaping
- }
-
- private getMarkdownStyles(): vscode.Uri[] {
- try {
- const stylePaths: string[] = require(path.join(this.markdownPath, "package.json"))["contributes"]["markdown.previewStyles"];
- return stylePaths.map((p: string) => vscode.Uri.file(path.join(this.markdownPath, p)).with({ scheme: "vscode-resource" }));
- } catch (error) {
- leetCodeChannel.appendLine("[Error] Fail to load built-in markdown style file.");
- return [];
- }
- }
-
private getWebViewContent(solution: Solution): string {
- const styles: string = this.getMarkdownStyles()
- .map((style: vscode.Uri) => ``)
- .join("\n");
+ const styles: string = this.mdEngine.getStylesHTML();
const { title, url, lang, author, votes } = solution;
- const head: string = this.markdown.render(`# [${title}](${url})`);
+ const head: string = this.mdEngine.render(`# [${title}](${url})`);
const auth: string = `[${author}](https://leetcode.com/${author}/)`;
- const info: string = this.markdown.render([
+ const info: string = this.mdEngine.render([
`| Language | Author | Votes |`,
`| :------: | :------: | :------: |`,
`| ${lang} | ${auth} | ${votes} |`,
].join("\n"));
- const body: string = this.markdown.render(solution.body);
+ const body: string = this.mdEngine.render(solution.body, {
+ lang: this.solution.lang,
+ host: "https://discuss.leetcode.com/",
+ });
return `
diff --git a/src/webview/MarkdownEngine.ts b/src/webview/MarkdownEngine.ts
new file mode 100644
index 00000000..bfff64d8
--- /dev/null
+++ b/src/webview/MarkdownEngine.ts
@@ -0,0 +1,105 @@
+import * as hljs from "highlight.js";
+import * as MarkdownIt from "markdown-it";
+import * as os from "os";
+import * as path from "path";
+import * as vscode from "vscode";
+import { leetCodeChannel } from "../leetCodeChannel";
+
+export class MarkdownEngine {
+
+ private readonly engine: MarkdownIt;
+ private readonly extRoot: string; // root path of vscode built-in markdown extension
+
+ public constructor() {
+ this.engine = this.initEngine();
+ this.extRoot = path.join(vscode.env.appRoot, "extensions", "markdown-language-features");
+ }
+
+ public get localResourceRoots(): vscode.Uri[] {
+ return [vscode.Uri.file(path.join(this.extRoot, "media"))];
+ }
+
+ public get styles(): vscode.Uri[] {
+ try {
+ const stylePaths: string[] = require(path.join(this.extRoot, "package.json"))["contributes"]["markdown.previewStyles"];
+ return stylePaths.map((p: string) => vscode.Uri.file(path.join(this.extRoot, p)).with({ scheme: "vscode-resource" }));
+ } catch (error) {
+ leetCodeChannel.appendLine("[Error] Fail to load built-in markdown style file.");
+ return [];
+ }
+ }
+
+ public getStylesHTML(): string {
+ return this.styles.map((style: vscode.Uri) => ``).join(os.EOL);
+ }
+
+ public render(md: string, env?: any): string {
+ return this.engine.render(md, env);
+ }
+
+ private initEngine(): MarkdownIt {
+ const md: MarkdownIt = new MarkdownIt({
+ linkify: true,
+ typographer: true,
+ highlight: (code: string, lang?: string): string => {
+ switch (lang && lang.toLowerCase()) {
+ case "mysql":
+ lang = "sql"; break;
+ case "json5":
+ lang = "json"; break;
+ case "python3":
+ lang = "python"; break;
+ }
+ if (lang && hljs.getLanguage(lang)) {
+ try {
+ return hljs.highlight(lang, code, true).value;
+ } catch (error) { /* do not highlight */ }
+ }
+ return ""; // use external default escaping
+ },
+ });
+
+ this.addCodeBlockHighlight(md);
+ this.addImageUrlCompletion(md);
+ this.addLinkValidator(md);
+ return md;
+ }
+
+ private addCodeBlockHighlight(md: MarkdownIt): void {
+ const codeBlock: MarkdownIt.TokenRender = md.renderer.rules["code_block"];
+ // tslint:disable-next-line:typedef
+ md.renderer.rules["code_block"] = (tokens, idx, options, env, self) => {
+ // if any token uses lang-specified code fence, then do not highlight code block
+ if (tokens.some((token: any) => token.type === "fence")) {
+ return codeBlock(tokens, idx, options, env, self);
+ }
+ // otherwise, highlight with default lang in env object.
+ const highlighted: string = options.highlight(tokens[idx].content, env.lang);
+ return [
+ ``,
+ highlighted || md.utils.escapeHtml(tokens[idx].content),
+ "
",
+ ].join(os.EOL);
+ };
+ }
+
+ private addImageUrlCompletion(md: MarkdownIt): void {
+ const image: MarkdownIt.TokenRender = md.renderer.rules["image"];
+ // tslint:disable-next-line:typedef
+ md.renderer.rules["image"] = (tokens, idx, options, env, self) => {
+ const imageSrc: string[] | undefined = tokens[idx].attrs.find((value: string[]) => value[0] === "src");
+ if (env.host && imageSrc && imageSrc[1].startsWith("/")) {
+ imageSrc[1] = `${env.host}${imageSrc[1]}`;
+ }
+ return image(tokens, idx, options, env, self);
+ };
+ }
+
+ private addLinkValidator(md: MarkdownIt): void {
+ const validateLink: (link: string) => boolean = md.validateLink;
+ md.validateLink = (link: string): boolean => {
+ // support file:// protocal link
+ return validateLink(link) || link.startsWith("file:");
+ };
+ }
+}