Skip to content

Commit c2c62cd

Browse files
authored
Merge pull request #20 from flyingcys/fix-markdwon
fix markdown UI renders
2 parents 41df4ce + f663b42 commit c2c62cd

File tree

11 files changed

+798
-1667
lines changed

11 files changed

+798
-1667
lines changed

package-lock.json

Lines changed: 315 additions & 1658 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,17 +188,28 @@
188188
"dependencies": {
189189
"@bugsplat/elfy": "^1.0.1",
190190
"@vue/shared": "^3.x.x",
191+
"@neilsustc/markdown-it-katex": "^1.0.0",
191192
"element-plus": "^2.4.2",
192193
"elfjs": "^2.2.0",
193194
"elfy": "^1.0.0",
194-
"marked": "^14.1.3",
195+
"highlight.js": "^11.10.0",
196+
"katex": "^0.16.11",
197+
"markdown-it": "^13.0.2",
198+
"markdown-it-deflist": "^2.1.0",
199+
"markdown-it-emoji": "^3.0.0",
200+
"markdown-it-footnote": "^4.0.0",
201+
"markdown-it-github-alerts": "^0.1.2",
202+
"markdown-it-sub": "^1.0.0",
203+
"markdown-it-sup": "^1.0.0",
204+
"markdown-it-task-lists": "^2.1.1",
195205
"vue": "^3.3.8",
196206
"vue-router": "^4.5.0"
197207
},
198208
"extensionDependencies": [
199209
"ms-python.python"
200210
],
201211
"devDependencies": {
212+
"@types/markdown-it": "^14.0.1",
202213
"@types/mocha": "^10.0.7",
203214
"@types/node": "^20.19.11",
204215
"@types/vscode": "^1.96.0",

src/markdown/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { MarkdownRenderer, markdownRenderer } from './renderer';
2+
export type { MarkdownRenderOptions } from './renderer';
3+
export { default as SlugifyMode } from './slugifyMode';

src/markdown/renderer.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import MarkdownIt from 'markdown-it';
2+
import markdownItTaskLists from 'markdown-it-task-lists';
3+
import markdownItGithubAlerts from 'markdown-it-github-alerts';
4+
import markdownItKatex from '@neilsustc/markdown-it-katex';
5+
import markdownItFootnote from 'markdown-it-footnote';
6+
import markdownItDeflist from 'markdown-it-deflist';
7+
import * as markdownItEmoji from 'markdown-it-emoji';
8+
import markdownItSub from 'markdown-it-sub';
9+
import markdownItSup from 'markdown-it-sup';
10+
import hljs from 'highlight.js';
11+
import { slugifyHeading } from './slugify';
12+
import SlugifyMode from './slugifyMode';
13+
14+
export interface MarkdownRenderOptions {
15+
breaks?: boolean;
16+
linkify?: boolean;
17+
enableMath?: boolean;
18+
slugifyMode?: SlugifyMode;
19+
mathMacros?: Record<string, string>;
20+
}
21+
22+
interface MathOptions {
23+
throwOnError?: boolean;
24+
macros?: Record<string, string>;
25+
}
26+
27+
interface EngineRecord {
28+
md: MarkdownIt;
29+
slugCount: Map<string, number>;
30+
}
31+
32+
export class MarkdownRenderer {
33+
private engines = new Map<string, EngineRecord>();
34+
35+
public render(text: string, options: MarkdownRenderOptions = {}): string {
36+
const enableMath = options.enableMath !== false;
37+
const slugifyMode = options.slugifyMode ?? SlugifyMode.GitHub;
38+
const key = this.composeEngineKey(enableMath, slugifyMode, options.mathMacros);
39+
const engineRecord = this.ensureEngine(key, enableMath, slugifyMode, options.mathMacros);
40+
41+
engineRecord.slugCount.clear();
42+
43+
engineRecord.md.set({
44+
breaks: options.breaks === true,
45+
linkify: options.linkify !== false,
46+
});
47+
48+
const env = Object.create(null);
49+
return engineRecord.md.render(text, env);
50+
}
51+
52+
private composeEngineKey(enableMath: boolean, slugifyMode: SlugifyMode, macros?: Record<string, string>): string {
53+
const macroEntries = macros ? Object.keys(macros).sort().map((k) => `${k}:${macros[k]}`) : [];
54+
return `${enableMath ? 'math' : 'nomath'}|${slugifyMode}|${macroEntries.join(',')}`;
55+
}
56+
57+
private ensureEngine(key: string, enableMath: boolean, slugifyMode: SlugifyMode, macros?: Record<string, string>): EngineRecord {
58+
let record = this.engines.get(key);
59+
if (!record) {
60+
const slugCount = new Map<string, number>();
61+
const md = this.createEngine(enableMath, slugCount, slugifyMode, macros);
62+
record = { md, slugCount };
63+
this.engines.set(key, record);
64+
}
65+
return record;
66+
}
67+
68+
private createEngine(enableMath: boolean, slugCount: Map<string, number>, slugifyMode: SlugifyMode, macros?: Record<string, string>): MarkdownIt {
69+
const md = new MarkdownIt({
70+
html: true,
71+
highlight: (str: string, lang?: string) => {
72+
if (lang) {
73+
const normalized = this.normalizeHighlightLang(lang);
74+
if (normalized && hljs.getLanguage(normalized)) {
75+
try {
76+
return hljs.highlight(str, { language: normalized, ignoreIllegals: true }).value;
77+
} catch {
78+
// ignore highlighting errors and fallback to default rendering
79+
}
80+
}
81+
}
82+
return '';
83+
}
84+
});
85+
86+
md.use(markdownItTaskLists, { enabled: true, label: true, labelAfter: false });
87+
md.use(markdownItGithubAlerts, { matchCaseSensitive: false });
88+
md.use(markdownItFootnote);
89+
md.use(markdownItDeflist);
90+
md.use(markdownItEmoji.full || markdownItEmoji);
91+
md.use(markdownItSub);
92+
md.use(markdownItSup);
93+
94+
if (enableMath) {
95+
require('katex/contrib/mhchem');
96+
const katexOptions: MathOptions = { throwOnError: false };
97+
if (macros && Object.keys(macros).length > 0) {
98+
katexOptions.macros = JSON.parse(JSON.stringify(macros));
99+
}
100+
md.use(markdownItKatex, katexOptions);
101+
}
102+
103+
this.addNamedHeaders(md, slugCount, slugifyMode);
104+
return md;
105+
}
106+
107+
private addNamedHeaders(md: MarkdownIt, slugCount: Map<string, number>, slugifyMode: SlugifyMode): void {
108+
const originalHeadingOpen = md.renderer.rules.heading_open ?? ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
109+
110+
md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
111+
const raw = tokens[idx + 1]?.content ?? '';
112+
const baseSlug = slugifyHeading(raw, env ?? Object.create(null), slugifyMode) || 'section';
113+
const finalSlug = this.resolveUniqueSlug(slugCount, baseSlug);
114+
tokens[idx].attrs = [...(tokens[idx].attrs || []), ['id', finalSlug]];
115+
116+
return originalHeadingOpen(tokens, idx, options, env, self);
117+
};
118+
}
119+
120+
private resolveUniqueSlug(slugCount: Map<string, number>, base: string): string {
121+
const previous = slugCount.get(base);
122+
if (previous === undefined) {
123+
slugCount.set(base, 0);
124+
return base;
125+
}
126+
127+
const next = previous + 1;
128+
slugCount.set(base, next);
129+
return `${base}-${next}`;
130+
}
131+
132+
private normalizeHighlightLang(lang: string): string {
133+
switch (lang.toLowerCase()) {
134+
case 'tsx':
135+
case 'typescriptreact':
136+
return 'jsx';
137+
case 'json5':
138+
case 'jsonc':
139+
return 'json';
140+
case 'c#':
141+
case 'csharp':
142+
return 'cs';
143+
default:
144+
return lang.toLowerCase();
145+
}
146+
}
147+
}
148+
149+
export const markdownRenderer = new MarkdownRenderer();

src/markdown/slugify.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import MarkdownIt from 'markdown-it';
2+
import SlugifyMode from './slugifyMode';
3+
4+
const inlineEngine = new MarkdownIt('commonmark');
5+
const utf8Encoder = new TextEncoder();
6+
7+
const Regexp_Github_Punctuation = /[^\p{L}\p{M}\p{Nd}\p{Nl}\p{Pc}\- ]/gu;
8+
const Regexp_Gitlab_Product_Suffix = /[ \t\r\n\f\v]*\**\((?:core|starter|premium|ultimate)(?:[ \t\r\n\f\v]+only)?\)\**/g;
9+
10+
function mdInlineToPlainText(text: string, env: object): string {
11+
const inlineTokens = inlineEngine.parseInline(text, env)[0]?.children ?? [];
12+
return inlineTokens.reduce<string>((result, token) => {
13+
switch (token.type) {
14+
case 'image':
15+
case 'html_inline':
16+
return result;
17+
default:
18+
return result + token.content;
19+
}
20+
}, '');
21+
}
22+
23+
const slugifyHandlers: Record<SlugifyMode, (rawContent: string, env: object) => string> = {
24+
[SlugifyMode.AzureDevOps]: (slug: string): string => {
25+
slug = slug.trim().toLowerCase().replace(/\p{Zs}/gu, '-');
26+
if (/^\d/.test(slug)) {
27+
slug = Array.from(utf8Encoder.encode(slug), (b) => `%${b.toString(16)}`).join('').toUpperCase();
28+
} else {
29+
slug = encodeURIComponent(slug);
30+
}
31+
return slug;
32+
},
33+
34+
[SlugifyMode.BitbucketCloud]: (slug: string, env: object): string => {
35+
return 'markdown-header-' + slugifyHandlers[SlugifyMode.GitHub](slug, env).replace(/-+/g, '-');
36+
},
37+
38+
[SlugifyMode.Gitea]: (slug: string): string => {
39+
return slug
40+
.replace(/^[^\p{L}\p{N}]+/u, '')
41+
.replace(/[^\p{L}\p{N}]+$/u, '')
42+
.replace(/[^\p{L}\p{N}]+/gu, '-')
43+
.toLowerCase();
44+
},
45+
46+
[SlugifyMode.GitHub]: (slug: string, env: object): string => {
47+
slug = mdInlineToPlainText(slug, env)
48+
.replace(Regexp_Github_Punctuation, '')
49+
.toLowerCase()
50+
.replace(/ /g, '-');
51+
return slug;
52+
},
53+
54+
[SlugifyMode.GitLab]: (slug: string, env: object): string => {
55+
slug = mdInlineToPlainText(slug, env)
56+
.replace(/^[ \t\r\n\f\v]+/, '')
57+
.replace(/[ \t\r\n\f\v]+$/, '')
58+
.toLowerCase()
59+
.replace(Regexp_Gitlab_Product_Suffix, '')
60+
.replace(Regexp_Github_Punctuation, '')
61+
.replace(/ /g, '-')
62+
.replace(/-+/g, '-');
63+
if (/^(\d+)$/.test(slug)) {
64+
slug = `anchor-${slug}`;
65+
}
66+
return slug;
67+
},
68+
69+
[SlugifyMode.VisualStudioCode]: (rawContent: string, env: object): string => {
70+
const plain = inlineEngine.parseInline(rawContent, env)[0]?.children?.reduce<string>((result, token) => result + token.content, '') ?? '';
71+
return encodeURI(
72+
plain
73+
.trim()
74+
.toLowerCase()
75+
.replace(/\s+/g, '-')
76+
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`·ˉ¨]/g, '')
77+
.replace(/^\-+/, '')
78+
.replace(/\-+$/, '')
79+
);
80+
},
81+
82+
[SlugifyMode.Zola]: (rawContent: string, env: object): string => {
83+
// 退化为 GitHub 行为;如需完整兼容,可后续引入 zola-slug wasm 实现。
84+
return slugifyHandlers[SlugifyMode.GitHub](rawContent, env);
85+
},
86+
};
87+
88+
export function slugifyHeading(raw: string, env: object, mode: SlugifyMode): string {
89+
const handler = slugifyHandlers[mode] ?? slugifyHandlers[SlugifyMode.GitHub];
90+
let slug = handler(raw, env);
91+
slug = slug
92+
.replace(/\s+/g, '-')
93+
.replace(/\u0000/g, '')
94+
.replace(/-+/g, '-')
95+
.replace(/^-+|-+$/g, '');
96+
return slug;
97+
}

src/markdown/slugifyMode.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const enum SlugifyMode {
2+
AzureDevOps = 'azureDevops',
3+
BitbucketCloud = 'bitbucket-cloud',
4+
Gitea = 'gitea',
5+
GitHub = 'github',
6+
GitLab = 'gitlab',
7+
VisualStudioCode = 'vscode',
8+
Zola = 'zola',
9+
}
10+
11+
export default SlugifyMode;

src/types/markdown-it.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
declare module 'markdown-it-task-lists';
2+
declare module 'markdown-it-github-alerts';
3+
declare module '@neilsustc/markdown-it-katex';
4+
declare module 'markdown-it-footnote';
5+
declare module 'markdown-it-deflist';
6+
declare module 'markdown-it-emoji';
7+
declare module 'markdown-it-sub';
8+
declare module 'markdown-it-sup';

src/vue/about/App.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<Banner sub-title="关于" />
44

55
<div class="rt-page__content content_area">
6-
<div v-html="readmeMarkdown"></div>
6+
<div class="markdown-body" v-html="readmeMarkdown"></div>
77

88
<el-button type="primary" @click="openRTThreadGitHub">Open RT-Thread/Github</el-button>
99
</div>
@@ -13,10 +13,11 @@
1313
<script setup lang="ts">
1414
import { onMounted, ref } from 'vue';
1515
import { useTheme } from '../composables/useTheme';
16-
import { imgUrl } from '../assets/img';
1716
import { sendCommand } from '../api/vscode';
18-
import { extensionInfo } from '../setting/data';
1917
import Banner from '../components/Banner.vue';
18+
import 'katex/dist/katex.min.css';
19+
import 'highlight.js/styles/github.css';
20+
import '../assets/markdown.css';
2021
2122
let readmeMarkdown = ref('');
2223

0 commit comments

Comments
 (0)