Files

905 rindas
32 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>云笔记</title>
<!-- Highlight.js 代码高亮 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #fafafa;
color: #333;
}
header {
background: #fff;
padding: 16px 24px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
header h1 {
font-size: 20px;
font-weight: 600;
color: #1a73e8;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.search-box {
display: flex;
align-items: center;
background: #f1f3f4;
border-radius: 24px;
padding: 8px 16px;
}
.search-box input {
border: none;
background: transparent;
outline: none;
width: 200px;
font-size: 14px;
}
.search-box svg {
width: 20px;
height: 20px;
fill: #666;
}
.main {
display: flex;
margin-top: 65px;
height: calc(100vh - 65px);
}
/* 左侧边栏 */
.sidebar {
width: 280px;
background: #fff;
border-right: 1px solid #e0e0e0;
overflow-y: auto;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-header h3 {
font-size: 14px;
font-weight: 600;
color: #333;
}
.sidebar-section {
padding: 12px;
}
.sidebar-section h4 {
font-size: 11px;
text-transform: uppercase;
color: #888;
margin-bottom: 8px;
font-weight: 500;
padding: 0 8px;
}
/* 树形目录 */
.tree-view {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.tree-item {
user-select: none;
}
.tree-node {
display: flex;
align-items: center;
padding: 8px 10px;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
gap: 6px;
}
.tree-node:hover {
background: #f5f5f5;
}
.tree-node.active {
background: #e3f2fd;
color: #1976d2;
}
.tree-toggle {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform 0.2s;
}
.tree-toggle svg {
width: 14px;
height: 14px;
fill: #888;
}
.tree-toggle.expanded {
transform: rotate(90deg);
}
.tree-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.tree-icon svg {
width: 16px;
height: 16px;
}
.tree-icon.folder svg { fill: #f9a825; }
.tree-icon.note svg { fill: #42a5f5; }
.tree-label {
flex: 1;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tree-children {
padding-left: 20px;
}
.tree-children.collapsed {
display: none;
}
.tree-item.pinned .tree-icon svg { fill: #f57c00; }
/* 筛选区域 */
.filter-area {
padding: 12px;
border-top: 1px solid #e0e0e0;
background: #fafafa;
}
.filter-item {
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
display: flex;
align-items: center;
gap: 10px;
color: #555;
transition: background 0.2s;
}
.filter-item:hover { background: #f1f3f4; }
.filter-item.active { background: #e3f2fd; color: #1976d2; }
.filter-item svg { width: 16px; height: 16px; }
.note-list {
border-top: 1px solid #e0e0e0;
padding: 8px;
}
.note-item {
padding: 14px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 4px;
transition: background 0.2s;
}
.note-item:hover { background: #f1f3f4; }
.note-item.active { background: #e8f0fe; }
.note-item h4 {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
color: #202124;
}
.note-item .preview {
font-size: 12px;
color: #666;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.note-item .meta {
font-size: 11px;
color: #999;
margin-top: 6px;
}
.note-item .tags {
display: flex;
gap: 4px;
margin-top: 6px;
flex-wrap: wrap;
}
.note-item .tag {
padding: 2px 8px;
background: #e8f0fe;
color: #1a73e8;
border-radius: 12px;
font-size: 11px;
}
/* 右侧内容区 */
.content {
flex: 1;
overflow-y: auto;
background: #fff;
display: flex;
}
/* 笔记目录(正文内) */
.toc-sidebar {
width: 220px;
border-right: 1px solid #e8e8e8;
padding: 24px 16px;
background: #fafafa;
flex-shrink: 0;
overflow-y: auto;
}
.toc-title {
font-size: 12px;
font-weight: 600;
color: #888;
text-transform: uppercase;
margin-bottom: 12px;
padding-left: 8px;
}
.toc-list {
list-style: none;
}
.toc-item {
margin-bottom: 4px;
}
.toc-link {
display: block;
padding: 6px 8px;
font-size: 13px;
color: #555;
text-decoration: none;
border-radius: 4px;
transition: all 0.15s;
border-left: 2px solid transparent;
}
.toc-link:hover {
background: #e8e8e8;
color: #333;
}
.toc-link.active {
background: #e3f2fd;
color: #1976d2;
border-left-color: #1976d2;
}
.toc-link.level-2 { padding-left: 16px; font-size: 12px; }
.toc-link.level-3 { padding-left: 24px; font-size: 12px; color: #777; }
/* 正文区域 */
.note-area {
flex: 1;
overflow-y: auto;
padding: 40px;
}
.note-detail {
max-width: 800px;
margin: 0 auto;
}
.note-detail h1 {
font-size: 32px;
font-weight: 600;
margin-bottom: 16px;
color: #202124;
}
.note-detail .meta {
font-size: 14px;
color: #666;
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid #e0e0e0;
}
.note-detail .meta span {
margin-right: 16px;
}
.note-detail .tags {
display: inline-flex;
gap: 8px;
}
.note-detail .tag {
padding: 4px 12px;
background: #e8f0fe;
color: #1a73e8;
border-radius: 16px;
font-size: 13px;
}
.note-content {
font-size: 16px;
line-height: 1.8;
color: #333;
}
.note-content h1, .note-content h2, .note-content h3 {
margin: 24px 0 12px 0;
color: #202124;
}
.note-content h1 { font-size: 28px; }
.note-content h2 { font-size: 24px; }
.note-content h3 { font-size: 20px; }
.note-content p { margin: 12px 0; }
.note-content code {
background: #f1f3f4;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 14px;
}
.note-content pre {
background: #f8f9fa;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 16px 0;
}
.note-content pre code {
background: none;
padding: 0;
}
.note-content blockquote {
border-left: 4px solid #1a73e8;
padding-left: 16px;
margin: 16px 0;
color: #555;
}
.note-content ul, .note-content ol {
margin: 12px 0;
padding-left: 24px;
}
.note-content li { margin: 4px 0; }
.note-content a { color: #1a73e8; }
/* 空状态 */
.empty-state {
text-align: center;
padding: 80px 40px;
color: #666;
}
.empty-state svg {
width: 80px;
height: 80px;
fill: #ddd;
margin-bottom: 20px;
}
.empty-state h2 {
font-size: 20px;
margin-bottom: 8px;
color: #333;
}
.empty-state p { font-size: 14px; }
/* 底部 */
footer {
text-align: center;
padding: 20px;
color: #999;
font-size: 12px;
}
footer a { color: #1a73e8; text-decoration: none; }
/* Toast */
.toast {
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 24px;
background: #333;
color: #fff;
border-radius: 8px;
display: none;
z-index: 1001;
}
.toast.show { display: block; }
.toast.success { background: #28a745; }
.toast.error { background: #dc3545; }
/* 响应式 */
@media (max-width: 768px) {
.sidebar { width: 100%; display: none; }
.sidebar.show { display: block; }
.content { padding: 20px; }
.note-detail h1 { font-size: 24px; }
}
</style>
</head>
<body>
<header>
<h1>云笔记</h1>
<div class="header-right">
<div class="search-box">
<svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="searchInput" placeholder="搜索笔记..." onkeyup="handleSearch(event)">
</div>
<a href="/admin/" style="color: #666; text-decoration: none; font-size: 14px;">管理</a>
</div>
</header>
<div class="main">
<div class="sidebar">
<div class="sidebar-header">
<svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:#1976d2;"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>
<h3>笔记目录</h3>
</div>
<div class="tree-view" id="treeView">
<div style="text-align: center; padding: 40px; color: #999;">加载中...</div>
</div>
<div class="filter-area">
<h4>快速筛选</h4>
<div class="filter-item active" onclick="filterAll()">
<svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
全部笔记
</div>
<div class="filter-item" onclick="filterFavorites()">
<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
收藏笔记
</div>
</div>
</div>
<div class="content" id="contentArea">
<div class="empty-state" id="emptyState">
<svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>
<h2>选择一篇笔记</h2>
<p>从左侧列表选择笔记查看内容</p>
</div>
<div class="toc-sidebar" id="tocSidebar" style="display: none;">
<div class="toc-title">目录</div>
<ul class="toc-list" id="tocList"></ul>
</div>
<div class="note-area">
<div class="note-detail" id="noteDetail" style="display: none;">
<h1 id="noteTitle"></h1>
<div class="meta">
<span id="noteCategory"></span>
<span id="noteDate"></span>
<div class="tags" id="noteTags"></div>
</div>
<div class="note-content" id="noteContent"></div>
</div>
</div>
</div>
</div>
<footer>
<a href="/admin/">管理后台</a>
</footer>
<div class="toast" id="toast"></div>
<script>
const API = '/api';
let treeData = [];
let flatNotes = []; // 扁平化的笔记列表(用于筛选)
let currentNote = null;
let currentFilter = { type: 'all' };
let expandedNodes = new Set();
async function init() {
await loadTree();
}
async function loadTree() {
try {
const res = await fetch(`${API}/tree`);
const data = await res.json();
if (data.code === 0) {
treeData = data.data;
renderTree();
}
} catch (err) {
document.getElementById('treeView').innerHTML =
'<div style="text-align: center; padding: 40px; color: #999;">加载失败</div>';
}
}
function renderTree() {
const container = document.getElementById('treeView');
if (treeData.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">暂无笔记</div>';
return;
}
// 构建树形结构
const tree = buildTree(treeData);
container.innerHTML = tree.map(node => renderTreeNode(node, 0)).join('');
}
// 构建树形结构
function buildTree(items) {
const map = {};
const roots = [];
// 先把所有节点转成有 children 的对象
items.forEach(item => {
map[item.id] = { ...item, children: [] };
});
// 再构建父子关系
items.forEach(item => {
if (item.parent_id && map[item.parent_id]) {
map[item.parent_id].children.push(map[item.id]);
} else {
roots.push(map[item.id]);
}
});
return roots;
}
function renderTreeNode(node, level) {
const hasChildren = node.children && node.children.length > 0;
const isExpanded = expandedNodes.has(node.id);
const isActive = currentNote && currentNote.id === node.id;
const pinnedClass = node.is_pinned ? 'pinned' : '';
let html = `
<div class="tree-item">
<div class="tree-node ${isActive ? 'active' : ''} ${pinnedClass}"
onclick="${node.is_folder ? `toggleNode(${node.id})` : `viewNote(${node.id})`}">
${hasChildren ? `
<span class="tree-toggle ${isExpanded ? 'expanded' : ''}" id="toggle-${node.id}">
<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</span>
` : `
<span class="tree-toggle"></span>
`}
<span class="tree-icon ${node.is_folder ? 'folder' : 'note'}">
${node.is_folder ? `
<svg viewBox="0 0 24 24"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
` : `
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
`}
</span>
<span class="tree-label">${escapeHtml(node.title)}</span>
</div>
${hasChildren ? `
<div class="tree-children ${isExpanded ? '' : 'collapsed'}" id="children-${node.id}">
${node.children.map(child => renderTreeNode(child, level + 1)).join('')}
</div>
` : ''}
</div>
`;
return html;
}
function toggleNode(id) {
if (expandedNodes.has(id)) {
expandedNodes.delete(id);
} else {
expandedNodes.add(id);
}
const toggle = document.getElementById(`toggle-${id}`);
const children = document.getElementById(`children-${id}`);
if (toggle) toggle.classList.toggle('expanded');
if (children) children.classList.toggle('collapsed');
}
async function viewNote(id) {
try {
// 先获取笔记信息检查是否有密码
const basicRes = await fetch(`${API}/notes/${id}`);
const basicData = await basicRes.json();
if (basicData.code !== 0) {
showToast(basicData.message || '加载失败', 'error');
return;
}
const basicNote = basicData.data;
// 如果有密码,需要验证
if (basicNote.has_password) {
const password = prompt('此笔记需要密码访问,请输入密码:');
if (!password) {
return; // 用户取消
}
const accessRes = await fetch(`${API}/notes/${id}/access`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: password })
});
const accessData = await accessRes.json();
if (accessData.code !== 0) {
showToast(accessData.message || '密码错误', 'error');
return;
}
currentNote = accessData.data;
} else {
currentNote = basicNote;
}
renderTree(); // 重新渲染以更新 active 状态
renderNoteDetail();
} catch (err) {
showToast('加载笔记失败', 'error');
}
}
function renderNoteDetail() {
if (!currentNote) {
document.getElementById('emptyState').style.display = 'block';
document.getElementById('noteDetail').style.display = 'none';
document.getElementById('tocSidebar').style.display = 'none';
return;
}
document.getElementById('emptyState').style.display = 'none';
document.getElementById('noteDetail').style.display = 'block';
document.getElementById('noteTitle').textContent = currentNote.title;
document.getElementById('noteCategory').textContent = currentNote.category || '未分类';
document.getElementById('noteDate').textContent = formatDate(currentNote.updated_at);
const tags = parseTags(currentNote.tags);
document.getElementById('noteTags').innerHTML = tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('');
const renderedContent = renderMarkdown(currentNote.content || '');
document.getElementById('noteContent').innerHTML = renderedContent;
// 生成目录
generateTOC(currentNote.content || '');
}
function generateTOC(content) {
const tocList = document.getElementById('tocList');
const tocSidebar = document.getElementById('tocSidebar');
// 提取标题
const headingRegex = /^(#{1,3})\s+(.+)$/gm;
const headings = [];
let match;
while ((match = headingRegex.exec(content)) !== null) {
const level = match[1].length;
const text = match[2].trim();
const id = text.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
headings.push({ level, text, id });
}
if (headings.length === 0) {
tocSidebar.style.display = 'none';
return;
}
tocSidebar.style.display = 'block';
tocList.innerHTML = headings.map(h =>
`<li class="toc-item">
<a href="#${h.id}" class="toc-link level-${h.level}" onclick="scrollToHeading('${h.id}')">${escapeHtml(h.text)}</a>
</li>`
).join('');
}
function scrollToHeading(id) {
const el = document.getElementById(id);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
async function filterAll() {
currentFilter = { type: 'all' };
updateFilterUI();
await loadTree();
}
async function filterFavorites() {
currentFilter = { type: 'favorites' };
updateFilterUI();
try {
const res = await fetch(`${API}/notes?favorite=true&page_size=100`);
const data = await res.json();
if (data.code === 0) {
flatNotes = data.data || [];
renderFlatNotes();
}
} catch (err) {
showToast('加载失败', 'error');
}
}
function renderFlatNotes() {
const container = document.getElementById('treeView');
if (flatNotes.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">暂无收藏笔记</div>';
return;
}
flatNotes.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
container.innerHTML = flatNotes.map(note => {
const tags = parseTags(note.tags);
const isActive = currentNote && currentNote.id === note.id;
return `
<div class="tree-item">
<div class="tree-node ${isActive ? 'active' : ''} ${note.is_pinned ? 'pinned' : ''}"
onclick="viewNote(${note.id})">
<span class="tree-toggle"></span>
<span class="tree-icon note">
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
</span>
<span class="tree-label">${escapeHtml(note.title)}</span>
${note.is_pinned ? '<span style="color:#f57c00;font-size:11px;">📌</span>' : ''}
</div>
</div>
`;
}).join('');
}
function updateFilterUI() {
document.querySelectorAll('.filter-item').forEach(el => el.classList.remove('active'));
const items = document.querySelectorAll('.filter-item');
if (currentFilter.type === 'all') {
items[0].classList.add('active');
} else if (currentFilter.type === 'favorites') {
items[1].classList.add('active');
}
}
async function handleSearch(event) {
if (event.key !== 'Enter') return;
const keyword = event.target.value.trim();
if (!keyword) {
await loadTree();
return;
}
try {
const res = await fetch(`${API}/notes/search?q=${encodeURIComponent(keyword)}`);
const data = await res.json();
if (data.code === 0) {
flatNotes = data.data || [];
renderFlatNotes();
}
} catch (err) {
showToast('搜索失败', 'error');
}
}
function parseTags(tagsJson) {
if (!tagsJson) return [];
try { return JSON.parse(tagsJson); } catch { return []; }
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('zh-CN', {
year: 'numeric', month: 'long', day: 'numeric'
});
}
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast show ${type}`;
setTimeout(() => toast.classList.remove('show'), 3000);
}
// 简单的 Markdown 渲染
function renderMarkdown(text) {
if (!text) return '<p style="color: #999;">无内容</p>';
// 先处理代码块,保留语言标识
const codeBlockRegex = /```(\w*)\n?([\s\S]*?)```/g;
const codeBlocks = [];
let index = 0;
text = text.replace(codeBlockRegex, (match, lang, code) => {
const placeholder = `__CODE_BLOCK_${index}__`;
codeBlocks.push({ lang: lang || 'plaintext', code: code.trim() });
index++;
return placeholder;
});
// 用占位符保护图片,避免被后续处理影响
const imgPlaceholders = [];
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(match, alt, src) {
const placeholder = '__IMG_' + imgPlaceholders.length + '__';
imgPlaceholders.push('<img src="' + src + '" alt="' + alt + '" style="max-width: 100%; border-radius: 4px;">');
return placeholder;
});
// 用占位符保护链接
const linkPlaceholders = [];
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text_, url) {
const placeholder = '__LINK_' + linkPlaceholders.length + '__';
linkPlaceholders.push('<a href="' + url + '" target="_blank">' + text_ + '</a>');
return placeholder;
});
// 转义 HTML(处理代码块内的内容和其他文本)
text = escapeHtml(text);
// 恢复链接(在转义之后)
linkPlaceholders.forEach(function(link, i) {
text = text.replace('__LINK_' + i + '__', link);
});
// 恢复图片(在转义之后)
imgPlaceholders.forEach(function(img, i) {
text = text.replace('__IMG_' + i + '__', img);
});
// 处理行内代码
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
// 恢复代码块并应用高亮
codeBlocks.forEach((block, i) => {
const highlighted = hljs.highlightAuto(block.code, block.lang !== 'plaintext' ? [block.lang] : undefined).value;
text = text.replace(
`__CODE_BLOCK_${i}__`,
`<pre><code class="hljs language-${block.lang}">${highlighted}</code></pre>`
);
});
// 标题(带 id 用于目录导航)
text = text.replace(/^### (.+)$/gm, (match, content) => {
const id = content.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
return `<h3 id="${id}">${content}</h3>`;
});
text = text.replace(/^## (.+)$/gm, (match, content) => {
const id = content.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
return `<h2 id="${id}">${content}</h2>`;
});
text = text.replace(/^# (.+)$/gm, (match, content) => {
const id = content.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
return `<h1 id="${id}">${content}</h1>`;
});
// 粗体斜体
text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
// 删除线
text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');
// 引用
text = text.replace(/^> (.*$)/gm, '<blockquote>$1</blockquote>');
// 无序列表
text = text.replace(/^[\-\*] (.*$)/gm, '<li>$1</li>');
text = text.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
// 有序列表
text = text.replace(/^\d+\. (.*$)/gm, '<li>$1</li>');
// 换行
text = text.replace(/\n\n/g, '</p><p>');
text = text.replace(/\n/g, '<br>');
return `<p>${text}</p>`;
}
init();
</script>
</body>
</html>