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 浏览器中的字体渲染问题

使用场景

这个模块特别适用于:

  • 技术文档网站的代码展示
  • 代码分享平台
  • 在线教程或博客中的代码片段展示
  • 需要支持多种编程语言语法高亮的场景

特色功能

  1. 多语言支持: 通过映射表支持从 Prism 语言 ID 到 Monaco 语言 ID 的转换
  2. URL 行号定位: 支持 #L42 格式的 URL 行号跳转
  3. 自适应高度: 根据代码内容自动调整编辑器高度
  4. 主题支持: 集成了主题切换功能
  5. 性能优化: 复用编辑器实例,避免重复创建
  6. 字体优化: 使用编程字体并启用连字符支持

这个实现提供了一个功能完整且用户体验良好的代码展示解决方案。

评论加载中...