// 代码高亮模块
import hljs from 'https://dpangzi.com/library/highlight.js/es/highlight.js';

// 已加载的语言缓存
const loadedLanguages = new Set();
// 正在加载的语言 Promise 缓存
const loadingLanguages = new Map();

/**
 * 语言名称映射表
 * 将常见的简写映射到 highlight.js 的标准语言名称
 */
const languageMap = {
    'cs': 'csharp',           // C# 简写
    'js': 'javascript',       // JavaScript 简写
    'ts': 'typescript',       // TypeScript 简写
    'py': 'python',           // Python 简写
    'rb': 'ruby',             // Ruby 简写
    'sh': 'bash',             // Shell/Bash 简写
    'yml': 'yaml',            // YAML 简写
    'md': 'markdown',         // Markdown 简写
    'rs': 'rust',             // Rust 简写
    'kt': 'kotlin',           // Kotlin 简写
    'pl': 'perl',             // Perl 简写
    'ps1': 'powershell',       // PowerShell 简写
    'ps': 'powershell',       // PowerShell 另一种简写
    'vb': 'vbnet',            // VB.NET 简写
    'fs': 'fsharp',           // F# 简写
    'clj': 'clojure',         // Clojure 简写
    'hs': 'haskell',          // Haskell 简写
    'erl': 'erlang',          // Erlang 简写
    'ex': 'elixir',           // Elixir 简写
    'exs': 'elixir',          // Elixir 脚本
    'dockerfile': 'dockerfile', // Dockerfile
    'makefile': 'makefile',   // Makefile
    'ini': 'ini',             // INI 配置文件
    'patch': 'diff'            // Patch (使用 diff 高亮)
};

/**
 * 回退语言配置
 * 当无法检测到语言时,使用这些语言作为回退
 * plaintext 是最基础的语言,通常已经内置,但为了确保可用,我们也会尝试加载
 */
const FALLBACK_LANGUAGE = 'plaintext';

/**
 * 常用语言列表(用于自动检测时的候选语言)
 * 这些语言会在自动检测时优先考虑
 */
const COMMON_LANGUAGES = [
    'javascript',
    'typescript',
    'python',
    'java',
    'csharp',
    'cpp',
    'c',
    'html',
    'css',
    'json',
    'xml',
    'sql',
    'bash',
    'shell',
    'plaintext'
];

/**
 * 预加载回退语言
 * 确保回退语言可用
 */
let fallbackLanguageLoaded = false;
async function ensureFallbackLanguage() {
    if (fallbackLanguageLoaded) return;

    try {
        await loadLanguage(FALLBACK_LANGUAGE);
        fallbackLanguageLoaded = true;
    } catch (error) {
        console.warn(`无法加载回退语言 ${FALLBACK_LANGUAGE}:`, error);
        // 如果 plaintext 加载失败,说明它可能是内置的,标记为已加载
        fallbackLanguageLoaded = true;
    }
}

/**
 * 映射语言名称
 */
function mapLanguage(language) {
    return languageMap[language.toLowerCase()] || language;
}

/**
 * 从代码块的 class 中提取语言名称
 * 支持格式:language-xxx, lang-xxx, language:xxx 等
 */
function extractLanguage(block) {
    const classList = Array.from(block.classList);
    let language = null;

    // 查找 language-xxx 或 lang-xxx 格式
    for (const className of classList) {
        if (className.startsWith('language-')) {
            language = className.replace('language-', '');
            break;
        }
        if (className.startsWith('lang-')) {
            language = className.replace('lang-', '');
            break;
        }
    }

    // 如果没有找到,尝试从父元素 pre 标签获取
    if (!language) {
        const pre = block.closest('pre');
        if (pre) {
            const preClasses = Array.from(pre.classList);
            for (const className of preClasses) {
                if (className.startsWith('language-')) {
                    language = className.replace('language-', '');
                    break;
                }
                if (className.startsWith('lang-')) {
                    language = className.replace('lang-', '');
                    break;
                }
            }
        }
    }

    // 如果找到语言,进行映射
    return language ? mapLanguage(language) : null;
}

/**
 * 动态加载语言文件
 */
async function loadLanguage(language) {
    // 如果已经加载过,直接返回
    if (loadedLanguages.has(language)) {
        return;
    }

    // 如果正在加载,返回加载中的 Promise
    if (loadingLanguages.has(language)) {
        return loadingLanguages.get(language);
    }

    // 创建加载 Promise
    const loadPromise = (async () => {
        try {
            // 尝试加载语言模块(先尝试 .min.js,如果失败则尝试 .js)
            let langModule;
            try {
                langModule = await import(`https://dpangzi.com/library/highlight.js/es/languages/${language}.min.js`);
            } catch (e) {
                // 如果 .min.js 不存在,尝试 .js
                langModule = await import(`https://dpangzi.com/library/highlight.js/es/languages/${language}.js`);
            }

            // 注册语言
            // highlight.js 的语言模块通常导出为 default 或直接导出函数
            const langFunction = langModule.default || langModule;
            if (typeof langFunction === 'function') {
                hljs.registerLanguage(language, langFunction);
            } else {
                console.warn(`语言模块 ${language} 导出格式不正确`);
                return;
            }

            loadedLanguages.add(language);
            loadingLanguages.delete(language);
        } catch (error) {
            console.warn(`无法加载语言模块: ${language}`, error);
            loadingLanguages.delete(language);
            // 不抛出错误,允许继续处理其他代码块
        }
    })();

    loadingLanguages.set(language, loadPromise);
    return loadPromise;
}

/**
 * 尝试自动检测代码语言并高亮
 * @param {HTMLElement} block - 代码块元素
 */
function tryAutoDetect(block) {
    try {
        // highlightAuto 需要语言已注册,所以我们只使用已加载的语言
        const availableLanguages = COMMON_LANGUAGES.filter(lang => loadedLanguages.has(lang));
        if (availableLanguages.length > 0) {
            const detected = hljs.highlightAuto(block.textContent, availableLanguages);
            if (detected && detected.language && loadedLanguages.has(detected.language)) {
                // 使用检测到的语言(直接使用 highlightAuto 返回的结果)
                block.innerHTML = detected.value;
                block.classList.add('hljs');
                return;
            }
        }
        // 如果没有检测到语言或语言未加载,使用回退语言
        if (loadedLanguages.has(FALLBACK_LANGUAGE)) {
            hljs.highlightElement(block, { language: FALLBACK_LANGUAGE });
        } else {
            // 回退语言也未加载,至少添加 hljs 类以应用基本样式
            block.classList.add('hljs');
        }
    } catch (autoError) {
        // 自动检测失败,至少添加 hljs 类以应用基本样式
        block.classList.add('hljs');
        console.warn('代码自动检测失败,使用默认样式:', autoError);
    }
}

/**
 * 高亮代码块
 */
export async function highlightCodeBlocks(container) {
    if (!container) return;

    // 确保回退语言已加载
    await ensureFallbackLanguage();

    const codeBlocks = container.querySelectorAll('pre code');
    const languagePromises = [];
    // 代码块和对应的语言映射
    const blockLanguageMap = new Map();

    // 收集所有需要加载的语言
    codeBlocks.forEach(block => {
        const language = extractLanguage(block);
        if (language) {
            // 存储代码块和语言的映射关系
            blockLanguageMap.set(block, language);
            languagePromises.push(loadLanguage(language));
        }
    });

    // 等待所有语言加载完成
    try {
        await Promise.allSettled(languagePromises);
    } catch (error) {
        console.warn('部分语言加载失败:', error);
    }
    COMMON_LANGUAGES.map(lang => {
        if (!loadedLanguages.has(lang) && !loadingLanguages.has(lang)) {
            return loadLanguage(lang).catch(() => {
                
            });
        }
        return Promise.resolve();
    });
    // 不等待常用语言加载完成,让它们在后台加载

    // 对所有代码块进行高亮
    codeBlocks.forEach(block => {
        if (block.classList.contains('hljs')) {
            block.classList.remove('hljs');
            block.removeAttribute('data-highlighted');
        }

        try {
            // 如果知道语言,使用映射后的语言名称进行高亮
            const language = blockLanguageMap.get(block);
            if (language) {
                hljs.highlightElement(block, { language: language });
            } else {
                // 没有明确语言,尝试自动检测或使用回退语言
                // 确保回退语言已加载(虽然已经在函数开始时加载,但这里再次确认)
                if (loadedLanguages.has(FALLBACK_LANGUAGE)) {
                    try {
                        // 使用回退语言
                        hljs.highlightElement(block, { language: FALLBACK_LANGUAGE });
                    } catch (fallbackError) {
                        // 如果回退语言失败,尝试自动检测
                        tryAutoDetect(block);
                    }
                } else {
                    // 回退语言未加载,尝试自动检测
                    tryAutoDetect(block);
                }
            }
        } catch (error) {
            console.warn('代码高亮失败:', error);
            // 如果高亮失败,至少添加 hljs 类以应用基本样式
            try {
                block.classList.add('hljs');
            } catch (e) {
                // 忽略错误,保持原样
            }
        }
    });
}

⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

下面是对这段代码的分块解释与整体工作流程说明,便于快速理解它的目的与实现细节。

概览

  • 这是一个基于 highlight.js 的动态代码高亮模块。它会:
    • 从 DOM 中查找
      ...
      代码块;
    • 根据 class(如 language-js、lang-ts 等)提取语言;
    • 动态按需 import 对应的 highlight.js 语言模块并注册;
    • 对代码块进行高亮,若无法确定语言则尝试自动检测或使用回退语言(plaintext);
    • 对并发加载和重复加载做缓存与去重处理。

重要数据结构与常量

  • hljs:从远程 URL 导入的 highlight.js 实例(主库)。
  • loadedLanguages(Set):已经成功注册(加载)的语言集合,避免重复注册。
  • loadingLanguages(Map):正在加载的语言 -> Promise,用于并发去重(同一语言只触发一次 import)。
  • languageMap:常见简称到 highlight.js 标准语言名的映射(例如 cs -> csharp, js -> javascript)。
  • FALLBACK_LANGUAGE:回退语言,默认为 'plaintext'。
  • COMMON_LANGUAGES:自动检测时优先考虑的一些常用语言名单。

主要函数与行为

  1. mapLanguage(language)
  • 将传入的语言名小写后(通过 languageMap)转换为 highlight.js 使用的标准名(如果没有映射则返回原值)。
  1. extractLanguage(block)
  • 从单个代码块元素(通常是 code 元素)的 class 中查找 language-xxx 或 lang-xxx。
  • 如果 code 本身没有,在其最近的父 pre 元素的 class 中继续查找。
  • 找到后通过 mapLanguage 映射并返回;找不到则返回 null。
  1. loadLanguage(language)
  • 按需动态 import 语言模块(先尝试 .min.js,失败再尝试 .js)。
  • 使用 loadingLanguages Map 来避免并发重复加载。
  • 加载到模块后,取模块的 default(或直接导出),如果是函数则调用 hljs.registerLanguage(language, fn) 注册语言。
  • 成功后将 language 加入 loadedLanguages 并从 loadingLanguages 删除;失败会记录警告,但不会抛出以中断流程。
  1. ensureFallbackLanguage()
  • 在第一次需要时,确保回退语言已加载。若加载失败(例如 plaintext 可能是内置的导致 import 失败),也会把标志设为已完成以避免重复尝试。
  1. tryAutoDetect(block)
  • 在没有已知语言时尝试自动检测:先构造一个“可用语言”数组(从 COMMON_LANGUAGES 中挑出 loadedLanguages 已注册的项),然后调用 hljs.highlightAuto(text, availableLanguages)。
  • 如果检测成功并且检测到的语言已注册,则直接使用返回的结果填充 block.innerHTML。
  • 检测失败则使用回退语言(如果可用),或至少添加 .hljs 类以应用基础样式。
  1. highlightCodeBlocks(container)(导出函数)
  • 主入口:传入容器元素(通常 document 或某个局部容器)。
  • 步骤:
    1. ensureFallbackLanguage():保证回退语言的可用性。
    2. 找出 container 内的所有 pre code 元素,遍历提取语言并为找到的语言调用 loadLanguage(收集这些 Promise)。
    3. await Promise.allSettled(languagePromises) 等待这些指定语言的加载结果(不会因单个失败而抛出)。
    4. 触发 COMMON_LANGUAGES 中未加载语言的后台加载(使用 map,但不 await,这些在后台并行加载)。
    5. 对每个代码块执行高亮:
      • 若该块明确指定了语言:使用 hljs.highlightElement(block, { language });
      • 否则:尝试用回退语言高亮,若回退失败再尝试 tryAutoDetect;
      • 出错时至少添加 .hljs 类保证基础样式。
  • 还会在开始对某个块处理之前移除已有的 hljs 标记和 data-highlighted 属性(如果存在),以便重新高亮。

错误处理与健壮性

  • 对语言加载失败、模块导出格式不正确、动态 import 抛错等都以 console.warn 报告,但不会中断对其他代码块的处理。
  • 使用 loadingLanguages Map 防止重复并发 import。
  • 回退策略:明确语言 -> 回退语言(plaintext)-> 自动检测 -> 最少添加 hljs 类。
  • highlightAuto 只会在已注册的常用语言之中进行检测,减少误判但可能导致无法检测到某些语言。

注意点与可能的改进

  • languageMap 假定输入是小写,但 extractLanguage 中先是直接取 class 后再 mapLanguage 调用 .toLowerCase(),总体是大小写安全的。
  • COMMON_LANGUAGES.map(...) 中启动了加载,但函数并没有收集或 await 这些 Promise(这是故意的“后台加载”行为)。如果需要确保这些语言加载完成,应使用 Promise.all 并 await。
  • 当 import 失败且该语言实际上内置于 hljs 中(或名字不匹配)时,当前代码会忽略错误并继续,但可能造成未高亮。可以在 loadLanguage 失败时尝试检查 hljs 是否已经内置该语言。
  • highlightAuto 依赖 loadedLanguages(已注册语言),如果某些常见语言尚未注册,则自动检测范围会受限。

总结

  • 该模块实现了一个按需加载、并发安全、以回退/自动检测为补偿的代码高亮方案,目的是减少初始包体积(通过动态 import 语言模块),并在页面运行时对代码块逐个高亮。总体设计考虑了性能(按需+后台预加载常用语言)、鲁棒性(try/catch、allSettled)和可扩展性(语言映射与动态注册)。
评论加载中...