Skip to content

Commit 0552af2

Browse files
Vigilansjdneo
authored andcommitted
Refactor solution webview to reuse markdown engine (LeetCode-OpenSource#224)
1 parent 5f895a2 commit 0552af2

File tree

2 files changed

+116
-53
lines changed

2 files changed

+116
-53
lines changed

src/leetCodeSolutionProvider.ts

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,28 @@
11
// Copyright (c) jdneo. All rights reserved.
22
// Licensed under the MIT license.
33

4-
import * as hljs from "highlight.js";
5-
import * as MarkdownIt from "markdown-it";
6-
import * as path from "path";
7-
import * as vscode from "vscode";
84
import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode";
9-
import { leetCodeChannel } from "./leetCodeChannel";
105
import { IProblem } from "./shared";
6+
import { MarkdownEngine } from "./webview/MarkdownEngine";
117

128
class LeetCodeSolutionProvider implements Disposable {
139

1410
private context: ExtensionContext;
1511
private panel: WebviewPanel | undefined;
16-
private markdown: MarkdownIt;
17-
private markdownPath: string; // path of vscode built-in markdown extension
12+
private mdEngine: MarkdownEngine;
1813
private solution: Solution;
1914

2015
public initialize(context: ExtensionContext): void {
2116
this.context = context;
22-
this.markdown = new MarkdownIt({
23-
linkify: true,
24-
typographer: true,
25-
highlight: this.codeHighlighter.bind(this),
26-
});
27-
this.markdownPath = path.join(vscode.env.appRoot, "extensions", "markdown-language-features");
28-
29-
// Override code_block rule for highlighting in solution language
30-
// tslint:disable-next-line:typedef
31-
this.markdown.renderer.rules["code_block"] = (tokens, idx, options, _, self) => {
32-
const highlight: string = options.highlight(tokens[idx].content, undefined);
33-
return [
34-
`<pre><code ${self.renderAttrs(tokens[idx])} >`,
35-
highlight || this.markdown.utils.escapeHtml(tokens[idx].content),
36-
"</code></pre>",
37-
].join("\n");
38-
};
17+
this.mdEngine = new MarkdownEngine();
3918
}
4019

4120
public async show(solutionString: string, problem: IProblem): Promise<void> {
4221
if (!this.panel) {
4322
this.panel = window.createWebviewPanel("leetCode.solution", "Top Voted Solution", ViewColumn.Active, {
4423
retainContextWhenHidden: true,
4524
enableFindWidget: true,
46-
localResourceRoots: [vscode.Uri.file(path.join(this.markdownPath, "media"))],
25+
localResourceRoots: this.mdEngine.localResourceRoots,
4726
});
4827

4928
this.panel.onDidDispose(() => {
@@ -76,41 +55,20 @@ class LeetCodeSolutionProvider implements Disposable {
7655
return solution;
7756
}
7857

79-
private codeHighlighter(code: string, lang: string | undefined): string {
80-
if (!lang) {
81-
lang = this.solution.lang;
82-
}
83-
if (hljs.getLanguage(lang)) {
84-
try {
85-
return hljs.highlight(lang, code, true).value;
86-
} catch (error) { /* do not highlight */ }
87-
}
88-
return ""; // use external default escaping
89-
}
90-
91-
private getMarkdownStyles(): vscode.Uri[] {
92-
try {
93-
const stylePaths: string[] = require(path.join(this.markdownPath, "package.json"))["contributes"]["markdown.previewStyles"];
94-
return stylePaths.map((p: string) => vscode.Uri.file(path.join(this.markdownPath, p)).with({ scheme: "vscode-resource" }));
95-
} catch (error) {
96-
leetCodeChannel.appendLine("[Error] Fail to load built-in markdown style file.");
97-
return [];
98-
}
99-
}
100-
10158
private getWebViewContent(solution: Solution): string {
102-
const styles: string = this.getMarkdownStyles()
103-
.map((style: vscode.Uri) => `<link rel="stylesheet" type="text/css" href="${style.toString()}">`)
104-
.join("\n");
59+
const styles: string = this.mdEngine.getStylesHTML();
10560
const { title, url, lang, author, votes } = solution;
106-
const head: string = this.markdown.render(`# [${title}](${url})`);
61+
const head: string = this.mdEngine.render(`# [${title}](${url})`);
10762
const auth: string = `[${author}](https://leetcode.com/${author}/)`;
108-
const info: string = this.markdown.render([
63+
const info: string = this.mdEngine.render([
10964
`| Language | Author | Votes |`,
11065
`| :------: | :------: | :------: |`,
11166
`| ${lang} | ${auth} | ${votes} |`,
11267
].join("\n"));
113-
const body: string = this.markdown.render(solution.body);
68+
const body: string = this.mdEngine.render(solution.body, {
69+
lang: this.solution.lang,
70+
host: "https://discuss.leetcode.com/",
71+
});
11472
return `
11573
<!DOCTYPE html>
11674
<html>

src/webview/MarkdownEngine.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import * as hljs from "highlight.js";
2+
import * as MarkdownIt from "markdown-it";
3+
import * as os from "os";
4+
import * as path from "path";
5+
import * as vscode from "vscode";
6+
import { leetCodeChannel } from "../leetCodeChannel";
7+
8+
export class MarkdownEngine {
9+
10+
private readonly engine: MarkdownIt;
11+
private readonly extRoot: string; // root path of vscode built-in markdown extension
12+
13+
public constructor() {
14+
this.engine = this.initEngine();
15+
this.extRoot = path.join(vscode.env.appRoot, "extensions", "markdown-language-features");
16+
}
17+
18+
public get localResourceRoots(): vscode.Uri[] {
19+
return [vscode.Uri.file(path.join(this.extRoot, "media"))];
20+
}
21+
22+
public get styles(): vscode.Uri[] {
23+
try {
24+
const stylePaths: string[] = require(path.join(this.extRoot, "package.json"))["contributes"]["markdown.previewStyles"];
25+
return stylePaths.map((p: string) => vscode.Uri.file(path.join(this.extRoot, p)).with({ scheme: "vscode-resource" }));
26+
} catch (error) {
27+
leetCodeChannel.appendLine("[Error] Fail to load built-in markdown style file.");
28+
return [];
29+
}
30+
}
31+
32+
public getStylesHTML(): string {
33+
return this.styles.map((style: vscode.Uri) => `<link rel="stylesheet" type="text/css" href="${style.toString()}">`).join(os.EOL);
34+
}
35+
36+
public render(md: string, env?: any): string {
37+
return this.engine.render(md, env);
38+
}
39+
40+
private initEngine(): MarkdownIt {
41+
const md: MarkdownIt = new MarkdownIt({
42+
linkify: true,
43+
typographer: true,
44+
highlight: (code: string, lang?: string): string => {
45+
switch (lang && lang.toLowerCase()) {
46+
case "mysql":
47+
lang = "sql"; break;
48+
case "json5":
49+
lang = "json"; break;
50+
case "python3":
51+
lang = "python"; break;
52+
}
53+
if (lang && hljs.getLanguage(lang)) {
54+
try {
55+
return hljs.highlight(lang, code, true).value;
56+
} catch (error) { /* do not highlight */ }
57+
}
58+
return ""; // use external default escaping
59+
},
60+
});
61+
62+
this.addCodeBlockHighlight(md);
63+
this.addImageUrlCompletion(md);
64+
this.addLinkValidator(md);
65+
return md;
66+
}
67+
68+
private addCodeBlockHighlight(md: MarkdownIt): void {
69+
const codeBlock: MarkdownIt.TokenRender = md.renderer.rules["code_block"];
70+
// tslint:disable-next-line:typedef
71+
md.renderer.rules["code_block"] = (tokens, idx, options, env, self) => {
72+
// if any token uses lang-specified code fence, then do not highlight code block
73+
if (tokens.some((token: any) => token.type === "fence")) {
74+
return codeBlock(tokens, idx, options, env, self);
75+
}
76+
// otherwise, highlight with default lang in env object.
77+
const highlighted: string = options.highlight(tokens[idx].content, env.lang);
78+
return [
79+
`<pre><code ${self.renderAttrs(tokens[idx])} >`,
80+
highlighted || md.utils.escapeHtml(tokens[idx].content),
81+
"</code></pre>",
82+
].join(os.EOL);
83+
};
84+
}
85+
86+
private addImageUrlCompletion(md: MarkdownIt): void {
87+
const image: MarkdownIt.TokenRender = md.renderer.rules["image"];
88+
// tslint:disable-next-line:typedef
89+
md.renderer.rules["image"] = (tokens, idx, options, env, self) => {
90+
const imageSrc: string[] | undefined = tokens[idx].attrs.find((value: string[]) => value[0] === "src");
91+
if (env.host && imageSrc && imageSrc[1].startsWith("/")) {
92+
imageSrc[1] = `${env.host}${imageSrc[1]}`;
93+
}
94+
return image(tokens, idx, options, env, self);
95+
};
96+
}
97+
98+
private addLinkValidator(md: MarkdownIt): void {
99+
const validateLink: (link: string) => boolean = md.validateLink;
100+
md.validateLink = (link: string): boolean => {
101+
// support file:// protocal link
102+
return validateLink(link) || link.startsWith("file:");
103+
};
104+
}
105+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy