NW' Blog
2026年7月10日 · 内容页面

markdown2article文章编辑器

全功能 Markdown 到 Astro 文章编辑器,附带实时预览、目录生成、一键导出。

markdown2article文章编辑器

用途

为博客作者提供一个可视化编辑环境,在单一页面中完成:

  1. 填写文章的元信息(title、meta、activeNav、评论开关等)
  2. 编辑 sidebar-left(tagline、badge 图标/文字)、sub-sidebar(标签列表)、sidebar-right(promo)
  3. 编写 Markdown 正文,实时渲染预览自动生成目录
  4. 通过对话框插入 HashedImageXMind 思维导图、SPZ 3D 高斯泼溅模型、终端动画代码片段
  5. 一键导出为可直接在项目中使用的 .astro 文件
  6. 从 URL 参数 ?load= 加载外部 Markdown 文件

功能总览

功能 位置 说明
页面标题 / 浏览器标题editor-settings 首行两个独立输入框,浏览器标题自动拼接 · NW' Blog
meta 日期、excerpteditor-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-panelGFM 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 处理器:

  1. 收集所有字段当前值
  2. 构建完整的 Astro frontmatter + HTML 模板
  3. 将 Markdown 内容分块(见下方导出逻辑详解)
  4. 生成 Blob → 自动下载 {slug}.astro
  5. 按钮图标临时切换为 ✅ 反馈(2 秒恢复)

URL 参数加载

访问 ?load=/path/to/file.md 时:

  • 自动 fetch 该路径
  • 成功则填入 MD 编辑器并触发预览更新

HTML 结构

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-editorMarkdown 编辑区,等宽字体,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-sizeMarkdown 字数统计(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 + JS appendChild(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 个元素),统一在顶部定义。

事件绑定

  • 所有编辑字段:inputupdatePreview()
  • MD 编辑器:inputupdateMdSize() + 300ms 防抖 → updatePreview()
  • 图标搜索:input → 过滤网格
  • 图标网格 click → 选中 + 关闭 + 更新 preview
  • 导出按钮 click → 构建 → 下载
  • 模式按钮 click → 切换 body class + 更新预览
  • TOC 点击 → 高亮 + 平滑滚动
  • 导入按钮 click → 触发隐藏的 <input type="file">
  • 对话框按钮 click → 切换显示 + 插入

初始加载

DOMContentLoaded 时:

  1. 调用 updatePreview() 填充初始状态
  2. 检查 ?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 风格)。

交互流程

  1. 点击按钮 → JS 将下拉菜单 appendChild 到 body(脱离 sidebar-left stacking context)
  2. positionIconDropdown() 计算按钮位置,防止右溢出
  3. 搜索框实时过滤(datase.icon.includes()
  4. 点击图标 → _selectedIcon 更新 + 关闭菜单 + 刷新预览
  5. 点击外部 → 关闭菜单
  6. 滚动时 → 重新定位

技术细节

  • position: fixed; z-index: 999999 + 脱离原始 DOM 树确保不被背景滤镜层遮挡
  • 宽度 323px,最大高度 240px,8 列网格,overflow-x: hidden

依赖

用途
markedMarkdown → HTML 解析(GFM + breaks)
Astro 内置BaseLayoutHashedImageCommentWidget 组件
Font Awesome 6.5图标库(CDN,由 BaseLayout 加载)

无额外运行时依赖。


维护笔记

常见坑点

  1. backdrop-filter 与 stacking context.sidebar-leftbackdrop-filter 会创建新堆叠上下文,内部 z-index 失效。图标下拉菜单的解决方案:JS 将元素移至 <body> + position: fixed
  2. :global() 作用域 — Astro 的 scoped CSS 无法穿透到 body。因此 preview-mode toggle 使用 :global(body.editor-mode-preview-active) 选择器。
  3. 评论区定位CommentWidget 渲染的 .comment-panel<main> 的第 4 个 child,依靠 global.cssgrid-column: 2 落在中间列下方。更改 main-layout grid 定义时需同步更新。
  4. data-sidebar-mode="both" — 必须设置在 <main> 上,以便 site.jsinitSidebarToggle() 正确激活状态机。
  5. 导出时原始 HTML 保留 — 自闭合标签(<HashedImage ... />)单行匹配立即刷出;<div> 块标签(terminal-codexmind-embed-wrappersplat-viewer-wrapper)进入多行累积状态机,追踪 <div> 嵌套深度。<code> 内部额外清洗 \n 6 前缀缩进以保护代码渲染。如果未来新增其他 Astro 组件标签,需要同步更新 RAW_TAGS 数组。
  6. -webkit-backdrop-filter — Safari 需要此前缀,浮动按钮和 mode-bar 均需同时提供标准写法。
  7. 评论开关控制 — 由 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 数组中追加图标名称,无需额外配置。
评论区 留下您的想法
教程