摘要
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);
- 滾動條美化過了
演示
https://demo.likeyunba.com/md-marked/
本文作者
TANKING