摘要
marked.min.js
是一个高效的 JavaScript Markdown
解析器,它能够将 Markdown
格式的文本转换为 HTML。作为一个轻量级的库,marked
在处理大规模的 Markdown
内容时表现出色,并且具备广泛的兼容性和可定制性。
本文将深入探讨如何使用 marked.min.j
s 来构建一个自定义的 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);
- 滚动条美化过了
演示
https://demo.likeyunba.com/md-marked/
本文作者
TANKING
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。