自制markdown编辑器

<!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('![', '](image-url)')"><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, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;");
    }

    // 行内元素解析
    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%;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王小玗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值