1. 引言

Typora 以所见即所得的沉浸式写作体验赢得了大量技术写作者的青睐,但原生功能的克制也意味着它缺少代码审查、格式化、定时保存等 IDE 式特性。好在 Typora 的内部架构基于 Electron,并且开放了插件机制,让开发者可以通过 JavaScript 与 CSS 为编辑器注入新能力。

通过插件,你可以把 Typora 变成一套轻量级 Markdown IDE:自动插入代码模板、集成外部 lint 工具、嵌入 Git 面板——甚至实现自己的图表渲染器。本文将从零起步,带你打通插件开发的全流程,从环境搭建到打包发布,每一步都有可运行的代码示例。

2. Typora 插件体系概览

Typora 的插件系统并非独立进程,而是直接运行在编辑器的渲染进程中。理解这一点,对后续开发至关重要:

  • 运行环境:Electron 渲染进程,Node.js API 可用,可访问 DOM 与文件系统。
  • 语言栈:JavaScript (ES6+)、CSS。
  • 插件位置:默认在 Typora 安装目录下的 resources/app/plugin/,也可以配置自定义路径。
  • 核心文件:每个插件至少包含一个 main.js 作为入口,以及一个可选的 style.css
  • 配置window.utools 或全局 Editor 对象提供丰富 API(虽无官方完整文档,但可通过内核代码反推)。

插件通过修改 Typora 的 plugin.json 来注册,格式如下:

{
  "plugins": [
    "my-plugin"
  ]
}

插件目录结构示例:

my-plugin/
├── main.js      // 插件逻辑
├── style.css    // 样式定制
└── manifest.json // 元信息(非必需)

启动时 Typora 会依次加载所有已注册插件的 main.js

3. 开发环境搭建

3.1 基础工具

  • Node.js(推荐 v16 以上),确保 nodenpm 可用。
  • 代码编辑器:VS Code 或其他,能写 JS 即可。
  • Typora 开发者模式:启动时添加参数 --enable-logging --remote-debugging-port=9222,方便调试。

Windows 下可通过命令行启动:

typora.exe --enable-logging

macOS 可通过终端:

open /Applications/Typora.app --args --enable-logging

3.2 开启开发者工具

Typora 本身是 Electron 应用,按 Ctrl+Shift+I(Windows/Linux)或 Cmd+Option+I(macOS)可打开开发者工具,里面的 Console 就是你写插件时的调试利器。

3.3 配置插件目录

为了避免修改安装目录带来权限问题,推荐创建一个独立插件文件夹,通过 Typora 的 preferences(偏好设置)中的高级选项指定外部插件路径。然后在指定文件夹下创建你的插件子目录。

4. 第一个插件:Hello World

我们现在写一个极简插件,功能:在「编辑」菜单中添加一个菜单项,点击后弹出一条提示。

4.1 目录与文件

在插件根目录下创建 hello-world 文件夹,内部写入三个文件:

  • main.js
  • style.css (暂时留空)
  • manifest.json

manifest.json

{
  "name": "hello-world",
  "version": "1.0.0",
  "description": "My first Typora plugin",
  "author": "Your Name",
  "main": "main.js"
}

main.js

// 插件入口
module.exports = {
  activate() {
    console.log('Hello World 插件已激活');
  },

  deactivate() {
    console.log('Hello World 插件已卸载');
  },

  // 其他扩展点
};

对于早期 Typora 版本,入口可能直接是一段自执行函数,将代码挂载到 window 或通过 utools 注册。若你的版本不支持模块系统,可以这样写:

window.addEventListener('load', () => {
  const { Menu } = require('electron').remote;
  const menu = Menu.getApplicationMenu();
  const editMenu = menu.items.find(i => i.label === '编辑');
  editMenu.submenu.append(new (require('electron').remote.MenuItem)({
    label: 'Say Hello',
    click: () => {
      alert('Hello from plugin!');
    }
  }));
  Menu.setApplicationMenu(menu);
  console.log('Hello World 注入成功');
});

4.2 注册插件

在 Typora 的 plugin.json 中加入 "hello-world",重启 Typora。打开菜单「编辑」→ 应该能看到「Say Hello」,点击即弹窗。

5. 核心 API 解析

Typora 向外暴露的 API 没有完整的官方文档,但通过调试工具可以发现几个关键对象。

5.1 Editor 全局对象

window.editor 是核心。常用方法:

// 获取当前编辑器中的 Markdown 源码
const content = editor.getValue();

// 替换选中文本或光标处插入
editor.replaceSelection('**粗体文本**');

// 获取选区信息
const range = editor.getSelection();
// → {start: {line, ch}, end: {line, ch}}

// 监听文本变化
editor.on('change', (cm, changeObj) => {
  // changeObj 包含 from, to, text, removed 等
});

// 获取光标所在行
const line = editor.getLine(editor.getCursor().line);

这些 API 都来自 CodeMirror,因为 Typora 的编辑内核就是 CodeMirror。

5.2 文件系统访问

通过 Node.js 的 fs 模块,你可以读写任意文件(前提是权限允许):

const fs = require('fs');
const path = require('path');

// 读取当前正在编辑的完整路径
const filePath = window.__typora_file_path__; // 非标准属性,某些版本可用

// 如果没有,可以通过文档对象获取
const currentFile = document.querySelector('title').innerText; // 标题即文件名

小心处理文件读写,避免覆盖用户内容。

5.3 UI 定制

  • 菜单注入:如 Hello World 示例所示,通过 electron.remote.Menu 动态修改菜单。
  • 工具栏按钮:Typora 的工具栏是 HTML 元素,可以通过 DOM 操作插入新按钮:
const toolbar = document.querySelector('.ty-toolbar');
const btn = document.createElement('button');
btn.innerHTML = '⭐ 收藏';
btn.onclick = () => { /* 你的逻辑 */ };
toolbar.appendChild(btn);
  • 面板:可模仿 Typora 侧边栏创建浮动面板,需要编写相应 HTML/CSS,并链接到插件 style.css 中。

5.4 快捷键注册

可以通过 editor.addKeyMap 绑定自定义快捷键:

editor.addKeyMap({
  'Ctrl-B': function(cm) {
    cm.replaceSelection('**' + cm.getSelection() + '**');
  }
});

6. 实战:代码片段插入器

目标:打造一个常用代码片段快速插入的插件,支持按语言分类浏览,选中一行代码模板即可插入到光标位置。

6.1 数据结构与界面

设计片段 JSON 文件:

{
  "python": [
    {"name": "读取文件", "code": "with open('${1:file}', 'r') as f:\n    data = f.read()"},
    {"name": "列表推导式", "code": "[x for x in range(${1:10})]"}
  ],
  "javascript": [
    {"name": "箭头函数", "code": "const ${1:fn} = (${2:args}) => { ${3} }"}
  ]
}

${1:placeholder} 语法借鉴了 VS Code 的代码片段,插入后光标可跳转占位符。

6.2 实现面板

main.js 中创建侧边栏面板:

function createSnippetPanel() {
  const panel = document.createElement('div');
  panel.id = 'snippet-panel';
  panel.innerHTML = `
    <select id="lang-select"></select>
    <ul id="snippet-list"></ul>
  `;
  document.body.appendChild(panel);

  // 加载 JSON 片段数据
  const snippets = require('./snippets.json');
  const select = panel.querySelector('#lang-select');
  Object.keys(snippets).forEach(lang => {
    const opt = document.createElement('option');
    opt.value = lang;
    opt.textContent = lang;
    select.appendChild(opt);
  });

  select.addEventListener('change', () => {
    const lang = select.value;
    const list = panel.querySelector('#snippet-list');
    list.innerHTML = '';
    snippets[lang].forEach(s => {
      const li = document.createElement('li');
      li.textContent = s.name;
      li.addEventListener('click', () => {
        const editor = window.editor;
        const text = s.code.replace(/\$\{\d+:.*?\}/g, ''); // 可后续扩展占位符跳转
        editor.replaceSelection(text);
      });
      list.appendChild(li);
    });
  });
  select.dispatchEvent(new Event('change'));
}

6.3 注册快捷键打开面板

editor.addKeyMap({
  'Ctrl-Shift-J': function() {
    const panel = document.getElementById('snippet-panel');
    panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
  }
});

6.4 样式美化(style.css)

#snippet-panel {
  position: fixed;
  right: 10px;
  top: 60px;
  width: 260px;
  background: #f5f5f5;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 10px;
  z-index: 1000;
  display: none;
}
#snippet-panel select {
  width: 100%;
  margin-bottom: 8px;
}
#snippet-list {
  list-style: none;
  padding: 0;
}
#snippet-list li {
  cursor: pointer;
  padding: 4px 8px;
  border-bottom: 1px solid #eee;
}
#snippet-list li:hover {
  background: #ddd;
}

现在,重启 Typora,按下 Ctrl+Shift+J 即可呼唤出代码片段面板,点击任何片段,它就会插入到编辑器当前光标处。

7. 高级玩法:集成外部工具

Typora 插件可以化身胶水,串联本地或在线工具。

7.1 调用 Prettier 格式化 Markdown

const { execSync } = require('child_process');
function formatMarkdown() {
  const content = editor.getValue();
  try {
    const formatted = execSync('npx prettier --parser markdown', {
      input: content,
      encoding: 'utf-8',
    });
    editor.setValue(formatted);
    console.log('格式化完成');
  } catch (e) {
    console.error('格式化出错', e);
  }
}

绑定到快捷键:

editor.addKeyMap({
  'Ctrl-Shift-F': formatMarkdown,
});

7.2 远程请求与 AI 辅助

你可以直接在插件里发起 HTTP 请求(需要引入 axios 或使用 net 模块),例如调用 OpenAI 接口生成摘要、翻译段落等。注意保护好 API Key,可提示用户配置在单独文件中。

7.3 自定义渲染

Typora 支持通过 markdown-it 扩展 Markdown 解析。例如,为自定义块语法转成高亮框:

// 需要在插件中重写 Typora 的解析器
const md = require('markdown-it')();
md.use((md) => {
  md.block.ruler.before('fence', 'mylang', function (state, startLine, endLine, silent) {
    // 解析 ::: tip ... ::: 并返回 token
  });
});

但直接修改 Typora 的内部解析器存在版本兼容风险,需谨慎。

8. 打包与分发

写完插件后,自然希望分享给其他 Typora 用户。

8.1 打包为 ZIP

最简单的分发方式:将插件文件夹压缩为 ZIP,并附上安装说明。用户在 Typora 的偏好设置中导入外部插件文件夹,或手动移动到插件目录。

8.2 使用 asar 打包(可选)

Typora 本身用 asar 打包资源,你也可以将插件打包成 plugin.asar,放到资源目录中。但跨平台兼容需自行测试。

8.3 发布到社区

  • 将源码上传到 GitHub,书写清晰的 README。
  • 在 Typora 官方拓展示例仓库提交 PR,或在论坛发文推荐。
  • 可利用 GitHub Release 附带 ZIP 下载。

8.4 考虑兼容性

  • 不同 Typora 版本的内核变动较大(尤其是 CodeMirror 升级),最好标明支持的最低版本。
  • 尽量使用稳定的全局 API,避免过多依赖内部私有属性。
  • package.json 中声明依赖并指明安装要求。

9. 总结

通过以上步骤,你已经掌握 Typora 插件开发的核心要诀:从简单的菜单注入到复杂的面板交互,再到集成外部工具链。每一行插件都是对 Typora 环境的个性化重塑,让它从一款纯粹的编辑器蜕变为你的专属 Markdown IDE。

建议你从一个小而美的痛点入手——比如图片自动压缩、TODO 统计面板——逐步迭代,享受创造工具的快感。代码即创意,开始动手吧!

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐