一个简洁高效的Markdown转PDF转换器,专注于HTML到PDF的转换过程,使用 html-docx.min.js 库实现。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown 转 Word 转换器 - 解决有序列表问题</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/html-docx-js/dist/html-docx.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #4361ee;
--secondary: #3a0ca3;
--accent: #4cc9f0;
--danger: #e63946;
--success: #2a9d8f;
--light: #f8f9fa;
--dark: #212529;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, var(--primary), var(--secondary));
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
padding: 20px 0 30px;
color: white;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.subtitle {
max-width: 800px;
margin: 0 auto 20px;
font-size: 1.1rem;
opacity: 0.9;
}
.app-container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
overflow: hidden;
margin-bottom: 30px;
display: flex;
flex-direction: column;
min-height: 70vh;
}
.editor-preview {
display: flex;
min-height: 500px;
}
.editor-section, .preview-section {
padding: 25px;
min-height: 400px;
flex: 1;
}
.editor-section {
background: var(--light);
border-right: 1px solid #e9ecef;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 12px;
border-bottom: 2px solid var(--primary);
}
.section-title {
font-size: 1.3rem;
color: var(--dark);
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.section-title i {
color: var(--primary);
}
.word-count {
background: var(--primary);
color: white;
padding: 3px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
textarea {
width: 100%;
height: 400px;
padding: 20px;
font-family: 'Fira Code', monospace;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 10px;
resize: none;
box-shadow: inset 0 2px 6px rgba(0,0,0,0.05);
line-height: 1.7;
background: white;
transition: all 0.3s;
}
textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(72, 149, 239, 0.2);
}
#preview {
height: 400px;
padding: 20px;
overflow-y: auto;
border: 1px solid #e9ecef;
border-radius: 10px;
background: white;
font-size: 16px;
line-height: 1.8;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
}
/* 预览样式 */
#preview h1,
#preview h2,
#preview h3 {
margin: 1.2em 0 0.8em;
color: var(--dark);
padding-bottom: 0.3em;
border-bottom: 1px solid #eee;
}
#preview h1 {
color: var(--primary);
border-bottom: 2px solid var(--primary);
}
#preview p {
margin: 1em 0;
}
#preview pre {
background: #2b2d42;
color: var(--light);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
margin: 1.5em 0;
}
#preview code {
font-family: 'Fira Code', monospace;
background: rgba(67, 97, 238, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.95em;
color: var(--danger);
}
.controls {
display: flex;
gap: 15px;
margin-top: 20px;
padding: 0 25px 25px;
flex-wrap: wrap;
}
.btn {
padding: 15px 25px;
font-size: 1.1rem;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.btn i {
font-size: 1.3em;
}
.btn-primary {
background: linear-gradient(to right, var(--primary), var(--secondary));
color: white;
}
.btn-word {
background: linear-gradient(to right, #1d4ed8, #3b82f6);
color: white;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 15px rgba(0,0,0,0.15);
}
.btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.format-info {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
border-top: 1px solid #e9ecef;
margin-top: 15px;
}
.info-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
color: var(--primary);
font-size: 1.2rem;
font-weight: 600;
}
.info-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.info-box {
background: white;
border-radius: 10px;
padding: 15px;
border-left: 4px solid var(--accent);
box-shadow: 0 3px 10px rgba(0,0,0,0.05);
}
.info-box h3 {
margin-bottom: 10px;
color: var(--dark);
}
.info-box p {
font-size: 0.95rem;
color: #495057;
}
.notification {
position: fixed;
top: 25px;
right: 25px;
padding: 18px 28px;
background: var(--success);
color: white;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
transform: translateX(200%);
transition: transform 0.4s ease;
z-index: 1000;
display: flex;
align-items: center;
gap: 15px;
font-size: 1.1rem;
}
.notification.show {
transform: translateX(0);
}
.notification i {
font-size: 1.8rem;
}
.notification.error {
background: var(--danger);
}
.progress-container {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
margin-top: 15px;
display: none;
}
.progress-bar {
height: 100%;
background: linear-gradient(to right, var(--primary), var(--secondary));
width: 0%;
transition: width 0.4s ease;
}
footer {
text-align: center;
color: white;
padding: 20px 0;
font-size: 1rem;
}
@media (max-width: 768px) {
.editor-preview {
flex-direction: column;
}
.editor-section {
border-right: none;
border-bottom: 1px solid #e9ecef;
}
.format-info {
flex-direction: column;
}
h1 {
font-size: 2rem;
}
.controls {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Markdown 转 Word 转换器</h1>
<p class="subtitle">专为解决有序列表乱码问题设计 - 在浏览器中直接转换Markdown为Word文档</p>
</header>
<div class="app-container">
<div class="editor-preview">
<div class="editor-section">
<div class="section-header">
<h2 class="section-title">
<i class="fas fa-edit"></i>
<span>Markdown 编辑器</span>
<span class="word-count" id="word-count">0 字</span>
</h2>
</div>
<textarea id="markdown-input" placeholder="在此输入Markdown文本..."># Markdown 转 Word 解决方案
## 有序列表问题已修复
此工具解决了Markdown转换为Word时有序列表(ol)的乱码问题:
### 问题原因分析
1. Word对HTML列表的渲染机制不同
2. 缺少Word兼容的列表样式
3. 嵌套列表的编号系统不兼容
4. 传统方法使用的CSS计数器不被Word支持
### 新解决方案
1. 使用`html-docx-js`库生成真正的Word文档
2. 添加Word专用的列表样式
3. 完全移除CSS计数器方法
4. 确保所有样式内联
### 完美呈现的列表示例
1. 第一级项目
1. 第二级项目
2. 另一个第二级项目
1. 第三级项目
2. 另一个第三级项目
2. 回到第一级
3. 最后一个第一级项目
> 提示:所有转换都在浏览器中完成,确保您的数据安全</textarea>
</div>
<div class="preview-section">
<div class="section-header">
<h2 class="section-title">
<i class="fas fa-file-word"></i>
<span>Word 预览</span>
</h2>
</div>
<div id="preview"></div>
</div>
</div>
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div class="format-info">
<div class="info-header">
<i class="fas fa-info-circle"></i>
<h2>格式兼容性说明</h2>
</div>
<div class="info-content">
<div class="info-box">
<h3>有序列表修复</h3>
<p>使用Word专用样式和html-docx-js库解决了有序列表乱码问题,支持多级嵌套列表。</p>
</div>
<div class="info-box">
<h3>真正的Word文档</h3>
<p>生成.docx格式的Word文档,而不是HTML格式的伪Word文件。</p>
</div>
<div class="info-box">
<h3>标题样式优化</h3>
<p>标题添加底部边框和特殊颜色,在Word中保持层次结构清晰。</p>
</div>
<div class="info-box">
<h3>段落与间距</h3>
<p>优化段落间距和行高,确保在Word中阅读体验良好。</p>
</div>
</div>
</div>
<div class="controls">
<button id="download-word" class="btn btn-word">
<i class="fas fa-file-word"></i>
<span>下载 Word 文档 (.docx)</span>
</button>
<button id="clear-btn" class="btn">
<i class="fas fa-trash-alt"></i>
<span>清空内容</span>
</button>
<button id="example-btn" class="btn btn-primary">
<i class="fas fa-file-code"></i>
<span>加载示例</span>
</button>
<button id="problem-btn" class="btn">
<i class="fas fa-bug"></i>
<span>查看问题示例</span>
</button>
</div>
</div>
<footer>
<p>? 2023 Markdown 转 Word 转换器 | 解决有序列表乱码问题 | 完全在浏览器端运行</p>
</footer>
</div>
<div class="notification" id="notification">
<i class="fas fa-check-circle"></i>
<span>Word文档已成功生成并下载!</span>
</div>
<script>
// 获取DOM元素
const markdownInput = document.getElementById('markdown-input');
const preview = document.getElementById('preview');
const downloadWordBtn = document.getElementById('download-word');
const clearBtn = document.getElementById('clear-btn');
const exampleBtn = document.getElementById('example-btn');
const problemBtn = document.getElementById('problem-btn');
const wordCount = document.getElementById('word-count');
const notification = document.getElementById('notification');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
// 配置marked
marked.setOptions({
breaks: true,
gfm: true
});
// 初始渲染
updatePreview();
updateStats();
// 输入时更新预览
markdownInput.addEventListener('input', function() {
updatePreview();
updateStats();
});
// 更新预览函数
function updatePreview() {
const markdownText = markdownInput.value;
preview.innerHTML = marked.parse(markdownText);
}
// 更新统计信息
function updateStats() {
const text = markdownInput.value;
const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length;
wordCount.textContent = words + ' 字';
}
// 核心功能:生成Word文档
function generateWord() {
try {
// 显示进度条
progressContainer.style.display = 'block';
progressBar.style.width = '25%';
// 获取处理后的HTML内容
const htmlContent = getWordHtmlContent();
progressBar.style.width = '50%';
// 生成文件名
const filename = getFilename();
progressBar.style.width = '75%';
// 使用html-docx-js生成Word文档
const converted = htmlDocx.asBlob(htmlContent);
progressBar.style.width = '100%';
// 下载文件
saveAs(converted, filename);
// 隐藏进度条
setTimeout(() => {
progressContainer.style.display = 'none';
progressBar.style.width = '0%';
}, 1000);
// 显示成功通知
showNotification('Word文档已成功生成并下载!', false);
} catch (error) {
console.error('生成Word文档时出错:', error);
showNotification('生成Word文档时出错: ' + error.message, true);
progressContainer.style.display = 'none';
progressBar.style.width = '0%';
}
}
// 获取适合Word的HTML内容(内联样式)
function getWordHtmlContent() {
// 创建临时容器
const tempDiv = document.createElement('div');
tempDiv.innerHTML = marked.parse(markdownInput.value);
// 添加Word兼容的样式(内联)
const styles = `
<style>
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: #333;
padding: 20px;
}
/* Word兼容的列表样式 */
ol, ul {
margin-left: 1.5em;
padding-left: 0;
}
li {
margin-bottom: 0.5em;
}
h1, h2, h3 {
margin: 1.2em 0 0.8em;
color: #212529;
padding-bottom: 0.3em;
border-bottom: 1px solid #eee;
}
h1 {
color: #4361ee;
border-bottom: 2px solid #4361ee;
}
p {
margin: 1em 0;
}
pre {
background: #2b2d42;
color: #f8f9fa;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
margin: 1.5em 0;
font-family: 'Fira Code', monospace;
}
code {
font-family: 'Fira Code', monospace;
background: rgba(67, 97, 238, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.95em;
color: #e63946;
}
blockquote {
border-left: 4px solid #4361ee;
padding-left: 15px;
margin: 1.5em 0;
color: #495057;
}
</style>
`;
// 创建完整的HTML文档
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${getFilename().replace('.docx', '')}</title>
${styles}
</head>
<body>
${tempDiv.innerHTML}
</body>
</html>
`;
}
// 获取文件名
function getFilename() {
const text = markdownInput.value;
let filename = "markdown-document";
// 尝试从第一行提取标题作为文件名
const firstLine = text.split('\n')[0];
if (firstLine && firstLine.startsWith('# ')) {
filename = firstLine.replace('#', '').trim();
// 移除文件名中的非法字符
filename = filename.replace(/[\\/:*?"<>|]/g, '');
}
return filename + '.docx';
}
// 显示通知
function showNotification(message, isError) {
const notification = document.getElementById('notification');
notification.innerHTML = `
<i class="fas ${isError ? 'fa-exclamation-circle' : 'fa-check-circle'}"></i>
<span>${message}</span>
`;
if (isError) {
notification.classList.add('error');
} else {
notification.classList.remove('error');
}
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
// 事件监听
downloadWordBtn.addEventListener('click', function() {
downloadWordBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 生成中...';
downloadWordBtn.disabled = true;
setTimeout(() => {
generateWord();
downloadWordBtn.innerHTML = '<i class="fas fa-file-word"></i> 下载 Word 文档 (.docx)';
downloadWordBtn.disabled = false;
}, 100);
});
clearBtn.addEventListener('click', function() {
markdownInput.value = '';
updatePreview();
updateStats();
showNotification('内容已清空', false);
});
exampleBtn.addEventListener('click', function() {
markdownInput.value = `# Markdown 转 Word 完美解决方案
## 有序列表示例 - 完美呈现
这个示例展示了如何正确显示有序列表:
### 多级嵌套列表
1. 第一级项目
1. 第二级项目
2. 另一个第二级项目
1. 第三级项目
2. 另一个第三级项目
2. 回到第一级
3. 最后一个第一级项目
### 复杂列表结构
1. 编程语言
1. 编译型语言
1. C
2. C++
3. Go
2. 解释型语言
1. Python
2. JavaScript
3. Ruby
2. 数据库系统
1. SQL数据库
1. MySQL
2. PostgreSQL
2. NoSQL数据库
1. MongoDB
2. Redis
## 其他格式支持
### 代码块
\`\`\`javascript
function generateWord() {
// 获取处理后的HTML内容
const htmlContent = getWordHtmlContent();
// 使用html-docx-js生成Word文档
const converted = htmlDocx.asBlob(htmlContent);
// 下载文件
saveAs(converted, 'document.docx');
}
\`\`\`
### 引用
> 这个解决方案完全在浏览器中运行,无需服务器处理,确保您的数据安全。
### 表格
| 功能 | 支持情况 | 备注 |
|--------------|----------|--------------------|
| 有序列表 | ? 完美 | 解决乱码问题 |
| 无序列表 | ? 完美 | |
| 代码块 | ? 良好 | 保留语法高亮 |
| 表格 | ? 良好 | 基本表格支持 |
| 图片 | ?? 有限 | 需要绝对路径 |
## 使用说明
1. 在左侧编辑区输入Markdown内容
2. 实时预览右侧的Word格式效果
3. 点击"下载Word文档"按钮
4. 在Microsoft Word中打开下载的文件
> 注意:所有处理都在您的浏览器中完成,确保数据安全`;
updatePreview();
updateStats();
showNotification('示例内容已加载', false);
});
problemBtn.addEventListener('click', function() {
markdownInput.value = `# 传统方法的问题演示
## 有序列表乱码问题
这是传统转换方法中会出现问题的列表:
1. 第一项
2. 第二项
1. 子项一
2. 子项二
3. 第三项
## 问题表现
在Word中打开时,可能出现:
1. 编号显示为乱码字符
2. 嵌套层级显示不正确
3. 编号顺序错误
4. 格式完全混乱
## 问题原因
- Word对HTML列表的渲染机制不同
- 传统方法使用的CSS计数器不被支持
- 嵌套列表的HTML结构不兼容
- 缺少针对Word的样式优化
> 使用本工具可以完美解决这些问题`;
updatePreview();
updateStats();
showNotification('问题示例已加载', false);
});
</script>
</body>
</html>