<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>支持表格的Markdown编辑器</title>
<!-- Prism.js 主题 CSS -->
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.min.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>支持表格的Markdown编辑器</h1>
<div class="editor-container">
<div class="editor-pane">
<div class="pane-header">
<span>编辑区</span>
<span id="save-status" class="status">未保存</span>
</div>
<div class="toolbar">
<button onclick="insertText('**', '**')"><i class="fas fa-bold"></i></button>
<button onclick="insertText('*', '*')"><i class="fas fa-italic"></i></button>
<button onclick="insertText('# ', '')">H1</button>
<button onclick="insertText('## ', '')">H2</button>
<button onclick="insertText('[', '](url)')"><i class="fas fa-link"></i></button>
<button onclick="insertText('- ', '')"><i class="fas fa-list-ul"></i></button>
<button onclick="insertText('1. ', '\n2.\n3.')"><i class="fas fa-list-ol"></i></button>
<button onclick="insertText('```javascript\n', '\n```')"><i class="fas fa-code"></i></button>
<button onclick="insertText('> ', '')">|</button>
<button onclick="insertText('')"><i class="fas fa-image"></i></button>
<button onclick="insertTable()"><i class="fas fa-table"></i></button>
<button onclick="saveContent()" class="save-btn"><i class="fas fa-save"></i></button>
<button onclick="clearContent()" class="clear-btn"><i class="fas fa-trash-alt"></i></button>
</div>
<textarea id="markdown-input" placeholder="在这里输入Markdown内容..."></textarea>
</div>
<div class="preview-pane">
<div class="pane-header">预览区</div>
<div id="markdown-preview" class="preview"></div>
</div>
</div>
</div>
<!-- 引入 Prism.js 核心和语言组件 -->
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-css.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-html.min.js"></script>
<!-- 引入自定义脚本 -->
<script src="markdown-parser.js"></script>
<script src="editor.js"></script>
</body>
</html>
/**
* 自制Markdown解析器markdown-parser.js
*/
const marked = (function() {
// 默认配置
let config = {
autoLinks: false, // 是否自动识别链接
headingIds: false, // 是否自动添加标题ID
customIds: false // 是否支持自定义ID
};
// 配置设置函数
function setOptions(options) {
config = {
...config,
...options
};
return this; // 支持链式调用
}
// 获取当前配置
function getOptions() {
return {...config}; // 返回副本避免外部修改
}
// 主解析函数
function parse(markdownText) {
const lines = markdownText.split('\n');
let html = '';
let inCodeBlock = false;
let inList = false;
let listType = '';
let inTable = false;
let tableRows = [];
let currentLanguage = '';
let idCounter = 0;
function generateId(text) {
idCounter++;
return text
? text.toLowerCase().replace(/[^\w]+/g, '-').replace(/^-|-$/g, '')
: `section-${idCounter}`;
}
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
// 代码块处理
if (line.startsWith('```')) {
if (inCodeBlock) {
html += `</code></pre>\n`;
inCodeBlock = false;
currentLanguage = '';
} else {
currentLanguage = line.slice(3).trim() || '';
html += `<pre class="language-${currentLanguage}"><code class="language-${currentLanguage}">`;
inCodeBlock = true;
}
continue;
}
if (inCodeBlock) {
html += escapeHtml(line) + '\n';
continue;
}
// 表格处理
const tableMatch = line.match(/^\|(.+)\|$/);
if (tableMatch) {
if (!inTable) {
inTable = true;
tableRows = [];
}
tableRows.push(line);
continue;
} else if (inTable) {
html += parseTable(tableRows);
inTable = false;
tableRows = [];
}
// 标题处理(支持自定义ID)
const headingMatch = line.match(/^(#{1,6})\s(.*?)(?:\s*#([\w-]+))?$/);
if (headingMatch) {
const level = headingMatch[1].length;
let text = headingMatch[2];
let id = headingMatch[3];
if (!id && config.headingIds) {
id = generateId(text);
}
text = parseInline(text);
html += `<h${level}${id ? ` id="${id}"` : ''}>${text}</h${level}>\n`;
continue;
}
// 列表处理
const unorderedListMatch = line.match(/^(\s*)[-*+]\s(.*)/);
const orderedListMatch = line.match(/^(\s*)\d+\.\s(.*)/);
if (unorderedListMatch || orderedListMatch) {
const match = unorderedListMatch || orderedListMatch;
const isOrdered = !!orderedListMatch;
const text = match[2];
const currentListType = isOrdered ? 'ol' : 'ul';
if (!inList || listType !== currentListType) {
if (inList) {
html += listType === 'ul' ? '</ul>\n' : '</ol>\n';
}
html += isOrdered ? '<ol>\n' : '<ul>\n';
inList = true;
listType = currentListType;
}
html += `<li>${parseInline(text)}</li>\n`;
continue;
} else if (inList && line.trim() === '') {
html += listType === 'ul' ? '</ul>\n' : '</ol>\n';
inList = false;
listType = '';
}
// 引用处理
const blockquoteMatch = line.match(/^>\s?(.*)/);
if (blockquoteMatch) {
if (!html.endsWith('<blockquote>\n')) {
html += '<blockquote>\n';
}
html += `<p>${parseInline(blockquoteMatch[1])}</p>\n`;
continue;
} else if (html.endsWith('</p>\n') && line.trim() === '') {
html += '</blockquote>\n';
}
// 分隔线处理
if (/^[-*_]{3,}$/.test(line.trim())) {
html += '<hr>\n';
continue;
}
// 段落处理
if (line.trim() !== '') {
if (html.endsWith('</p>\n') || html === '') {
html += '<p>';
} else {
html += '<br>';
}
html += parseInline(line);
} else if (html.endsWith('</p>\n') === false && html !== '') {
html += '</p>\n';
}
}
// 关闭未闭合的标签
if (inList) {
html += listType === 'ul' ? '</ul>\n' : '</ol>\n';
}
if (inCodeBlock) {
html += '</code></pre>\n';
}
if (inTable) {
html += parseTable(tableRows);
}
if (html.endsWith('</p>\n') === false && html !== '') {
html += '</p>\n';
}
return html;
}
// 表格解析
function parseTable(rows) {
if (rows.length < 2) return '';
let html = '<table>\n';
const headerCells = rows[0].split('|').map(cell => cell.trim()).filter(cell => cell);
const alignRow = rows[1].split('|').map(cell => cell.trim()).filter(cell => cell);
html += '<thead>\n<tr>\n';
for (let i = 0; i < headerCells.length; i++) {
const align = getAlign(alignRow[i]);
html += `<th${align}>${parseInline(headerCells[i])}</th>\n`;
}
html += '</tr>\n</thead>\n';
html += '<tbody>\n';
for (let i = 2; i < rows.length; i++) {
const cells = rows[i].split('|').map(cell => cell.trim()).filter(cell => cell);
if (cells.length === 0) continue;
html += '<tr>\n';
for (let j = 0; j < headerCells.length; j++) {
const align = getAlign(alignRow[j]);
const content = j < cells.length ? parseInline(cells[j]) : '';
html += `<td${align}>${content}</td>\n`;
}
html += '</tr>\n';
}
html += '</tbody>\n</table>\n';
return html;
}
// 获取表格对齐方式
function getAlign(alignCell) {
if (!alignCell) return '';
const left = alignCell.startsWith(':');
const right = alignCell.endsWith(':');
if (left && right) return ' style="text-align:center"';
if (left) return ' style="text-align:left"';
if (right) return ' style="text-align:right"';
return '';
}
// HTML转义
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// 行内元素解析
function parseInline(text) {
// 处理自定义ID
if (config.customIds) {
text = text.replace(/\{#([\w-]+)\}/g, (match, id) => {
return `<span id="${id}"></span>`;
});
}
// 处理图片和显式链接(优先级最高)
text = text.replace(/!\[([^\]]+)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// 处理自动链接
if (config.autoLinks) {
text = text.replace(/(^|\s)(https?|ftp):\/\/[^\s<]+[^<.,:;"')\]\s]/g, (match, prefix) => {
// 检查是否已经在<a>标签内
if (/<a\b[^>]*>.*?<\/a>/i.test(match)) {
return match;
}
return `${prefix}<a href="${match.trim()}">${match.trim()}</a>`;
});
}
// 处理其他行内元素
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
text = text.replace(/(\*\*|__)(.*?)\1/g, '<strong>$2</strong>');
text = text.replace(/(\*|_)(.*?)\1/g, '<em>$2</em>');
text = text.replace(/~~(.*?)~~/g, '<del>$1</del>');
return text;
}
return {
parse,
setOptions,
getOptions
};
})();
/*editor.js*/
// 获取DOM元素
const markdownInput = document.getElementById('markdown-input');
const markdownPreview = document.getElementById('markdown-preview');
const saveStatus = document.getElementById('save-status');
// 初始化内容
function initContent() {
marked.setOptions({
autoLinks: true,
headingIds: false,
customIds: true
});
const savedContent = localStorage.getItem('markdownContent');
if (savedContent) {
markdownInput.value = savedContent;
saveStatus.textContent = '已加载保存的内容';
setTimeout(() => {
saveStatus.textContent = '已保存';
}, 2000);
} else {
// 默认示例内容
const initialContent = `# 支持表格的Markdown编辑器
这是一个支持表格的 **Markdown 编辑器**,使用自制的解析器和 Prism.js 代码高亮。
## 功能特点
- 实时预览
- 代码语法高亮
- 表格支持
- 本地保存功能
- 支持列表
1.a
2.b
3.c
## 表格示例
| 姓名 | 年龄 | 职业 |
| ---- | ---- | ---- |
| 张三 | 25 | 工程师 |
| 李四 | 30 | 设计师 |
| 王五 | 28 | 产品经理 |
## 代码示例
\`\`\`javascript
// JavaScript 示例
function hello() {
console.log('Hello, Markdown!');
return {
name: 'Prism.js',
version: '1.29.0'
};
}
\`\`\`
> [点击这里](https://example.com) 访问示例网站。`;
markdownInput.value = initialContent;
saveStatus.textContent = '未保存';
}
updatePreview();
}
// 初始化
initContent();
// 输入时实时更新预览
markdownInput.addEventListener('input', () => {
updatePreview();
saveStatus.textContent = '未保存';
});
// 更新预览函数
function updatePreview() {
const markdownText = markdownInput.value;
const htmlText = marked.parse(markdownText);
markdownPreview.innerHTML = htmlText;
// 使用 Prism.js 高亮代码块
if (window.Prism) {
Prism.highlightAllUnder(markdownPreview);
}
}
// 插入文本函数
function insertText(prefix, suffix) {
const textarea = markdownInput;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
const beforeText = textarea.value.substring(0, start);
const afterText = textarea.value.substring(end);
textarea.value = beforeText + prefix + selectedText + suffix + afterText;
// 设置光标位置
if (selectedText.length > 0) {
textarea.selectionStart = start + prefix.length;
textarea.selectionEnd = end + prefix.length;
} else {
textarea.selectionStart = textarea.selectionEnd = start + prefix.length;
}
textarea.focus();
updatePreview();
saveStatus.textContent = '未保存';
}
// 插入表格
function insertTable() {
const tableTemplate = `| 标题1 | 标题2 | 标题3 |
| ---- | ---- | ---- |
| 内容1 | 内容2 | 内容3 |
| 内容4 | 内容5 | 内容6 |`;
insertText(tableTemplate, '');
}
// 保存内容到本地存储
function saveContent() {
const content = markdownInput.value;
localStorage.setItem('markdownContent', content);
saveStatus.textContent = '已保存';
// 显示保存成功的提示
const originalText = saveStatus.textContent;
saveStatus.textContent = '保存成功!';
setTimeout(() => {
saveStatus.textContent = originalText;
}, 1000);
}
// 清空内容
function clearContent() {
if (confirm('确定要清空所有内容吗?')) {
markdownInput.value = '';
updatePreview();
localStorage.removeItem('markdownContent');
saveStatus.textContent = '已清空';
}
}
// 自动保存功能(每30秒自动保存一次)
setInterval(() => {
if (saveStatus.textContent === '未保存') {
saveContent();
}
}, 30000);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #2c3e50;
}
.editor-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.editor-pane, .preview-pane {
flex: 1 1 45%;
min-width: 300px;
}
.pane-header {
background-color: #2c3e50;
color: white;
padding: 10px 15px;
border-radius: 5px 5px 0 0;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
textarea {
width: 100%;
height: 500px;
padding: 15px;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 5px 5px;
font-family: 'Courier New', monospace;
font-size: 14px;
resize: none;
background-color: #fff;
}
.preview {
width: 100%;
height: 500px;
padding: 15px;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 5px 5px;
overflow-y: auto;
background-color: #fff;
}
.toolbar {
display: flex;
gap: 5px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.toolbar button {
padding: 5px 10px;
background-color: #3498db;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
}
.toolbar button:hover {
background-color: #2980b9;
}
.save-btn {
background-color: #27ae60 !important;
}
.save-btn:hover {
background-color: #219653 !important;
}
.clear-btn {
background-color: #e74c3c !important;
}
.clear-btn:hover {
background-color: #c0392b !important;
}
.status {
font-size: 12px;
color: #7f8c8d;
font-style: italic;
}
/* 预览区域的样式 */
.preview h1, .preview h2, .preview h3, .preview h4, .preview h5, .preview h6 {
margin: 1em 0 0.5em 0;
color: #2c3e50;
}
.preview p {
margin: 0 0 1em 0;
}
.preview ul, .preview ol {
margin: 0 0 1em 2em;
}
.preview blockquote {
border-left: 4px solid #3498db;
padding-left: 1em;
margin: 0 0 1em 0;
color: #7f8c8d;
}
.preview code:not([class*="language-"]) {
background-color: #f8f8f8;
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #c7254e;
}
.preview img {
max-width: 100%;
}
.preview a {
color: #3498db;
text-decoration: none;
}
.preview a:hover {
text-decoration: underline;
}
.preview table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
.preview table th, .preview table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.preview table th {
background-color: #f2f2f2;
font-weight: bold;
}
.preview ol, .preview ul {
margin-left:4em
}
@media (max-width: 768px) {
.editor-pane, .preview-pane {
flex: 1 1 100%;
}
}