头图

摘要

marked.min.js 是一个高效的 JavaScript Markdown 解析器,它能够将 Markdown 格式的文本转换为 HTML。作为一个轻量级的库,marked 在处理大规模的 Markdown 内容时表现出色,并且具备广泛的兼容性和可定制性。

本文将深入探讨如何使用 marked.min.js 来构建一个自定义的 Markdown 解析器,涵盖其核心功能、配置选项以及如何在不同的应用场景中进行优化与集成。此外,还会介绍如何通过自定义渲染器、扩展功能来增强 marked 的解析能力,使其更适应复杂的 Markdown 语法和特殊需求。

index.html

加载本地data.md文件:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Markdown 解析器</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f6f6f6;
            line-height: 1.6;
            color: #333;
        }
        .container {
            display: flex;
            max-width: 1200px;
            margin: 0 auto;
            gap: 20px;
            height: calc(100vh - 40px);
        }
        .left-panel {
            width: 300px;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            overflow-y: auto;
            padding: 20px;
        }
        .right-panel {
            flex: 1;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            padding: 20px;
            overflow-y: auto;
            position: relative;
        }
        /* 滚动条美化 - Webkit 浏览器 */
        .left-panel::-webkit-scrollbar,
        .right-panel::-webkit-scrollbar {
            width: 8px; /* 滚动条宽度 */
        }
        .left-panel::-webkit-scrollbar-track,
        .right-panel::-webkit-scrollbar-track {
            background: #f6f6f6; /* 轨道背景色 */
            border-radius: 4px;
        }
        .left-panel::-webkit-scrollbar-thumb,
        .right-panel::-webkit-scrollbar-thumb {
            background: #bbbbbb; /* 滚动条滑块颜色 */
            border-radius: 4px;
            transition: background 0.2s;
        }
        .left-panel::-webkit-scrollbar-thumb:hover,
        .right-panel::-webkit-scrollbar-thumb:hover {
            background: #999999; /* 悬停时颜色加深 */
        }
        
        /* 滚动条美化 - Firefox */
        .left-panel {
            scrollbar-width: thin; /* 细滚动条 */
            scrollbar-color: #bbbbbb #f6f6f6; /* 滑块颜色 轨道颜色 */
        }
        .right-panel {
            scrollbar-width: thin;
            scrollbar-color: #bbbbbb #f6f6f6;
        }
        
        /* 滚动条美化 - Webkit 浏览器 (for code blocks) */
        pre::-webkit-scrollbar {
            width: 8px; /* 滚动条宽度 */
            height: 8px; /* 水平滚动条高度 */
        }
        pre::-webkit-scrollbar-track {
            background: #f6f6f6; /* 轨道背景色,与面板一致 */
            border-radius: 4px;
        }
        pre::-webkit-scrollbar-thumb {
            background: #bbbbbb; /* 滑块颜色,与面板一致 */
            border-radius: 4px;
            transition: background 0.2s;
        }
        pre::-webkit-scrollbar-thumb:hover {
            background: #999999; /* 悬停时颜色加深,与面板一致 */
        }
        
        /* 滚动条美化 - Firefox (for code blocks) */
        pre {
            scrollbar-width: thin; /* 细滚动条 */
            scrollbar-color: #bbbbbb #f6f6f6; /* 滑块颜色 轨道颜色,与面板一致 */
        }
        .toc {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        .toc li {
            margin: 10px 0;
            display: flex;
            align-items: center;
        }
        .toc .level-tag {
            display: inline-block;
            width: 28px;
            text-align: center;
            font-size: 12px;
            color: #888;
            margin-right: 8px;
        }
        .toc a {
            text-decoration: none;
            color: #555;
            font-size: 14px;
            transition: color 0.2s;
            cursor: pointer;
        }
        .toc a:hover {
            color: #2c82ff;
        }
        .toc .level-2 { margin-left: 16px; }
        .toc .level-3 { margin-left: 32px; }
        h1, h2, h3, h4, h5, h6 {
            color: #2c3e50;
            margin: 1.5em 0 0.5em;
            font-weight: 600;
        }
        h1 { font-size: 26px; border-bottom: 1px solid #eee; padding-bottom: 8px; }
        h2 { font-size: 22px; }
        h3 { font-size: 18px; }
        h4 { font-size: 16px; }
        h5 { font-size: 14px; }
        h6 { font-size: 12px; color: #777; }
        blockquote {
            margin: 1em 0;
            padding: 10px 15px;
            background: #f9f9f9;
            border-left: 4px solid #2c82ff;
            color: #555;
            border-radius: 4px;
            word-break: break-all;
            overflow-wrap: break-word;
            max-width: 100%;
        }
        blockquote p { margin: 0; }
        ul, ol {
            padding-left: 24px;
            margin: 1em 0;
        }
        ul li {
            list-style: none;
            position: relative;
        }
        ol li { margin: 5px 0; }
        code {
            font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
            background: #f5f5f5;
            padding: 2px 6px;
            border-radius: 4px;
            color: #e96900;
            font-size: 0.9em;
        }
        pre {
            position: relative;
            background: #2d2d2d;
            color: #f8f8f2;
            padding: 15px;
            border-radius: 6px;
            overflow-x: auto;
            margin: 1em 0;
            font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }
        pre code {
            background: none;
            padding: 0;
            color: inherit;
        }
        .copy-btn {
            position: absolute;
            top: 10px;
            right: 10px;
            padding: 4px 8px;
            background: #444;
            color: #fff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            transition: background 0.2s;
        }
        .copy-btn:hover {
            background: #666;
        }
        p { margin: 1.2em 0; }
        a { color: #2c82ff; text-decoration: none; }
        a:hover { text-decoration: underline; }
        .error { color: #e74c3c; padding: 10px; }
        img {
            max-width: 100%;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="left-panel">
            <h2>目录</h2>
            <ul id="toc" class="toc"></ul>
        </div>
        <div id="markdownOutput" class="right-panel"></div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <script>
        const output = document.getElementById('markdownOutput');
        const toc = document.getElementById('toc');

        function generateTOC(html) {
            output.innerHTML = html;
            toc.innerHTML = '';

            const headings = output.querySelectorAll('h1, h2, h3, h4, h5, h6');
            if (headings.length === 0) {
                toc.innerHTML = '<li>未找到标题</li>';
                console.warn('未在内容中找到任何标题');
                return;
            }

            headings.forEach((heading, index) => {
                const level = parseInt(heading.tagName.charAt(1));
                const id = `heading-${index}`;
                heading.id = id;

                const titleText = heading.textContent.trim() || heading.innerText.trim();
                if (!titleText) {
                    console.warn(`标题 ${id} 无内容,已跳过`);
                    return;
                }

                const li = document.createElement('li');
                li.className = `level-${level}`;
                const levelTag = document.createElement('span');
                levelTag.className = 'level-tag';
                levelTag.textContent = `H${level}`;
                const a = document.createElement('a');
                a.href = `#${id}`;
                a.textContent = titleText;
                a.addEventListener('click', (e) => {
                    e.preventDefault();
                    const target = output.querySelector(`#${id}`);
                    if (target) {
                        const offsetTop = target.offsetTop - 20;
                        output.scrollTo({
                            top: offsetTop,
                            behavior: 'smooth'
                        });
                    } else {
                        console.error(`无法找到目标标题: #${id}`);
                    }
                });
                li.appendChild(levelTag);
                li.appendChild(a);
                toc.appendChild(li);
            });

            // 添加复制按钮到代码块
            const codeBlocks = output.querySelectorAll('pre');
            codeBlocks.forEach((pre, index) => {
                const btn = document.createElement('button');
                btn.className = 'copy-btn';
                btn.textContent = '复制';
                btn.addEventListener('click', () => {
                    const code = pre.querySelector('code')?.textContent || pre.textContent;
                    navigator.clipboard.writeText(code).then(() => {
                        btn.textContent = '已复制';
                        setTimeout(() => { btn.textContent = '复制'; }, 2000);
                    }).catch(err => console.error('复制失败:', err));
                });
                pre.appendChild(btn);
            });

            console.log(`共渲染 ${headings.length} 个标题`);
        }

        fetch('data.md')
            .then(response => {
                if (!response.ok) {
                    throw new Error('无法加载 data.md 文件');
                }
                return response.text();
            })
            .then(markdownText => {
                const html = marked.parse(markdownText);
                generateTOC(html);
            })
            .catch(error => {
                output.innerHTML = `<div class="error">错误: ${error.message}</div>`;
                console.error('加载文件失败:', error);
            });

        marked.setOptions({
            breaks: true,
            gfm: true
        });
    </script>
</body>
</html>

解析效果

  • 左侧导航栏自动获取md文件的标题作为节点;
  • 好看的样式,对引用块、代码块、链接等常用语法样式进行优化;
  • 代码块支持一键复制;
  • 点击导航栏的标题可以滚动到指定位置;
  • 导航栏的标题显示当前的类型(H1-H5);
  • 滚动条美化过了

image.png

演示

https://demo.likeyunba.com/md-marked/

本文作者

TANKING


TANKING
4.8k 声望519 粉丝

热爱分享,热爱创作,热爱研究。


« 上一篇
开通了爱发电