// 代码高亮模块
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);
- 对并发加载和重复加载做缓存与去重处理。
- 从 DOM 中查找
重要数据结构与常量
- hljs:从远程 URL 导入的 highlight.js 实例(主库)。
- loadedLanguages(Set):已经成功注册(加载)的语言集合,避免重复注册。
- loadingLanguages(Map):正在加载的语言 -> Promise,用于并发去重(同一语言只触发一次 import)。
- languageMap:常见简称到 highlight.js 标准语言名的映射(例如 cs -> csharp, js -> javascript)。
- FALLBACK_LANGUAGE:回退语言,默认为 'plaintext'。
- COMMON_LANGUAGES:自动检测时优先考虑的一些常用语言名单。
主要函数与行为
- mapLanguage(language)
- 将传入的语言名小写后(通过 languageMap)转换为 highlight.js 使用的标准名(如果没有映射则返回原值)。
- extractLanguage(block)
- 从单个代码块元素(通常是 code 元素)的 class 中查找 language-xxx 或 lang-xxx。
- 如果 code 本身没有,在其最近的父 pre 元素的 class 中继续查找。
- 找到后通过 mapLanguage 映射并返回;找不到则返回 null。
- loadLanguage(language)
- 按需动态 import 语言模块(先尝试 .min.js,失败再尝试 .js)。
- 使用 loadingLanguages Map 来避免并发重复加载。
- 加载到模块后,取模块的 default(或直接导出),如果是函数则调用 hljs.registerLanguage(language, fn) 注册语言。
- 成功后将 language 加入 loadedLanguages 并从 loadingLanguages 删除;失败会记录警告,但不会抛出以中断流程。
- ensureFallbackLanguage()
- 在第一次需要时,确保回退语言已加载。若加载失败(例如 plaintext 可能是内置的导致 import 失败),也会把标志设为已完成以避免重复尝试。
- tryAutoDetect(block)
- 在没有已知语言时尝试自动检测:先构造一个“可用语言”数组(从 COMMON_LANGUAGES 中挑出 loadedLanguages 已注册的项),然后调用 hljs.highlightAuto(text, availableLanguages)。
- 如果检测成功并且检测到的语言已注册,则直接使用返回的结果填充 block.innerHTML。
- 检测失败则使用回退语言(如果可用),或至少添加 .hljs 类以应用基础样式。
- highlightCodeBlocks(container)(导出函数)
- 主入口:传入容器元素(通常 document 或某个局部容器)。
- 步骤:
- ensureFallbackLanguage():保证回退语言的可用性。
- 找出 container 内的所有 pre code 元素,遍历提取语言并为找到的语言调用 loadLanguage(收集这些 Promise)。
- await Promise.allSettled(languagePromises) 等待这些指定语言的加载结果(不会因单个失败而抛出)。
- 触发 COMMON_LANGUAGES 中未加载语言的后台加载(使用 map,但不 await,这些在后台并行加载)。
- 对每个代码块执行高亮:
- 若该块明确指定了语言:使用 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)和可扩展性(语言映射与动态注册)。
评论加载中...