update
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
# SPDX-FileCopyrightText: 2025 QinHan
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
import re # <--- 步骤 1: 导入 re 模块
|
||||
from dataclasses import dataclass
|
||||
import jinja2
|
||||
import markdown
|
||||
@@ -15,6 +14,17 @@ class MD2HTMLExporterConfig(MDExporterConfig):
|
||||
cdn: bool = True
|
||||
|
||||
|
||||
# 预读取本地静态文件(加速)
|
||||
_LOCAL_CACHE = {}
|
||||
|
||||
|
||||
def _get_local_content(path: str) -> str:
|
||||
"""从本地读取文件内容,使用缓存加速"""
|
||||
if path not in _LOCAL_CACHE:
|
||||
_LOCAL_CACHE[path] = resource_path(path).read_text(encoding="utf-8")
|
||||
return _LOCAL_CACHE[path]
|
||||
|
||||
|
||||
class MD2HTMLExporter(MDExporter):
|
||||
def __init__(self, config: MD2HTMLExporterConfig = None):
|
||||
config = config or MD2HTMLExporterConfig()
|
||||
@@ -22,70 +32,38 @@ class MD2HTMLExporter(MDExporter):
|
||||
self.cdn = config.cdn
|
||||
|
||||
def export(self, document: MarkdownDocument) -> Document:
|
||||
cdn = self.cdn
|
||||
html_template = resource_path("template/markdown.html").read_text(encoding="utf-8")
|
||||
|
||||
# CDN 基础 URL
|
||||
cdn_base = "https://s4.zstatic.net/ajax/libs"
|
||||
|
||||
def fetch_text(url_or_path: str) -> str:
|
||||
"""从 URL 或本地文件获取文本内容"""
|
||||
# 检测 CDN 是否可用
|
||||
def can_access_cdn(url: str) -> bool:
|
||||
try:
|
||||
if url_or_path.startswith("http"):
|
||||
import httpx
|
||||
response = httpx.get(url_or_path, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
else:
|
||||
return resource_path(url_or_path).read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to fetch {url_or_path}: {e}")
|
||||
return ""
|
||||
import httpx
|
||||
response = httpx.get(url, timeout=2.0)
|
||||
return response.status_code == 200
|
||||
except:
|
||||
return False
|
||||
|
||||
# 辅助函数:将 CSS 中的字体 URL 替换为 CDN 链接
|
||||
def replace_font_urls(css_content: str) -> str:
|
||||
"""将 CSS 中的 url(fonts/xxx) 替换为 CDN URL"""
|
||||
def replace(match):
|
||||
url_path = match.group(1)
|
||||
if 'fonts/' in url_path:
|
||||
font_filename = url_path.split('/')[-1]
|
||||
return f'url({cdn_base}/KaTeX/0.16.9/fonts/{font_filename})'
|
||||
return match.group(0)
|
||||
return re.sub(r'url\(([^)]*fonts/[^)]*)\)', replace, css_content)
|
||||
# CDN 可用时直接用链接
|
||||
if self.cdn and can_access_cdn(f"{cdn_base}/KaTeX/0.16.9/katex.min.js"):
|
||||
pico = r'<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/picocss/2.1.1/pico.min.css" />'
|
||||
katex_css = r'<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/KaTeX/0.16.9/katex.min.css" />'
|
||||
katex_js = r'<script src="https://s4.zstatic.net/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>'
|
||||
copy_tex_css = r'<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.css" />'
|
||||
copy_tex_js = r'<script src="https://s4.zstatic.net/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.js"></script>'
|
||||
auto_render = r'<script src="https://s4.zstatic.net/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>'
|
||||
mermaid = r'<script src="https://s4.zstatic.net/ajax/libs/mermaid/10.6.1/mermaid.min.js"></script>'
|
||||
else:
|
||||
# CDN 不可用时,嵌入本地文件
|
||||
pico = f'<style>{_get_local_content("static/pico.css")}</style>'
|
||||
katex_css = f'<style>{_get_local_content("static/katex/katex.css")}</style>'
|
||||
katex_js = f'<script>{_get_local_content("static/katex/katex.js")}</script>'
|
||||
copy_tex_css = f'<style>{_get_local_content("static/katex/copy-tex.min.css")}</style>'
|
||||
copy_tex_js = f'<script>{_get_local_content("static/katex/copy-tex.min.js")}</script>'
|
||||
auto_render = f'<script>{_get_local_content("static/autoRender.js")}</script>'
|
||||
mermaid = f'<script>{_get_local_content("static/mermaid.js")}</script>'
|
||||
|
||||
# 辅助函数:包装为 style/script 标签
|
||||
def tag(content: str, tag_type: str) -> str:
|
||||
if tag_type == "style":
|
||||
return f"<style>\n{content}\n</style>"
|
||||
return f"<script>\n{content}\n</script>"
|
||||
|
||||
# Pico CSS
|
||||
pico_url = f"{cdn_base}/picocss/2.1.1/pico.min.css"
|
||||
pico = tag(fetch_text(pico_url), "style")
|
||||
|
||||
# KaTeX CSS (字体使用 CDN)
|
||||
katex_css_url = f"{cdn_base}/KaTeX/0.16.9/katex.min.css"
|
||||
katex_css_content = fetch_text(katex_css_url)
|
||||
katex_css_content = replace_font_urls(katex_css_content)
|
||||
katex_css = tag(katex_css_content, "style")
|
||||
|
||||
# KaTeX JS
|
||||
katex_js_url = f"{cdn_base}/KaTeX/0.16.9/katex.min.js"
|
||||
katex_js = tag(fetch_text(katex_js_url), "script")
|
||||
|
||||
# copy-tex CSS
|
||||
copy_tex_css_url = f"{cdn_base}/KaTeX/0.16.9/contrib/copy-tex.min.css"
|
||||
copy_tex_css = tag(fetch_text(copy_tex_css_url), "style")
|
||||
|
||||
# copy-tex JS
|
||||
copy_tex_js_url = f"{cdn_base}/KaTeX/0.16.9/contrib/copy-tex.min.js"
|
||||
copy_tex_js = tag(fetch_text(copy_tex_js_url), "script")
|
||||
|
||||
# auto-render JS
|
||||
auto_render_url = f"{cdn_base}/KaTeX/0.16.9/contrib/auto-render.min.js"
|
||||
auto_render = tag(fetch_text(auto_render_url), "script")
|
||||
|
||||
# renderMathInElement 配置
|
||||
render_math_in_element = r"""
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
@@ -96,20 +74,13 @@ class MD2HTMLExporter(MDExporter):
|
||||
],
|
||||
throwOnError: false,
|
||||
errorColor: '#F5CF27',
|
||||
macros: {
|
||||
"\\f": "#1f(#2)"
|
||||
},
|
||||
macros: { "\\f": "#1f(#2)" },
|
||||
trust: true,
|
||||
strict: false
|
||||
})
|
||||
});
|
||||
</script>"""
|
||||
|
||||
# mermaid JS
|
||||
mermaid_url = f"{cdn_base}/mermaid/10.6.1/mermaid.min.js"
|
||||
mermaid = tag(fetch_text(mermaid_url), "script")
|
||||
|
||||
# 扩展配置
|
||||
extensions = [
|
||||
'markdown.extensions.tables',
|
||||
'pymdownx.arithmatex',
|
||||
|
||||
@@ -12,203 +12,148 @@
|
||||
<style>
|
||||
@page {
|
||||
margin: 1.5cm 1cm 1.5cm 2cm;
|
||||
@top-left {
|
||||
content: "{{title}}";
|
||||
font-size: 10pt;
|
||||
color: #666;
|
||||
}
|
||||
@top-right {
|
||||
content: counter(page);
|
||||
font-size: 10pt;
|
||||
color: #666;
|
||||
}
|
||||
@bottom-left {
|
||||
content: "Powered by DocuTranslate";
|
||||
font-size: 8pt;
|
||||
color: #999;
|
||||
}
|
||||
@bottom-right {
|
||||
content: counter(page) " / " counter(pages);
|
||||
font-size: 10pt;
|
||||
color: #666;
|
||||
}
|
||||
@top-left { content: "{{title}}"; font-size: 10pt; color: #666; }
|
||||
@top-right { content: counter(page); font-size: 10pt; color: #666; }
|
||||
@bottom-left { content: "Powered by DocuTranslate"; font-size: 8pt; color: #999; }
|
||||
@bottom-right { content: counter(page) " / " counter(pages); font-size: 10pt; color: #666; }
|
||||
}
|
||||
|
||||
@media print {
|
||||
#toc-toggle, #toc-sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
html {
|
||||
padding: 0 !important;
|
||||
}
|
||||
main {
|
||||
padding: 0 !important;
|
||||
}
|
||||
#toc-btn, #toc-sidebar { display: none !important; }
|
||||
html, main { padding: 0 !important; }
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 15px;
|
||||
}
|
||||
html { font-size: 15px; }
|
||||
main { padding: 2vh 10vw; }
|
||||
|
||||
main {
|
||||
padding: 2vh 10vw;
|
||||
}
|
||||
|
||||
#toc-toggle {
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 30px;
|
||||
height: 60px;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 5px 5px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
#toc-toggle:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
body:hover #toc-toggle {
|
||||
opacity: 1;
|
||||
/* 目录按钮 */
|
||||
#toc-btn {
|
||||
position: fixed; left: 10px; top: 50%; transform: translateY(-50%);
|
||||
width: 30px; height: 50px;
|
||||
background: #f8f8f8; border: 1px solid #ddd; border-radius: 0 4px 4px 0;
|
||||
cursor: pointer; z-index: 1000;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 14px; color: #666;
|
||||
opacity: 0; transition: left 0.3s ease, opacity 0.2s;
|
||||
}
|
||||
#toc-btn:hover { background: #eee; }
|
||||
#toc-btn.visible { opacity: 1; }
|
||||
#toc-btn.open { left: 270px; }
|
||||
|
||||
/* 侧边栏 */
|
||||
#toc-sidebar {
|
||||
position: fixed;
|
||||
left: -280px;
|
||||
top: 0;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid #ddd;
|
||||
overflow-y: auto;
|
||||
z-index: 999;
|
||||
transition: left 0.3s ease;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#toc-sidebar.open {
|
||||
left: 0;
|
||||
position: fixed; left: -260px; top: 0; width: 260px; height: 100vh;
|
||||
background: #fafafa; border-right: 1px solid #ddd;
|
||||
z-index: 999; transition: left 0.3s ease;
|
||||
padding: 15px; box-sizing: border-box;
|
||||
}
|
||||
#toc-sidebar.open { left: 0; }
|
||||
|
||||
#toc-sidebar h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
margin: 0 0 15px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
#toc-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#toc-list li {
|
||||
margin: 5px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#toc-list { list-style: none; padding: 0; margin: 0; }
|
||||
#toc-list li { margin: 2px 0; }
|
||||
#toc-list li a {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 3px 0;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
#toc-list li a:hover {
|
||||
color: #007bff;
|
||||
background: #e8e8e8;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
#toc-list .toc-h1 { margin-left: 0; font-weight: 600; }
|
||||
#toc-list .toc-h2 { margin-left: 15px; }
|
||||
#toc-list .toc-h3 { margin-left: 30px; font-size: 12px; }
|
||||
#toc-list .toc-h4 { margin-left: 45px; font-size: 12px; }
|
||||
#toc-list .toc-h5 { margin-left: 60px; font-size: 11px; color: #888; }
|
||||
#toc-list .toc-h6 { margin-left: 75px; font-size: 11px; color: #888; }
|
||||
#toc-list .toc-h1 {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
#toc-list .toc-h1 > a { color: #333; }
|
||||
#toc-list .toc-h2 { margin-left: 0; }
|
||||
#toc-list .toc-h2 > a { padding-left: 24px; font-size: 14px; }
|
||||
#toc-list .toc-h3 { margin-left: 0; }
|
||||
#toc-list .toc-h3 > a { padding-left: 36px; font-size: 13px; color: #666; }
|
||||
#toc-list .toc-h4 { margin-left: 0; }
|
||||
#toc-list .toc-h4 > a { padding-left: 48px; font-size: 12px; color: #888; }
|
||||
#toc-list .toc-h5 > a { padding-left: 60px; font-size: 12px; color: #888; }
|
||||
#toc-list .toc-h6 > a { padding-left: 72px; font-size: 12px; color: #888; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Toggle Button -->
|
||||
<div id="toc-toggle" title="目录">☰</div>
|
||||
|
||||
<!-- Sidebar TOC -->
|
||||
<!-- 目录按钮 -->
|
||||
<div id="toc-btn" title="目录">☰</div>
|
||||
<!-- 侧边栏 -->
|
||||
<div id="toc-sidebar">
|
||||
<h3>目录</h3>
|
||||
<ul id="toc-list"></ul>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
{{markdown}}
|
||||
</main>
|
||||
<main>{{markdown}}</main>
|
||||
|
||||
</body>
|
||||
{{renderMathInElement}}
|
||||
{{mermaid}}
|
||||
<script>
|
||||
// Generate TOC from headings
|
||||
function generateTOC() {
|
||||
const main = document.querySelector('main');
|
||||
const headings = main.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
const tocList = document.getElementById('toc-list');
|
||||
(function(){
|
||||
const btn = document.getElementById('toc-btn');
|
||||
const sidebar = document.getElementById('toc-sidebar');
|
||||
const list = document.getElementById('toc-list');
|
||||
const main = document.querySelector('main');
|
||||
|
||||
if (headings.length === 0) {
|
||||
document.getElementById('toc-toggle').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
// 生成目录
|
||||
const headings = main.querySelectorAll('h1, h2, h3, h4');
|
||||
if (headings.length === 0) { btn.style.display = 'none'; return; }
|
||||
|
||||
headings.forEach((heading, index) => {
|
||||
if (!heading.id) {
|
||||
heading.id = 'heading-' + index;
|
||||
}
|
||||
|
||||
const level = heading.tagName.toLowerCase();
|
||||
const text = heading.textContent;
|
||||
const li = document.createElement('li');
|
||||
li.className = 'toc-' + level;
|
||||
li.innerHTML = `<a href="#${heading.id}">${text}</a>`;
|
||||
tocList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
function toggleTOC() {
|
||||
const sidebar = document.getElementById('toc-sidebar');
|
||||
sidebar.classList.toggle('open');
|
||||
}
|
||||
|
||||
document.getElementById('toc-toggle').addEventListener('click', toggleTOC);
|
||||
|
||||
// Generate TOC after content loads
|
||||
generateTOC();
|
||||
|
||||
setTimeout(() => {
|
||||
const KatexErrors = document.getElementsByClassName("katex-error")
|
||||
for (const katexError of KatexErrors) {
|
||||
katexError.innerHTML = katexError.title
|
||||
}
|
||||
}, 200)
|
||||
</script>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
securityLevel: 'loose',
|
||||
startOnLoad: true
|
||||
headings.forEach((h, i) => {
|
||||
if (!h.id) h.id = 'h-' + i;
|
||||
const li = document.createElement('li');
|
||||
li.className = 'toc-' + h.tagName.toLowerCase();
|
||||
li.innerHTML = '<a href="#' + h.id + '">' + h.textContent + '</a>';
|
||||
list.appendChild(li);
|
||||
});
|
||||
</script>
|
||||
|
||||
// 切换目录
|
||||
function toggleTOC() {
|
||||
const open = sidebar.classList.toggle('open');
|
||||
btn.classList.toggle('open', open);
|
||||
btn.textContent = open ? '✕' : '☰';
|
||||
}
|
||||
btn.onclick = toggleTOC;
|
||||
|
||||
// 左侧滑动/悬停检测
|
||||
let touchStartX = 0;
|
||||
|
||||
document.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; });
|
||||
document.addEventListener('touchmove', e => {
|
||||
const x = e.touches[0].clientX;
|
||||
if (touchStartX > 30 && x < 50 && !sidebar.classList.contains('open')) {
|
||||
btn.classList.add('visible');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', e => {
|
||||
if (e.clientX < 50 && !sidebar.classList.contains('open')) {
|
||||
btn.classList.add('visible');
|
||||
} else {
|
||||
btn.classList.remove('visible');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
setTimeout(() => {
|
||||
for (const e of document.getElementsByClassName('katex-error')) e.innerHTML = e.title;
|
||||
}, 200);
|
||||
|
||||
mermaid.initialize({ securityLevel: 'loose', startOnLoad: true });
|
||||
</script>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user