From 69a6686ed374e2a6d9d7bbc4c36d3470e69d7ad9 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Tue, 19 Mar 2019 22:40:47 +0800 Subject: [PATCH 1/6] Add markdown engine --- src/webview/markdownEngine.ts | 91 +++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/webview/markdownEngine.ts diff --git a/src/webview/markdownEngine.ts b/src/webview/markdownEngine.ts new file mode 100644 index 00000000..493d100e --- /dev/null +++ b/src/webview/markdownEngine.ts @@ -0,0 +1,91 @@ +import * as hljs from "highlight.js"; +import * as MarkdownIt from "markdown-it"; +import * as path from "path"; +import * as vscode from "vscode"; +import { leetCodeChannel } from "../leetCodeChannel"; + +export class MarkdownEngine { + + public readonly engine: MarkdownIt; + public 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("\n"); + } + + private initEngine(): MarkdownIt { + const md: MarkdownIt = new MarkdownIt({ + linkify: true, + typographer: true, + highlight: (code: string, lang?: string): string => { + if (lang && ["tsx", "typescriptreact"].indexOf(lang.toLocaleLowerCase()) >= 0) { + lang = "jsx"; + } + if (lang && lang.toLocaleLowerCase() === "python3") { + lang = "python"; + } + if (lang && lang.toLocaleLowerCase() === "c#") { + lang = "cs"; + } + if (lang && lang.toLocaleLowerCase() === "json5") { + lang = "json"; + } + 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.addLinkValidator(md); + return md; + } + + private addCodeBlockHighlight(md: MarkdownIt): void { + const origin: 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 origin(tokens, idx, options, env, self); + } + // otherwise, highlight with undefined lang, which could be handled in outside logic. + const highlighted: string = options.highlight(tokens[idx].content, undefined); + return [ + `
`,
+                highlighted || md.utils.escapeHtml(tokens[idx].content),
+                "
", + ].join("\n"); + }; + } + + 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:"); + }; + } +} From a5be4fea66d2878d7a095e870cd532f35b4e4c39 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Tue, 19 Mar 2019 22:41:54 +0800 Subject: [PATCH 2/6] Refactor solution webview with MarkdownEngine --- src/leetCodeSolutionProvider.ts | 56 ++++++--------------------------- src/webview/markdownEngine.ts | 8 +++++ 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index ddf32163..8dcba314 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -1,40 +1,26 @@ // 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 markdown: 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"); + this.markdown = new MarkdownEngine(); - // Override code_block rule for highlighting in solution language + // The @types typedef of `highlight` is wrong, which should return a string. // 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"); + const highlight = this.markdown.options.highlight as (code: string, lang?: string) => string; + this.markdown.options.highlight = (code: string, lang?: string): string => { + return highlight(code, lang || this.solution.lang); }; } @@ -43,7 +29,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.markdown.localResourceRoots, }); this.panel.onDidDispose(() => { @@ -76,32 +62,8 @@ 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.markdown.getStylesHTML(); const { title, url, lang, author, votes } = solution; const head: string = this.markdown.render(`# [${title}](${url})`); const auth: string = `[${author}](https://leetcode.com/${author}/)`; diff --git a/src/webview/markdownEngine.ts b/src/webview/markdownEngine.ts index 493d100e..fadee8b4 100644 --- a/src/webview/markdownEngine.ts +++ b/src/webview/markdownEngine.ts @@ -28,10 +28,18 @@ export class MarkdownEngine { } } + public get options(): MarkdownIt.Options { + return (this.engine as any).options; + } + public getStylesHTML(): string { return this.styles.map((style: vscode.Uri) => ``).join("\n"); } + public render(md: string, env?: any): string { + return this.engine.render(md, env); + } + private initEngine(): MarkdownIt { const md: MarkdownIt = new MarkdownIt({ linkify: true, From 508369226f1eb6afc15e7b21eb3df9252b20fe1f Mon Sep 17 00:00:00 2001 From: Vigilans Date: Tue, 19 Mar 2019 22:42:43 +0800 Subject: [PATCH 3/6] Add image url completion for solution markdown --- src/leetCodeSolutionProvider.ts | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index 8dcba314..00385c61 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -1,6 +1,7 @@ // Copyright (c) jdneo. All rights reserved. // Licensed under the MIT license. +import { TokenRender } from "markdown-it"; import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; import { IProblem } from "./shared"; import { MarkdownEngine } from "./webview/markdownEngine"; @@ -15,13 +16,8 @@ class LeetCodeSolutionProvider implements Disposable { public initialize(context: ExtensionContext): void { this.context = context; this.markdown = new MarkdownEngine(); - - // The @types typedef of `highlight` is wrong, which should return a string. - // tslint:disable-next-line:typedef - const highlight = this.markdown.options.highlight as (code: string, lang?: string) => string; - this.markdown.options.highlight = (code: string, lang?: string): string => { - return highlight(code, lang || this.solution.lang); - }; + this.addMdDefaultHighlight(); // use solution language if highting block lang is undefined + this.addMdImageUrlCompletion(); // complete the image path url with leetcode hostname } public async show(solutionString: string, problem: IProblem): Promise { @@ -49,6 +45,27 @@ class LeetCodeSolutionProvider implements Disposable { } } + private addMdDefaultHighlight(): void { + // The @types typedef of `highlight` is wrong, which should return a string. + // tslint:disable-next-line:typedef + const highlight = this.markdown.options.highlight as (code: string, lang?: string) => string; + this.markdown.options.highlight = (code: string, lang?: string): string => { + return highlight(code, lang || this.solution.lang); + }; + } + + private addMdImageUrlCompletion(): void { + const image: TokenRender = this.markdown.engine.renderer.rules["image"]; + // tslint:disable-next-line:typedef + this.markdown.engine.renderer.rules["image"] = (tokens, idx, options, env, self) => { + const imageSrc: string[] | undefined = tokens[idx].attrs.find((value: string[]) => value[0] === "src"); + if (imageSrc && imageSrc[1].startsWith("/")) { + imageSrc[1] = `https://discuss.leetcode.com${imageSrc[1]}`; + } + return image(tokens, idx, options, env, self); + }; + } + private parseSolution(raw: string): Solution { const solution: Solution = new Solution(); // [^] matches everything including \n, yet can be replaced by . in ES2018's `m` flag From d042c06faa39e5b365794ed50364c36b3bccda8f Mon Sep 17 00:00:00 2001 From: Vigilans Date: Wed, 20 Mar 2019 19:48:13 +0800 Subject: [PATCH 4/6] Address comments in review --- src/leetCodeSolutionProvider.ts | 31 ++++------------------- src/webview/markdownEngine.ts | 44 ++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 43 deletions(-) diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index 00385c61..f9cedf29 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -1,10 +1,9 @@ // Copyright (c) jdneo. All rights reserved. // Licensed under the MIT license. -import { TokenRender } from "markdown-it"; import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; import { IProblem } from "./shared"; -import { MarkdownEngine } from "./webview/markdownEngine"; +import { MarkdownEngine } from "./webview/MarkdownEngine"; class LeetCodeSolutionProvider implements Disposable { @@ -16,8 +15,6 @@ class LeetCodeSolutionProvider implements Disposable { public initialize(context: ExtensionContext): void { this.context = context; this.markdown = new MarkdownEngine(); - this.addMdDefaultHighlight(); // use solution language if highting block lang is undefined - this.addMdImageUrlCompletion(); // complete the image path url with leetcode hostname } public async show(solutionString: string, problem: IProblem): Promise { @@ -45,27 +42,6 @@ class LeetCodeSolutionProvider implements Disposable { } } - private addMdDefaultHighlight(): void { - // The @types typedef of `highlight` is wrong, which should return a string. - // tslint:disable-next-line:typedef - const highlight = this.markdown.options.highlight as (code: string, lang?: string) => string; - this.markdown.options.highlight = (code: string, lang?: string): string => { - return highlight(code, lang || this.solution.lang); - }; - } - - private addMdImageUrlCompletion(): void { - const image: TokenRender = this.markdown.engine.renderer.rules["image"]; - // tslint:disable-next-line:typedef - this.markdown.engine.renderer.rules["image"] = (tokens, idx, options, env, self) => { - const imageSrc: string[] | undefined = tokens[idx].attrs.find((value: string[]) => value[0] === "src"); - if (imageSrc && imageSrc[1].startsWith("/")) { - imageSrc[1] = `https://discuss.leetcode.com${imageSrc[1]}`; - } - return image(tokens, idx, options, env, self); - }; - } - private parseSolution(raw: string): Solution { const solution: Solution = new Solution(); // [^] matches everything including \n, yet can be replaced by . in ES2018's `m` flag @@ -89,7 +65,10 @@ class LeetCodeSolutionProvider implements Disposable { `| :------: | :------: | :------: |`, `| ${lang} | ${auth} | ${votes} |`, ].join("\n")); - const body: string = this.markdown.render(solution.body); + const body: string = this.markdown.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 index fadee8b4..d2b4e3bc 100644 --- a/src/webview/markdownEngine.ts +++ b/src/webview/markdownEngine.ts @@ -1,5 +1,6 @@ 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"; @@ -33,7 +34,7 @@ export class MarkdownEngine { } public getStylesHTML(): string { - return this.styles.map((style: vscode.Uri) => ``).join("\n"); + return this.styles.map((style: vscode.Uri) => ``).join(os.EOL); } public render(md: string, env?: any): string { @@ -45,17 +46,13 @@ export class MarkdownEngine { linkify: true, typographer: true, highlight: (code: string, lang?: string): string => { - if (lang && ["tsx", "typescriptreact"].indexOf(lang.toLocaleLowerCase()) >= 0) { - lang = "jsx"; - } - if (lang && lang.toLocaleLowerCase() === "python3") { - lang = "python"; - } - if (lang && lang.toLocaleLowerCase() === "c#") { - lang = "cs"; - } - if (lang && lang.toLocaleLowerCase() === "json5") { - lang = "json"; + 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 { @@ -67,25 +64,38 @@ export class MarkdownEngine { }); this.addCodeBlockHighlight(md); + this.addImageUrlCompletion(md); this.addLinkValidator(md); return md; } private addCodeBlockHighlight(md: MarkdownIt): void { - const origin: MarkdownIt.TokenRender = md.renderer.rules["code_block"]; + 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 origin(tokens, idx, options, env, self); + return codeBlock(tokens, idx, options, env, self); } - // otherwise, highlight with undefined lang, which could be handled in outside logic. - const highlighted: string = options.highlight(tokens[idx].content, undefined); + // 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("\n"); + ].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); }; } From 86354ced4eecaf6a67b5767199546ac60b060446 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Wed, 20 Mar 2019 20:00:18 +0800 Subject: [PATCH 5/6] Solve case sensitive renaming problem --- src/webview/{markdownEngine.ts => MarkdownEngine.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/webview/{markdownEngine.ts => MarkdownEngine.ts} (100%) diff --git a/src/webview/markdownEngine.ts b/src/webview/MarkdownEngine.ts similarity index 100% rename from src/webview/markdownEngine.ts rename to src/webview/MarkdownEngine.ts From 33ed68a47a9d6351732923b0a23e04fb0eaf189d Mon Sep 17 00:00:00 2001 From: Vigilans Date: Wed, 20 Mar 2019 21:01:30 +0800 Subject: [PATCH 6/6] Further address comments in review --- src/leetCodeSolutionProvider.ts | 14 +++++++------- src/webview/MarkdownEngine.ts | 8 ++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index f9cedf29..f78abc43 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -9,12 +9,12 @@ class LeetCodeSolutionProvider implements Disposable { private context: ExtensionContext; private panel: WebviewPanel | undefined; - private markdown: MarkdownEngine; + private mdEngine: MarkdownEngine; private solution: Solution; public initialize(context: ExtensionContext): void { this.context = context; - this.markdown = new MarkdownEngine(); + this.mdEngine = new MarkdownEngine(); } public async show(solutionString: string, problem: IProblem): Promise { @@ -22,7 +22,7 @@ class LeetCodeSolutionProvider implements Disposable { this.panel = window.createWebviewPanel("leetCode.solution", "Top Voted Solution", ViewColumn.Active, { retainContextWhenHidden: true, enableFindWidget: true, - localResourceRoots: this.markdown.localResourceRoots, + localResourceRoots: this.mdEngine.localResourceRoots, }); this.panel.onDidDispose(() => { @@ -56,16 +56,16 @@ class LeetCodeSolutionProvider implements Disposable { } private getWebViewContent(solution: Solution): string { - const styles: string = this.markdown.getStylesHTML(); + 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/", }); diff --git a/src/webview/MarkdownEngine.ts b/src/webview/MarkdownEngine.ts index d2b4e3bc..bfff64d8 100644 --- a/src/webview/MarkdownEngine.ts +++ b/src/webview/MarkdownEngine.ts @@ -7,8 +7,8 @@ import { leetCodeChannel } from "../leetCodeChannel"; export class MarkdownEngine { - public readonly engine: MarkdownIt; - public readonly extRoot: string; // root path of vscode built-in markdown extension + private readonly engine: MarkdownIt; + private readonly extRoot: string; // root path of vscode built-in markdown extension public constructor() { this.engine = this.initEngine(); @@ -29,10 +29,6 @@ export class MarkdownEngine { } } - public get options(): MarkdownIt.Options { - return (this.engine as any).options; - } - public getStylesHTML(): string { return this.styles.map((style: vscode.Uri) => ``).join(os.EOL); }