Markdown 转 word核心实现

一个简洁高效的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>