Skip to content

Repository files navigation

MDEditor

CI Release Go Reference

一个 Go 写的 Markdown 工具,由两个平级入口组成:

入口 状态 做什么
mdreader 网页阅读器 ✅ 已交付 只读。指向一个 Markdown 目录,在浏览器里读
mdeditor 本地编辑器 ✅ 已交付 编辑与实时预览,桌面窗口

两者共用同一个渲染内核,但互不依赖 —— 只想读的人不必安装编辑器。这是产品的核心取舍, 写进了项目宪法的第一条原则。

快速开始

# 一条命令装好(需要 Go 1.25+)
go install github.com/yanking/mdeditor/cmd/mdreader@latest

# 本机模式:绑定回环,不需要令牌,自动开浏览器
mdreader ~/notes

从源码构建:

git clone https://github.com/yanking/mdeditor && cd mdeditor
make build      # 产物在 bin/mdreader,14M 单文件
make install    # 装到 ~/.local/bin
make help       # 看全部目标

打开后你会看到三栏:左边是文档目录树,中间是正文,右边是当前文档的大纲。 顶栏可以切主题(浅色/深色/跟随系统)和调字号(五档),选择会被记住。

本地编辑器

左边写,右边看,改一个字预览就跟着变。

go install github.com/yanking/mdeditor/cmd/mdeditor@latest
mdeditor ~/notes

它不动你没动过的东西。 CRLF 行尾不会被顺手转成 LF,缺失的尾随换行不会被补上, BOM 不会被去掉,文件权限保持原样。这条听起来像洁癖,实际决定了它能不能用在 版本控制的目录里 —— 一个「顺手规范化」的编辑器会把「改了一行」的提交变成全文件改动, code review 就没法做了。

保存是原子的:先写同目录临时文件、Sync 落盘、还原权限,最后 rename 顶替。 中途断电或被 kill,你拿到的要么是完整的旧版本,要么是完整的新版本,不会是半个文件。

编辑期间文件被外部改了(另一个编辑器、git checkout、同步工具),保存时会停下来问, 给你「用我的覆盖 / 重新加载磁盘版本 / 取消」三条路 —— 它不会替你选。

全程离线,不外发任何内容。 公式用的 KaTeX 是编译进二进制的,不是 CDN; 断网时公式、高亮、预览、保存全都照常。这一条有测试守着(tests/e2e-editor/offline.mjs 拦截浏览器发出的每一个请求,目的地不是本机的一律算失败)。

写入边界就是你指定的那个目录,之外的文件一个字节都不会变(readonly_boundary_test.go 用全目录快照对比来验证这件事,而不是靠「代码里没有多余的写调用」)。

两种形态

编辑器有两种形态,能力完全相同,差别只在界面由谁承载:

形态 怎么跑 运行时前提
桌面 mdeditor ~/notes 平台原生 webview(见下表)
降级 mdeditor-server ~/notes 后用浏览器打开 ,纯静态单文件

降级形态不是阉割版:文件管理、冲突处理、主题字号、离线公式,一样不少。 装不上桌面运行时、或者在服务器和容器里用,就用它。

从源码构建降级形态:make editor-serverCGO_ENABLED=0,可交叉编译到全部平台)。

桌面形态的运行时前提

桌面形态用系统自带的 webview 而不是自带一份 Chromium,二进制因此只有十几兆而不是一百多兆。 代价是各平台要有对应的运行时:

平台 需要什么 怎么装
macOS 11+ 系统自带 WKWebView
Windows 10/11 WebView2 Runtime Win11 自带;Win10 从 微软官方页面
Linux WebKitGTK 6.0 + GTK 4 见下
# Debian / Ubuntu 24.04+
sudo apt install libwebkitgtk-6.0-4 libgtk-4-1
# Fedora
sudo dnf install webkitgtk6.0 gtk4
# Arch
sudo pacman -S webkitgtk-6.0 gtk4

从源码构建桌面形态还需要对应的 -dev / -devel 包(Debian 系是 libwebkitgtk-6.0-dev libgtk-4-dev)。

老一些的发行版只有 WebKitGTK 4.1,可以用 go build -tags gtk3 回退。 如果这些都嫌麻烦 —— 直接用降级形态,它什么都不需要。

编辑器快捷键

操作 macOS Windows / Linux
保存 ⌘S Ctrl+S
新建 ⌘N Ctrl+N
打开目录 ⌘O Ctrl+O
关闭窗口 ⌘W Ctrl+W
字号增减 ⌘+ / ⌘- Ctrl++ / Ctrl+-

菜单项不自己实现功能,只是转发给界面里已有的那条路径 —— 所以降级形态下菜单没了, 功能在顶栏和快捷键上照样都在。

它能渲染什么

  • 基础语法:CommonMark + GFM(表格、任务列表、删除线、脚注、自动链接)
  • 代码高亮:服务端完成,配色跟随主题切换
  • 数学公式:行内 $...$ 与块级 $$...$$,KaTeX 内嵌在二进制里,断网照样显示
  • 图片:本地相对路径与远程地址,自动懒加载
  • 视频![说明](demo.mp4) 直接渲染成播放器;原始 <video> 标签也支持,可拖动进度条
  • 第三方内嵌:文档里的任意 <iframe> 都会渲染,不设域名白名单

相对路径以文档所在目录为基准解析,所以 guide/intro.md 里写 ![](pic.png) 指的是 guide/pic.png

公网部署

MDREADER_TOKEN='一个至少16位的随机串' ./bin/mdreader --listen 0.0.0.0:7654 /srv/docs

把打印出来的 http://<地址>:7654/?token=... 发给别人,对方点开就能读,不用注册也不用装东西。

绑定非回环地址时必须提供令牌,否则拒绝启动。没有 --no-auth 之类的绕过开关。

三笔要认下来的账

公网模式是一组明确的取舍,不是安全疏漏。用之前请确认你接受这三点:

  1. 根目录即公开边界。 令牌通过之后没有细粒度权限 —— 根目录里的每个文件都对持链接者可见。 不该被看到的内容不要放进去。
  2. 令牌随 URL 传递。 那条初始链接会留在浏览器历史和中间代理日志里。 我们做了两道缓解:所有响应带 Referrer-Policy: no-referrer(否则页面里每个第三方 iframe 和远程图片都会把带令牌的完整 URL 经 Referer 发给对方),以及首次访问后换发 HttpOnly Cookie 并把 URL 里的令牌清掉。剩下没法消除的只有那条初始链接本身 —— 换 --token 可让旧链接立即失效。
  3. 内嵌不设白名单。 第三方页面会拿到访问者的 IP 与浏览器信息,其内部行为不受本产品控制。 我们能保证的是它拿不到阅读器这一侧的任何东西,以及读者随时能一键关掉。

安全边界

文档常常来自不受信任的来源,所以渲染管线是三层围栏:

  1. 净化层:剥离文档自带的 <script>on* 事件属性、javascript:data:text/html
  2. iframe 围栏:每个内嵌强制沙箱加载,不给 allow-top-navigation; 与阅读器同源的内嵌额外去掉 allow-same-origin(那才是沙箱逃逸的组合); 一律 referrerpolicy="no-referrer"
  3. CSP 兜底script-src 'self' + 单条内联脚本的 sha256 白名单。 万一净化层将来出现绕过,浏览器也不会执行漏网的脚本。

内嵌可以一键全部关闭。关闭时 src 会被摘到 data-src —— 真的阻止请求, 而不是拿 CSS 把它藏起来(被隐藏的 iframe 照样联网)。

命令行

mdreader

mdreader [flags] <root-dir>
Flag 默认 说明
--listen 127.0.0.1:7654 绑定回环 = 本机模式;其他地址 = 公网模式
--token 公网模式必填。也可用 MDREADER_TOKEN(推荐,不进 shell 历史)
--open true 启动后自动开浏览器(仅本机模式)
--theme system 首次访问的默认主题:light / dark / system
--highlight github 代码高亮样式(chroma 样式名)
--cache-mb 64 渲染缓存上限
--max-doc-mb 50 单篇渲染上限。超过则拒绝并说明,绝不静默截断

超大文档分三档:5MB 以内正常渲染并缓存;5MB 到上限之间完整渲染但跳过缓存 (一篇 30MB 的文档不该把整个缓存挤空);超过上限拒绝渲染并显示实际大小。 内容要么完整呈现,要么明确拒绝 —— 假装成功后给出半篇文章是最坏的结果。

mdeditor

mdeditor [flags] <root-dir>
Flag 默认 说明
--highlight github 代码高亮样式(与阅读器同一套,两端配色因此必然一致)
--max-doc-mb 50 单篇打开上限
--version 打印版本号

<root-dir> 既是能看到的范围,也是写入边界 —— 编辑器不会碰它之外的任何文件。

主题、字号、布局与窗口位置存在系统配置目录(os.UserConfigDir()/mdeditor/prefs.json), 不写进你的文档目录

降级形态多一个环境变量:WAILS_SERVER_PORT 指定监听端口。

开发

bash scripts/gate.sh        # 合并门禁:9 步(格式、vet、lint、测试、基准、结构、只读、降级形态、薄外壳)
go test ./...               # 含黄金样例、保真、原子性、冲突、只读边界
CHROME=<chrome路径> node tests/e2e/run.mjs          # 阅读器浏览器行为测试(48 条)
CHROME=<chrome路径> node tests/e2e-editor/run.mjs   # 编辑器端到端(80 条,含离线与不外发)

编辑器的端到端测试跑在降级形态上:桌面形态需要真实 webview 与显示服务, 无头环境里驱动不了。这不是将就 —— 降级形态承载了 FR-001~042 的全部能力, 所以在它上面跑通覆盖的正是产品的绝大部分。桌面独有的那几项 (原生窗口、菜单、关窗拦截、多显示器、DPI)在 人工验收清单里,三平台各走一遍。

黄金样例

渲染行为由 testdata/golden/ 下的样例定义,而不是由实现定义。 改渲染内核时先写样例、看它失败、再动实现

go test ./internal/render -run TestGolden -update 能重新生成期望文件,但它是便利工具, 不是免责声明 —— 跑完直接提交、让 diff 无人过目,等于让实现自己定义正确性。 每一处 diff 都要能解释。

结构约束

internal/render 是全仓库唯一的 Markdown 解析与渲染实现。 internal/serverinternal/configinternal/editorcmd/ 不得直接 import goldmark / chroma / bluemonday —— 门禁里有一条静态检查守着。 两个入口的渲染结果因此是结构性一致的,而不是靠两边同步维护: tests/parity/ 拿 11 个黄金样例逐字节比对阅读器与编辑器的输出。

Wails 只能出现在 cmd/mdeditorinternal/ 下一律不许 import。 门禁会把整个 cmd/mdeditor 移走再编译一次 internal/... —— 编不过就说明外壳不够薄。 这既缓解 Wails v3 beta 期的 API 变动风险,也是降级形态能与桌面形态共用同一份业务代码的前提。

文档

About

Go 写的 Markdown 阅读器:公式、图片、视频、任意第三方内嵌;三主题五档字号、目录树与文章大纲;单一自包含二进制,支持本机与公网令牌两种部署

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages