markdown2article文章编辑器
全功能 Markdown 到 Astro 文章编辑器,附带实时预览、目录生成、一键导出。
用途
为博客作者提供一个可视化编辑环境,在单一页面中完成:
- 填写文章的元信息(title、meta、activeNav、评论开关等)
- 编辑 sidebar-left(tagline、badge 图标/文字)、sub-sidebar(标签列表)、sidebar-right(promo)
- 编写 Markdown 正文,实时渲染预览并自动生成目录
- 通过对话框插入
HashedImage、XMind思维导图、SPZ3D 高斯泼溅模型、终端动画代码片段 - 一键导出为可直接在项目中使用的
.astro文件 - 从 URL 参数
?load=加载外部 Markdown 文件
功能总览
| 功能 | 位置 | 说明 |
|---|---|---|
| 页面标题 / 浏览器标题 | editor-settings 首行 | 两个独立输入框,浏览器标题自动拼接 · NW' Blog |
| meta 日期、excerpt | editor-settings 第二行 | 可选摘要,空时隐藏 |
| activeNav 选择 | editor-settings 第二行 | 控制导航栏高亮项 |
| 评论区开关 | editor-settings 第二行 | 勾选时评论区在预览中可见 |
| 标签编辑 | sub-sidebar textarea | 每行一个 名称|链接 格式 |
| tagline 编辑 | sidebar-left input | 控制头像下方描述文字 |
| badge 编辑 | sidebar-left | 图标选择器 + 文字输入 → 实时更新 badge |
| 图标选择器 | sidebar-left → 选择图标 | 弹出固定定位面板,搜索 + 点击选择 |
| 导入 MD 文件 | 工具栏 → 导入 MD | 读取本地 .md 文件填入编辑器 |
| 插入 HashedImage | 工具栏 → 插入 HashedImage | 对话框:路径、alt、figcaption、实际大小勾选 |
| 插入 XMind | 工具栏 → 插入 XMind | 对话框:JSON 路径、深色模式、布局、线条样式 |
| 插入 SPZ | 工具栏 → 插入 SPZ | 对话框:.spz 路径、自动加载勾选 |
| 插入终端代码 | 工具栏 → 插入终端 | 对话框:标题、代码内容,自动转义 HTML |
| MD 编辑器 | article-panel | 等宽字体 textarea,字数统计显示 KB 值 |
| 实时预览 | article-panel | GFM Markdown → sanitized HTML |
| 目录 | sidebar-right | 自动提取 h1~h4,点击平滑滚动,层级缩进 |
| promo 编辑 | sidebar-right | 可编辑标题 + 内容,预览区同步 |
| 模式切换 | fixed mode-bar | 编辑/预览模式;预览模式隐藏全部编辑控件 |
| 导出按钮 | fixed float-btn | 下载完整 .astro 文件 |
| URL 加载 | 查询参数 | ?load=/path/to/file.md 自动加载远程 Markdown |
页面布局
┌─ editor-mode-bar (fixed top:70, z-index:1000) ──────────┐
│ [编辑] [预览] │
└──────────────────────────────────────────────────────────┘
┌─ main.main-layout[data-sidebar-mode="both"] ─────────────┐
│ ┌─ left-column ──┐ ┌─ article-panel ─────┐ ┌─ sidebar-right ┐ │
│ │ sidebar-left │ │ editor-settings │ │ TOC │ │
│ │ logo/avatar │ │ toolbar + dialogs │ │ promo editor │ │
│ │ tagline │ │ md-editor textarea │ │ promo display │ │
│ │ badge │ │ rendered-preview │ └────────────────┘ │
│ │ edit controls │ └──────────────────────┘ │
│ │ sub-sidebar │ CommentWidget │
│ │ tag list │ (4th grid child) │
│ └────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌─ editor-float-btns (fixed bottom:76 left:24) ───────────┐
│ [export] │
└──────────────────────────────────────────────────────────┘ 三种列宽由 global.css 的 .main-layout 定义 — 280px minmax(0,1fr) 260px。
核心工作流
编辑字段 → 实时预览
所有文本/选择输入框绑定 input 事件 → updatePreview():
- 更新 h1(来自 pageTitle)、meta、excerpt
- 更新 tagline、badge 图标与文字
- 解析标签 textarea → 渲染
<ul class="tag-list"> - 更新 promo 标题与正文
- 拼接浏览器标题显示
- 调用
renderMarkdown()刷新 Markdown 预览与 TOC - 根据评论开关显示/隐藏评论区
Markdown 编辑 → 渲染预览 + TOC
- 输入防抖 300ms 后触发
updatePreview() renderMarkdown()使用marked.parse()+ GFM 扩展 → HTML- 经过
sanitizeHtml()过滤:移除<script>、<style>、<iframe>(已保留)、on*属性、javascript:链接等 generateTOC()扫描预览区 h1~h4,为每个标题生成带data-level的<li>,点击平滑滚动
插入特有组件
四个对话框均用 display:none/block 切换。插入时调用 insertToEditor(tag):
- 在 textarea 光标位置插入标签
- 自动更新字符统计和预览
- HashedImage:
<HashedImage src="..." alt="..." />,可选class="content-image-actual" - XMind:生成
<iframe>指向/Xmind/XmindViewer_embed.html?dark=...&layout=...&data=... - SPZ:生成
<iframe>指向/spz/spzViewer.html?url=...;非自动加载模式额外渲染点击播放占位 - 终端代码:生成
.terminal-code结构的 HTML 块(标题栏 + 红黄绿控制点 +<pre><code>),自动对代码内容做 HTML 转义,保持用户原始缩进
编辑/预览切换
通过 document.body.classList 切换 editor-mode-preview-active:
- CSS 中
:global(body.editor-mode-preview-active) .editor-settings等全部display: none !important - 预览模式隐藏所有编辑控件和浮动导出按钮
- 评论区在预览模式下由
updatePreview()根据复选框显示/隐藏
导出 .astro 文件
单击浮动导出按钮 → 执行 DOM.exportBtn 的 click 处理器:
- 收集所有字段当前值
- 构建完整的 Astro frontmatter + HTML 模板
- 将 Markdown 内容分块(见下方导出逻辑详解)
- 生成 Blob → 自动下载
{slug}.astro - 按钮图标临时切换为 ✅ 反馈(2 秒恢复)
URL 参数加载
访问 ?load=/path/to/file.md 时:
- 自动
fetch该路径 - 成功则填入 MD 编辑器并触发预览更新
HTML 结构
<BaseLayout title="...">
<main class="main-layout" data-sidebar-mode="both">
<!-- 第 1 列 -->
<div class="left-column">
<aside class="sidebar-left"> ← logo, avatar, tagline, badge, editor-field 控件
<aside class="sub-sidebar"> ← h2 + editor-field(textarea) + 动态 tag-list
</div>
<!-- 第 2 列 -->
<article class="article-panel"> ← editor-settings, toolbar, dialogs, md-editor, preview
<!-- 第 3 列 -->
<aside class="sidebar-right"> ← TOC + promo-editor + promo-display
<!-- 第 4 个子元素(仍在 main 内) -->
<CommentWidget /> ← 评论区
</main>
<!-- 以下在 </main> 外部,独立定位 -->
<div id="editor-mode-bar"> ← fixed top:70
<div class="editor-float-btns"> ← fixed bottom:76 left:24 (导出按钮)
</BaseLayout>
注意:CommentWidget 放在
<main>内部作为第 4 个 grid child,利用global.css中.comment-panel { grid-column: 2; }定位到中间列下方。
CSS 架构
设计系统
| Token | 值 |
|---|---|
| 背景色基 | #09090f / #1a1c2a |
| 文字色 | #dcd7cc |
| 强调色(accent) | #ff6925 |
| 玻璃态 | backdrop-filter: blur(18px) |
| 边框 | rgba(255,255,255,0.08~0.20) |
| 圆角 | 8px / 12px / 14px |
关键类
| 类名 | 用途 |
|---|---|
.editor-mode-bar | 固定顶栏,编辑/预览切换按钮 |
.editor-mode-btn--active | 模式激活态(橙色背景) |
.editor-settings / .editor-settings-row | 页面设置区,flex 布局 |
.editor-label | 字段标签,橙色左边框 + 大写 + hover 高亮 |
.editor-input | 通用输入框,暗色主题风格 |
.editor-select | 自定义下拉框,appearance: none + SVG 箭头 + focus glow |
.editor-checkbox | 自定义复选框,appearance: none + 橙色勾选 + SVG checkmark |
.editor-textarea | 通用多行输入,可调整高度 |
.editor-md-editor | Markdown 编辑区,等宽字体,min-height: 323px |
.editor-icon-dropdown | 图标选择弹出面板,position: fixed; z-index: 999999 |
.editor-icon-grid | 图标网格 8列,带搜索过滤 |
.editor-dialog | 插入对话框,玻璃态背景 |
.editor-toolbar / .editor-toolbar-btn | 工具栏按钮组 |
.editor-float-btn | 浮动按钮 44×44px,带 -webkit-backdrop-filter 兼容 |
.editor-md-size | Markdown 字数统计(KB) |
预览模式 CSS
使用 :global(body.editor-mode-preview-active) 选择器隐藏全部编辑控件(display: none !important)。各控件有 animation: editorFadeIn 入场动画。
滚动条
.editor-textarea、.editor-md-editor、.editor-icon-dropdown、.editor-icon-grid 均自定义为 thin 橙色滚动条。
注意
backdrop-filter会创建新 stacking context → 图标下拉菜单用position: fixed+ JSappendChild(body)规避-webkit-backdrop-filter前缀确保 Safari 兼容
JavaScript 模块
核心函数
| 函数 | 位置 | 职责 |
|---|---|---|
sanitizeHtml(html) | 工具 | DOMParser 过滤 XSS:移除 script/style/iframe/on*/javascript: |
updateMdSize() | 工具 | TextEncoder 计算 MD 字节数,显示 KB |
renderMarkdown() | 渲染 | marked.parse() → sanitizeHtml() → 写入预览 + TOC |
generateTOC() | 渲染 | 扫描 h1~h4 → data-level 列表,id 处理,平滑滚动 |
updatePreview() | 核心 | 收集所有字段 → 同步到预览 DOM + 渲染 Markdown + 切换评论区 |
positionIconDropdown() | 图标 | 计算按钮位置,防止右侧溢出,固定定位 |
insertToEditor(tag) | 插入 | 在光标处插入文本,更新统计 + 预览 |
escapeAttr(s) | 导出 | HTML 属性编码(& " < >) |
export logic | 导出 | 见下方导出逻辑详解 |
DOM 引用
DOM 对象集中管理所有 getElementById 引用(~50 个元素),统一在顶部定义。
事件绑定
- 所有编辑字段:
input→updatePreview() - MD 编辑器:
input→updateMdSize()+ 300ms 防抖 →updatePreview() - 图标搜索:
input→ 过滤网格 - 图标网格
click→ 选中 + 关闭 + 更新 preview - 导出按钮
click→ 构建 → 下载 - 模式按钮
click→ 切换 body class + 更新预览 - TOC 点击 → 高亮 + 平滑滚动
- 导入按钮
click→ 触发隐藏的<input type="file"> - 对话框按钮
click→ 切换显示 + 插入
初始加载
DOMContentLoaded 时:
- 调用
updatePreview()填充初始状态 - 检查
?load=URL 参数 → 异步 fetch 填入编辑器
导出逻辑详解
导出按钮是编辑器最核心的输出功能,执行流程:
1. 收集字段
从所有编辑控件读取当前值:title、meta、activeNav、excerpt、tagline、badge、tags、promo、comment toggle、MD 内容。
2. 从 Markdown 提取 h1
用正则 /^#\s+(.+)/m 从 MD 内容中提取第一个一级标题用作 <h1>,如果不存在则回退到 pageTitle。
3. 构建 Astro 模板
生成完整的 Astro 文件字符串,包含:
- frontmatter
---区块(BaseLayout、HashedImage、条件性 CommentWidget 导入) <BaseLayout>包裹结构- sidebar-left / sub-sidebar / article-panel / sidebar-right 完整骨架
4. Markdown 区块分割
导出器将 Markdown 内容逐行分析,分割为两种类型:
| 块类型 | 规则 | 导出结果 |
|---|---|---|
| 普通 Markdown | 非空行 + 非原始 HTML 标签 | 包裹 <div class="section-block"> |
| 原始 HTML 块(自闭合) | 以 <HashedImage 开头且以 /> 结尾 | 保持原样,不包裹 section-block |
原始 HTML 块(多行 <div>) | 以 <div class="..."> 开头且匹配 RAW_TAGS | 进入多行累积状态机,追踪 <div>/</div> 嵌套深度,闭合后整块保留,不包裹 section-block |
| 代码块 | 行首 ``` 切换状态 | 代码块整体视为一个块 |
| 空行 | 连续块分隔符 | 触发 flushBlock() |
多行累积:当遇到<div class="terminal-code"、<div class="xmind-embed-wrapper"、<div class="splat-viewer-wrapper"时,导出器进入inRawHtml状态,逐行累积直到匹配的</div>闭合。同时清洗<code>内部因统一缩进产生的多余前导空格(仅移除\n 6,保留用户原始缩进)。
分割完成后,所有块按顺序拼接,形成文章的完整正文。
5. 条件输出
- 如果评论区开关打开 → 导入 CommentWidget + 渲染评论区,关闭
</main>和</BaseLayout> - 如果评论区关闭 → 省略导入和渲染
6. 下载
从标题生成 slug → 创建 Blob → 自动触发 <a> 下载 {slug}.astro。
图标选择器
FA_ICONS 数组包含 ~180 个 Font Awesome 6.5 图标(fa- 前缀,均为 fas 风格)。
交互流程
- 点击按钮 → JS 将下拉菜单
appendChild到 body(脱离 sidebar-left stacking context) positionIconDropdown()计算按钮位置,防止右溢出- 搜索框实时过滤(
datase.icon.includes()) - 点击图标 →
_selectedIcon更新 + 关闭菜单 + 刷新预览 - 点击外部 → 关闭菜单
- 滚动时 → 重新定位
技术细节
position: fixed; z-index: 999999+ 脱离原始 DOM 树确保不被背景滤镜层遮挡- 宽度 323px,最大高度 240px,8 列网格,overflow-x: hidden
依赖
| 包 | 用途 |
|---|---|
marked | Markdown → HTML 解析(GFM + breaks) |
| Astro 内置 | BaseLayout、HashedImage、CommentWidget 组件 |
| Font Awesome 6.5 | 图标库(CDN,由 BaseLayout 加载) |
无额外运行时依赖。
维护笔记
常见坑点
- backdrop-filter 与 stacking context —
.sidebar-left的backdrop-filter会创建新堆叠上下文,内部z-index失效。图标下拉菜单的解决方案:JS 将元素移至<body>+position: fixed。 :global()作用域 — Astro 的 scoped CSS 无法穿透到body。因此 preview-mode toggle 使用:global(body.editor-mode-preview-active)选择器。- 评论区定位 —
CommentWidget渲染的.comment-panel是<main>的第 4 个 child,依靠global.css的grid-column: 2落在中间列下方。更改main-layoutgrid 定义时需同步更新。 data-sidebar-mode="both"— 必须设置在<main>上,以便site.js的initSidebarToggle()正确激活状态机。- 导出时原始 HTML 保留 — 自闭合标签(
<HashedImage ... />)单行匹配立即刷出;<div>块标签(terminal-code、xmind-embed-wrapper、splat-viewer-wrapper)进入多行累积状态机,追踪<div>嵌套深度。<code>内部额外清洗\n 6前缀缩进以保护代码渲染。如果未来新增其他 Astro 组件标签,需要同步更新RAW_TAGS数组。 -webkit-backdrop-filter— Safari 需要此前缀,浮动按钮和 mode-bar 均需同时提供标准写法。- 评论开关控制 — 由
updatePreview()中的DOM.commentPanel.style.display控制,预览模式切换时也要手动调用updatePreview()确保同步。
扩展指南
- 新增编辑字段:在 HTML 中添加 input/textarea/select → DOM 对象加引用 → LIVE_FIELDS 数组加入 key →
updatePreview()中加入同步逻辑 → export 逻辑中加入收集。 - 新增插入组件类型:在工具栏加按钮 + 对话框 → 对话框确认时调用
insertToEditor(tag)→ 如果插入的是 Astro 组件或<div>块,在RAW_TAGS中加入前缀以确保导出时不被包裹 section-block。对于<div>块还需要确保带.terminal-code类触发多行累积逻辑。 - 新增 FA 图标:在
FA_ICONS数组中追加图标名称,无需额外配置。