import * as monaco from 'monaco-editor';
import { util } from './util.ts';
const MONACO_FONT_FAMILY: string =
'"Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Cascadia Mono", "Courier New", monospace';
const MONACO_FONT_LIGATURES: boolean = true;
/**
* 服务端语言 ID / Prism 语言 ID → Monaco 语言 ID
*/
const LANG_MAP: Record<string, string> = {
markup: 'html',
sh: 'shell',
bash: 'shell',
ps1: 'powershell',
docker: 'dockerfile',
ignore: 'plaintext',
git: 'plaintext',
sln: 'ini',
text: 'plaintext',
};
/**
* Monaco 语言 ID → 中文显示名称
*/
const LANG_DISPLAY: Record<string, string> = {
csharp: 'C#',
javascript: 'JavaScript',
typescript: 'TypeScript',
html: 'HTML',
css: 'CSS',
less: 'Less',
scss: 'SCSS',
json: 'JSON',
xml: 'XML',
sql: 'SQL',
python: 'Python',
java: 'Java',
cpp: 'C++',
c: 'C',
go: 'Go',
rust: 'Rust',
php: 'PHP',
ruby: 'Ruby',
shell: 'Shell',
powershell: 'PowerShell',
yaml: 'YAML',
markdown: 'Markdown',
razor: 'Razor',
ini: 'INI',
dockerfile: 'Dockerfile',
plaintext: '纯文本',
};
/**
* Monaco 编辑器模块
* 负责在页面内渲染代码片段,支持高亮和自动计算高度
*/
export class MonacoEditor {
private _editor: monaco.editor.IStandaloneCodeEditor | null = null;
private _hostEl: HTMLElement | null = null;
private _currentModel: monaco.editor.ITextModel | null = null;
private _decorations: monaco.editor.IEditorDecorationsCollection | null = null;
private _clickDisposable: monaco.IDisposable | null = null;
/**
* 在指定容器内显示代码
* @param hostEl 挂载容器(.monaco-host 元素)
* @param code 源代码文本
* @param lang Prism 语言 ID,自动映射为 Monaco 语言 ID
*/
public async show(hostEl: HTMLElement, code: string, lang: string): Promise<void> {
const monacoLang = this._toMonacoLang(lang);
this._setHeight(hostEl, code);
if (!(window as any).MonacoEnvironment) {
(window as any).MonacoEnvironment = {
getWorkerUrl: function () {
return (
'data:text/javascript;charset=utf-8,' +
encodeURIComponent(`
self.MonacoEnvironment = { baseUrl: '' };
`)
);
},
};
}
if (!this._editor || this._hostEl !== hostEl) {
if (this._editor) {
this._clickDisposable?.dispose();
this._editor.dispose();
this._editor = null;
}
if (this._currentModel) {
this._currentModel.dispose();
this._currentModel = null;
}
this._editor = monaco.editor.create(hostEl, {
model: null,
readOnly: true,
theme: util.monacoTheme(),
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 13,
fontFamily: MONACO_FONT_FAMILY,
fontLigatures: MONACO_FONT_LIGATURES,
fontWeight: 'normal',
lineNumbers: 'on',
automaticLayout: true,
renderWhitespace: 'none',
contextmenu: false,
wordWrap: 'off',
smoothScrolling: true,
padding: { top: 8, bottom: 8 },
});
this._decorations = this._editor.createDecorationsCollection();
this._clickDisposable = this._bindLineClick();
this._hostEl = hostEl;
}
const hashLine = this._parseHashLine();
if (hashLine) {
const disposable = this._editor.onDidContentSizeChange(() => {
disposable.dispose();
this._editor!.revealLineInCenter(hashLine);
});
}
const oldModel = this._currentModel;
this._currentModel = monaco.editor.createModel(code, monacoLang);
this._editor.setModel(this._currentModel);
if (oldModel) {
oldModel.dispose();
}
this._stabilizeTextMetrics();
if (hashLine) {
this._highlightLine(hashLine);
}
monaco.editor.setTheme(util.monacoTheme());
this._renderLangBadge(hostEl, monacoLang);
}
/**
* 销毁 Editor 实例(切换到非代码页面时调用)
*/
public destroy(): void {
this._clickDisposable?.dispose();
this._clickDisposable = null;
if (this._currentModel) {
this._currentModel.dispose();
this._currentModel = null;
}
if (this._editor) {
this._editor.dispose();
this._editor = null;
}
this._hostEl = null;
this._decorations = null;
}
/**
* 根据代码行数自适应设置容器高度
* @param hostEl 挂载容器元素
* @param code 源代码文本
*/
private _setHeight(hostEl: HTMLElement, code: string): void {
const lineCount = code.split('\n').length;
const lineHeight = 19;
const maxHeight = Math.floor(window.innerHeight * 0.8);
const minHeight = 300;
const height = Math.max(minHeight, Math.min(lineCount * lineHeight + 40, maxHeight));
hostEl.style.height = `${height}px`;
hostEl.style.width = '100%';
hostEl.style.display = 'block';
hostEl.style.position = 'relative';
}
/**
* Prism 语言 ID 映射至 Monaco 语言 ID
* @param lang 原始语言 ID
* @returns Monaco 语言 ID
*/
private _toMonacoLang(lang: string): string {
return LANG_MAP[lang] ?? lang;
}
/**
* 映射 Monaco 语言 ID 至首字母大写等中文显示名称
* @param monacoLang Monaco 语言 ID
* @returns 中文或格式化后的语言名称
*/
private _getDisplayName(monacoLang: string): string {
return LANG_DISPLAY[monacoLang] ?? monacoLang;
}
/**
* 在容器右下角渲染语言徽章
* @param hostEl 挂载容器元素
* @param monacoLang Monaco 语言 ID
*/
private _renderLangBadge(hostEl: HTMLElement, monacoLang: string): void {
let badge = hostEl.querySelector<HTMLElement>('.monaco-lang-badge');
if (!badge) {
badge = document.createElement('div');
badge.className = 'monaco-lang-badge';
hostEl.appendChild(badge);
}
badge.textContent = this._getDisplayName(monacoLang);
}
/**
* 绑定编辑器的鼠标点击事件
* @returns 事件监听的销毁对象
*/
private _bindLineClick(): monaco.IDisposable {
return this._editor!.onMouseDown((e: monaco.editor.IEditorMouseEvent) => {
const pos = e.target?.position;
if (!pos) {
return;
}
this._highlightLine(pos.lineNumber);
const newUrl = `${window.location.pathname}${window.location.search}#L${pos.lineNumber}`;
window.history.replaceState(null, '', newUrl);
});
}
/**
* 解析 URL Hash 中的行号(格式 #L42),无效时返回 null
* @returns 行号整数,或不可用时返回 null
*/
private _parseHashLine(): number | null {
const match = window.location.hash.match(/^#L(\d+)$/);
if (!match) {
return null;
}
const n = parseInt(match[1], 10);
return n >= 1 ? n : null;
}
/**
* 高亮指定行(替换之前的高亮)
* @param lineNumber 行号
*/
private _highlightLine(lineNumber: number): void {
if (!this._editor || !this._decorations) {
return;
}
this._decorations.set([
{
range: new monaco.Range(lineNumber, 1, lineNumber, 1),
options: {
isWholeLine: true,
className: 'monaco-highlighted-line',
linesDecorationsClassName: 'monaco-highlighted-line-gutter',
},
},
]);
}
/**
* 强制编辑器重新进行字体与布局测量,修复 Chromium 选区/光标偶发偏移
*/
private _stabilizeTextMetrics(): void {
if (!this._editor) {
return;
}
this._editor.updateOptions({
fontFamily: MONACO_FONT_FAMILY,
fontLigatures: MONACO_FONT_LIGATURES,
});
this._editor.layout();
const applyFinalLayout = () => {
if (!this._editor) {
return;
}
this._editor.layout();
};
if (document.fonts?.ready) {
document.fonts.ready.then(() => {
requestAnimationFrame(applyFinalLayout);
});
} else {
requestAnimationFrame(applyFinalLayout);
}
}
}
export const monacoEditor = new MonacoEditor();
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
这是一个基于 Monaco Editor(VS Code 编辑器的核心)的代码展示模块,主要用于在网页中渲染代码片段并提供语法高亮功能。让我详细解释一下这个代码的结构和功能:
主要功能
1. 配置常量
const MONACO_FONT_FAMILY = // 定义了代码编辑器的字体优先级
const MONACO_FONT_LIGATURES = true // 启用字体连字符
2. 语言映射
- LANG_MAP: 将服务端或 Prism 的语言 ID 映射为 Monaco 编辑器支持的语言 ID
- LANG_DISPLAY: 将 Monaco 语言 ID 映射为用户友好的中文显示名称
3. MonacoEditor 类的核心方法
show() 方法
这是主要的入口方法,负责:
- 接收容器元素、代码内容和语言类型
- 自动计算并设置容器高度
- 创建或复用 Monaco 编辑器实例
- 配置编辑器选项(只读、主题、字体等)
- 处理 URL hash 中的行号跳转(如
#L42) - 渲染语言徽章
destroy() 方法
清理资源,销毁编辑器实例和相关监听器
4. 辅助功能
高度自适应
_setHeight() // 根据代码行数自动计算合适的容器高度
行号跳转与高亮
- 支持点击行号更新 URL hash
- 支持从 URL hash 解析行号并跳转
- 高亮指定行
字体渲染优化
_stabilizeTextMetrics() // 修复 Chromium 浏览器中的字体渲染问题
使用场景
这个模块特别适用于:
- 技术文档网站的代码展示
- 代码分享平台
- 在线教程或博客中的代码片段展示
- 需要支持多种编程语言语法高亮的场景
特色功能
- 多语言支持: 通过映射表支持从 Prism 语言 ID 到 Monaco 语言 ID 的转换
- URL 行号定位: 支持
#L42格式的 URL 行号跳转 - 自适应高度: 根据代码内容自动调整编辑器高度
- 主题支持: 集成了主题切换功能
- 性能优化: 复用编辑器实例,避免重复创建
- 字体优化: 使用编程字体并启用连字符支持
这个实现提供了一个功能完整且用户体验良好的代码展示解决方案。
AI 正在分析代码…
评论加载中...