写在前面

Typora 是一款以“所见即所得”著称的 Markdown 编辑器,其简洁优雅的设计赢得了大量开发者和写作者的青睐。用户在输入 Markdown 语法时,内容即时渲染为格式化文本,这种无缝体验极大降低了写作时的认知负荷。Typora 原生支持 100+ 种编程语言的语法高亮、LaTeX 数学公式渲染、Mermaid 流程图绘制等基础功能。

然而,Typora 官方并未正式提供插件 API,这在某种程度上限制了用户对编辑器的深度定制能力。好在开源社区的力量是强大的——通过逆向工程、代码注入以及社区贡献的插件框架,开发者完全可以在 Typora 上实现媲美 IDE 的扩展能力。目前,Typora 插件的开发主要围绕一个核心开源项目展开:obgnail/typora_plugin。该项目为 Typora 提供了超过 60 个内置插件,涵盖标签页管理、多关键词搜索、代码增强、图表支持等功能。

重要提示:Typora 官方并未正式提供插件 API,当前社区插件方案通过对 Typora 的程序文件进行修改来实现功能扩展。使用此类方案时,请确保遵守 Typora 软件许可协议,并在个人学习与使用的合理范围内进行。对开源项目的扩展应尽可能通过贡献代码的方式实现。

本指南将带你从零开始,全面掌握 Typora 插件的开发技能,从最基础的环境搭建、核心框架理解,到实战项目落地、主题定制,最终打造一个专属于自己的 IDE 式写作环境。全文约 2 万字,建议先通读了解全貌,再根据实际需要深入各章节。

第 1 章 核心开发概念:从配置到架构的深度解构

在动手写代码前,务必备份重要数据,确保理解社区插件系统的基本约束。

1.1 为什么需要为 Typora 开发插件?

Typora 的核心优势在于“即时预览”,但原生功能仍存在局限:多文件管理不足(原生 Typora 无法同时打开多个文档并在标签页间快速切换);搜索能力有限(缺少跨文件的复杂关键词组合搜索);编辑增强缺失(没有章节折叠、代码块增强、模板系统等现代化编辑特性);工作流割裂(与 Git、图床、CI/CD 等开发工具的集成需手动处理)。

这正是插件系统的价值所在。通过插件,开发者可以将 Typora 从一个优雅的 Markdown 编辑器升级为功能完备的技术文档写作平台。

1.2 Typora 插件的实现原理

Typora 基于 Electron 框架构建。Electron 应用采用“主进程 + 渲染进程”的双进程架构:主进程负责创建窗口和管理应用生命周期,而每个窗口运行独立的渲染进程,本质上是一个 Chromium 浏览器实例。这种架构为逆向工程提供了可能,因为我们可以通过 DevTools 访问和修改渲染进程的 JavaScript 执行环境。

Typora 插件的核心实现原理包括以下几个层面:

1.2.1 前端注入原理

window.html 是 Typora 的初始文件,开发者可以在其中写入 <script> 标签来注入插件脚本,其原理和 Tampermonkey 脚本相似。

具体来说,插件系统通过以下步骤实现前端功能扩展:

  1. 脚本注入:在 window.html 中插入插件入口脚本 ./plugin/index.js

  2. DOM 操作:插件脚本在渲染进程中执行,可以直接访问和修改 Typora 的 DOM 结构

  3. 事件拦截:通过监听和劫持编辑器事件,在合适时机注入自定义逻辑

1.2.2 后端注入原理

Typora 的插件系统实现了更深层次的 Electron 应用扩展:

  1. 模块导入:Typora 暴露了 reqnode 函数(对 Node.js require 的封装),可以使用 reqnode(‘path’) 导入 Node.js 的内置库,如 path、fs 等

  2. 代码执行:Typora 使用了 executeJavaScript 功能,可以用此注入 JS 代码,从而劫持后端关键对象

  3. Electron 对象劫持:理论上劫持了 electron 对象,你甚至可以在 Typora 里实现几乎任何 Electron 应用级别的功能

通过 ClientCommand.execForAll 和 JSBridge.invoke 等机制,插件还可以实现跨窗口通信和多窗口管理。

1.2.3 安全沙箱设计

为防止不安全插件,成熟的插件系统需要:

  • 限制文件系统访问范围

  • 隔离执行环境

  • 实现敏感操作权限控制

  • 通过许可机制约束插件的资源消耗和API调用

1.3 插件系统的核心架构

插件生态围绕 typora_plugin 项目构建,采用三层架构设计:

  • 核心基础设施层:提供配置加载、插件加载、共享服务(utils)、国际化等功能

  • 插件基类层:定义 IPlugin、BasePlugin、BaseCustomPlugin 三大基类

  • 功能插件层:60+ 内置插件 + 用户自定义插件

所有插件遵循标准的七阶段生命周期模型,通过统一的 API 与 Typora 交互。

1.4 扩展能力矩阵

在理解插件架构之前,我们先明确 Typora 扩展生态中的三种核心能力:

扩展方式 核心特点 适用场景
CSS 主题定制 视觉样式全覆盖、无逻辑侵入 字体、配色、排版、自定义美化
自定义上传脚本 外部流程集成、语言无关、单次触发 图片、文件、自定义命令等上传处理
插件脚本 动态、交互、可复用、功能强大 AI 补全、表格增强、数据展示、快捷键扩展

本指南将聚焦于第三种——插件脚本开发,这是实现 IDE 式写作环境的核心能力。

第 2 章 环境准备与快速上手

2.1 基础工具链

Typora 插件开发环境搭建极其简单,无需编译、无需复杂配置,插件文件直接生效,零配置环境大大降低了入门门槛。

必备工具

工具 用途 说明
Typora 目标编辑器 建议版本 ≥ 1.0(支持插件机制)
Node.js JavaScript 运行时 插件开发的基础环境,Typora 基于 Electron 框架,其插件环境完整支持 Node.js 运行时
VS Code 代码编辑器 推荐用于插件开发,支持 JavaScript 语法高亮和调试
Git 版本控制 获取源码和管理版本

2.2 版本要求

  • Typora 版本要求:支持插件机制的版本(建议 Typora ≥ 1.0)

  • Node.js 支持:模块系统仅支持 CommonJS 规范,ESM 模块需通过构建工具转换;建议使用 ES6 语法

  • 核心开发语言:JavaScript(Node.js 环境)

2.3 获取插件框架源码

bash

# 克隆项目仓库(建议使用 GitCode 镜像,访问更稳定)
git clone https://gitcode.com/gh_mirrors/ty/typora_plugin

# 或直接下载 ZIP 压缩包
# 访问 https://github.com/obgnail/typora_plugin

2.4 安装插件系统

方法一:自动安装(推荐)
  1. 下载插件:从 GitHub 克隆或下载 ZIP 压缩包

  2. 定位 Typora 资源路径

    • Windows:Typora/resources/app/ 或 %appdata%\Typora\resources\

    • Linux:~/.config/Typora/resources/

    • macOS:~/Library/Application Support/abnerworks.Typora/

  3. 复制插件文件夹:将解压得到的 plugin 文件夹复制到该路径下

  4. 执行安装脚本

    • Windows:进入 plugin/bin,双击运行 install_windows_amd_x64.exe

    • Linux:以管理员权限运行 install_linux.sh

    • Arch Linux:yay -S typora-plugin

  5. 验证安装:重启 Typora,右键点击编辑区域,看是否出现“常用插件”菜单项

方法二:手动安装(适合喜欢自定义的高级用户)
  1. 在 Typora 安装路径中找到包含 window.html 的文件夹(不同版本的 Typora 文件夹结构可能不同,通常位于 Typora/resources/window.html 或 Typora/resources/app/window.html

  2. 将源码的 plugin 文件夹粘贴进该文件夹下

  3. 打开文件 A/window.html,搜索 <script src=“./app/window/frame.js” defer=“defer”></script>,并在后面加入 <script src=“./plugin/index.js” defer=“defer”></script>

  4. 保存并重启 Typora

2.5 核心配置文件

插件的核心配置文件位于 plugin/global/settings/ 目录下:

  • settings.default.toml:默认配置,不应直接修改

  • settings.user.toml:用户配置文件,启用、禁用、配置插件的控制面板

你可以在 settings.user.toml 中禁用不需要的插件(如 ai_assistant 等),并为各插件定义私有逻辑。

2.6 验证安装成功

重启 Typora 后,在正文区域点击鼠标右键,弹出右键菜单栏,如果能看到“常用插件”栏目,说明一切顺利。

第 3 章 插件架构与核心 API 剖析

3.1 插件基类与接口

插件系统定义了三个核心抽象:

3.1.1 IPlugin 接口

IPlugin 是根接口,定义了所有插件必须遵循的七阶段生命周期方法。每个插件,无论是基础插件还是自定义插件,都继承自此类。构造函数初始化五个实例属性:

属性 类型 说明
this.fixedName string 插件的唯一标识符
this.pluginName string 插件的显示名称(来自配置或 i18n)
this.config object 插件特定的配置设置
this.setting object 来自 TOML 文件的配置
this.i18n object 绑定到该插件的国际化对象
3.1.2 BasePlugin 类

BasePlugin 是最常用的基类,用于需要复杂初始化的系统级插件。它扩展了 IPlugin,并提供了 call(action, meta) 方法,用于暴露多个操作的特性插件。

适用场景:需要注册快捷键、添加菜单项、管理多个操作行为的复杂功能插件。

3.1.3 BaseCustomPlugin 类

BaseCustomPlugin 采用 selector/hint/callback 模式,专门设计用于上下文感知、操作特定文档元素的插件。它通过 CustomPlugin 编排器实现自动集成到右键菜单。

对于用户创建的插件,应使用 BaseCustomPlugin。这提供了与右键菜单系统、快捷键管理的自动集成,并通过 CustomPlugin 管理器实现动态操作生成。

3.2 插件生命周期

插件加载遵循标准流程:

阶段 方法 说明
版本检查 checkVersion() 检查 Typora 版本兼容性,如不满足则停止加载
初始化 onLoad() 首先调用,负责加载配置和初始化状态
就绪 onDidMount() 在 Typora 主界面渲染完毕后调用
布局调整 onLayout() 窗口大小变化时触发
执行 callback() / process() 实现具体的业务逻辑
卸载与清理 onUnload() 卸载时调用,用于清理创建的 DOM 元素和注销事件监听

3.3 核心全局对象与 API

注入插件后,开发者可以通过以下核心全局对象访问 Typora 内部功能:

  • window.typora:访问编辑器实例、文档对象、UI 组件

  • 事件系统:监听文档变化、选区变化、文件保存等

  • 菜单与命令系统:注册自定义菜单项和快捷键

  • 文件系统访问:读写本地文件、管理附件资源

3.4 Utils 层与公共服务

utils 层是插件与编辑器交互的核心桥梁:

  • utils.fetch:安全发起 HTTP 请求的替代方法,解决 Typora 插件环境中 fetch API 的限制,特别是在处理 HTTPS 请求时的跨域问题

  • utils.decorator:用于装饰 Typora 原生方法,注入自定义逻辑,实现 AOP 风格的功能增强

  • utils.getPluginFunction(fixedName, funcName):获取指定插件的函数引用,用于跨插件调用和热键绑定

  • utils.getAnchorNode(selector):获取当前光标所在的匹配元素

  • utils.hotkeyHub:快捷键注册中心,统一管理插件的全局快捷键

第 4 章 开发你的第一个插件

让我们从零开始,创建一个完整的“你好世界”插件,逐步掌握插件开发的完整流程。

4.1 配置声明:TOML 配置文件

自定义插件使用 TOML(Tom‘s Obvious, Minimal Language)进行配置。创建或修改 ./plugin/global/settings/custom_plugin.user.toml 文件,添加以下配置:

toml

# custom_plugin.user.toml
[helloWorld]
name = "你好世界"              # 插件在右键菜单中显示的名称
enable = true                 # 控制插件是否启用
hide = false                  # 控制插件是否在右键菜单中隐藏
order = 1                     # 插件在右键菜单中的显示顺序(数值越大越靠后)
hotkey = "ctrl+alt+u"         # 定义触发插件的快捷键
console_message = "I am in process"   # 将在控制台输出的信息(自定义配置项)
show_message = "this is hello world plugin"  # 将在提示框中显示的信息

# name、enable、hide、order 是必须项,其余是插件个性化的配置[reference:43]

4.2 编写插件代码

在 ./plugin/custom/plugins/ 目录下创建 helloWorld.js 文件,编写继承 BaseCustomPlugin 的类并导出为 plugin

javascript

// ./plugin/custom/plugins/helloWorld.js

class helloWorld extends BaseCustomPlugin {
  // 1. 最先执行,用于检查插件运行的前提条件
  // 如果条件不满足,返回 this.utils.PLUGIN_LOAD_ABORT 以终止插件加载
  prepare = async () => {
    // 实际开发中请替换为有意义的条件检查
    if (false) {
      return this.utils.PLUGIN_LOAD_ABORT
    }
  }

  // 2. 注册 CSS 样式。返回一个字符串,该字符串会自动作为 <style> 标签插入到 DOM 中
  style = () => `
    #hello-world { 
      position: fixed;
      bottom: 20px;
      right: 20px;
      padding: 10px 15px;
      background: #4CAF50;
      color: white;
      border-radius: 8px;
      font-family: monospace;
      z-index: 9999;
    }
  `

  // 3. 注册 DOM 元素。可以返回 Element 类型或表示元素的字符串
  html = () => "<div id='hello-world' style='display: none;'></div>"

  // 4. 注册右键菜单的提示信息
  hint = () => "点击显示欢迎信息"

  // 5. 注册触发 callback 的快捷键
  hotkey = () => [this.config.hotkey]

  // 6. 插件的初始化,通常在这里获取或设置 DOM 元素
  init = () => {
    this.myDiv = document.querySelector("#hello-world")
  }

  // 7. process 方法在插件初始化完成后(执行上述注册逻辑后)自动运行
  process = () => {
    // 可以通过 this.config 获取 TOML 文件中的所有配置项
    console.log(this.config.console_message)
    console.log("[helloWorldPlugin]: ", this)
    console.log(this.myDiv)
  }

  // 8. callback 方法在点击右键菜单选项或键入快捷键时自动调用
  // 注意:如果未定义 callback 函数,该插件将无法通过右键菜单点击触发
  // anchorNode 参数表示调用此插件时,光标所在的 Element
  callback = anchorNode => {
    // 显示提示框
    alert(this.config.show_message)
    
    // 临时显示并隐藏提示元素
    if (this.myDiv) {
      this.myDiv.style.display = 'block'
      setTimeout(() => {
        this.myDiv.style.display = 'none'
      }, 2000)
    }
    
    // 在控制台输出光标位置信息,便于调试
    console.log("当前光标所在元素:", anchorNode)
  }
}

// 导出插件类
module.exports = { plugin: helloWorld }

4.3 验证插件

  1. 重启 Typora:使配置文件生效

  2. 打开开发者工具:帮助 → 启用调试模式(Mac)或 视图 → 切换开发者工具(Windows/Linux)

  3. 测试触发

    • 右键点击编辑器区域 → 常用插件 → 自定义插件 → 你好世界

    • 或直接按 Ctrl+Alt+U

  4. 检查控制台:打开 Chrome DevTools,检查控制台是否输出了 I am in process

4.4 示例:表格智能扩展插件

创建一个更加实用的插件——表格智能扩展工具:

javascript

// ./plugin/custom/plugins/tableHelper.js

class tableHelper extends BaseCustomPlugin {
  // 插件仅在光标位于表格单元格内时可用
  selector = () => "td, th"
  
  style = () => `
    .table-toolbar {
      position: absolute;
      background: white;
      border: 1px solid #ccc;
      border-radius: 4px;
      padding: 4px 8px;
      display: none;
      gap: 8px;
      z-index: 1000;
      box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    }
    .table-toolbar button {
      background: #f5f5f5;
      border: 1px solid #ddd;
      border-radius: 3px;
      padding: 2px 8px;
      cursor: pointer;
      font-size: 12px;
    }
    .table-toolbar button:hover {
      background: #e0e0e0;
    }
  `
  
  html = () => `
    <div class="table-toolbar" id="table-toolbar">
      <button id="btn-add-col">+列</button>
      <button id="btn-add-row">+行</button>
      <button id="btn-del-col">-列</button>
      <button id="btn-del-row">-行</button>
    </div>
  `
  
  hint = () => "表格编辑工具:添加/删除行列"
  
  init = () => {
    this.toolbar = document.querySelector("#table-toolbar")
    this.currentTable = null
    
    // 绑定按钮事件
    document.querySelector("#btn-add-col")?.addEventListener("click", () => this.addColumn())
    document.querySelector("#btn-add-row")?.addEventListener("click", () => this.addRow())
    document.querySelector("#btn-del-col")?.addEventListener("click", () => this.deleteColumn())
    document.querySelector("#btn-del-row")?.addEventListener("click", () => this.deleteRow())
  }
  
  // 当光标进入表格区域时显示工具栏
  process = () => {
    // 监听光标移动,显示/隐藏工具栏
    document.addEventListener("selectionchange", this.handleSelectionChange.bind(this))
  }
  
  handleSelectionChange = () => {
    const selection = window.getSelection()
    if (!selection.rangeCount) return
    
    const node = selection.anchorNode
    const td = node?.nodeType === 3 ? node.parentElement : node?.closest?.("td, th")
    
    if (td && this.toolbar) {
      this.currentTable = td.closest("table")
      const rect = td.getBoundingClientRect()
      this.toolbar.style.display = "flex"
      this.toolbar.style.top = `${rect.top - 30}px`
      this.toolbar.style.left = `${rect.left}px`
    } else if (this.toolbar) {
      this.toolbar.style.display = "none"
    }
  }
  
  // 添加新行
  addRow = () => {
    if (!this.currentTable) return
    const firstRow = this.currentTable.rows[0]
    const newRow = this.currentTable.insertRow()
    for (let i = 0; i < firstRow.cells.length; i++) {
      const newCell = newRow.insertCell()
      newCell.innerHTML = "&nbsp;"
      newCell.contentEditable = "true"
    }
  }
  
  // 删除最后一行
  deleteRow = () => {
    if (!this.currentTable || this.currentTable.rows.length <= 1) return
    this.currentTable.deleteRow(-1)
  }
  
  // 添加新列
  addColumn = () => {
    if (!this.currentTable) return
    for (let i = 0; i < this.currentTable.rows.length; i++) {
      const newCell = this.currentTable.rows[i].insertCell()
      newCell.innerHTML = "&nbsp;"
      newCell.contentEditable = "true"
    }
  }
  
  // 删除最后一列
  deleteColumn = () => {
    if (!this.currentTable || this.currentTable.rows[0].cells.length <= 1) return
    for (let i = 0; i < this.currentTable.rows.length; i++) {
      this.currentTable.rows[i].deleteCell(-1)
    }
  }
  
  callback = () => {
    // 右键菜单点击时,可执行其他操作
    console.log("表格工具已激活")
  }
}

module.exports = { plugin: tableHelper }

第 5 章 实战项目:打造 IDE 式写作环境

本章将通过多个实战项目,全面覆盖 IDE 式写作环境的核心功能实现:快捷键管理系统、命令面板、代码增强工具、图表集成和外部自动化控制。

5.1 快捷键管理系统

hotkeys 插件提供注册表,用于将键盘快捷键绑定到任意函数或自定义 JavaScript 代码。

5.1.1 自定义快捷键配置

在 settings.user.toml 中添加自定义快捷键:

toml

[hotkeys]
CUSTOM_HOTKEYS = [
  # 代码执行方式
  { hotkey = "ctrl+shift+f", evil = "formatDocument()" },
  
  # 调用其他插件方法
  { hotkey = "ctrl+alt+p", plugin = "templater", func = "insertTemplate" },
  
  # 上下文感知的快捷键
  { hotkey = "ctrl+shift+u", closestSelector = "pre", evil = "toggleCodeBlockFold(this)" }
]
5.1.2 快捷键处理流程

Hotkeys 插件通过以下逻辑解析回调:

  1. Code 评估:如果存在 evil 字段,使用 eval(evil) 执行

  2. 插件引用:如果提供了 plugin 和 func,使用 utils.getPluginFunction 获取目标方法

  3. 选择器包装:如果定义了 closestSelector,回调将被包装,仅在通过 utils.getAnchorNode(closestSelector) 找到匹配元素时执行

5.2 命令面板实现

命令面板是 IDE 式写作环境的核心组件,允许用户通过输入命令快速执行各种操作。

5.2.1 命令面板基础架构

javascript

// custom/plugins/commandPalette.js

class commandPalette extends BaseCustomPlugin {
  selector = () => "body"
  
  style = () => `
    .command-palette-overlay {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0,0,0,0.5);
      z-index: 10000;
      display: none;
      justify-content: center;
      padding-top: 20vh;
    }
    .command-palette {
      width: 600px;
      background: white;
      border-radius: 12px;
      box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25);
      overflow: hidden;
    }
    .command-palette input {
      width: 100%;
      padding: 16px 20px;
      font-size: 16px;
      border: none;
      border-bottom: 1px solid #e5e7eb;
      outline: none;
      font-family: monospace;
    }
    .command-list {
      max-height: 400px;
      overflow-y: auto;
    }
    .command-item {
      padding: 10px 20px;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 12px;
      transition: background 0.15s;
    }
    .command-item:hover, .command-item.selected {
      background: #f3f4f6;
    }
    .command-icon {
      width: 24px;
      text-align: center;
    }
    .command-title {
      flex: 1;
      font-weight: 500;
    }
    .command-desc {
      font-size: 12px;
      color: #6b7280;
    }
  `
  
  html = () => `
    <div class="command-palette-overlay" id="command-palette">
      <div class="command-palette">
        <input type="text" id="command-input" placeholder="输入命令..." autocomplete="off">
        <div class="command-list" id="command-list"></div>
      </div>
    </div>
  `
  
  hotkey = () => ["ctrl+shift+p"]  // 标准 VSCode 风格
  
  init = () => {
    this.overlay = document.querySelector("#command-palette")
    this.input = document.querySelector("#command-input")
    this.list = document.querySelector("#command-list")
    this.commands = []
    this.selectedIndex = -1
    
    this.registerCommands()
    this.bindEvents()
  }
  
  registerCommands = () => {
    // 注册内置命令
    this.commands = [
      { title: "打开设置", icon: "⚙️", action: () => this.utils.openSettings() },
      { title: "切换只读模式", icon: "📖", action: () => this.utils.toggleReadOnly() },
      { title: "插入思维导图", icon: "🧠", action: () => this.utils.insertMindmap() },
      { title: "格式化文档", icon: "✨", action: () => this.utils.formatDocument() },
      { title: "导出为 PDF", icon: "📄", action: () => this.utils.exportPDF() },
      { title: "新建笔记", icon: "➕", action: () => this.utils.newNote() },
      { title: "插入日期时间", icon: "📅", action: () => this.insertDateTime() },
      { title: "运行代码块", icon: "▶️", action: () => this.runCodeBlock() }
    ]
  }
  
  insertDateTime = () => {
    const now = new Date()
    const dateStr = now.toLocaleString('zh-CN')
    const selection = window.getSelection()
    if (selection.rangeCount) {
      const range = selection.getRangeAt(0)
      range.deleteContents()
      range.insertNode(document.createTextNode(dateStr))
    }
    this.hide()
  }
  
  runCodeBlock = () => {
    // 获取当前光标所在的代码块并执行
    const node = this.utils.getAnchorNode("pre")
    if (node) {
      const code = node.querySelector("code")?.innerText
      if (code) {
        try {
          new Function(code)()
          console.log("代码块执行成功")
        } catch(e) {
          console.error("代码块执行失败:", e)
        }
      }
    }
    this.hide()
  }
  
  bindEvents = () => {
    this.input.addEventListener("input", this.filterCommands.bind(this))
    this.input.addEventListener("keydown", this.handleKeydown.bind(this))
    this.overlay.addEventListener("click", (e) => {
      if (e.target === this.overlay) this.hide()
    })
  }
  
  filterCommands = () => {
    const query = this.input.value.toLowerCase()
    const filtered = query === "" 
      ? this.commands 
      : this.commands.filter(cmd => cmd.title.toLowerCase().includes(query))
    
    this.list.innerHTML = filtered.map((cmd, idx) => `
      <div class="command-item" data-index="${idx}">
        <span class="command-icon">${cmd.icon || "📝"}</span>
        <span class="command-title">${cmd.title}</span>
        <span class="command-desc">${cmd.desc || ""}</span>
      </div>
    `).join("")
    
    // 绑定点击事件
    document.querySelectorAll(".command-item").forEach(el => {
      el.addEventListener("click", () => {
        const idx = parseInt(el.dataset.index)
        filtered[idx].action()
        this.hide()
      })
    })
    
    this.selectedIndex = 0
    this.updateSelection()
  }
  
  handleKeydown = (e) => {
    const items = document.querySelectorAll(".command-item")
    if (e.key === "ArrowDown") {
      e.preventDefault()
      this.selectedIndex = Math.min(this.selectedIndex + 1, items.length - 1)
      this.updateSelection()
    } else if (e.key === "ArrowUp") {
      e.preventDefault()
      this.selectedIndex = Math.max(this.selectedIndex - 1, 0)
      this.updateSelection()
    } else if (e.key === "Enter") {
      e.preventDefault()
      items[this.selectedIndex]?.click()
    } else if (e.key === "Escape") {
      this.hide()
    }
  }
  
  updateSelection = () => {
    document.querySelectorAll(".command-item").forEach((el, idx) => {
      if (idx === this.selectedIndex) {
        el.classList.add("selected")
        el.scrollIntoView({ block: "nearest" })
      } else {
        el.classList.remove("selected")
      }
    })
  }
  
  show = () => {
    this.overlay.style.display = "flex"
    setTimeout(() => this.input.focus(), 100)
    this.input.value = ""
    this.filterCommands()
  }
  
  hide = () => {
    this.overlay.style.display = "none"
  }
  
  callback = () => {
    this.show()
  }
}

module.exports = { plugin: commandPalette }

5.3 斜杠命令系统集成

slash_commands 插件提供内联自动补全系统,允许用户通过输入配置的触发字符(默认为 /)触发命令、插入文本片段或执行动态生成器。

javascript

// 扩展 slash_commands 配置(在 settings.user.toml 中)
[slash_commands]
trigger = "/"

[[slash_commands.commands]]
name = "代码块"
description = "插入带语言标识的代码块"
snippet = """
```{{language}}
{{cursor}}

"""

[[slash_commands.commands]]
name = "表格"
description = "插入 3x3 表格模板"
snippet = """

列1 列2 列3
"""

[[slash_commands.commands]]
name = "mermaid思维导图"
description = "插入 Mermaid 思维导图"
snippet = """

"""

text

### 5.4 代码块增强:折叠与复制

`fence_enhance` 插件为代码块提供一键复制代码、代码块折叠等功能[reference:50]。

**核心实现原理**:

```javascript
// fence_enhance 插件的核心逻辑
function enhanceCodeBlock() {
  document.querySelectorAll('.md-fences').forEach(fence => {
    // 添加复制按钮
    const copyBtn = document.createElement('button')
    copyBtn.textContent = '复制'
    copyBtn.onclick = () => {
      const code = fence.querySelector('code').innerText
      navigator.clipboard.writeText(code)
    }
    
    // 添加折叠/展开功能
    const toggleBtn = document.createElement('button')
    toggleBtn.textContent = '▼'
    let collapsed = false
    toggleBtn.onclick = () => {
      collapsed = !collapsed
      fence.style.maxHeight = collapsed ? '50px' : 'none'
      fence.style.overflow = collapsed ? 'hidden' : 'auto'
      toggleBtn.textContent = collapsed ? '▶' : '▼'
    }
    
    fence.prepend(copyBtn, toggleBtn)
  })
}

5.5 外部自动化控制:JSON-RPC API

JSON-RPC 插件通过 JSON-RPC 2.0 协议向外部进程暴露 Typora 的功能。这使得开发者可以将 Typora 作为外部自动化工作流的一部分,通过外部 Python 脚本或 VSCode 宏来驱动 Typora 进行批量编辑、导出或状态获取。

5.5.1 启用 JSON-RPC

在 settings.user.toml 中配置:

toml

[json_rpc]
enable = true
port = 8899           # JSON-RPC 服务端口
host = "127.0.0.1"    # 仅允许本地访问,保证安全
5.5.2 外部调用示例

Python 客户端

python

import json
import requests

def call_typora(method, params=None):
    payload = {
        "jsonrpc": "2.0",
        "method": method,
        "params": params or [],
        "id": 1
    }
    response = requests.post("http://127.0.0.1:8899", json=payload)
    return response.json()

# 打开文件
call_typora("openFile", ["/path/to/document.md"])

# 插入文本
call_typora("insertText", ["## 这是通过外部脚本插入的内容"])

# 导出为 PDF
call_typora("exportPDF", ["/path/to/output.pdf"])

# 获取当前文档内容
content = call_typora("getCurrentContent")
print(content)

Node.js 客户端

javascript

const WebSocket = require('ws')
const ws = new WebSocket('ws://127.0.0.1:8899')

ws.on('open', () => {
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    method: 'executeCommand',
    params: ['command_palette.show'],
    id: 1
  }))
})

5.6 图表与可视化集成

Typora 插件系统集成多个可视化工具,包括 ECharts、Chart.js、DrawIO、PlantUML、Mermaid 等。

5.6.1 ECharts 插件配置

toml

# settings.user.toml
[echarts]
enable = true
theme = "dark"  # 或 "light"

# 自定义图表模板
[[echarts.templates]]
name = "折线图"
code = """
{
  xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] },
  yAxis: { type: 'value' },
  series: [{ data: [120, 200, 150, 80, 70, 110, 130], type: 'line' }]
}
"""
5.6.2 PlantUML 图表

通过 docker 部署 PlantUML 渲染器,实现图表在 Markdown 中的内联渲染。

text

```plantuml
@startuml
用户 -> Typora: 编写文档
Typora -> 插件系统: 调用图表渲染
插件系统 -> PlantUML: 请求渲染
PlantUML --> 插件系统: 返回图片
插件系统 --> Typora: 显示图表
@enduml

text

### 5.7 性能优化:truncate_text 插件

对于包含大量内容的 Markdown 文档,Typora 原生渲染会出现严重的重排(Reflow)卡顿。`truncate_text` 插件通过 DOM 层的 `display: none` 隐藏不可见内容,避免修改源文件,渲染性能提升显著且保持源码纯净[reference:55]。

**核心原理**:

```javascript
class truncateText {
  // 监听滚动事件,动态渲染可见区域
  onScroll = () => {
    const viewportTop = window.scrollY
    const viewportBottom = viewportTop + window.innerHeight
    
    document.querySelectorAll('.paragraph, .md-heading').forEach(el => {
      const rect = el.getBoundingClientRect()
      const elTop = rect.top + window.scrollY
      const elBottom = elTop + rect.height
      
      // 距离视口超过 3 屏的内容隐藏
      const shouldHide = elBottom < viewportTop - window.innerHeight * 2 ||
                         elTop > viewportBottom + window.innerHeight * 2
      
      el.style.display = shouldHide ? 'none' : ''
    })
  }
}

5.8 章节折叠系统

collapse_paragraph 插件允许通过 Ctrl+点击标题来折叠/展开章节。

javascript

// 实现章节折叠的核心逻辑
class collapseParagraph {
  init = () => {
    document.addEventListener('click', (e) => {
      const target = e.target
      const heading = target.closest('h1, h2, h3, h4, h5, h6')
      
      if (heading && (e.ctrlKey || e.metaKey)) {
        e.preventDefault()
        this.toggleCollapse(heading)
      }
    })
  }
  
  toggleCollapse = (heading) => {
    let next = heading.nextElementSibling
    let collapsed = heading.dataset.collapsed === 'true'
    
    while (next && !next.matches('h1, h2, h3, h4, h5, h6')) {
      next.style.display = collapsed ? '' : 'none'
      next = next.nextElementSibling
    }
    
    heading.dataset.collapsed = !collapsed
  }
}

第 6 章 主题定制与 CSS 注入

Typora 将所有样式都使用 CSS 实现,主题菜单下显示的每个主题都对应一个 .css 文件。通过自定义 CSS,可以从视觉上彻底改变编辑器的外观。

6.1 CSS 加载顺序

Typora 按以下顺序加载 CSS 文件:

  1. Typora 的基础样式

  2. 当前主题的 CSS 文件

  3. 主题文件夹内的 base.user.css 文件

  4. 主题文件夹内的 {current-theme}.user.css 文件

6.2 添加自定义 CSS

方法一:全局自定义(推荐)

在主题文件夹下创建 base.user.css,追加的自定义 CSS 将应用于所有主题:

css

/* base.user.css - 应用于所有主题 */

/* 自定义代码块样式 */
.md-fences {
  background-color: #282c34 !important;
  border-radius: 8px !important;
  padding: 16px !important;
  font-family: 'Fira Code', monospace !important;
  font-size: 13px !important;
}

/* 自定义标题样式 */
h1 {
  border-bottom: 3px solid #4CAF50 !important;
  padding-bottom: 8px !important;
}

h2 {
  border-left: 4px solid #2196F3 !important;
  padding-left: 12px !important;
}

/* 增强引用块 */
blockquote {
  background: #f5f7fa !important;
  border-left: 4px solid #8bc34a !important;
  padding: 12px 20px !important;
  border-radius: 0 8px 8px 0 !important;
}

/* 表格样式优化 */
table {
  border-collapse: collapse !important;
  width: 100% !important;
  margin: 16px 0 !important;
}

th {
  background: #e8f4f8 !important;
  padding: 10px !important;
}

td {
  padding: 8px !important;
  border: 1px solid #ddd !important;
}

方法二:特定主题定制

针对特定主题(如 “GitHub”),创建 github.user.css

css

/* 仅对 GitHub 主题生效 */
:root {
  --bg-color: #f6f8fa;
  --text-color: #24292e;
}

/* 暗色模式切换 */
@media (prefers-color-scheme: dark) {
  :root {
    --bg-color: #0d1117;
    --text-color: #c9d1d9;
  }
  
  body {
    background-color: var(--bg-color);
    color: var(--text-color);
  }
}

6.3 CSS 变量覆盖

如果需要定义字体、颜色或背景,建议覆盖现有的 CSS 变量:

css

:root {
  /* 字体设置 */
  --font-family: 'Inter', 'SF Pro Text', 'Segoe UI', sans-serif;
  --monospace-font: 'JetBrains Mono', 'Fira Code', monospace;
  
  /* 颜色方案 */
  --primary-color: #10b981;
  --secondary-color: #3b82f6;
  --accent-color: #f59e0b;
  
  /* 编辑器宽度 */
  --editor-max-width: 900px;
}

/* 限制编辑器最大宽度,提升大屏阅读体验 */
#write {
  max-width: var(--editor-max-width);
  margin: 0 auto;
}

6.4 CSS 调试方法

在 macOS 上:帮助 → 启用调试模式,在 Typora 中任意位置右键单击,选择“Inspect Elements”即可打开开发者工具。

在 Windows/Linux 上:视图 → 切换开发者工具菜单项打开 DevTools。

第 7 章 高级开发技巧

7.1 事件中心机制

成熟的插件系统需要完善的事件分发机制。在 Typora 插件实现中,通常会构建一个事件中心(Event Hub)来管理各种生命周期事件:

javascript

class EventHub {
  constructor() {
    this.events = {}
  }
  
  on(event, callback) {
    if (!this.events[event]) this.events[event] = []
    this.events[event].push(callback)
  }
  
  emit(event, ...args) {
    (this.events[event] || []).forEach(cb => cb(...args))
  }
  
  off(event, callback) {
    if (!this.events[event]) return
    this.events[event] = this.events[event].filter(cb => cb !== callback)
  }
}

// 使用示例
const editorEvents = new EventHub()
editorEvents.on('document:save', (filename) => {
  console.log(`文档已保存: ${filename}`)
})

7.2 版本兼容性处理

不同 Typora 版本内部实现可能有差异,解决方案包括:

  • 实现版本检测机制,针对不同版本适配

  • 提供 API 兼容层,封装版本差异

  • 关键函数的多版本适配

javascript

function getCompatibleAPI() {
  const version = navigator.userAgent.match(/Typora\/([\d.]+)/)?.[1] || '1.0.0'
  
  if (version.startsWith('0.')) {
    // 旧版本 API
    return { getEditor: () => window.typora.oldEditor }
  } else {
    // 新版本 API
    return { getEditor: () => window.typora.editor }
  }
}

7.3 性能优化策略

插件系统可能影响编辑器性能,需要:

  • 延迟非关键操作:使用 setTimeout 或 requestIdleCallback

  • 批量 DOM 操作:减少重排和重绘次数

  • 避免频繁的事件监听:使用节流(throttle)和防抖(debounce)

javascript

// 防抖函数
function debounce(fn, delay) {
  let timer = null
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// 节流函数
function throttle(fn, interval) {
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= interval) {
      lastTime = now
      fn.apply(this, args)
    }
  }
}

// 使用示例
const handleScroll = throttle(() => {
  console.log('滚动位置:', window.scrollY)
}, 100)

window.addEventListener('scroll', handleScroll)

7.4 跨平台兼容性

Typora 插件主要支持 Windows 和 Linux 系统,Mac 兼容性正在测试中。开发插件时需要注意:

  1. 路径处理:使用 Node.js 的 path 模块处理跨平台路径

  2. 快捷键差异:Mac 上 Ctrl 对应 Command,需统一处理

  3. 文件系统:不同平台的文件权限和路径分隔符差异

javascript

const path = reqnode('path')
const fs = reqnode('fs')

function getConfigPath() {
  const platform = process.platform
  if (platform === 'win32') {
    return path.join(process.env.APPDATA, 'Typora', 'config.json')
  } else if (platform === 'darwin') {
    return path.join(process.env.HOME, 'Library/Application Support/abnerworks.Typora', 'config.json')
  } else {
    return path.join(process.env.HOME, '.config/Typora', 'config.json')
  }
}

7.5 调试技巧

  1. 使用 console.log 输出调试信息:在插件代码中添加 console.log() 语句,查看 DevTools 控制台

  2. 利用 Typora 开发者工具检查元素:直接右键 Inspect 查看 DOM 结构和样式

  3. 分阶段测试插件功能:逐个功能模块测试,便于定位问题

  4. 版本检测:在插件开头检查 Typora 版本

javascript

class MyPlugin extends BaseCustomPlugin {
  checkVersion = () => {
    const required = '1.0.0'
    const current = this.utils.getTyporaVersion()
    if (this.utils.compareVersion(current, required) < 0) {
      console.warn(`插件需要 Typora ${required} 或更高版本,当前版本 ${current}`)
      return this.utils.PLUGIN_LOAD_ABORT
    }
  }
}

7.6 安全实践

  1. 限制文件系统访问:仅允许访问指定目录

  2. 对用户输入进行消毒(Sanitize):防止 XSS 攻击

  3. 使用 HTTPS 请求:避免中间人攻击

第 8 章 插件生态与社区资源

8.1 内置插件功能一览

Typora 插件系统提供了丰富的内置插件,涵盖界面增强、内容处理、可视化渲染等多个领域:

分类 插件 功能
界面增强 window_tab, toolbar, right_click_menu, pie_menu 标签页管理、工具栏、右键菜单、圆盘菜单
内容处理 text_stylize, md_padding, slash_commands, templater 文本样式化、中英文自动加空格、斜杠命令、模板系统
可视化渲染 markmap, echarts, plantUML, abc, calendar 思维导图、ECharts图表、PlantUML图表、ABC乐谱、日历
效率工具 command_palette, hotkeys, markdownLint VSCode风格命令面板、全局快捷键、格式规范检测
文件管理 file_counter, extractRangeToNewFile, fullPathCopy 文件计数、选区提取到新文件、复制标题路径
文档增强 collapse_paragraph, collapse_list, collapse_table, auto_number 章节/列表/表格折叠、自动编号
导出工具 export_enhance, asset_root_redirect 导出HTML图片保留、资源路径重定向
特殊功能 cipher, json_rpc, bingSpeech, truncate_text 文件加密、外部控制、朗读、大文件性能优化

8.2 开源生态

8.3 参与贡献

对开源项目的扩展应尽可能通过贡献代码的方式实现,这种方式的可持续性更强。若项目有商业应用需求,建议考虑官方授权的定制方案。

贡献方式:

  1. 在 GitHub 上 fork 项目仓库

  2. 创建功能分支开发新插件

  3. 提交 Pull Request,附上详细的功能说明和使用文档

  4. 参与 Issue 讨论,帮助其他用户解决问题

第 9 章 常见问题与解决方案

Q1:安装插件后右键菜单没有出现“常用插件”?

  • 检查 window.html 中是否正确添加了 <script src=“./plugin/index.js” defer=“defer”></script>

  • 检查插件文件夹是否放置在正确路径(推荐使用 everything 搜索 window.html 的位置)

  • 重启 Typora 使更改生效

Q2:部分插件功能无法使用?

  • 检查 settings.user.toml 中该插件的 enable 是否设置为 true

  • 检查 Typora 版本是否满足插件要求

  • 查看 DevTools 控制台的错误信息

Q3:插件影响了 Typora 的稳定性?

  • 尝试禁用部分插件,通过二分法定位问题插件

  • 更新插件到最新版本

  • 检查是否有插件冲突,部分功能重复的插件可能产生冲突

Q4:Mac 系统支持情况如何?

目前主要支持 Windows 和 Linux 系统,Mac 兼容性正在测试中。部分功能可能在 Mac 上不工作,建议在 macOS 上使用前先查看项目的 Issue 反馈。

Q5:如何自定义右键菜单?

通过 right_click_menu 插件,你可以完全自定义右键菜单的布局和功能。配置文件位于 plugin/global/settings/right_click_menu.user.toml

写在最后:从编辑器到工作台

通过本指南的学习,你应该已经掌握了:

  • Typora 插件的实现原理(前端注入 + 后端劫持)

  • 完整的开发环境搭建和安装配置

  • 核心插件架构(IPlugin、BasePlugin、BaseCustomPlugin 三大基类)

  • 七阶段生命周期模型

  • 从零创建自定义插件的完整流程

  • IDE 式写作环境的实战组件(快捷键、命令面板、代码增强、外部自动化控制)

  • 主题定制与 CSS 注入

  • 高级技巧与调试方法

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐