(() => {
const lineNumberPreferenceKey = 'xfe-code-line-numbers';
const languageExtensions = {
bash: 'sh', shell: 'sh', sh: 'sh',
c: 'c', cpp: 'cpp', csharp: 'cs', cs: 'cs',
css: 'css', cmd: 'cmd', dos: 'cmd',
html: 'html', java: 'java', javascript: 'js', js: 'js',
json: 'json', markdown: 'md', md: 'md',
powershell: 'ps1', ps1: 'ps1', python: 'py', py: 'py',
razor: 'cshtml', sql: 'sql', text: 'txt', plaintext: 'txt',
typescript: 'ts', ts: 'ts', xaml: 'xaml', xml: 'xml', yaml: 'yml', yml: 'yml'
};
const createButton = (action, icon, label) => {
const button = document.createElement('button');
button.className = 'code-tool-button';
button.type = 'button';
button.dataset.codeAction = action;
button.title = label;
button.setAttribute('aria-label', label);
button.innerHTML = `<i class="bi ${icon}" aria-hidden="true"></i>`;
return button;
};
const setButtonIcon = (button, icon) => {
const element = button.querySelector('i');
if (element) element.className = `bi ${icon}`;
};
const getLineNumberPreference = () => {
try {
return localStorage.getItem(lineNumberPreferenceKey) !== 'off';
} catch {
return true;
}
};
const applyLineNumberPreference = visible => {
document.querySelectorAll('pre.code-block-enhanced').forEach(pre => {
pre.classList.toggle('code-lines-visible', visible);
const button = pre.querySelector('[data-code-action="lines"]');
if (!button) return;
button.classList.toggle('is-active', visible);
button.setAttribute('aria-pressed', String(visible));
button.title = visible ? '隐藏行号' : '显示行号';
button.setAttribute('aria-label', button.title);
});
try {
localStorage.setItem(lineNumberPreferenceKey, visible ? 'on' : 'off');
} catch {
// Storage can be unavailable in privacy modes; the current page still works.
}
};
const copyText = async text => {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
};
const getLanguage = code => {
const languageClass = [...code.classList].find(value =>
value.startsWith('language-') || value.startsWith('lang-'));
return languageClass?.replace(/^(language-|lang-)/, '').toLowerCase() || 'text';
};
const createLineNumbers = source => {
const normalized = source.replace(/\r\n?/g, '\n');
const content = normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
const lineCount = Math.max(1, content.split('\n').length);
const gutter = document.createElement('span');
gutter.className = 'code-line-numbers';
gutter.setAttribute('aria-hidden', 'true');
const fragment = document.createDocumentFragment();
for (let index = 1; index <= lineCount; index += 1) {
const line = document.createElement('span');
line.textContent = String(index);
fragment.appendChild(line);
}
gutter.appendChild(fragment);
return gutter;
};
const downloadCode = (source, language, requestedName) => {
const extension = languageExtensions[language] || 'txt';
const blob = new Blob([source], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = requestedName || `code.${extension}`;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
};
const updateExpandButton = (pre, button) => {
const expanded = document.fullscreenElement === pre || pre.classList.contains('is-expanded');
setButtonIcon(button, expanded ? 'bi-fullscreen-exit' : 'bi-arrows-fullscreen');
button.classList.toggle('is-active', expanded);
button.title = expanded ? '退出全屏' : '全屏查看';
button.setAttribute('aria-label', button.title);
button.setAttribute('aria-pressed', String(expanded));
};
const toggleExpanded = async (pre, button) => {
if (document.fullscreenElement === pre) {
await document.exitFullscreen();
return;
}
if (pre.classList.contains('is-expanded')) {
pre.classList.remove('is-expanded');
document.body.classList.remove('code-block-expanded');
updateExpandButton(pre, button);
return;
}
if (pre.requestFullscreen) {
try {
await pre.requestFullscreen();
return;
} catch {
// Fall back to an in-page overlay when fullscreen is unavailable.
}
}
document.querySelectorAll('pre.code-block-enhanced.is-expanded').forEach(item =>
item.classList.remove('is-expanded'));
pre.classList.add('is-expanded');
document.body.classList.add('code-block-expanded');
updateExpandButton(pre, button);
};
const enhanceBlock = pre => {
if (pre.dataset.codeToolsApplied === 'true' ||
pre.classList.contains('mermaid') ||
pre.closest('.mermaid-wrapper')) return;
const code = pre.querySelector('code');
if (!code || code.classList.contains('language-mermaid') || code.classList.contains('lang-mermaid')) return;
const source = code.textContent || '';
const language = getLanguage(code);
pre.dataset.codeToolsApplied = 'true';
pre.dataset.featuresApplied = 'true';
pre.classList.add('code-block-enhanced');
pre.querySelectorAll(':scope > .lang-label, :scope > .copy-btn').forEach(item => item.remove());
const toolbar = document.createElement('span');
toolbar.className = 'code-block-toolbar';
const languageLabel = document.createElement('span');
languageLabel.className = 'code-language-label';
languageLabel.textContent = language;
const actions = document.createElement('span');
actions.className = 'code-block-actions';
const linesButton = createButton('lines', 'bi-list-ol', '隐藏行号');
const downloadButton = createButton('download', 'bi-download', '下载代码');
const expandButton = createButton('expand', 'bi-arrows-fullscreen', '全屏查看');
const copyButton = createButton('copy', 'bi-copy', '复制代码');
actions.append(linesButton, downloadButton, expandButton, copyButton);
toolbar.append(languageLabel, actions);
const viewport = document.createElement('span');
viewport.className = 'code-block-viewport';
viewport.append(createLineNumbers(source), code);
pre.replaceChildren(toolbar, viewport);
const showLines = getLineNumberPreference();
pre.classList.toggle('code-lines-visible', showLines);
linesButton.classList.toggle('is-active', showLines);
linesButton.setAttribute('aria-pressed', String(showLines));
linesButton.title = showLines ? '隐藏行号' : '显示行号';
linesButton.setAttribute('aria-label', linesButton.title);
linesButton.addEventListener('click', () =>
applyLineNumberPreference(!pre.classList.contains('code-lines-visible')));
downloadButton.addEventListener('click', () =>
downloadCode(source, language, pre.dataset.codeDownloadName));
expandButton.addEventListener('click', () => toggleExpanded(pre, expandButton));
copyButton.addEventListener('click', async () => {
try {
await copyText(source);
setButtonIcon(copyButton, 'bi-check-lg');
copyButton.classList.add('is-success');
copyButton.title = '已复制';
copyButton.setAttribute('aria-label', '代码已复制');
window.setTimeout(() => {
setButtonIcon(copyButton, 'bi-copy');
copyButton.classList.remove('is-success');
copyButton.title = '复制代码';
copyButton.setAttribute('aria-label', '复制代码');
}, 1500);
} catch {
copyButton.title = '复制失败';
copyButton.setAttribute('aria-label', '复制失败');
}
});
document.addEventListener('fullscreenchange', () => updateExpandButton(pre, expandButton));
};
const enhance = (container = document) => {
const blockSelector = '.markdown-body pre, pre[data-code-block-tools]';
const blocks = [...container.querySelectorAll(blockSelector)];
if (container.matches?.(blockSelector)) blocks.unshift(container);
blocks.forEach(enhanceBlock);
};
document.addEventListener('keydown', event => {
if (event.key !== 'Escape') return;
const expanded = document.querySelector('pre.code-block-enhanced.is-expanded');
if (!expanded) return;
expanded.classList.remove('is-expanded');
document.body.classList.remove('code-block-expanded');
const button = expanded.querySelector('[data-code-action="expand"]');
if (button) updateExpandButton(expanded, button);
});
window.XFECodeBlocks = { enhance };
document.addEventListener('DOMContentLoaded', () => enhance(document));
})();
(() => {
const lineNumberPreferenceKey = 'xfe-code-line-numbers';
const languageExtensions = {
bash: 'sh', shell: 'sh', sh: 'sh',
c: 'c', cpp: 'cpp', csharp: 'cs', cs: 'cs',
css: 'css', cmd: 'cmd', dos: 'cmd',
html: 'html', java: 'java', javascript: 'js', js: 'js',
json: 'json', markdown: 'md', md: 'md',
powershell: 'ps1', ps1: 'ps1', python: 'py', py: 'py',
razor: 'cshtml', sql: 'sql', text: 'txt', plaintext: 'txt',
typescript: 'ts', ts: 'ts', xaml: 'xaml', xml: 'xml', yaml: 'yml', yml: 'yml'
};
const createButton = (action, icon, label) => {
const button = document.createElement('button');
button.className = 'code-tool-button';
button.type = 'button';
button.dataset.codeAction = action;
button.title = label;
button.setAttribute('aria-label', label);
button.innerHTML = `<i class="bi ${icon}" aria-hidden="true"></i>`;
return button;
};
const setButtonIcon = (button, icon) => {
const element = button.querySelector('i');
if (element) element.className = `bi ${icon}`;
};
const getLineNumberPreference = () => {
try {
return localStorage.getItem(lineNumberPreferenceKey) !== 'off';
} catch {
return true;
}
};
const applyLineNumberPreference = visible => {
document.querySelectorAll('pre.code-block-enhanced').forEach(pre => {
pre.classList.toggle('code-lines-visible', visible);
const button = pre.querySelector('[data-code-action="lines"]');
if (!button) return;
button.classList.toggle('is-active', visible);
button.setAttribute('aria-pressed', String(visible));
button.title = visible ? '隐藏行号' : '显示行号';
button.setAttribute('aria-label', button.title);
});
try {
localStorage.setItem(lineNumberPreferenceKey, visible ? 'on' : 'off');
} catch {
// Storage can be unavailable in privacy modes; the current page still works.
}
};
const copyText = async text => {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
};
const getLanguage = code => {
const languageClass = [...code.classList].find(value =>
value.startsWith('language-') || value.startsWith('lang-'));
return languageClass?.replace(/^(language-|lang-)/, '').toLowerCase() || 'text';
};
const createLineNumbers = source => {
const normalized = source.replace(/\r\n?/g, '\n');
const content = normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
const lineCount = Math.max(1, content.split('\n').length);
const gutter = document.createElement('span');
gutter.className = 'code-line-numbers';
gutter.setAttribute('aria-hidden', 'true');
const fragment = document.createDocumentFragment();
for (let index = 1; index <= lineCount; index += 1) {
const line = document.createElement('span');
line.textContent = String(index);
fragment.appendChild(line);
}
gutter.appendChild(fragment);
return gutter;
};
const downloadCode = (source, language, requestedName) => {
const extension = languageExtensions[language] || 'txt';
const blob = new Blob([source], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = requestedName || `code.${extension}`;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
};
const updateExpandButton = (pre, button) => {
const expanded = document.fullscreenElement === pre || pre.classList.contains('is-expanded');
setButtonIcon(button, expanded ? 'bi-fullscreen-exit' : 'bi-arrows-fullscreen');
button.classList.toggle('is-active', expanded);
button.title = expanded ? '退出全屏' : '全屏查看';
button.setAttribute('aria-label', button.title);
button.setAttribute('aria-pressed', String(expanded));
};
const toggleExpanded = async (pre, button) => {
if (document.fullscreenElement === pre) {
await document.exitFullscreen();
return;
}
if (pre.classList.contains('is-expanded')) {
pre.classList.remove('is-expanded');
document.body.classList.remove('code-block-expanded');
updateExpandButton(pre, button);
return;
}
if (pre.requestFullscreen) {
try {
await pre.requestFullscreen();
return;
} catch {
// Fall back to an in-page overlay when fullscreen is unavailable.
}
}
document.querySelectorAll('pre.code-block-enhanced.is-expanded').forEach(item =>
item.classList.remove('is-expanded'));
pre.classList.add('is-expanded');
document.body.classList.add('code-block-expanded');
updateExpandButton(pre, button);
};
const enhanceBlock = pre => {
if (pre.dataset.codeToolsApplied === 'true' ||
pre.classList.contains('mermaid') ||
pre.closest('.mermaid-wrapper')) return;
const code = pre.querySelector('code');
if (!code || code.classList.contains('language-mermaid') || code.classList.contains('lang-mermaid')) return;
const source = code.textContent || '';
const language = getLanguage(code);
pre.dataset.codeToolsApplied = 'true';
pre.dataset.featuresApplied = 'true';
pre.classList.add('code-block-enhanced');
pre.querySelectorAll(':scope > .lang-label, :scope > .copy-btn').forEach(item => item.remove());
const toolbar = document.createElement('span');
toolbar.className = 'code-block-toolbar';
const languageLabel = document.createElement('span');
languageLabel.className = 'code-language-label';
languageLabel.textContent = language;
const actions = document.createElement('span');
actions.className = 'code-block-actions';
const linesButton = createButton('lines', 'bi-list-ol', '隐藏行号');
const downloadButton = createButton('download', 'bi-download', '下载代码');
const expandButton = createButton('expand', 'bi-arrows-fullscreen', '全屏查看');
const copyButton = createButton('copy', 'bi-copy', '复制代码');
actions.append(linesButton, downloadButton, expandButton, copyButton);
toolbar.append(languageLabel, actions);
const viewport = document.createElement('span');
viewport.className = 'code-block-viewport';
viewport.append(createLineNumbers(source), code);
pre.replaceChildren(toolbar, viewport);
const showLines = getLineNumberPreference();
pre.classList.toggle('code-lines-visible', showLines);
linesButton.classList.toggle('is-active', showLines);
linesButton.setAttribute('aria-pressed', String(showLines));
linesButton.title = showLines ? '隐藏行号' : '显示行号';
linesButton.setAttribute('aria-label', linesButton.title);
linesButton.addEventListener('click', () =>
applyLineNumberPreference(!pre.classList.contains('code-lines-visible')));
downloadButton.addEventListener('click', () =>
downloadCode(source, language, pre.dataset.codeDownloadName));
expandButton.addEventListener('click', () => toggleExpanded(pre, expandButton));
copyButton.addEventListener('click', async () => {
try {
await copyText(source);
setButtonIcon(copyButton, 'bi-check-lg');
copyButton.classList.add('is-success');
copyButton.title = '已复制';
copyButton.setAttribute('aria-label', '代码已复制');
window.setTimeout(() => {
setButtonIcon(copyButton, 'bi-copy');
copyButton.classList.remove('is-success');
copyButton.title = '复制代码';
copyButton.setAttribute('aria-label', '复制代码');
}, 1500);
} catch {
copyButton.title = '复制失败';
copyButton.setAttribute('aria-label', '复制失败');
}
});
document.addEventListener('fullscreenchange', () => updateExpandButton(pre, expandButton));
};
const enhance = (container = document) => {
const blockSelector = '.markdown-body pre, pre[data-code-block-tools]';
const blocks = [...container.querySelectorAll(blockSelector)];
if (container.matches?.(blockSelector)) blocks.unshift(container);
blocks.forEach(enhanceBlock);
};
document.addEventListener('keydown', event => {
if (event.key !== 'Escape') return;
const expanded = document.querySelector('pre.code-block-enhanced.is-expanded');
if (!expanded) return;
expanded.classList.remove('is-expanded');
document.body.classList.remove('code-block-expanded');
const button = expanded.querySelector('[data-code-action="expand"]');
if (button) updateExpandButton(expanded, button);
});
window.XFECodeBlocks = { enhance };
document.addEventListener('DOMContentLoaded', () => enhance(document));
})();