Markdown 转 PDF核心实现

一个简洁高效的Markdown转PDF转换器,专注于HTML到PDF的转换过程,使用html2pdf.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 转 PDF 转换器</title>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        * {
            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, #4361ee, #3a0ca3);
            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);
        }
        
        .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: #f8f9fa;
            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 #4361ee;
        }
        
        .section-title {
            font-size: 1.3rem;
            color: #2b2d42;
            font-weight: 600;
        }
        
        .word-count {
            background: #4361ee;
            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;
        }
        
        textarea:focus {
            outline: none;
            border-color: #4895ef;
            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: #2b2d42;
            padding-bottom: 0.3em;
            border-bottom: 1px solid #eee;
        }
        
        #preview h1 {
            color: #4361ee;
            border-bottom: 2px solid #4361ee;
        }
        
        #preview p {
            margin: 1em 0;
        }
        
        #preview pre {
            background: #2b2d42;
            color: #f8f9fa;
            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: #e63946;
        }
        
        .controls {
            display: flex;
            gap: 15px;
            margin-top: 20px;
            padding: 0 25px 25px;
        }
        
        .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, #4361ee, #3f37c9);
            color: white;
        }
        
        .btn-pdf {
            background: linear-gradient(to right, #e63946, #c1121f);
            color: white;
        }
        
        .btn:hover {
            transform: translateY(-3px);
            box-shadow: 0 8px 15px rgba(0,0,0,0.15);
        }
        
        .pdf-settings {
            display: flex;
            gap: 15px;
            margin-top: 15px;
            background: #f8f9fa;
            padding: 20px;
            border-radius: 10px;
            border-top: 1px solid #e9ecef;
        }
        
        .setting-group {
            display: flex;
            flex-direction: column;
            gap: 8px;
            flex: 1;
        }
        
        .setting-group label {
            font-weight: 500;
            color: #2b2d42;
            font-size: 0.95rem;
        }
        
        select, input {
            padding: 12px 15px;
            border: 1px solid #ced4da;
            border-radius: 8px;
            font-size: 1rem;
            background: white;
        }
        
        .notification {
            position: fixed;
            top: 25px;
            right: 25px;
            padding: 18px 28px;
            background: #2a9d8f;
            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;
        }
        
        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;
            }
            
            .pdf-settings {
                flex-direction: column;
            }
            
            h1 {
                font-size: 2rem;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>Markdown 转 PDF 转换器</h1>
            <p>在浏览器中直接转换Markdown为PDF文档</p>
        </header>
        
        <div class="app-container">
            <div class="editor-preview">
                <div class="editor-section">
                    <div class="section-header">
                        <h2 class="section-title">
                            <span>Markdown 编辑器</span>
                            <span class="word-count" id="word-count">0 字</span>
                        </h2>
                    </div>
                    <textarea id="markdown-input" placeholder="在此输入Markdown文本..."># Markdown 转 PDF 示例

## 核心功能演示

此工具使用 **html2pdf.js** 将HTML内容转换为PDF文档,完全在浏览器中运行。

### 主要特点

- 100% 客户端转换(无服务器交互)
- 支持自定义页面设置
- 实时Markdown预览
- 简单易用的界面

### 代码示例

```javascript
// 核心转换函数
function generatePDF() {
  const element = document.getElementById('preview');
  const options = {
    margin: [10, 10, 10, 10],
    filename: 'document.pdf',
    image: { type: 'jpeg', quality: 0.98 },
    html2canvas: { scale: 2 },
    jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
  };
  
  html2pdf().set(options).from(element).save();
}
转换过程说明
将Markdown转换为HTML
使用html2pdf.js处理HTML元素
生成PDF文件并下载
提示:所有处理都在您的浏览器中完成,确保隐私安全</textarea>
</div>
            <div class="preview-section">
                <div class="section-header">
                    <h2 class="section-title">
                        <span>PDF 预览</span>
                    </h2>
                </div>
                <div id="preview"></div>
            </div>
        </div>
        
        <div class="pdf-settings">
            <div class="setting-group">
                <label for="page-size">页面尺寸:</label>
                <select id="page-size">
                    <option value="a4">A4 (210×297mm)</option>
                    <option value="letter">Letter (216×279mm)</option>
                    <option value="legal">Legal (216×356mm)</option>
                </select>
            </div>
            
            <div class="setting-group">
                <label for="page-orientation">页面方向:</label>
                <select id="page-orientation">
                    <option value="portrait">纵向</option>
                    <option value="landscape">横向</option>
                </select>
            </div>
            
            <div class="setting-group">
                <label for="filename">文件名:</label>
                <input type="text" id="filename" value="markdown-document" placeholder="输入文件名">
            </div>
        </div>
        
        <div class="controls">
            <button id="download-pdf" class="btn btn-pdf">
                <i class="fas fa-file-pdf"></i>
                <span>生成并下载PDF</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>
        </div>
    </div>
    
    <footer>
        <p>© 2023 Markdown 转 PDF 转换器 | 完全在浏览器端运行</p>
    </footer>
</div>

<div class="notification" id="notification">
    <i class="fas fa-check-circle"></i>
    <span>PDF文档已成功生成并下载!</span>
</div>

<script>
    // 获取DOM元素
    const markdownInput = document.getElementById('markdown-input');
    const preview = document.getElementById('preview');
    const downloadPdfBtn = document.getElementById('download-pdf');
    const clearBtn = document.getElementById('clear-btn');
    const exampleBtn = document.getElementById('example-btn');
    const wordCount = document.getElementById('word-count');
    const notification = document.getElementById('notification');
    const pageSizeSelect = document.getElementById('page-size');
    const pageOrientationSelect = document.getElementById('page-orientation');
    const filenameInput = document.getElementById('filename');
    
    // 配置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 + ' 字';
    }
    
    // 核心功能:HTML转PDF
    function generatePDF() {
        const element = preview;
        const options = {
            margin: [10, 10, 10, 10],
            filename: filenameInput.value + '.pdf',
            image: { type: 'jpeg', quality: 0.98 },
            html2canvas: { 
                scale: 2,
                useCORS: true,
                letterRendering: true
            },
            jsPDF: { 
                unit: 'mm', 
                format: pageSizeSelect.value,
                orientation: pageOrientationSelect.value
            }
        };
        
        // 显示生成中的提示
        downloadPdfBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 生成中...';
        downloadPdfBtn.disabled = true;
        
        // 生成PDF
        html2pdf().set(options).from(element).save().then(() => {
            // 显示成功通知
            notification.classList.add('show');
            setTimeout(() => {
                notification.classList.remove('show');
            }, 3000);
            
            // 恢复按钮状态
            downloadPdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> 生成并下载PDF';
            downloadPdfBtn.disabled = false;
        });
    }
    
    // 事件监听
    downloadPdfBtn.addEventListener('click', generatePDF);
    
    clearBtn.addEventListener('click', function() {
        markdownInput.value = '';
        updatePreview();
        updateStats();
    });
    
    exampleBtn.addEventListener('click', function() {
        markdownInput.value = `# 示例文档
		HTML 转 PDF 实现
这个示例展示了如何将HTML内容转换为PDF文档:

技术要点
使用 html2pdf.js 库实现转换
完全在浏览器端完成
支持自定义页面设置
无需服务器处理
转换代码
\`\`\`javascript
function convertToPDF() {
const element = document.getElementById('content');

const options = {
margin: 10,
filename: 'document.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};

html2pdf().set(options).from(element).save();
}
\`\`\`

使用说明
在编辑区编写Markdown内容
调整PDF设置(页面尺寸、方向等)
点击"生成并下载PDF"按钮
查看生成的PDF文件
注意:所有处理都在您的浏览器中完成,确保数据安全`;
updatePreview();
updateStats();
});
</script>

</body> </html>