Markdown → PDF 桌面工具,Electron + 内置 Chromium 排版。
左边写 Markdown,右边所见即所得地预览 A4 纸张,一键导出与浏览器排版一致的 PDF。零外部依赖,三端安装包由 GitHub Actions 在原生 runner 上构建。
- 实时预览:预览与导出共用同一套渲染管线(
src/shared/markdown.js+src/shared/themes.js),纸张按“适应宽度”自动缩放。 - 编辑器:CodeMirror 6,Markdown 语法高亮、行号、查找面板。
- 会话:自动保存草稿、最近文档、侧边栏文件夹浏览、可拖拽分栏、界面缩放、拖拽文件或文件夹到窗口直接打开。
- 四种主题:GitHub(代码 / 表格 / 引用)、学术论文(宋体、首行缩进、居中标题)、商务报告(深蓝标题、蓝系表格)、极简(无边框、紧凑留白)。
- 页面设置:A4 / A5 / Letter / Legal,纵向 / 横向,三档页边距(约 12 / 19 / 25 mm)。
- 页眉页脚:页眉取文档标题或自定义文案(支持
{title}、{date}),页脚“第 X 页 / 共 Y 页”。 - 文档结构:自动目录(H1–H3,带内部跳转,可单独占页)、一级标题另起一页、PDF 书签大纲、Tagged PDF 结构标签;预览内点击目录条目或脚注引用可平滑跳转。
- Markdown 能力:CommonMark + 表格、任务清单、脚注(
[^1])、代码高亮(highlight.js,带语言标签条)、KaTeX 公式($...$、$$...$$)、相对路径图片。
- PDF:直接调用 Chromium 的
webContents.printToPDF,不需要 Chrome / Puppeteer / wkhtmltopdf。 - HTML:单文件产出,KaTeX 字体以 base64 内嵌,换机器打开不掉字。
- 批量:整个文件夹(含子目录,最多 4 层)一次导出,带进度显示。
- 命令行:无界面导出,结果一行 JSON,便于脚本消费。
| 项 | 要求 |
|---|---|
| 系统 | macOS 11+(Apple Silicon / Intel)、Windows 10+(x64)、Linux(x64、glibc 2.28+) |
| Node.js | 18+(开发用,实测 22.23.2) |
| 桌面环境 | Linux 需要 X11 / Wayland 与中文字体,见常见问题 |
git clone <repo> M2P && cd M2P
npm install
npm start # 构建 + 启动桌面应用开发时开 watch 模式,改源码自动重建,之后在应用里按 ⌘R(Windows / Linux 为 Ctrl+R)重载窗口:
npm run dev首次 npm install 要下载 Electron 二进制,建议走镜像:
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install应用本身的源码与导出逻辑不依赖平台特性,差异只在打包配置里。
打包交给 CI,本地只写代码。工作流在 .github/workflows/ci.yml。
| 平台 | 运行器 | 产物 |
|---|---|---|
| macOS(Apple Silicon) | macos-15 |
M2P-<version>-arm64.dmg、M2P-<version>-arm64.zip |
| macOS(Intel) | macos-15-intel |
M2P-<version>-x64.dmg、M2P-<version>-x64.zip |
| Windows(x64) | windows-latest |
M2P-<version>-win-x64-setup.exe(NSIS,可选安装目录) |
| Linux(x64) | ubuntu-latest |
M2P-<version>-x64.AppImage、M2P-<version>-x64.deb |
| Job | 触发 | 做什么 |
|---|---|---|
smoke |
push / PR | Ubuntu 上用 Xvfb 无头导出 demo PDF,断言页数、A4 尺寸、目录文案与 /Outlines 书签;产物 demo-pdf |
package |
v* tag 或手动 |
四个矩阵项并行打包,各自上传 artifact |
release |
v* tag |
汇总三端安装包,建 GitHub Release 并生成说明 |
# 1. 改 package.json 的 version(CI 会按 tag 覆盖,保持一致更省心)
# 2. 打 tag 并推送
git tag v1.0.1
git push origin v1.0.1推送后 Actions 自动产出上表文件并挂到 Release 页。也可以到 Actions 页面手动 Run workflow,只出 artifact 不发 Release。
仓库没有 Apple Developer ID,也没有 Windows 代码签名证书,所以安装包未签名、未公证:
| 平台 | 首次打开的现象 | 处理 |
|---|---|---|
| macOS | “无法验证开发者”或“已损坏,无法打开” | xattr -cr /Applications/M2P.app,详见下文 |
| Windows | SmartScreen“Windows 已保护你的电脑” | 更多信息 → 仍要运行 |
| Linux | AppImage 无执行位 / 缺少 FUSE | chmod +x M2P-*.AppImage;sudo apt install libfuse2 |
要正式分发,就在仓库 Secrets 里放证书,并在 build.mac 补 identity / notarize、build.win 补 signtool。
npm run pack # 生成 .app(release/mac-<arch>/)
npm run dist # 生成 dmg + zip(release/)打包配置在 package.json 的 build 字段:appId: com.m2p.desktop,图标 mac / Linux 用 assets/icon.png(1024×1024,自动转 icns 与多尺寸 png),Windows 用 assets/icon.ico(16–256)。
重新生成图标(一次产出 icon.png 与 icon.ico):
npm run icon # 需要 Python 3 + Pillow侧边栏开关 · 文档名 · 主题 · 纸张 · 缩放 · 设置 · 导出 PDF。
下表按 macOS 标注;Windows / Linux 上 ⌘ 对应 Ctrl(悬停提示会按平台自动切换)。
| 快捷键 | 功能 |
|---|---|
| ⌘N | 新建文档 |
| ⌘O / ⇧⌘O | 打开文件 / 打开文件夹 |
| ⌘S / ⇧⌘S | 保存 / 另存为 |
| ⌘E | 导出 PDF |
| ⌘F | 编辑器内查找 |
| ⌘B | 显示 / 隐藏侧边栏 |
| ⌘⌥P | 显示 / 隐藏预览 |
| ⌘+ / ⌘- / ⌘0 | 放大 / 缩小 / 实际大小 |
| ⌘, | 打开设置 |
| Esc | 关闭弹窗 |
设置项改动即时生效并自动保存:
| 平台 | 配置文件 |
|---|---|
| macOS | ~/Library/Application Support/M2P/settings.json |
| Windows | %APPDATA%\M2P\settings.json |
| Linux | ~/.config/M2P/settings.json |
完整示例见 examples/demo.md,覆盖以下语法:
| 类别 | 支持 |
|---|---|
| 基础 | 标题(H1–H6)、段落、加粗、斜体、删除线、行内代码、引用块、分割线 |
| 列表 | 无序列表、有序列表、嵌套列表、任务清单(- [x] / - [ ]) |
| 表格 | GFM 表格,跨页自动重复表头 |
| 代码 | 围栏代码块、语言标签条、highlight.js 高亮(JS / Python / Bash / CSS 等) |
| 数学 | KaTeX 行内公式 $E=mc^2$、块级公式 $$...$$ |
| 脚注 | [^1] 引用标记 + [^1]: 定义,文末自动收集,附回链 |
| 链接与图片 | 外部链接(系统浏览器打开)、相对路径图片(以文档所在目录为基准) |
| 目录 | 开启 --toc 后自动生成 H1–H3 目录,带内部跳转链接 |
适合脚本、CI 与批量处理。
# 单个文件
npm start -- --export ./doc.md --out ./doc.pdf --theme academic --toc
# 指定纸张与分页
npm start -- --export ./doc.md --out ./doc.pdf --page-size A5 --chapter-break
# 导出 HTML(输出名用 .html)
npm start -- --export ./doc.md --out ./doc.html --html
# 整个文件夹(递归,含子目录)
npm start -- --export-folder ./docs --theme report --toc也可以直接调 Electron 可执行文件,避开 npm 的 -- 转发(Windows 上用 npx electron .):
./node_modules/.bin/electron . --export ./doc.md --out ./doc.pdf --theme github --toc| 参数 | 说明 |
|---|---|
--export <file.md> |
输入 Markdown |
--out <file.pdf> |
输出路径(带 --html 时为 .html) |
--export-folder <dir> |
批量导出目录 |
--theme <id> |
github / academic / report / minimal |
--page-size <size> |
A4 / A5 / Letter / Legal |
--landscape |
横向 |
--toc |
生成目录 |
--chapter-break |
一级标题另起一页 |
--no-header |
关闭页眉与页码 |
--html |
导出 HTML 而非 PDF |
结果一行 JSON 写到 stdout,例如 {"ok":true,"path":"/tmp/demo.pdf","pages":5,"bytes":854210},失败时 {"ok":false,"error":"..."} 并以退出码 1 结束。
开发辅助参数:--open <file> 启动时载入文档、--modal 启动即打开设置、--capture <png> 截图后退出。
M2P/
├── .github/workflows/ci.yml # CI:无头冒烟 + 三端打包 + Release
├── build.mjs # esbuild 构建:main / preload / renderer + 资源拷贝
├── package.json # 依赖、脚本、electron-builder 三端配置
├── package-lock.json # 锁定依赖版本,CI 用 npm ci 安装
├── LICENSE # MIT
├── assets/
│ ├── icon.png # 应用图标(1024×1024,mac / Linux 用)
│ └── icon.ico # Windows 图标(16–256 多尺寸)
├── examples/demo.md # 功能演示文档(验收样本)
├── tools/
│ ├── make-icon.py # 用 Pillow 生成 icon.png
│ └── make-ico.py # 由 icon.png 生成多尺寸 icon.ico
└── src/
├── main/
│ ├── main.js # 主进程:窗口、菜单、设置持久化、IPC、CLI 分支
│ └── exporter.js # 导出管线:HTML → 隐藏窗口 → printToPDF → 写文件
├── preload/preload.js # contextBridge 暴露 window.m2p
├── renderer/
│ ├── index.html # 界面骨架(CSP 已开启)
│ ├── style.css # 界面样式
│ └── app.js # 渲染进程:编辑器、预览、设置、导出、拖拽、快捷键
└── shared/
├── markdown.js # markdown-it 实例、目录生成、整篇 HTML 拼装
├── themes.js # 基础排版 CSS + 四套主题 + 代码高亮配色
└── defaults.js # 纸张尺寸、主题清单、默认设置
构建产物在 dist/,打包产物在 release/,两者都不入库。
printToPDF 复用应用内的 Chromium,零外部依赖、启动快;页眉页脚的 pageNumber / totalPages 模板与 generateDocumentOutline(PDF 书签)都是原生支持。
buildHtmlDocument() 只拼一份 HTML:预览塞进 iframe.srcdoc,导出写成临时 HTML 交给隐藏窗口打印。唯一差异是预览把页面固定为纸张像素宽度,靠 transform: scale() 适应预览区,字号、边距、分页规则都不变。
katex.min.css 内部是相对 fonts/ 的路径,临时 HTML 放在 /tmp 时会失效。导出前读取 CSS,把 url(./fonts/…) 改写成绝对 file:// 再内联;预览侧以 file:// 注入样式表。导出 HTML 时进一步转成 base64。
边距以英寸存储(Electron margins 单位),开启页眉页脚时纵向至少 0.72in,避免页眉页脚被裁掉。chapter-break 让 h1 另起一页(首个除外),目录可独占一页;表格 thead 设 display: table-header-group 跨页重复表头,tr { break-inside: avoid } 防止行被切断。
临时 HTML 会剥离 <script>、<iframe>、on*=、srcdoc;打印窗口 sandbox: true、nodeIntegration: false;主窗口开 contextIsolation,只经 preload 暴露白名单 IPC;界面设置 CSP,预览 iframe 为 allow-same-origin;外部链接走系统浏览器,主窗口拦截内部导航。
预览 iframe 是 sandbox="allow-same-origin",浏览器对 #锚点 的默认 fragment navigation 会清空整个 iframe 内容。所以在 onPreviewLoad 里统一拦截:外部链接(http/https/mailto)调系统浏览器打开;内部锚点先 decodeURIComponent 解码(目录链接的 href 是 URL 编码的中文),再用 getElementById 查找目标(不依赖 CSS 选择器,避免 id 以数字开头导致 querySelector 报错),最后 scrollIntoView 平滑滚动。
DMG 里的应用没有签名、也没公证,首次打开会被拦下,提示“无法验证开发者”或“M2P 已损坏,无法打开”。拖进 /Applications 后执行一行:
xattr -cr /Applications/M2P.app在 DMG 挂载窗口里直接双击、或 DMG 本身报错,就先清 DMG 的隔离属性(换成你下载的文件名):
xattr -cr ~/Downloads/M2P-1.0.0-arm64.dmg清完仍提示“已损坏”(Apple Silicon 上偶发),再补一次本地 ad-hoc 签名:
codesign --force --deep --sign - /Applications/M2P.app提示 Operation not permitted 时加 sudo。xattr 只清除“来自网络下载”的隔离标记,不改应用内容;不建议用 sudo spctl --master-disable 关掉全局 Gatekeeper。
- macOS 自带苹方 / 宋体,正常;
- Windows 用系统自带的微软雅黑,正常;
- Linux 需装中文字体:
sudo apt install fonts-noto-cjk(Debian / Ubuntu)或sudo dnf install google-noto-sans-cjk-fonts。
相对路径以 Markdown 文件所在目录为基准,未保存的新文档没有目录基准,先保存再预览或导出。
预览允许 https: 图片,但离线或需要登录的图床仍然加载不了;导出 PDF 走隐藏打印窗口,不受预览 CSP 限制。
每个文件都要走一次完整排版与打印,属预期行为,进度显示在状态栏。
GUI 子系统的 exe 没有标准输出流。CLI 导出建议用开发环境(./node_modules/.bin/electron .)或 Linux / macOS 版;Windows 上也可以用 --out 指定文件,直接看文件是否生成。
极少数发行版缺 SUID sandbox helper,启动加 --no-sandbox 可绕过(仅建议在受信环境使用)。