Typora插件开发指南:打造专属IDE式写作环境 - 底层原理|逆向分析|Hook内部API等硬核技术



Typora插件开发指南:打造专属IDE式写作环境

  • 底层原理、逆向分析、从零构建插件框架、Hook内部API等硬核技术
  • 手把手教你用JS/HTML扩展Typora,构建开发者定制化Markdown编辑器
  • 目录
  • 一、摘要
    • [1.1 文章概览](#1.1 文章概览)
    • [1.2 目标读者](#1.2 目标读者)
    • [1.3 核心价值](#1.3 核心价值)
  • 二、Typora插件系统概述
    • [2.1 Typora插件架构解析](#2.1 Typora插件架构解析)
    • [2.2 插件系统核心概念](#2.2 插件系统核心概念)
      • [2.2.1 插件基类体系](#2.2.1 插件基类体系)
      • [2.2.2 IPlugin接口](#2.2.2 IPlugin接口)
      • [2.2.3 BaseCustomPlugin选择器模式](#2.2.3 BaseCustomPlugin选择器模式)
    • [2.3 插件生命周期管理](#2.3 插件生命周期管理)
    • [2.4 插件安全机制](#2.4 插件安全机制)
  • 三、开发环境搭建
    • [3.1 Typora安装与配置](#3.1 Typora安装与配置)
      • [3.1.1 版本要求](#3.1.1 版本要求)
      • [3.1.2 获取插件框架源码](#3.1.2 获取插件框架源码)
      • [3.1.3 安装插件框架](#3.1.3 安装插件框架)
    • [3.2 插件开发目录结构](#3.2 插件开发目录结构)
      • [3.2.1 标准目录结构](#3.2.1 标准目录结构)
      • [3.2.2 自定义插件目录](#3.2.2 自定义插件目录)
    • [3.3 开发工具链配置](#3.3 开发工具链配置)
      • [3.3.1 基础工具](#3.3.1 基础工具)
      • [3.3.2 推荐VS Code扩展](#3.3.2 推荐VS Code扩展)
    • [3.4 调试环境设置](#3.4 调试环境设置)
      • [3.4.1 开启调试模式](#3.4.1 开启调试模式)
      • [3.4.2 打开开发者工具](#3.4.2 打开开发者工具)
      • [3.4.3 控制台调试](#3.4.3 控制台调试)
  • 四、插件核心API详解
    • [4.1 IPlugin基类深度剖析](#4.1 IPlugin基类深度剖析)
      • [4.1.1 构造函数](#4.1.1 构造函数)
      • [4.1.2 生命周期方法详解](#4.1.2 生命周期方法详解)
    • [4.2 BasePlugin扩展类实战](#4.2 BasePlugin扩展类实战)
    • [4.3 BaseCustomPlugin选择器模式](#4.3 BaseCustomPlugin选择器模式)
      • [4.3.1 高级选择器模式](#4.3.1 高级选择器模式)
    • [4.4 Utils框架与共享服务](#4.4 Utils框架与共享服务)
      • [4.4.1 系统信息](#4.4.1 系统信息)
      • [4.4.2 插件管理](#4.4.2 插件管理)
      • [4.4.3 文件系统操作](#4.4.3 文件系统操作)
      • [4.4.4 DOM操作](#4.4.4 DOM操作)
    • [4.5 事件系统与消息传递](#4.5 事件系统与消息传递)
      • [4.5.1 事件中心](#4.5.1 事件中心)
      • [4.5.2 核心事件](#4.5.2 核心事件)
      • [4.5.3 动态动作系统](#4.5.3 动态动作系统)
    • [4.6 DOM操作与UI扩展](#4.6 DOM操作与UI扩展)
      • [4.6.1 注入自定义UI](#4.6.1 注入自定义UI)
      • [4.6.2 操作按钮插件](#4.6.2 操作按钮插件)
  • 五、基础插件开发实战
    • [5.1 Hello World插件](#5.1 Hello World插件)
      • [5.1.1 创建插件文件](#5.1.1 创建插件文件)
      • [5.1.2 配置插件](#5.1.2 配置插件)
      • [5.1.3 测试插件](#5.1.3 测试插件)
    • [5.2 菜单扩展插件](#5.2 菜单扩展插件)
      • [5.2.1 添加自定义菜单项](#5.2.1 添加自定义菜单项)
    • [5.3 快捷键绑定插件](#5.3 快捷键绑定插件)
      • [5.3.1 使用hotkey方法注册快捷键](#5.3.1 使用hotkey方法注册快捷键)
      • [5.3.2 使用HotkeysPlugin配置快捷键](#5.3.2 使用HotkeysPlugin配置快捷键)
    • [5.4 状态栏扩展插件](#5.4 状态栏扩展插件)
      • [5.4.1 添加状态栏信息](#5.4.1 添加状态栏信息)
    • [5.5 侧边栏面板插件](#5.5 侧边栏面板插件)
      • [5.5.1 创建侧边栏面板](#5.5.1 创建侧边栏面板)
  • 六、高级功能插件开发
    • [6.1 文件操作与I/O插件](#6.1 文件操作与I/O插件)
      • [6.1.1 文件系统访问](#6.1.1 文件系统访问)
    • [6.2 代码片段管理插件](#6.2 代码片段管理插件)
      • [6.2.1 代码片段数据结构](#6.2.1 代码片段数据结构)
    • [6.3 实时预览增强插件](#6.3 实时预览增强插件)
    • [6.4 语法高亮扩展插件](#6.4 语法高亮扩展插件)
    • [6.5 AI辅助写作插件](#6.5 AI辅助写作插件)
      • [6.5.1 AI插件基础结构](#6.5.1 AI插件基础结构)
    • [6.6 JSON-RPC外部API集成](#6.6 JSON-RPC外部API集成)
      • [6.6.1 JSON-RPC API方法](#6.6.1 JSON-RPC API方法)
      • [6.6.2 外部调用示例](#6.6.2 外部调用示例)
      • [6.6.3 安全警告](#6.6.3 安全警告)
  • 七、UI/UX深度定制
    • [7.1 自定义主题与样式](#7.1 自定义主题与样式)
      • [7.1.1 通过custom.css定制](#7.1.1 通过custom.css定制)
      • [7.1.2 通过base.user.css定制](#7.1.2 通过base.user.css定制)
    • [7.2 动态组件开发](#7.2 动态组件开发)
      • [7.2.1 创建可交互组件](#7.2.1 创建可交互组件)
    • [7.3 拖拽交互实现](#7.3 拖拽交互实现)
    • [7.4 响应式布局设计](#7.4 响应式布局设计)
    • [7.5 动画效果集成](#7.5 动画效果集成)
  • 八、性能优化与调试
    • [8.1 插件性能监控](#8.1 插件性能监控)
    • [8.2 内存泄漏检测](#8.2 内存泄漏检测)
    • [8.3 调试技巧与工具](#8.3 调试技巧与工具)
      • [8.3.1 使用Chrome DevTools调试](#8.3.1 使用Chrome DevTools调试)
      • [8.3.2 日志系统](#8.3.2 日志系统)
    • [8.4 错误处理与日志](#8.4 错误处理与日志)
      • [8.4.1 全局错误捕获](#8.4.1 全局错误捕获)
      • [8.4.2 插件中使用错误处理](#8.4.2 插件中使用错误处理)
    • [8.5 性能优化策略](#8.5 性能优化策略)
      • [8.5.1 优化清单](#8.5.1 优化清单)
      • [8.5.2 防抖/节流工具](#8.5.2 防抖/节流工具)
  • 九、插件发布与分发
    • [9.1 插件打包规范](#9.1 插件打包规范)
      • [9.1.1 插件文件结构](#9.1.1 插件文件结构)
      • [9.1.2 package.json配置](#9.1.2 package.json配置)
    • [9.2 版本管理策略](#9.2 版本管理策略)
      • [9.2.1 语义化版本](#9.2.1 语义化版本)
      • [9.2.2 版本更新日志](#9.2.2 版本更新日志)
    • [9.3 插件市场发布](#9.3 插件市场发布)
      • [9.3.1 发布到GitHub](#9.3.1 发布到GitHub)
      • [9.3.2 发布到社区](#9.3.2 发布到社区)
    • [9.4 用户反馈收集](#9.4 用户反馈收集)
    • [9.5 持续集成部署](#9.5 持续集成部署)
      • [9.5.1 GitHub Actions配置](#9.5.1 GitHub Actions配置)
  • 十、安全与最佳实践
    • [10.1 安全编码规范](#10.1 安全编码规范)
      • [10.1.1 输入验证](#10.1.1 输入验证)
      • [10.1.2 安全配置](#10.1.2 安全配置)
    • [10.2 权限最小化原则](#10.2 权限最小化原则)
      • [10.2.1 权限声明](#10.2.1 权限声明)
    • [10.3 XSS防护策略](#10.3 XSS防护策略)
      • [10.3.1 内容安全策略](#10.3.1 内容安全策略)
      • [10.3.2 安全DOM操作](#10.3.2 安全DOM操作)
    • [10.4 数据隐私保护](#10.4 数据隐私保护)
      • [10.4.1 敏感数据处理](#10.4.1 敏感数据处理)
    • [10.5 代码审查要点](#10.5 代码审查要点)
      • [10.5.1 审查清单](#10.5.1 审查清单)
      • [10.5.2 代码质量检查](#10.5.2 代码质量检查)
  • 十一、真实案例分析
    • [11.1 代码片段管理器插件](#11.1 代码片段管理器插件)
      • [11.1.1 功能需求](#11.1.1 功能需求)
      • [11.1.2 技术实现](#11.1.2 技术实现)
    • [11.2 实时协作编辑插件](#11.2 实时协作编辑插件)
      • [11.2.1 架构设计](#11.2.1 架构设计)
    • [11.3 智能目录生成插件](#11.3 智能目录生成插件)
      • [11.3.1 功能实现](#11.3.1 功能实现)
    • [11.4 图片优化处理插件](#11.4 图片优化处理插件)
      • [11.4.1 功能实现](#11.4.1 功能实现)
    • [11.5 多语言翻译插件](#11.5 多语言翻译插件)
      • [11.5.1 功能实现](#11.5.1 功能实现)
  • 十二、未来展望与生态
    • [12.1 Typora插件生态现状](#12.1 Typora插件生态现状)
    • [12.2 新版本API展望](#12.2 新版本API展望)
    • [12.3 跨平台兼容性](#12.3 跨平台兼容性)
    • [12.4 社区贡献指南](#12.4 社区贡献指南)
      • [12.4.1 如何贡献](#12.4.1 如何贡献)
      • [12.4.2 贡献规范](#12.4.2 贡献规范)
    • [12.5 企业级应用前景](#12.5 企业级应用前景)
  • 十三、总结
    • [13.1 核心要点回顾](#13.1 核心要点回顾)
    • [13.2 学习路径建议](#13.2 学习路径建议)
    • [13.3 开发者成长路线](#13.3 开发者成长路线)
  • 十四、附录
    • [14.1 API参考手册](#14.1 API参考手册)
      • [14.1.1 IPlugin生命周期方法](#14.1.1 IPlugin生命周期方法)
      • [14.1.2 Utils框架核心方法](#14.1.2 Utils框架核心方法)
      • [14.1.3 事件列表](#14.1.3 事件列表)
    • [14.2 快捷键大全](#14.2 快捷键大全)
      • [14.2.1 常用快捷键](#14.2.1 常用快捷键)
      • [14.2.2 自定义快捷键配置](#14.2.2 自定义快捷键配置)
    • [14.3 常见问题解答](#14.3 常见问题解答)
      • [Q1: 插件安装后不生效?](#Q1: 插件安装后不生效?)
      • [Q2: 如何调试插件?](#Q2: 如何调试插件?)
      • [Q3: 插件更新后不生效?](#Q3: 插件更新后不生效?)
      • [Q4: 如何获取当前文档内容?](#Q4: 如何获取当前文档内容?)
      • [Q5: 如何插入文本到编辑器?](#Q5: 如何插入文本到编辑器?)
    • [14.4 学习资源推荐](#14.4 学习资源推荐)
      • [14.4.1 官方资源](#14.4.1 官方资源)
      • [14.4.2 社区资源](#14.4.2 社区资源)
      • [14.4.3 推荐阅读](#14.4.3 推荐阅读)
    • [14.5 术语表](#14.5 术语表)
  • 十五、详细资料
    • [15.1 官方文档索引](#15.1 官方文档索引)
    • [15.2 第三方库推荐](#15.2 第三方库推荐)
    • [15.3 示例代码仓库](#15.3 示例代码仓库)
    • [15.4 社区论坛资源](#15.4 社区论坛资源)
    • [15.5 进阶学习路径](#15.5 进阶学习路径)
      • [15.5.1 前端技术栈](#15.5.1 前端技术栈)
      • [15.5.2 桌面应用开发](#15.5.2 桌面应用开发)
      • [15.5.3 插件工程化](#15.5.3 插件工程化)

底层原理、逆向分析、从零构建插件框架、Hook内部API等硬核技术

手把手教你用JS/HTML扩展Typora,构建开发者定制化Markdown编辑器


目录

一、摘要

  • 1.1 文章概览
  • 1.2 目标读者
  • 1.3 核心价值

二、Typora插件系统概述

  • 2.1 Typora插件架构解析
  • 2.2 插件系统核心概念
  • 2.3 插件生命周期管理
  • 2.4 插件安全机制

三、开发环境搭建

  • 3.1 Typora安装与配置
  • 3.2 插件开发目录结构
  • 3.3 开发工具链配置
  • 3.4 调试环境设置

四、插件核心API详解

  • 4.1 IPlugin基类深度剖析
  • 4.2 BasePlugin扩展类实战
  • 4.3 BaseCustomPlugin选择器模式
  • 4.4 Utils框架与共享服务
  • 4.5 事件系统与消息传递
  • 4.6 DOM操作与UI扩展

五、基础插件开发实战

  • 5.1 Hello World插件
  • 5.2 菜单扩展插件
  • 5.3 快捷键绑定插件
  • 5.4 状态栏扩展插件
  • 5.5 侧边栏面板插件

六、高级功能插件开发

  • 6.1 文件操作与I/O插件
  • 6.2 代码片段管理插件
  • 6.3 实时预览增强插件
  • 6.4 语法高亮扩展插件
  • 6.5 AI辅助写作插件
  • 6.6 JSON-RPC外部API集成

七、UI/UX深度定制

  • 7.1 自定义主题与样式
  • 7.2 动态组件开发
  • 7.3 拖拽交互实现
  • 7.4 响应式布局设计
  • 7.5 动画效果集成

八、性能优化与调试

  • 8.1 插件性能监控
  • 8.2 内存泄漏检测
  • 8.3 调试技巧与工具
  • 8.4 错误处理与日志
  • 8.5 性能优化策略

九、插件发布与分发

  • 9.1 插件打包规范
  • 9.2 版本管理策略
  • 9.3 插件市场发布
  • 9.4 用户反馈收集
  • 9.5 持续集成部署

十、安全与最佳实践

  • 10.1 安全编码规范
  • 10.2 权限最小化原则
  • 10.3 XSS防护策略
  • 10.4 数据隐私保护
  • 10.5 代码审查要点

十一、真实案例分析

  • 11.1 代码片段管理器插件
  • 11.2 实时协作编辑插件
  • 11.3 智能目录生成插件
  • 11.4 图片优化处理插件
  • 11.5 多语言翻译插件

十二、未来展望与生态

  • 12.1 Typora插件生态现状
  • 12.2 新版本API展望
  • 12.3 跨平台兼容性
  • 12.4 社区贡献指南
  • 12.5 企业级应用前景

十三、总结

  • 13.1 核心要点回顾
  • 13.2 学习路径建议
  • 13.3 开发者成长路线

十四、附录

  • 14.1 API参考手册
  • 14.2 快捷键大全
  • 14.3 常见问题解答
  • 14.4 学习资源推荐
  • 14.5 术语表

十五、详细资料

  • 15.1 官方文档索引
  • 15.2 第三方库推荐
  • 15.3 示例代码仓库
  • 15.4 社区论坛资源
  • 15.5 进阶学习路径

一、摘要

1.1 文章概览

本文是一份全面深入的Typora插件开发指南,旨在帮助开发者从零开始掌握Typora插件开发的完整技术栈。通过系统化的知识结构和丰富的实战案例,读者将学会如何利用JavaScript、HTML和CSS技术栈,为Typora构建功能强大、用户体验优秀的定制化插件。文章涵盖从基础环境搭建到高级功能实现的全过程,包含超过100个代码示例和50个最佳实践建议,总字数约40000字。

Typora是一款以"所见即所得"著称的Markdown编辑器,其简洁优雅的设计赢得了大量开发者和写作者的青睐。Typora原生支持100多种编程语言的语法高亮、LaTeX数学公式渲染、Mermaid流程图绘制、Emoji表情、目录生成、脚注、表格等丰富的Markdown扩展功能。然而,Typora官方并未提供正式的插件API,当前社区插件方案主要围绕一个核心开源项目obgnail/typora_plugin展开。该项目为Typora提供了超过60个内置插件,涵盖标签页管理、多关键词搜索、代码增强、图表支持等功能。

1.2 目标读者

  • 前端开发者:希望扩展Typora功能,提升写作体验的技术人员
  • 技术写作者:需要定制化写作环境,提高内容生产效率的专业人士
  • 开源贡献者:有意参与Typora插件生态建设,为社区贡献价值的开发者
  • 工具链爱好者:喜欢深度定制开发工具,追求极致工作效率的技术极客

1.3 核心价值

  • 技术深度:深入剖析Typora插件系统架构,掌握底层实现原理
  • 实践导向:提供可直接运行的代码示例,降低学习门槛
  • 系统完整:覆盖插件开发全生命周期,从开发到发布完整流程
  • 前沿技术:集成AI、实时协作等现代技术,打造下一代写作体验
  • 工程化思维:强调代码质量、性能优化和安全防护,培养专业开发习惯

二、Typora插件系统概述

2.1 Typora插件架构解析

Typora基于Electron框架构建。Electron是一个使用JavaScript、HTML和CSS构建跨平台桌面应用程序的开源框架,它结合了Chromium渲染引擎和Node.js运行时。Electron采用双进程架构:主进程负责创建窗口和管理应用程序生命周期,每个应用程序只有一个主进程;渲染进程负责渲染网页界面,每个窗口运行独立的渲染进程,本质上是一个Chromium浏览器实例。

这套架构为插件开发提供了天然支撑。渲染进程暴露了完整的DOM操作能力,开发者可以通过开发者工具访问和修改页面环境,注入自定义代码。Node.js集成使得插件可以访问文件系统、网络、进程等系统级资源。

Typora插件系统采用三层架构设计

复制代码
┌─────────────────────────────────────────────────────┐
│                  功能插件层                          │
│   60+ 内置插件 + 用户自定义插件                      │
├─────────────────────────────────────────────────────┤
│                  插件基类层                          │
│   IPlugin | BasePlugin | BaseCustomPlugin           │
├─────────────────────────────────────────────────────┤
│                  核心基础设施层                      │
│   配置加载 | 插件加载 | Utils | 国际化 | 事件中心    │
├─────────────────────────────────────────────────────┤
│                  Typora原生层                        │
│   File | ClientCommand | JSBridge | File.editor    │
└─────────────────────────────────────────────────────┘

核心基础设施层 :位于plugin/global/core/,提供插件运行时的核心基础设施,包括事件中心、国际化支持、样式模板引擎等基础服务。

插件基类层 :定义了IPluginBasePluginBaseCustomPlugin三大基类。

功能插件层:包括60+内置插件和用户自定义插件。

2.2 插件系统核心概念

2.2.1 插件基类体系

Typora插件系统提供了两个主要的基类,分别适用于不同的扩展需求:

基类 适用场景 关键方法 集成方式
BasePlugin 复杂核心插件,需要完整的系统访问权限 call(action, meta) 手动处理事件和UI
BaseCustomPlugin 用户定义的上下文感知扩展 selector(), hint(), callback() 自动集成到"自定义插件"菜单

2.2.2 IPlugin接口

所有插件都继承自IPlugin,它定义了标准的生命周期钩子:

javascript 复制代码
class IPlugin {
    constructor(fixedName, setting, i18n) {
        this.fixedName = fixedName;      // 插件唯一标识符
        this.pluginName = setting.NAME;   // 插件显示名称
        this.config = setting;            // 插件配置
        this.utils = utils;               // 工具框架
        this.i18n = i18n;                // 国际化
    }
    
    async beforeProcess() {}  // 预处理阶段
    style() {}               // 样式注入
    styleTemplate() {}       // 样式模板
    html() {}                // HTML元素注入
    hotkey() {}              // 快捷键注册
    init() {}                // 初始化阶段
    process() {}             // 主处理逻辑
    afterProcess() {}        // 清理阶段
}

2.2.3 BaseCustomPlugin选择器模式

BaseCustomPlugin实现了一个selector/hint/callback模式,专门为上下文敏感的操作设计:

  • selector:返回CSS选择器字符串,只有当光标位于匹配该选择器的元素内部时,插件的菜单项才会显示
  • hint:返回用作菜单提示的字符串
  • callback :当菜单项被点击或快捷键被按下时执行的函数,接收anchorNode(匹配选择器的DOM元素)作为参数

2.3 插件生命周期管理

插件系统遵循一个七阶段生命周期模型

复制代码
加载 → beforeProcess → style → styleTemplate → html → hotkey → init → process → afterProcess
阶段 方法 用途 是否必须
1 beforeProcess() 预初始化检查,异步数据加载
2 style() 返回CSS字符串用于内联注入
3 styleTemplate() 返回模板参数用于动态CSS
4 html() 返回HTML字符串用于DOM注入
5 hotkey() 注册键盘快捷键
6 init() 初始化插件状态和实体
7 process() 绑定事件监听器,执行主要逻辑

LoadPlugins函数(定义在plugin/global/core/plugin.js中)作为引擎,管理每个插件的初始化序列。

2.4 插件安全机制

插件系统通过以下机制保障安全性:

  1. 沙箱隔离:插件运行在独立的渲染进程中,与主进程隔离
  2. 权限控制:通过配置系统控制插件可访问的API范围
  3. 配置校验settings.user.toml配置文件经过schema验证
  4. 动态加载:插件在启动时动态加载,便于管理和禁用

三、开发环境搭建

3.1 Typora安装与配置

3.1.1 版本要求

  • Typora ≥ 0.9.98(推荐使用最新稳定版)
  • 支持插件系统的Typora版本(≥ 1.0)

3.1.2 获取插件框架源码

社区插件方案主要围绕obgnail/typora_plugin项目展开:

bash 复制代码
# 克隆项目仓库
git clone https://github.com/obgnail/typora_plugin.git

# 或使用GitCode镜像(访问更稳定)
git clone https://gitcode.com/obgnail/typora_plugin.git

3.1.3 安装插件框架

将克隆的插件框架部署到Typora的插件目录:

WindowsC:\Program Files\Typora\resources\app\plugin\

macOS/Applications/Typora.app/Contents/Resources/app/plugin/

Linux/opt/typora/resources/app/plugin/

3.2 插件开发目录结构

3.2.1 标准目录结构

复制代码
typora_plugin/
├── plugin/
│   ├── global/
│   │   ├── core/              # 核心基础设施
│   │   │   ├── plugin.js      # 插件加载器
│   │   │   ├── index.js       # 入口点
│   │   │   └── utils/         # 工具函数
│   │   └── settings/          # 配置系统
│   ├── custom/                # 自定义插件
│   │   ├── plugins/           # 用户插件存放目录 ⭐
│   │   └── README.md
│   ├── hotkeys.js             # 快捷键插件
│   ├── action_buttons.js      # 操作按钮插件
│   └── json_rpc/              # JSON-RPC插件
├── config/                    # 配置文件
└── code/                      # 代码片段

3.2.2 自定义插件目录

用户自定义插件必须放在./plugin/custom/plugins/目录下:

复制代码
plugin/custom/plugins/
├── myFirstPlugin.js
├── codeSnippetManager.js
├── aiAssistant.js
└── ...

3.3 开发工具链配置

3.3.1 基础工具

工具 用途 说明
Typora 目标编辑器 建议版本 ≥ 0.9.98
Node.js JavaScript运行时 插件开发的基础环境
npm / yarn 包管理工具 用于依赖管理
VS Code 代码编辑器 推荐用于插件开发
Git 版本控制 获取源码和管理版本

3.3.2 推荐VS Code扩展

  • ESLint:JavaScript代码检查
  • Prettier:代码格式化
  • GitLens:Git增强
  • Debugger for Chrome:调试支持

3.4 调试环境设置

3.4.1 开启调试模式

  1. 打开Typora
  2. 进入 文件 → 偏好设置
  3. 通用 中找到 高级设置
  4. 勾选 "开启调试模式"

3.4.2 打开开发者工具

  • Windows/LinuxCtrl + Shift + I
  • macOSCmd + Option + I
  • 或者右键 → 检查元素

3.4.3 控制台调试

在开发者工具中选择 Console(控制台) 标签,可以:

  • 查看插件日志输出
  • 执行JavaScript代码测试
  • 检查DOM元素状态
  • 监控网络请求

四、插件核心API详解

4.1 IPlugin基类深度剖析

4.1.1 构造函数

IPlugin构造函数初始化五个实例属性:

javascript 复制代码
class IPlugin {
    constructor(fixedName, setting, i18n) {
        this.fixedName = fixedName;      // 插件唯一标识符
        this.pluginName = setting.NAME;   // 显示名称(来自配置或i18n)
        this.config = setting;            // 插件配置对象
        this.utils = utils;               // Utils框架实例
        this.i18n = i18n;                // 国际化实例
    }
}

4.1.2 生命周期方法详解

javascript 复制代码
class MyPlugin extends IPlugin {
    // 1. 预处理 - 用于异步数据加载和前置检查
    async beforeProcess() {
        // 可以返回 stopLoadPluginError 来阻止插件加载
        const data = await this.loadData();
        if (!data) {
            return this.utils.stopLoadPluginError;
        }
        this.data = data;
    }
    
    // 2. 样式注入 - 返回CSS字符串
    style() {
        return `
            .my-plugin-container {
                border: 1px solid #ccc;
                padding: 10px;
            }
        `;
    }
    
    // 3. 样式模板 - 支持动态CSS
    styleTemplate() {
        return {
            background: this.config.BACKGROUND_COLOR || '#ffffff'
        };
    }
    
    // 4. HTML注入 - 返回HTML字符串
    html() {
        return `<div id="my-plugin-panel">Hello World</div>`;
    }
    
    // 5. 快捷键注册
    hotkey() {
        return {
            'Ctrl+Shift+M': this.handleHotkey.bind(this)
        };
    }
    
    // 6. 初始化
    init() {
        this.state = {};
        this.registerEventListeners();
    }
    
    // 7. 主处理逻辑
    process() {
        // 绑定事件、启动服务等
        this.utils.eventHub.on('typora:ready', this.onReady.bind(this));
    }
    
    // 8. 后处理(清理)
    afterProcess() {
        // 清理资源
    }
}

4.2 BasePlugin扩展类实战

BasePlugin适用于需要完整系统访问权限的复杂插件:

javascript 复制代码
// plugin/custom/plugins/myBasePlugin.js
const { BasePlugin } = require('../global/core/plugin');

class MyBasePlugin extends BasePlugin {
    constructor(fixedName, setting, i18n) {
        super(fixedName, setting, i18n);
        this.actions = {
            'do-something': this.doSomething.bind(this),
            'get-data': this.getData.bind(this)
        };
    }
    
    // BasePlugin使用call(action, meta)模式
    call(action, meta) {
        if (this.actions[action]) {
            return this.actions[action](meta);
        }
        throw new Error(`Unknown action: ${action}`);
    }
    
    doSomething(meta) {
        console.log('Doing something with:', meta);
        // 执行复杂逻辑
        return { success: true };
    }
    
    getData(meta) {
        return this.utils.getFilePath();
    }
    
    // 完整生命周期支持
    process() {
        // 注册到插件系统
        this.utils.eventHub.on('file:opened', this.onFileOpened.bind(this));
    }
}

module.exports = { plugin: MyBasePlugin };

4.3 BaseCustomPlugin选择器模式

BaseCustomPlugin实现了selector/hint/callback模式,专为上下文敏感操作设计:

javascript 复制代码
// plugin/custom/plugins/myCustomPlugin.js
const { BaseCustomPlugin } = require('../global/core/plugin');

class MyCustomPlugin extends BaseCustomPlugin {
    // 必需:选择器 - 决定插件何时可用
    selector = (context) => {
        // 返回CSS选择器,当光标在匹配元素内时显示菜单项
        return 'pre code, .code-block';
    }
    
    // 必需:主逻辑
    callback = (anchorNode) => {
        // anchorNode是匹配selector的DOM元素
        const code = anchorNode.textContent;
        // 执行操作,如格式化、翻译等
        this.formatCode(code);
    }
    
    // 可选:提示文本
    hint = (context) => {
        return '格式化代码';
    }
    
    formatCode(code) {
        // 代码格式化逻辑
        console.log('Formatting:', code);
    }
}

module.exports = { plugin: MyCustomPlugin };

4.3.1 高级选择器模式

javascript 复制代码
class AdvancedCustomPlugin extends BaseCustomPlugin {
    // 动态选择器 - 根据上下文返回不同选择器
    selector = (context) => {
        const { editor } = context;
        const selection = editor.getSelection();
        if (selection && selection.length > 0) {
            return '.selected-text';
        }
        return 'p, h1, h2, h3, h4, h5, h6';
    }
    
    // 动态提示
    hint = (context) => {
        const { anchorNode } = context;
        if (anchorNode && anchorNode.tagName === 'PRE') {
            return '处理代码块';
        }
        return '处理文本';
    }
    
    callback = (anchorNode) => {
        // 根据节点类型执行不同操作
        if (anchorNode.tagName === 'PRE') {
            this.handleCodeBlock(anchorNode);
        } else {
            this.handleText(anchorNode);
        }
    }
}

4.4 Utils框架与共享服务

Utils框架是一个综合工具类,为所有插件提供共享基础设施:

4.4.1 系统信息

javascript 复制代码
// 获取运行时环境信息
const utils = this.utils;

console.log(utils.nodeVersion);      // Node.js版本
console.log(utils.electronVersion);  // Electron版本
console.log(utils.chromeVersion);    // Chrome版本
console.log(utils.typoraVersion);    // Typora版本
console.log(utils.isBetaVersion);    // 是否Beta版本
console.log(utils.separator);        // 路径分隔符
console.log(utils.tempFolder);       // 临时目录

4.4.2 插件管理

javascript 复制代码
// 获取插件实例
const basePlugin = utils.getBasePlugin('pluginName');
const customPlugin = utils.getCustomPlugin('pluginName');
const allBasePlugins = utils.getAllBasePlugins();
const allCustomPlugins = utils.getAllCustomPlugins();

// 调用其他插件的方法
const result = utils.callPluginFunction('pluginName', 'methodName', arg1, arg2);

4.4.3 文件系统操作

javascript 复制代码
// 文件操作
const { Path, Fs, FsExtra } = utils.Package;

// 获取当前文件路径
const currentFile = utils.getFilePath();

// 获取用户主目录
const homeDir = utils.getHomeDir();

// 读取文件
const content = FsExtra.readFileSync('/path/to/file', 'utf-8');

4.4.4 DOM操作

javascript 复制代码
// DOM工具
const $ = utils.$;  // jQuery风格选择器

// 获取锚点节点
const anchorNode = utils.getAnchorNode('pre code');

// 注入样式
utils.registerStyle('myPlugin', `
    .my-class { color: red; }
`);

4.5 事件系统与消息传递

4.5.1 事件中心

插件系统通过事件中心管理各种生命周期事件:

javascript 复制代码
// 监听事件
this.utils.eventHub.on('event:name', (data) => {
    console.log('Event triggered:', data);
});

// 触发事件
this.utils.eventHub.emit('event:name', { key: 'value' });

// 一次性监听
this.utils.eventHub.once('plugin:loaded', () => {
    console.log('Plugin loaded once');
});

// 移除监听
this.utils.eventHub.off('event:name', handler);

4.5.2 核心事件

事件名称 触发时机 数据
typora:ready Typora完全加载 -
file:opened 文件打开 文件路径
file:saved 文件保存 文件路径
file:closed 文件关闭 文件路径
selection:changed 选区变化 选区信息
content:changed 内容变化 变更数据
plugin:loaded 插件加载完成 插件名称
allPluginsHadInjected 所有插件注入完成 -

4.5.3 动态动作系统

动态动作支持上下文感知的菜单项和命令:

javascript 复制代码
// 更新动态动作
this.utils.updatePluginDynamicActions('pluginName', anchorNode, false);

// 执行动态动作
this.utils.callPluginDynamicAction('pluginName', 'actionName');

// 组合更新和执行
this.utils.updateAndCallPluginDynamicAction(
    'pluginName', 
    'actionName', 
    anchorNode, 
    false
);

4.6 DOM操作与UI扩展

4.6.1 注入自定义UI

javascript 复制代码
class UIPlugin extends BaseCustomPlugin {
    html() {
        return `
            <div id="my-panel" style="position: fixed; right: 20px; top: 100px; 
                 background: white; border: 1px solid #ccc; padding: 15px; 
                 border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
                <h4>我的面板</h4>
                <button id="my-action-btn">执行操作</button>
            </div>
        `;
    }
    
    process() {
        // 注入完成后绑定事件
        const panel = document.getElementById('my-panel');
        const btn = document.getElementById('my-action-btn');
        if (btn) {
            btn.addEventListener('click', this.handleClick.bind(this));
        }
    }
    
    handleClick() {
        const content = this.utils.getFilePath();
        console.log('Current file:', content);
    }
}

4.6.2 操作按钮插件

action_buttons插件可在编辑器界面添加可配置的UI按钮:

javascript 复制代码
// 在配置中定义按钮
const BUTTONS = [
    {
        name: '格式化',
        icon: '🔧',
        evil: 'utils.formatCode()',
        tooltip: '格式化当前代码'
    },
    {
        name: '导出',
        icon: '📤',
        plugin: 'exportPlugin',
        func: 'exportToPDF',
        tooltip: '导出为PDF'
    }
];

五、基础插件开发实战

5.1 Hello World插件

5.1.1 创建插件文件

plugin/custom/plugins/目录下创建helloWorld.js

javascript 复制代码
// plugin/custom/plugins/helloWorld.js
const { BaseCustomPlugin } = require('../global/core/plugin');

class HelloWorldPlugin extends BaseCustomPlugin {
    // 选择器 - 在任意位置可用
    selector = () => {
        return 'body';
    }
    
    // 提示文本
    hint = () => {
        return 'Hello World! 点击显示问候';
    }
    
    // 主逻辑
    callback = (anchorNode) => {
        // 使用Utils框架显示通知
        this.utils.showNotification('Hello World!', '欢迎使用Typora插件开发!');
        
        // 在控制台输出
        console.log('Hello World from Typora plugin!');
        console.log('Current node:', anchorNode);
        
        // 获取当前文件信息
        const filePath = this.utils.getFilePath();
        console.log('Current file:', filePath);
    }
}

module.exports = { plugin: HelloWorldPlugin };

5.1.2 配置插件

在配置文件中启用插件:

toml 复制代码
# plugin/global/settings/settings.user.toml
[custom_plugins]
helloWorld = true

5.1.3 测试插件

  1. 重启Typora
  2. 在编辑区域右键点击
  3. 在自定义插件菜单中找到"Hello World! 点击显示问候"
  4. 点击查看效果

5.2 菜单扩展插件

5.2.1 添加自定义菜单项

javascript 复制代码
// plugin/custom/plugins/customMenu.js
const { BaseCustomPlugin } = require('../global/core/plugin');

class CustomMenuPlugin extends BaseCustomPlugin {
    selector = () => 'body';
    
    hint = () => '显示自定义菜单';
    
    callback = (anchorNode) => {
        // 创建自定义上下文菜单
        this.showCustomMenu(anchorNode);
    }
    
    showCustomMenu(anchorNode) {
        const menuItems = [
            { label: '插入时间戳', action: this.insertTimestamp.bind(this) },
            { label: '转换为大写', action: this.convertToUpper.bind(this) },
            { label: '统计字数', action: this.countWords.bind(this) },
            { label: '---', separator: true },
            { label: '打开设置', action: this.openSettings.bind(this) }
        ];
        
        // 使用Utils框架显示菜单
        this.utils.showContextMenu(menuItems);
    }
    
    insertTimestamp() {
        const timestamp = new Date().toLocaleString();
        this.utils.insertText(timestamp);
    }
    
    convertToUpper() {
        const selection = this.utils.getSelection();
        if (selection) {
            this.utils.replaceSelection(selection.toUpperCase());
        }
    }
    
    countWords() {
        const content = this.utils.getEditorContent();
        const words = content.split(/\s+/).filter(w => w.length > 0).length;
        this.utils.showNotification(`总字数: ${words}`);
    }
    
    openSettings() {
        // 打开设置面板
        this.utils.openSettings();
    }
}

module.exports = { plugin: CustomMenuPlugin };

5.3 快捷键绑定插件

5.3.1 使用hotkey方法注册快捷键

javascript 复制代码
// plugin/custom/plugins/hotkeyPlugin.js
const { BasePlugin } = require('../global/core/plugin');

class HotkeyPlugin extends BasePlugin {
    // 注册快捷键
    hotkey() {
        return {
            'Ctrl+Shift+F': this.formatDocument.bind(this),
            'Ctrl+Shift+S': this.saveAndExport.bind(this),
            'Ctrl+Alt+T': this.insertTable.bind(this),
            'F12': this.openDevTools.bind(this)
        };
    }
    
    formatDocument() {
        // 格式化整个文档
        const content = this.utils.getEditorContent();
        const formatted = this.formatMarkdown(content);
        this.utils.setEditorContent(formatted);
        this.utils.showNotification('文档已格式化');
    }
    
    saveAndExport() {
        this.utils.saveFile();
        this.exportToHTML();
    }
    
    insertTable() {
        const table = `
| 列1 | 列2 | 列3 |
|-----|-----|-----|
| 数据1 | 数据2 | 数据3 |
| 数据4 | 数据5 | 数据6 |
`;
        this.utils.insertText(table);
    }
    
    openDevTools() {
        // 打开开发者工具
        require('electron').remote.getCurrentWindow().webContents.openDevTools();
    }
    
    formatMarkdown(content) {
        // 简单的Markdown格式化
        return content
            .replace(/\n{3,}/g, '\n\n')
            .replace(/[ \t]+$/gm, '');
    }
}

module.exports = { plugin: HotkeyPlugin };

5.3.2 使用HotkeysPlugin配置快捷键

通过hotkeys插件声明式绑定快捷键:

javascript 复制代码
// 在配置中定义自定义快捷键
const CUSTOM_HOTKEYS = [
    {
        name: '插入日期',
        keys: 'Ctrl+Shift+D',
        evil: 'utils.insertText(new Date().toISOString().split("T")[0])'
    },
    {
        name: '运行脚本',
        keys: 'Ctrl+Shift+R',
        plugin: 'myScriptPlugin',
        func: 'run',
        closestSelector: 'pre code'
    }
];

5.4 状态栏扩展插件

5.4.1 添加状态栏信息

javascript 复制代码
// plugin/custom/plugins/statusBar.js
const { BasePlugin } = require('../global/core/plugin');

class StatusBarPlugin extends BasePlugin {
    process() {
        // 等待Typora完全加载
        this.utils.eventHub.on('typora:ready', () => {
            this.createStatusBarItems();
        });
        
        // 监听文件变化更新状态
        this.utils.eventHub.on('file:opened', this.updateStatus.bind(this));
        this.utils.eventHub.on('content:changed', this.updateWordCount.bind(this));
    }
    
    createStatusBarItems() {
        // 获取状态栏容器
        const statusBar = document.querySelector('.status-bar');
        if (!statusBar) return;
        
        // 创建状态项
        this.statusItems = {
            wordCount: this.createStatusItem('单词: 0'),
            fileInfo: this.createStatusItem('未命名'),
            lineCol: this.createStatusItem('行: 1 列: 1')
        };
        
        // 添加到状态栏
        Object.values(this.statusItems).forEach(item => {
            statusBar.appendChild(item);
        });
        
        // 初始更新
        this.updateStatus();
    }
    
    createStatusItem(text) {
        const span = document.createElement('span');
        span.className = 'status-bar-item';
        span.style.cssText = 'margin: 0 10px; font-size: 12px; color: #888;';
        span.textContent = text;
        return span;
    }
    
    updateStatus() {
        const filePath = this.utils.getFilePath();
        const fileName = filePath ? filePath.split(/[\\/]/).pop() : '未命名';
        this.statusItems.fileInfo.textContent = `📄 ${fileName}`;
        this.updateWordCount();
    }
    
    updateWordCount() {
        const content = this.utils.getEditorContent();
        const words = content.split(/\s+/).filter(w => w.length > 0).length;
        this.statusItems.wordCount.textContent = `📝 单词: ${words}`;
        
        // 更新行列信息
        const selection = window.getSelection();
        if (selection && selection.anchorNode) {
            const range = selection.getRangeAt(0);
            // 计算行列...
        }
    }
}

module.exports = { plugin: StatusBarPlugin };

5.5 侧边栏面板插件

5.5.1 创建侧边栏面板

javascript 复制代码
// plugin/custom/plugins/sidebarPanel.js
const { BasePlugin } = require('../global/core/plugin');

class SidebarPanelPlugin extends BasePlugin {
    html() {
        return `
            <div id="my-sidebar-panel" style="
                position: fixed;
                left: 0;
                top: 0;
                width: 280px;
                height: 100vh;
                background: #f5f5f5;
                border-right: 1px solid #ddd;
                padding: 20px;
                z-index: 999;
                transform: translateX(-100%);
                transition: transform 0.3s ease;
                overflow-y: auto;
            ">
                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
                    <h3 style="margin: 0;">📋 面板</h3>
                    <button id="close-sidebar" style="background: none; border: none; font-size: 20px; cursor: pointer;">✕</button>
                </div>
                <div id="sidebar-content">
                    <p>加载中...</p>
                </div>
                <button id="toggle-sidebar" style="
                    position: absolute;
                    right: -40px;
                    top: 50%;
                    transform: translateY(-50%);
                    background: #f5f5f5;
                    border: 1px solid #ddd;
                    border-left: none;
                    padding: 10px 8px;
                    cursor: pointer;
                    border-radius: 0 4px 4px 0;
                ">◀</button>
            </div>
        `;
    }
    
    process() {
        this.utils.eventHub.on('typora:ready', () => {
            this.setupPanel();
        });
    }
    
    setupPanel() {
        const panel = document.getElementById('my-sidebar-panel');
        if (!panel) return;
        
        // 显示面板
        setTimeout(() => {
            panel.style.transform = 'translateX(0)';
        }, 500);
        
        // 关闭按钮
        const closeBtn = document.getElementById('close-sidebar');
        if (closeBtn) {
            closeBtn.addEventListener('click', () => {
                panel.style.transform = 'translateX(-100%)';
            });
        }
        
        // 切换按钮
        const toggleBtn = document.getElementById('toggle-sidebar');
        if (toggleBtn) {
            toggleBtn.addEventListener('click', () => {
                const current = panel.style.transform;
                panel.style.transform = current === 'translateX(0)' ? 
                    'translateX(-100%)' : 'translateX(0)';
                toggleBtn.textContent = current === 'translateX(0)' ? '▶' : '◀';
            });
        }
        
        // 加载内容
        this.loadContent();
    }
    
    loadContent() {
        const content = document.getElementById('sidebar-content');
        if (!content) return;
        
        // 显示文档结构
        const structure = this.getDocumentStructure();
        content.innerHTML = this.renderStructure(structure);
    }
    
    getDocumentStructure() {
        const editor = document.querySelector('.typora-edit');
        if (!editor) return [];
        
        const headings = editor.querySelectorAll('h1, h2, h3, h4, h5, h6');
        return Array.from(headings).map(h => ({
            level: parseInt(h.tagName[1]),
            text: h.textContent.trim(),
            id: h.id || `heading-${Date.now()}`
        }));
    }
    
    renderStructure(items) {
        if (items.length === 0) {
            return '<p style="color: #999;">暂无标题</p>';
        }
        
        let html = '<ul style="list-style: none; padding: 0;">';
        items.forEach(item => {
            const indent = (item.level - 1) * 20;
            html += `
                <li style="padding-left: ${indent}px; margin: 5px 0; cursor: pointer; 
                           color: #333; hover: color: #007acc;"
                    onclick="document.getElementById('${item.id}')?.scrollIntoView()">
                    ${'#'.repeat(item.level)} ${item.text}
                </li>
            `;
        });
        html += '</ul>';
        return html;
    }
}

module.exports = { plugin: SidebarPanelPlugin };

六、高级功能插件开发

6.1 文件操作与I/O插件

6.1.1 文件系统访问

javascript 复制代码
// plugin/custom/plugins/fileIO.js
const { BasePlugin } = require('../global/core/plugin');
const fs = require('fs-extra');
const path = require('path');

class FileIOPlugin extends BasePlugin {
    process() {
        this.registerCommands();
    }
    
    registerCommands() {
        // 注册到命令系统
        this.utils.commandHub.register('file:batch-rename', this.batchRename.bind(this));
        this.utils.commandHub.register('file:export-all', this.exportAll.bind(this));
        this.utils.commandHub.register('file:backup', this.createBackup.bind(this));
    }
    
    async batchRename(pattern) {
        const files = await this.getMarkdownFiles();
        const renamed = [];
        
        for (const file of files) {
            const dir = path.dirname(file);
            const ext = path.extname(file);
            const base = path.basename(file, ext);
            
            // 应用重命名模式
            const newName = this.applyPattern(pattern, base);
            const newPath = path.join(dir, newName + ext);
            
            if (newPath !== file) {
                await fs.move(file, newPath);
                renamed.push({ from: file, to: newPath });
            }
        }
        
        this.utils.showNotification(`已重命名 ${renamed.length} 个文件`);
        return renamed;
    }
    
    async getMarkdownFiles() {
        const currentDir = path.dirname(this.utils.getFilePath());
        const files = await fs.readdir(currentDir);
        return files
            .filter(f => f.endsWith('.md'))
            .map(f => path.join(currentDir, f));
    }
    
    applyPattern(pattern, base) {
        // 支持变量替换: {date}, {time}, {index}等
        const now = new Date();
        return pattern
            .replace(/{date}/g, now.toISOString().split('T')[0])
            .replace(/{time}/g, now.toTimeString().slice(0, 8).replace(/:/g, '-'))
            .replace(/{base}/g, base);
    }
    
    async exportAll() {
        const files = await this.getMarkdownFiles();
        const exportDir = path.join(path.dirname(files[0]), 'export');
        await fs.ensureDir(exportDir);
        
        for (const file of files) {
            const content = await fs.readFile(file, 'utf-8');
            const html = this.convertToHTML(content);
            const name = path.basename(file, '.md');
            await fs.writeFile(path.join(exportDir, `${name}.html`), html);
        }
        
        this.utils.showNotification(`已导出 ${files.length} 个文件到 ${exportDir}`);
    }
    
    convertToHTML(markdown) {
        // 调用Typora的转换功能
        return this.utils.convertMarkdownToHTML(markdown);
    }
    
    async createBackup() {
        const file = this.utils.getFilePath();
        if (!file) return;
        
        const backupDir = path.join(path.dirname(file), '.backup');
        await fs.ensureDir(backupDir);
        
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const backupName = `${path.basename(file, '.md')}_${timestamp}.md`;
        const backupPath = path.join(backupDir, backupName);
        
        await fs.copy(file, backupPath);
        this.utils.showNotification(`已创建备份: ${backupName}`);
    }
}

module.exports = { plugin: FileIOPlugin };

6.2 代码片段管理插件

6.2.1 代码片段数据结构

javascript 复制代码
// plugin/custom/plugins/snippetManager.js
const { BaseCustomPlugin } = require('../global/core/plugin');

class SnippetManager extends BaseCustomPlugin {
    constructor(fixedName, setting, i18n) {
        super(fixedName, setting, i18n);
        this.snippets = this.loadSnippets();
    }
    
    selector = () => 'body';
    
    hint = () => '📋 代码片段管理';
    
    callback = (anchorNode) => {
        this.showSnippetPanel();
    }
    
    loadSnippets() {
        // 从配置文件或本地存储加载
        const defaultSnippets = [
            {
                id: 'snippet-1',
                name: 'React组件',
                language: 'javascript',
                prefix: 'rc',
                body: `import React from 'react';

const ${1:ComponentName} = () => {
    return (
        <div>
            ${2:/* 内容 */}
        </div>
    );
};

export default ${1:ComponentName};`,
                description: 'React函数组件模板'
            },
            {
                id: 'snippet-2',
                name: 'Python函数',
                language: 'python',
                prefix: 'def',
                body: `def ${1:function_name}(${2:params}):
    """${3:文档字符串}"""
    ${4:pass}`,
                description: 'Python函数定义'
            },
            {
                id: 'snippet-3',
                name: 'SQL查询',
                language: 'sql',
                prefix: 'sel',
                body: `SELECT ${1:columns}
FROM ${2:table}
WHERE ${3:condition}
ORDER BY ${4:column} ${5:DESC};`,
                description: 'SQL SELECT查询'
            }
        ];
        
        // 从存储加载自定义片段
        const custom = this.loadCustomSnippets();
        return [...defaultSnippets, ...custom];
    }
    
    loadCustomSnippets() {
        try {
            const path = this.utils.getMountFolder();
            const filePath = path + '/snippets.json';
            if (this.utils.isExists(filePath)) {
                return JSON.parse(this.utils.getString(filePath));
            }
        } catch (e) {
            console.error('加载自定义片段失败:', e);
        }
        return [];
    }
    
    showSnippetPanel() {
        // 创建面板
        const panel = document.createElement('div');
        panel.id = 'snippet-panel';
        panel.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 20px;
            width: 500px;
            max-height: 70vh;
            overflow-y: auto;
            z-index: 10000;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
        `;
        
        // 标题
        panel.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
                <h3 style="margin: 0;">📋 代码片段</h3>
                <button id="close-snippet-panel" style="background: none; border: none; font-size: 20px; cursor: pointer;">✕</button>
            </div>
            <div style="margin-bottom: 15px;">
                <input id="snippet-search" placeholder="搜索片段..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
            </div>
            <div id="snippet-list"></div>
        `;
        
        document.body.appendChild(panel);
        
        // 渲染片段列表
        this.renderSnippetList();
        
        // 事件绑定
        document.getElementById('close-snippet-panel').addEventListener('click', () => {
            panel.remove();
        });
        
        document.getElementById('snippet-search').addEventListener('input', (e) => {
            this.filterSnippets(e.target.value);
        });
    }
    
    renderSnippetList(filter = '') {
        const container = document.getElementById('snippet-list');
        if (!container) return;
        
        const filtered = filter ? 
            this.snippets.filter(s => 
                s.name.toLowerCase().includes(filter.toLowerCase()) ||
                s.description.toLowerCase().includes(filter.toLowerCase()) ||
                s.language.includes(filter.toLowerCase())
            ) : this.snippets;
        
        container.innerHTML = filtered.map(snippet => `
            <div class="snippet-item" data-id="${snippet.id}" style="
                padding: 10px;
                margin: 5px 0;
                border: 1px solid #eee;
                border-radius: 4px;
                cursor: pointer;
                transition: background 0.2s;
                hover: background: #f0f0f0;
            ">
                <div style="display: flex; justify-content: space-between;">
                    <span><strong>${snippet.name}</strong></span>
                    <span style="color: #888; font-size: 12px;">${snippet.language}</span>
                </div>
                <div style="color: #666; font-size: 13px;">${snippet.description}</div>
                <div style="color: #999; font-size: 12px;">前缀: ${snippet.prefix}</div>
            </div>
        `).join('');
        
        // 绑定点击事件
        container.querySelectorAll('.snippet-item').forEach(el => {
            el.addEventListener('click', () => {
                const id = el.dataset.id;
                const snippet = this.snippets.find(s => s.id === id);
                if (snippet) {
                    this.insertSnippet(snippet);
                    document.getElementById('snippet-panel')?.remove();
                }
            });
        });
    }
    
    filterSnippets(query) {
        this.renderSnippetList(query);
    }
    
    insertSnippet(snippet) {
        // 处理占位符
        let code = snippet.body;
        const placeholders = code.match(/\$\{\d+:([^}]*)\}/g) || [];
        
        // 插入代码
        this.utils.insertText(code);
        
        // 如果有占位符,移动到第一个占位符位置
        if (placeholders.length > 0) {
            // 实现Tab跳转逻辑
            this.setupTabNavigation(placeholders);
        }
        
        this.utils.showNotification(`已插入: ${snippet.name}`);
    }
    
    setupTabNavigation(placeholders) {
        // 简化实现:移动到插入位置
        // 完整实现需要跟踪占位符位置并支持Tab跳转
        console.log('Placeholders:', placeholders);
    }
}

module.exports = { plugin: SnippetManager };

6.3 实时预览增强插件

javascript 复制代码
// plugin/custom/plugins/previewEnhance.js
const { BasePlugin } = require('../global/core/plugin');

class PreviewEnhancePlugin extends BasePlugin {
    constructor(fixedName, setting, i18n) {
        super(fixedName, setting, i18n);
        this.previewTimeout = null;
        this.isPreviewMode = false;
    }
    
    process() {
        this.utils.eventHub.on('content:changed', this.onContentChanged.bind(this));
        this.utils.eventHub.on('typora:ready', this.setupPreviewEnhancements.bind(this));
    }
    
    setupPreviewEnhancements() {
        // 添加预览控制按钮
        this.addPreviewControls();
        
        // 增强预览样式
        this.enhancePreviewStyles();
    }
    
    addPreviewControls() {
        const toolbar = document.querySelector('.typora-toolbar');
        if (!toolbar) return;
        
        const btn = document.createElement('button');
        btn.className = 'preview-toggle-btn';
        btn.textContent = '🔍 增强预览';
        btn.style.cssText = `
            margin-left: 10px;
            padding: 4px 12px;
            border: 1px solid #ccc;
            border-radius: 4px;
            background: white;
            cursor: pointer;
        `;
        
        btn.addEventListener('click', this.togglePreviewMode.bind(this));
        toolbar.appendChild(btn);
    }
    
    togglePreviewMode() {
        this.isPreviewMode = !this.isPreviewMode;
        const editor = document.querySelector('.typora-edit');
        const preview = document.querySelector('.typora-preview');
        
        if (this.isPreviewMode) {
            // 进入增强预览模式
            editor.style.display = 'none';
            preview.style.width = '100%';
            preview.style.maxWidth = '100%';
            preview.style.padding = '40px';
            this.applyEnhancedStyles();
            this.utils.showNotification('已进入增强预览模式');
        } else {
            // 退出预览模式
            editor.style.display = '';
            preview.style.width = '';
            preview.style.maxWidth = '';
            preview.style.padding = '';
            this.removeEnhancedStyles();
            this.utils.showNotification('已退出增强预览模式');
        }
    }
    
    enhancePreviewStyles() {
        // 注入增强样式
        this.utils.registerStyle('preview-enhance', `
            .typora-preview {
                font-size: 16px;
                line-height: 1.8;
            }
            .typora-preview h1 {
                border-bottom: 2px solid #007acc;
                padding-bottom: 10px;
            }
            .typora-preview h2 {
                border-bottom: 1px solid #ddd;
                padding-bottom: 8px;
            }
            .typora-preview pre {
                border-radius: 6px;
                padding: 16px;
                background: #f6f8fa;
                border: 1px solid #e1e4e8;
            }
            .typora-preview blockquote {
                border-left: 4px solid #007acc;
                padding-left: 20px;
                color: #555;
            }
            .typora-preview table {
                border-collapse: collapse;
                width: 100%;
            }
            .typora-preview th, .typora-preview td {
                border: 1px solid #ddd;
                padding: 8px 12px;
            }
            .typora-preview th {
                background: #f6f8fa;
            }
        `);
    }
    
    applyEnhancedStyles() {
        // 添加阅读模式样式
        const style = document.createElement('style');
        style.id = 'enhanced-preview-style';
        style.textContent = `
            .typora-preview {
                max-width: 900px !important;
                margin: 0 auto !important;
                font-size: 17px !important;
                line-height: 1.9 !important;
            }
            .typora-preview h1 { font-size: 2.2em !important; }
            .typora-preview h2 { font-size: 1.8em !important; }
            .typora-preview h3 { font-size: 1.5em !important; }
            .typora-preview img { max-width: 100%; border-radius: 4px; }
            .typora-preview .task-list-item { list-style: none; }
        `;
        document.head.appendChild(style);
    }
    
    removeEnhancedStyles() {
        const style = document.getElementById('enhanced-preview-style');
        if (style) style.remove();
    }
    
    onContentChanged() {
        // 防抖处理
        clearTimeout(this.previewTimeout);
        this.previewTimeout = setTimeout(() => {
            this.updatePreviewStats();
        }, 1000);
    }
    
    updatePreviewStats() {
        const content = this.utils.getEditorContent();
        const stats = this.calculateStats(content);
        // 显示在状态栏
        this.displayStats(stats);
    }
    
    calculateStats(content) {
        const words = content.split(/\s+/).filter(w => w.length > 0).length;
        const chars = content.replace(/\s/g, '').length;
        const lines = content.split('\n').length;
        const headings = (content.match(/^#{1,6}\s/gm) || []).length;
        const codeBlocks = (content.match(/```/g) || []).length / 2;
        
        return { words, chars, lines, headings, codeBlocks };
    }
    
    displayStats(stats) {
        // 在状态栏显示统计信息
        const statusBar = document.querySelector('.status-bar');
        if (!statusBar) return;
        
        let statsEl = document.getElementById('preview-stats');
        if (!statsEl) {
            statsEl = document.createElement('span');
            statsEl.id = 'preview-stats';
            statsEl.style.cssText = 'margin: 0 15px; font-size: 12px; color: #888;';
            statusBar.appendChild(statsEl);
        }
        
        statsEl.textContent = `📊 ${stats.words}词 · ${stats.chars}字 · ${stats.lines}行 · ${stats.headings}标题 · ${stats.codeBlocks}代码块`;
    }
}

module.exports = { plugin: PreviewEnhancePlugin };

6.4 语法高亮扩展插件

javascript 复制代码
// plugin/custom/plugins/syntaxHighlight.js
const { BasePlugin } = require('../global/core/plugin');

class SyntaxHighlightPlugin extends BasePlugin {
    constructor(fixedName, setting, i18n) {
        super(fixedName, setting, i18n);
        this.highlighters = {};
        this.languageMap = {
            'js': 'javascript',
            'ts': 'typescript',
            'py': 'python',
            'rb': 'ruby',
            'go': 'go',
            'rs': 'rust',
            'swift': 'swift',
            'kt': 'kotlin',
            'java': 'java',
            'c': 'c',
            'cpp': 'cpp',
            'cs': 'csharp',
            'php': 'php',
            'html': 'html',
            'css': 'css',
            'scss': 'scss',
            'sql': 'sql',
            'json': 'json',
            'xml': 'xml',
            'yaml': 'yaml',
            'toml': 'toml',
            'dockerfile': 'dockerfile',
            'sh': 'bash',
            'bash': 'bash',
            'zsh': 'bash'
        };
    }
    
    process() {
        this.setupSyntaxHighlighting();
        this.registerLanguageSupport();
    }
    
    setupSyntaxHighlighting() {
        // 注入自定义高亮样式
        this.utils.registerStyle('syntax-highlight', `
            .code-block pre code {
                font-family: 'Fira Code', 'JetBrains Mono', monospace;
                font-size: 14px;
                line-height: 1.6;
            }
            
            /* 自定义颜色主题 */
            .hljs-keyword { color: #c792ea; font-weight: bold; }
            .hljs-string { color: #c3e88d; }
            .hljs-comment { color: #546e7a; font-style: italic; }
            .hljs-function { color: #82aaff; }
            .hljs-number { color: #f78c6c; }
            .hljs-operator { color: #89ddff; }
            .hljs-class { color: #ffcb6b; }
            .hljs-variable { color: #eeffff; }
            .hljs-constant { color: #f78c6c; }
            
            /* 行号支持 */
            .code-block .line-numbers {
                display: inline-block;
                width: 30px;
                color: #546e7a;
                text-align: right;
                padding-right: 15px;
                user-select: none;
            }
            
            /* 高亮特定行 */
            .code-block .highlight-line {
                background: rgba(130, 170, 255, 0.15);
                display: block;
                margin: 0 -15px;
                padding: 0 15px;
            }
        `);
    }
    
    registerLanguageSupport() {
        // 注册语言检测器
        this.utils.registerLanguageDetector((code, defaultLang) => {
            return this.detectLanguage(code, defaultLang);
        });
    }
    
    detectLanguage(code, defaultLang) {
        // 基于代码内容检测语言
        const patterns = {
            'python': /^import |^def |^class |if __name__/m,
            'javascript': /^const |^let |^var |=>|function\s*\(/m,
            'typescript': /^interface |^type |^enum |:\s*(string|number|boolean)/m,
            'java': /^public class |^private |^protected |System\.out/m,
            'go': /^func |^package |^import |:=/m,
            'rust': /^fn |^pub |^let mut |-> /m,
            'ruby': /^def |^class |^require |attr_/m,
            'php': /^\$|^function |^class |echo /m,
            'sql': /^SELECT |^INSERT |^UPDATE |^DELETE |^CREATE /mi,
            'html': /<[a-z]+[^>]*>/mi,
            'css': /^[.#][a-zA-Z-]+\s*{/m,
            'json': /^\{[\s\n]*"[^"]+"/m,
            'yaml': /^[a-zA-Z-]+:\s/m,
            'bash': /^#!/bin|^echo |^export |^for .* in /m
        };
        
        for (const [lang, pattern] of Object.entries(patterns)) {
            if (pattern.test(code)) {
                return lang;
            }
        }
        
        return defaultLang || 'plaintext';
    }
    
    // 扩展方法:高亮特定行
    highlightLines(code, lines) {
        const codeLines = code.split('\n');
        return codeLines.map((line, index) => {
            const lineNum = index + 1;
            if (lines.includes(lineNum)) {
                return `<span class="highlight-line">${line}</span>`;
            }
            return line;
        }).join('\n');
    }
    
    // 扩展方法:添加行号
    addLineNumbers(code) {
        const lines = code.split('\n');
        const maxWidth = String(lines.length).length;
        return lines.map((line, index) => {
            const num = String(index + 1).padStart(maxWidth, ' ');
            return `<span class="line-numbers">${num}</span>${line}`;
        }).join('\n');
    }
}

module.exports = { plugin: SyntaxHighlightPlugin };

6.5 AI辅助写作插件

6.5.1 AI插件基础结构

javascript 复制代码
// plugin/custom/plugins/aiAssistant.js
const { BaseCustomPlugin } = require('../global/core/plugin');

class AIAssistantPlugin extends BaseCustomPlugin {
    constructor(fixedName, setting, i18n) {
        super(fixedName, setting, i18n);
        this.apiKey = setting.API_KEY || '';
        this.apiEndpoint = setting.API_ENDPOINT || 'https://api.openai.com/v1/chat/completions';
        this.model = setting.MODEL || 'gpt-3.5-turbo';
        this.history = [];
        this.maxHistory = 20;
    }
    
    selector = () => 'body';
    
    hint = () => '🤖 AI助手';
    
    callback = (anchorNode) => {
        this.showAIPanel();
    }
    
    showAIPanel() {
        const panel = document.createElement('div');
        panel.id = 'ai-panel';
        panel.style.cssText = `
            position: fixed;
            bottom: 80px;
            right: 30px;
            width: 400px;
            max-height: 500px;
            background: white;
            border: 1px solid #ddd;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.2);
            z-index: 10000;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        `;
        
        panel.innerHTML = `
            <div style="padding: 15px; background: #f8f9fa; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center;">
                <span style="font-weight: bold;">🤖 AI助手</span>
                <div>
                    <button id="ai-minimize" style="background: none; border: none; cursor: pointer; margin-right: 8px;">─</button>
                    <button id="ai-close" style="background: none; border: none; cursor: pointer;">✕</button>
                </div>
            </div>
            <div id="ai-messages" style="flex: 1; padding: 15px; overflow-y: auto; min-height: 200px; max-height: 300px;">
                <div style="color: #999; text-align: center; padding: 20px;">
                    输入问题开始与AI对话
                </div>
            </div>
            <div style="padding: 10px; border-top: 1px solid #ddd; display: flex; gap: 8px;">
                <select id="ai-action" style="padding: 6px; border: 1px solid #ddd; border-radius: 4px; flex-shrink: 0;">
                    <option value="chat">💬 对话</option>
                    <option value="polish">✨ 润色</option>
                    <option value="translate">🌐 翻译</option>
                    <option value="summarize">📝 总结</option>
                    <option value="continue">✍️ 续写</option>
                </select>
                <input id="ai-input" placeholder="输入消息..." style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
                <button id="ai-send" style="padding: 8px 16px; background: #007acc; color: white; border: none; border-radius: 4px; cursor: pointer;">发送</button>
            </div>
        `;
        
        document.body.appendChild(panel);
        this.bindAIEvents(panel);
    }
    
    bindAIEvents(panel) {
        const input = document.getElementById('ai-input');
        const sendBtn = document.getElementById('ai-send');
        const closeBtn = document.getElementById('ai-close');
        const minBtn = document.getElementById('ai-minimize');
        const actionSelect = document.getElementById('ai-action');
        
        sendBtn.addEventListener('click', () => {
            this.handleAIAction(input.value, actionSelect.value);
        });
        
        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                this.handleAIAction(input.value, actionSelect.value);
            }
        });
        
        closeBtn.addEventListener('click', () => {
            panel.remove();
        });
        
        let isMinimized = false;
        minBtn.addEventListener('click', () => {
            const messages = document.getElementById('ai-messages');
            const controls = document.querySelector('#ai-panel > div:last-child');
            if (isMinimized) {
                messages.style.display = '';
                controls.style.display = '';
                minBtn.textContent = '─';
                panel.style.maxHeight = '500px';
            } else {
                messages.style.display = 'none';
                controls.style.display = 'none';
                minBtn.textContent = '□';
                panel.style.maxHeight = '50px';
            }
            isMinimized = !isMinimized;
        });
    }
    
    async handleAIAction(input, action) {
        if (!input.trim()) return;
        
        const messagesContainer = document.getElementById('ai-messages');
        if (!messagesContainer) return;
        
        // 显示用户消息
        this.addMessage(messagesContainer, 'user', input);
        
        // 清空输入
        document.getElementById('ai-input').value = '';
        
        // 显示加载状态
        const loadingId = this.addLoadingMessage(messagesContainer);
        
        try {
            // 构建提示词
            const prompt = this.buildPrompt(action, input);
            
            // 调用AI API
            const response = await this.callAI(prompt);
            
            // 移除加载状态
            this.removeLoadingMessage(loadingId);
            
            // 显示AI回复
            this.addMessage(messagesContainer, 'assistant', response);
            
            // 保存历史
            this.saveHistory(action, input, response);
            
        } catch (error) {
            this.removeLoadingMessage(loadingId);
            this.addMessage(messagesContainer, 'error', `错误: ${error.message}`);
        }
    }
    
    buildPrompt(action, input) {
        const prompts = {
            'polish': `请润色以下文本,使其更加流畅和专业:\n\n${input}`,
            'translate': `请将以下文本翻译成中文:\n\n${input}`,
            'summarize': `请总结以下文本的核心要点:\n\n${input}`,
            'continue': `请继续以下文本的写作,保持风格一致:\n\n${input}`,
            'chat': input
        };
        return prompts[action] || input;
    }
    
    async callAI(prompt) {
        // 构建消息历史
        const messages = [
            { role: 'system', content: '你是一个专业的写作助手,帮助用户处理Markdown文档。' }
        ];
        
        // 添加历史消息
        for (const h of this.history.slice(-this.maxHistory)) {
            messages.push({ role: h.role, content: h.content });
        }
        
        messages.push({ role: 'user', content: prompt });
        
        const response = await fetch(this.apiEndpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${this.apiKey}`
            },
            body: JSON.stringify({
                model: this.model,
                messages: messages,
                temperature: 0.7,
                max_tokens: 2000
            })
        });
        
        if (!response.ok) {
            throw new Error(`API请求失败: ${response.status}`);
        }
        
        const data = await response.json();
        return data.choices[0].message.content;
    }
    
    addMessage(container, role, content) {
        const div = document.createElement('div');
        const isUser = role === 'user';
        div.style.cssText = `
            margin: 8px 0;
            padding: 10px 14px;
            border-radius: 8px;
            max-width: 85%;
            ${isUser ? 'align-self: flex-end; background: #007acc; color: white; margin-left: auto;' : 
                        'align-self: flex-start; background: #f0f0f0; color: #333;'}
            word-wrap: break-word;
        `;
        div.textContent = content;
        container.appendChild(div);
        container.scrollTop = container.scrollHeight;
    }
    
    addLoadingMessage(container) {
        const id = 'loading-' + Date.now();
        const div = document.createElement('div');
        div.id = id;
        div.style.cssText = 'margin: 8px 0; padding: 10px 14px; border-radius: 8px; background: #f0f0f0; color: #999; align-self: flex-start;';
        div.textContent = '🤔 思考中...';
        container.appendChild(div);
        container.scrollTop = container.scrollHeight;
        return id;
    }
    
    removeLoadingMessage(id) {
        const el = document.getElementById(id);
        if (el) el.remove();
    }
    
    saveHistory(action, input, response) {
        this.history.push(
            { role: 'user', content: input },
            { role: 'assistant', content: response }
        );
        if (this.history.length > this.maxHistory * 2) {
            this.history = this.history.slice(-this.maxHistory * 2);
        }
    }
}

module.exports = { plugin: AIAssistantPlugin };

6.6 JSON-RPC外部API集成

JSON-RPC插件通过JSON-RPC 2.0协议将Typora的能力暴露给外部进程。这使得外部程序(Python、Node.js等)可以编程控制Typora,自动化文档操作,将Typora集成到更广阔的开发工作流中。

6.6.1 JSON-RPC API方法

插件暴露三个核心RPC方法:

1. ping - 验证连接和插件活跃性

javascript 复制代码
// 返回: "pong from typora-plugin"

2. invokePlugin - 调用其他插件的方法

javascript 复制代码
// 参数: [pluginName, functionName, ...args]
// 示例: invokePlugin('snippetManager', 'insertSnippet', 'snippet-1')

3. eval - 执行任意JavaScript代码

javascript 复制代码
// 参数: [code]
// 示例: eval('utils.getFilePath()')

6.6.2 外部调用示例

Python调用示例

python 复制代码
import json
import requests

def call_typora_plugin(plugin_name, func_name, *args):
    url = "http://localhost:12345/jsonrpc"  # 默认端口
    payload = {
        "jsonrpc": "2.0",
        "method": "invokePlugin",
        "params": [plugin_name, func_name, *args],
        "id": 1
    }
    response = requests.post(url, json=payload)
    return response.json()

# 调用AI助手
result = call_typora_plugin('aiAssistant', 'handleAIAction', '请总结这段文字', 'summarize')
print(result)

Node.js调用示例

javascript 复制代码
const axios = require('axios');

async function callTypora(method, params) {
    const response = await axios.post('http://localhost:12345/jsonrpc', {
        jsonrpc: '2.0',
        method: method,
        params: params,
        id: Date.now()
    });
    return response.data;
}

// 获取当前文件路径
const result = await callTypora('eval', ['utils.getFilePath()']);
console.log('Current file:', result.result);

6.6.3 安全警告

JSON-RPC插件仅面向开发者使用。一旦启用,外部客户端将获得对Typora进程的重要控制权。应通过配置将其绑定到127.0.0.1,以防止未授权的远程访问。

七、UI/UX深度定制

7.1 自定义主题与样式

7.1.1 通过custom.css定制

在Typora配置文件夹中创建custom.css文件:

css 复制代码
/* %APPDATA%/Typora/custom.css (Windows) */
/* ~/Library/Application Support/Typora/custom.css (macOS) */
/* ~/.config/Typora/custom.css (Linux) */

/* 自定义编辑器字体 */
#write {
    font-family: 'JetBrains Mono', 'Fira Code', monospace;
    font-size: 16px;
    line-height: 1.8;
}

/* 自定义标题样式 */
#write h1 {
    font-size: 2.2em;
    border-bottom: 3px solid #007acc;
    padding-bottom: 12px;
    margin-top: 30px;
}

#write h2 {
    font-size: 1.8em;
    border-bottom: 2px solid #e1e4e8;
    padding-bottom: 8px;
    margin-top: 25px;
}

/* 代码块样式增强 */
#write pre {
    border-radius: 8px;
    padding: 16px;
    background: #1e1e1e;
    border: 1px solid #333;
}

#write pre code {
    color: #d4d4d4;
    font-family: 'Fira Code', monospace;
    font-size: 14px;
}

/* 表格样式 */
#write table {
    border-collapse: collapse;
    width: 100%;
    margin: 16px 0;
}

#write th, #write td {
    border: 1px solid #ddd;
    padding: 8px 12px;
}

#write th {
    background: #f0f4f8;
    font-weight: bold;
}

/* 引用块样式 */
#write blockquote {
    border-left: 4px solid #007acc;
    padding-left: 20px;
    color: #555;
    background: #f8f9fa;
    border-radius: 0 4px 4px 0;
    margin: 16px 0;
}

/* 任务列表样式 */
#write .task-list-item {
    list-style: none;
}

#write .task-list-item input[type="checkbox"] {
    margin-right: 8px;
    transform: scale(1.1);
}

/* 图片样式 */
#write img {
    max-width: 100%;
    border-radius: 6px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    margin: 16px 0;
}

/* 滚动条样式 */
::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}

::-webkit-scrollbar-track {
    background: #f0f0f0;
}

::-webkit-scrollbar-thumb {
    background: #c1c7cd;
    border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
    background: #a0a6ac;
}

7.1.2 通过base.user.css定制

如果要修改CSS样式并应用到所有主题,应修改base.user.css文件:

css 复制代码
/* 在所有主题中生效的全局样式 */
body {
    --primary-color: #007acc;
    --secondary-color: #c792ea;
    --background-color: #ffffff;
    --text-color: #333333;
}

/* 暗色主题适配 */
@media (prefers-color-scheme: dark) {
    body {
        --background-color: #1e1e1e;
        --text-color: #d4d4d4;
    }
}

7.2 动态组件开发

7.2.1 创建可交互组件

javascript 复制代码
// plugin/custom/plugins/dynamicComponent.js
const { BasePlugin } = require('../global/core/plugin');

class DynamicComponentPlugin extends BasePlugin {
    html() {
        return `
            <div id="dynamic-component" style="
                position: fixed;
                bottom: 20px;
                right: 20px;
                background: white;
                border-radius: 12px;
                box-shadow: 0 4px 20px rgba(0,0,0,0.15);
                padding: 16px;
                z-index: 9999;
                min-width: 200px;
                transition: all 0.3s ease;
            ">
                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
                    <span style="font-weight: bold; color: #333;">📊 文档统计</span>
                    <button id="component-close" style="background: none; border: none; cursor: pointer; color: #999;">✕</button>
                </div>
                <div id="component-content">
                    <div>📝 字符: <span id="stat-chars">0</span></div>
                    <div>📊 单词: <span id="stat-words">0</span></div>
                    <div>📄 行数: <span id="stat-lines">0</span></div>
                    <div>🔖 标题: <span id="stat-headings">0</span></div>
                    <div>💻 代码块: <span id="stat-codeblocks">0</span></div>
                </div>
                <div style="margin-top: 10px; display: flex; gap: 6px; flex-wrap: wrap;">
                    <button class="stat-action" data-action="refresh" style="padding: 4px 12px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; font-size: 12px;">🔄 刷新</button>
                    <button class="stat-action" data-action="export" style="padding: 4px 12px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; font-size: 12px;">📤 导出</button>
                </div>
            </div>
        `;
    }
    
    process() {
        this.utils.eventHub.on('typora:ready', () => {
            this.setupComponent();
        });
        
        this.utils.eventHub.on('content:changed', () => {
            this.updateStats();
        });
    }
    
    setupComponent() {
        // 关闭按钮
        const closeBtn = document.getElementById('component-close');
        if (closeBtn) {
            closeBtn.addEventListener('click', () => {
                const component = document.getElementById('dynamic-component');
                if (component) component.remove();
            });
        }
        
        // 动作按钮
        document.querySelectorAll('.stat-action').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const action = e.target.dataset.action;
                if (action === 'refresh') this.updateStats();
                if (action === 'export') this.exportStats();
            });
        });
        
        // 初始更新
        this.updateStats();
        
        // 使组件可拖拽
        this.makeDraggable(document.getElementById('dynamic-component'));
    }
    
    updateStats() {
        const content = this.utils.getEditorContent();
        if (!content) return;
        
        const chars = content.replace(/\s/g, '').length;
        const words = content.split(/\s+/).filter(w => w.length > 0).length;
        const lines = content.split('\n').length;
        const headings = (content.match(/^#{1,6}\s/gm) || []).length;
        const codeBlocks = (content.match(/```/g) || []).length / 2;
        
        document.getElementById('stat-chars').textContent = chars;
        document.getElementById('stat-words').textContent = words;
        document.getElementById('stat-lines').textContent = lines;
        document.getElementById('stat-headings').textContent = headings;
        document.getElementById('stat-codeblocks').textContent = codeBlocks;
    }
    
    exportStats() {
        const stats = {
            chars: document.getElementById('stat-chars').textContent,
            words: document.getElementById('stat-words').textContent,
            lines: document.getElementById('stat-lines').textContent,
            headings: document.getElementById('stat-headings').textContent,
            codeBlocks: document.getElementById('stat-codeblocks').textContent,
            timestamp: new Date().toISOString(),
            file: this.utils.getFilePath()
        };
        
        const json = JSON.stringify(stats, null, 2);
        this.utils.showNotification('统计已导出到控制台');
        console.log('Document Stats:', json);
    }
    
    makeDraggable(element) {
        if (!element) return;
        
        let isDragging = false;
        let offsetX, offsetY;
        
        element.addEventListener('mousedown', (e) => {
            if (e.target.tagName === 'BUTTON') return;
            isDragging = true;
            const rect = element.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;
            element.style.cursor = 'grabbing';
            element.style.userSelect = 'none';
        });
        
        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            element.style.left = (e.clientX - offsetX) + 'px';
            element.style.top = (e.clientY - offsetY) + 'px';
            element.style.right = 'auto';
            element.style.bottom = 'auto';
        });
        
        document.addEventListener('mouseup', () => {
            isDragging = false;
            element.style.cursor = '';
            element.style.userSelect = '';
        });
    }
}

module.exports = { plugin: DynamicComponentPlugin };

7.3 拖拽交互实现

javascript 复制代码
// 拖拽工具函数
class DragDropManager {
    constructor() {
        this.dragData = null;
        this.dragOverlay = null;
    }
    
    enableDrag(element, data) {
        element.draggable = true;
        element.addEventListener('dragstart', (e) => {
            this.dragData = data;
            e.dataTransfer.effectAllowed = 'copy';
            e.dataTransfer.setData('text/plain', JSON.stringify(data));
            element.style.opacity = '0.5';
        });
        
        element.addEventListener('dragend', () => {
            element.style.opacity = '1';
        });
    }
    
    enableDrop(element, onDrop) {
        element.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'copy';
            element.style.border = '2px dashed #007acc';
        });
        
        element.addEventListener('dragleave', () => {
            element.style.border = '';
        });
        
        element.addEventListener('drop', (e) => {
            e.preventDefault();
            element.style.border = '';
            const data = JSON.parse(e.dataTransfer.getData('text/plain'));
            onDrop(data, element);
        });
    }
}

7.4 响应式布局设计

javascript 复制代码
// 响应式面板
class ResponsivePanel {
    constructor() {
        this.breakpoints = {
            mobile: 480,
            tablet: 768,
            desktop: 1024
        };
        this.currentBreakpoint = this.getBreakpoint();
        this.setupResizeListener();
    }
    
    getBreakpoint() {
        const width = window.innerWidth;
        if (width < this.breakpoints.mobile) return 'mobile';
        if (width < this.breakpoints.tablet) return 'tablet';
        if (width < this.breakpoints.desktop) return 'desktop';
        return 'wide';
    }
    
    setupResizeListener() {
        let resizeTimeout;
        window.addEventListener('resize', () => {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(() => {
                const newBreakpoint = this.getBreakpoint();
                if (newBreakpoint !== this.currentBreakpoint) {
                    this.currentBreakpoint = newBreakpoint;
                    this.onBreakpointChange(newBreakpoint);
                }
            }, 200);
        });
    }
    
    onBreakpointChange(breakpoint) {
        const panel = document.getElementById('responsive-panel');
        if (!panel) return;
        
        switch(breakpoint) {
            case 'mobile':
                panel.style.width = '100%';
                panel.style.maxWidth = '100%';
                panel.style.height = '50vh';
                panel.style.bottom = '0';
                panel.style.right = '0';
                panel.style.borderRadius = '12px 12px 0 0';
                break;
            case 'tablet':
                panel.style.width = '70%';
                panel.style.maxWidth = '500px';
                panel.style.height = '60vh';
                panel.style.bottom = '20px';
                panel.style.right = '20px';
                panel.style.borderRadius = '12px';
                break;
            default:
                panel.style.width = '400px';
                panel.style.maxWidth = '400px';
                panel.style.height = '70vh';
                panel.style.bottom = '20px';
                panel.style.right = '20px';
                panel.style.borderRadius = '12px';
        }
    }
}

7.5 动画效果集成

javascript 复制代码
// 动画工具
class AnimationUtils {
    static fadeIn(element, duration = 300) {
        element.style.opacity = '0';
        element.style.display = 'block';
        element.style.transition = `opacity ${duration}ms ease`;
        requestAnimationFrame(() => {
            element.style.opacity = '1';
        });
        setTimeout(() => {
            element.style.transition = '';
        }, duration);
    }
    
    static fadeOut(element, duration = 300) {
        element.style.transition = `opacity ${duration}ms ease`;
        element.style.opacity = '0';
        setTimeout(() => {
            element.style.display = 'none';
            element.style.transition = '';
        }, duration);
    }
    
    static slideIn(element, direction = 'right', duration = 300) {
        const transforms = {
            'right': 'translateX(100%)',
            'left': 'translateX(-100%)',
            'top': 'translateY(-100%)',
            'bottom': 'translateY(100%)'
        };
        element.style.transform = transforms[direction] || transforms.right;
        element.style.transition = `transform ${duration}ms ease`;
        requestAnimationFrame(() => {
            element.style.transform = 'translate(0)';
        });
        setTimeout(() => {
            element.style.transition = '';
        }, duration);
    }
    
    static pulse(element) {
        element.style.animation = 'pulse 0.5s ease';
        setTimeout(() => {
            element.style.animation = '';
        }, 500);
    }
}

// 注入关键帧动画
const animationStyles = document.createElement('style');
animationStyles.textContent = `
    @keyframes pulse {
        0% { transform: scale(1); }
        50% { transform: scale(1.05); }
        100% { transform: scale(1); }
    }
    @keyframes spin {
        from { transform: rotate(0deg); }
        to { transform: rotate(360deg); }
    }
    @keyframes shimmer {
        0% { background-position: -200% 0; }
        100% { background-position: 200% 0; }
    }
`;
document.head.appendChild(animationStyles);

八、性能优化与调试

8.1 插件性能监控

javascript 复制代码
// plugin/custom/plugins/performanceMonitor.js
const { BasePlugin } = require('../global/core/plugin');

class PerformanceMonitorPlugin extends BasePlugin {
    constructor(fixedName, setting, i18n) {
        super(fixedName, setting, i18n);
        this.metrics = {
            renderTime: [],
            fileLoadTime: [],
            memoryUsage: [],
            eventLatency: []
        };
        this.maxMetrics = 100;
    }
    
    process() {
        this.startMonitoring();
        this.registerPerformanceHooks();
    }
    
    startMonitoring() {
        // 监控内存使用
        setInterval(() => {
            const memory = process.memoryUsage();
            this.metrics.memoryUsage.push({
                timestamp: Date.now(),
                heapUsed: memory.heapUsed,
                heapTotal: memory.heapTotal,
                rss: memory.rss
            });
            if (this.metrics.memoryUsage.length > this.maxMetrics) {
                this.metrics.memoryUsage.shift();
            }
        }, 30000);
    }
    
    registerPerformanceHooks() {
        // 监控文件加载时间
        this.utils.eventHub.on('file:opened', (path) => {
            const start = performance.now();
            this.utils.eventHub.once('file:rendered', () => {
                const duration = performance.now() - start;
                this.metrics.fileLoadTime.push({ path, duration });
                if (this.metrics.fileLoadTime.length > this.maxMetrics) {
                    this.metrics.fileLoadTime.shift();
                }
            });
        });
        
        // 监控渲染性能
        let renderStart = 0;
        this.utils.eventHub.on('content:changed', () => {
            renderStart = performance.now();
        });
        this.utils.eventHub.on('content:rendered', () => {
            if (renderStart) {
                const duration = performance.now() - renderStart;
                this.metrics.renderTime.push(duration);
                if (this.metrics.renderTime.length > this.maxMetrics) {
                    this.metrics.renderTime.shift();
                }
            }
        });
    }
    
    getPerformanceReport() {
        const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
        return {
            avgRenderTime: avg(this.metrics.renderTime),
            avgFileLoadTime: avg(this.metrics.fileLoadTime.map(m => m.duration)),
            currentMemory: this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1],
            totalMetrics: {
                renderTime: this.metrics.renderTime.length,
                fileLoadTime: this.metrics.fileLoadTime.length,
                memoryUsage: this.metrics.memoryUsage.length
            }
        };
    }
}

module.exports = { plugin: PerformanceMonitorPlugin };

8.2 内存泄漏检测

javascript 复制代码
// 内存泄漏检测工具
class MemoryLeakDetector {
    constructor() {
        this.registeredElements = new Map();
        this.eventListeners = new Map();
        this.intervals = new Set();
        this.timeouts = new Set();
    }
    
    registerElement(element, source) {
        const id = this.generateId();
        this.registeredElements.set(id, {
            element,
            source,
            timestamp: Date.now()
        });
        return id;
    }
    
    unregisterElement(id) {
        this.registeredElements.delete(id);
    }
    
    registerEventListener(target, event, handler, source) {
        const id = this.generateId();
        this.eventListeners.set(id, {
            target,
            event,
            handler,
            source,
            timestamp: Date.now()
        });
        target.addEventListener(event, handler);
        return id;
    }
    
    unregisterEventListener(id) {
        const listener = this.eventListeners.get(id);
        if (listener) {
            listener.target.removeEventListener(listener.event, listener.handler);
            this.eventListeners.delete(id);
        }
    }
    
    registerInterval(interval, source) {
        this.intervals.add({ interval, source, timestamp: Date.now() });
        return interval;
    }
    
    clearInterval(interval) {
        clearInterval(interval);
        this.intervals.delete(interval);
    }
    
    generateId() {
        return 'leak-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
    }
    
    detectLeaks() {
        const now = Date.now();
        const threshold = 60000; // 1分钟
        const leaks = [];
        
        // 检查注册的元素
        for (const [id, data] of this.registeredElements) {
            if (!document.contains(data.element)) {
                leaks.push({
                    type: 'element',
                    id,
                    source: data.source,
                    age: now - data.timestamp,
                    message: 'Element removed from DOM but not unregistered'
                });
            }
        }
        
        // 检查事件监听器
        for (const [id, data] of this.eventListeners) {
            if (!document.contains(data.target)) {
                leaks.push({
                    type: 'eventListener',
                    id,
                    source: data.source,
                    age: now - data.timestamp,
                    message: `Event listener on removed element: ${data.event}`
                });
            }
        }
        
        return leaks;
    }
    
    cleanup() {
        // 清理所有注册的资源
        for (const [id, data] of this.eventListeners) {
            data.target.removeEventListener(data.event, data.handler);
        }
        this.eventListeners.clear();
        
        for (const interval of this.intervals) {
            clearInterval(interval);
        }
        this.intervals.clear();
        
        this.registeredElements.clear();
    }
}

8.3 调试技巧与工具

8.3.1 使用Chrome DevTools调试

  1. 打开开发者工具Ctrl+Shift+I (Windows/Linux) 或 Cmd+Option+I (macOS)
  2. 断点调试:在Sources面板中设置断点
  3. 控制台调试:在Console中执行JavaScript代码
  4. 性能分析:使用Performance面板分析性能瓶颈

8.3.2 日志系统

javascript 复制代码
// 日志工具
class Logger {
    constructor(pluginName) {
        this.pluginName = pluginName;
        this.logLevel = 'info';
        this.logs = [];
        this.maxLogs = 1000;
    }
    
    setLevel(level) {
        const levels = ['debug', 'info', 'warn', 'error'];
        if (levels.includes(level)) {
            this.logLevel = level;
        }
    }
    
    debug(...args) {
        if (this.shouldLog('debug')) {
            this.log('DEBUG', ...args);
        }
    }
    
    info(...args) {
        if (this.shouldLog('info')) {
            this.log('INFO', ...args);
        }
    }
    
    warn(...args) {
        if (this.shouldLog('warn')) {
            this.log('WARN', ...args);
        }
    }
    
    error(...args) {
        if (this.shouldLog('error')) {
            this.log('ERROR', ...args);
        }
    }
    
    shouldLog(level) {
        const levels = ['debug', 'info', 'warn', 'error'];
        return levels.indexOf(level) >= levels.indexOf(this.logLevel);
    }
    
    log(level, ...args) {
        const timestamp = new Date().toISOString();
        const message = `[${timestamp}] [${this.pluginName}] [${level}] ${args.join(' ')}`;
        console.log(message);
        
        this.logs.push({ timestamp, level, message });
        if (this.logs.length > this.maxLogs) {
            this.logs.shift();
        }
    }
    
    getLogs(level) {
        if (level) {
            return this.logs.filter(log => log.level === level);
        }
        return this.logs;
    }
    
    exportLogs() {
        return this.logs.map(log => log.message).join('\n');
    }
}

8.4 错误处理与日志

8.4.1 全局错误捕获

javascript 复制代码
// 错误处理工具
class ErrorHandler {
    constructor(pluginName) {
        this.pluginName = pluginName;
        this.errorCallbacks = [];
        this.setupGlobalHandler();
    }
    
    setupGlobalHandler() {
        // 捕获未处理的Promise异常
        window.addEventListener('unhandledrejection', (event) => {
            this.handleError('Unhandled Promise Rejection', event.reason);
        });
        
        // 捕获全局异常
        window.addEventListener('error', (event) => {
            this.handleError('Uncaught Exception', event.error || event.message);
        });
    }
    
    handleError(type, error) {
        const errorInfo = {
            type,
            message: error?.message || String(error),
            stack: error?.stack,
            timestamp: new Date().toISOString(),
            plugin: this.pluginName
        };
        
        console.error(`[${this.pluginName}] Error:`, errorInfo);
        
        // 通知所有错误回调
        this.errorCallbacks.forEach(cb => {
            try {
                cb(errorInfo);
            } catch (e) {
                console.error('Error in error callback:', e);
            }
        });
    }
    
    onError(callback) {
        this.errorCallbacks.push(callback);
        return () => {
            const index = this.errorCallbacks.indexOf(callback);
            if (index > -1) {
                this.errorCallbacks.splice(index, 1);
            }
        };
    }
    
    wrap(fn, context) {
        return async (...args) => {
            try {
                return await fn.apply(context, args);
            } catch (error) {
                this.handleError('Wrapped Function Error', error);
                throw error;
            }
        };
    }
}

8.4.2 插件中使用错误处理

javascript 复制代码
// 在插件中使用错误处理
class SafePlugin extends BaseCustomPlugin {
    constructor(fixedName, setting, i18n) {
        super(fixedName, setting, i18n);
        this.errorHandler = new ErrorHandler(fixedName);
        this.logger = new Logger(fixedName);
        
        // 设置错误回调
        this.errorHandler.onError((error) => {
            this.logger.error('Plugin error:', error);
            this.utils.showNotification(`插件错误: ${error.message}`, 'error');
        });
    }
    
    // 使用错误包装执行敏感操作
    async safeOperation() {
        return this.errorHandler.wrap(async () => {
            // 可能出错的操作
            const data = await this.loadData();
            this.processData(data);
        }, this)();
    }
}

8.5 性能优化策略

8.5.1 优化清单

  1. 减少DOM操作:批量更新DOM,使用DocumentFragment
  2. 防抖和节流:对高频事件使用防抖/节流
  3. 懒加载:按需加载插件模块
  4. 缓存计算结果:使用Memoization
  5. 异步处理:使用async/await避免阻塞
  6. 减少事件监听器:使用事件委托
  7. 优化CSS选择器:避免使用复杂选择器
  8. 限制日志输出:生产环境减少日志

8.5.2 防抖/节流工具

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

// 节流函数
function throttle(fn, limit) {
    let inThrottle = false;
    return function(...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            setTimeout(() => {
                inThrottle = false;
            }, limit);
        }
    };
}

// 使用示例
class OptimizedPlugin extends BasePlugin {
    process() {
        // 防抖处理内容变化
        const debouncedUpdate = debounce(this.updateContent.bind(this), 500);
        this.utils.eventHub.on('content:changed', debouncedUpdate);
        
        // 节流处理滚动事件
        const throttledScroll = throttle(this.handleScroll.bind(this), 100);
        document.addEventListener('scroll', throttledScroll);
    }
}

九、插件发布与分发

9.1 插件打包规范

9.1.1 插件文件结构

复制代码
my-plugin/
├── plugin/
│   └── custom/
│       └── plugins/
│           └── myPlugin.js    # 插件主文件
├── config/
│   └── settings.user.toml     # 配置文件
├── README.md                  # 说明文档
├── LICENSE                    # 许可证
└── package.json               # 包信息(可选)

9.1.2 package.json配置

json 复制代码
{
  "name": "typora-my-plugin",
  "version": "1.0.0",
  "description": "Typora插件描述",
  "main": "plugin/custom/plugins/myPlugin.js",
  "keywords": ["typora", "plugin", "markdown"],
  "author": "Your Name",
  "license": "MIT",
  "typora": {
    "minVersion": "0.9.98",
    "maxVersion": "2.0.0",
    "type": "custom"
  }
}

9.2 版本管理策略

9.2.1 语义化版本

遵循SemVer规范:MAJOR.MINOR.PATCH

  • MAJOR:不兼容的API变更
  • MINOR:向后兼容的功能新增
  • PATCH:向后兼容的问题修复

9.2.2 版本更新日志

markdown 复制代码
# Changelog

## [1.2.0] - 2026-06-01
### Added
- 新增AI辅助写作功能
- 新增代码片段管理

### Fixed
- 修复快捷键冲突问题
- 修复大文件渲染卡顿

## [1.1.0] - 2026-05-15
### Added
- 新增状态栏扩展
- 新增自定义菜单

### Changed
- 优化插件加载性能
- 更新API文档

## [1.0.0] - 2026-05-01
### Added
- 初始版本发布
- 基础插件框架
- Hello World示例

9.3 插件市场发布

9.3.1 发布到GitHub

  1. 创建GitHub仓库
  2. 上传插件源码
  3. 编写README文档
  4. 创建Release版本
  5. 使用GitHub Issues收集反馈

9.3.2 发布到社区

  1. 在Typora社区论坛发布
  2. 在技术博客分享插件开发经验
  3. 在GitHub上提交Pull Request到主项目

9.4 用户反馈收集

javascript 复制代码
// 反馈收集工具
class FeedbackCollector {
    constructor() {
        this.feedbackEndpoint = 'https://api.example.com/feedback';
        this.sessionId = this.generateSessionId();
    }
    
    generateSessionId() {
        return 'session-' + Date.now() + '-' + Math.random().toString(36).substr(2, 8);
    }
    
    async collectFeedback(type, data) {
        try {
            const payload = {
                sessionId: this.sessionId,
                type: type, // 'error', 'feature', 'bug', 'general'
                data: data,
                timestamp: new Date().toISOString(),
                plugin: this.pluginName,
                version: this.version,
                platform: navigator.platform,
                typoraVersion: this.utils.typoraVersion
            };
            
            await fetch(this.feedbackEndpoint, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            });
        } catch (error) {
            console.error('Feedback collection failed:', error);
        }
    }
    
    trackFeatureUsage(featureName) {
        this.collectFeedback('feature_usage', {
            feature: featureName,
            duration: 0
        });
    }
}

9.5 持续集成部署

9.5.1 GitHub Actions配置

yaml 复制代码
# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm install
        
      - name: Run tests
        run: npm test
        
      - name: Build plugin
        run: npm run build
        
      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            dist/*.js
            README.md
            LICENSE

十、安全与最佳实践

10.1 安全编码规范

10.1.1 输入验证

javascript 复制代码
// 输入验证工具
class InputValidator {
    static validatePath(path) {
        // 防止路径遍历攻击
        if (path.includes('..') || path.includes('~')) {
            throw new Error('Invalid path');
        }
        return path;
    }
    
    static validateCommand(command) {
        // 防止命令注入
        const dangerous = [';', '&&', '||', '|', '`', '$('];
        for (const char of dangerous) {
            if (command.includes(char)) {
                throw new Error(`Invalid character in command: ${char}`);
            }
        }
        return command;
    }
    
    static sanitizeHTML(html) {
        // 防止XSS攻击
        const temp = document.createElement('div');
        temp.textContent = html;
        return temp.innerHTML;
    }
}

10.1.2 安全配置

javascript 复制代码
// 安全配置示例
const SECURE_CONFIG = {
    // 禁用危险的eval
    allowEval: false,
    
    // 限制文件系统访问
    fileSystemAccess: {
        allowedPaths: ['/documents', '/projects'],
        deniedPaths: ['/etc', '/system']
    },
    
    // 网络请求限制
    networkAccess: {
        allowedDomains: ['api.example.com'],
        blockedDomains: ['*']
    },
    
    // 插件权限
    permissions: {
        'file:read': true,
        'file:write': false,
        'network:request': true,
        'system:command': false
    }
};

10.2 权限最小化原则

10.2.1 权限声明

javascript 复制代码
// 插件权限声明
class SecurePlugin extends BasePlugin {
    constructor(fixedName, setting, i18n) {
        super(fixedName, setting, i18n);
        
        // 声明所需权限
        this.permissions = {
            'file:read': true,      // 读取文件
            'file:write': false,    // 不写入文件
            'network:request': true, // 网络请求
            'clipboard:read': false, // 不读取剪贴板
            'clipboard:write': true  // 写入剪贴板
        };
        
        // 验证权限
        this.validatePermissions();
    }
    
    validatePermissions() {
        // 检查权限是否被授予
        for (const [perm, required] of Object.entries(this.permissions)) {
            if (required && !this.hasPermission(perm)) {
                throw new Error(`Missing permission: ${perm}`);
            }
        }
    }
    
    hasPermission(perm) {
        // 从配置中检查权限
        return this.config.PERMISSIONS?.[perm] || false;
    }
}

10.3 XSS防护策略

10.3.1 内容安全策略

javascript 复制代码
// CSP配置
class CSPManager {
    static setup() {
        const meta = document.createElement('meta');
        meta.httpEquiv = 'Content-Security-Policy';
        meta.content = `
            default-src 'self';
            script-src 'self' 'unsafe-inline';
            style-src 'self' 'unsafe-inline';
            img-src 'self' data: https:;
            connect-src 'self' https:;
        `;
        document.head.appendChild(meta);
    }
    
    static sanitizeInput(input) {
        // 移除潜在的XSS向量
        return input
            .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
            .replace(/on\w+\s*=/gi, '')
            .replace(/javascript:/gi, '')
            .replace(/data:text\/html/gi, '');
    }
}

10.3.2 安全DOM操作

javascript 复制代码
// 安全的DOM操作
class SafeDOM {
    static createElement(tag, content, attributes = {}) {
        const el = document.createElement(tag);
        
        // 使用textContent而不是innerHTML
        if (content) {
            el.textContent = content;
        }
        
        // 安全设置属性
        for (const [key, value] of Object.entries(attributes)) {
            if (key.startsWith('on')) {
                // 拒绝事件处理器属性
                continue;
            }
            el.setAttribute(key, value);
        }
        
        return el;
    }
    
    static setHTML(element, html) {
        // 使用DOMPurify或类似库清理HTML
        const clean = this.sanitizeHTML(html);
        element.innerHTML = clean;
    }
}

10.4 数据隐私保护

10.4.1 敏感数据处理

javascript 复制代码
// 数据隐私工具
class PrivacyManager {
    constructor() {
        this.encryptionKey = null;
        this.storage = new Map();
    }
    
    // 加密存储敏感数据
    async storeSecure(key, data) {
        const encrypted = await this.encrypt(data);
        this.storage.set(key, encrypted);
    }
    
    async retrieveSecure(key) {
        const encrypted = this.storage.get(key);
        if (!encrypted) return null;
        return await this.decrypt(encrypted);
    }
    
    async encrypt(data) {
        // 使用Web Crypto API
        const encoder = new TextEncoder();
        const dataBuffer = encoder.encode(JSON.stringify(data));
        
        // 生成或获取密钥
        const key = await this.getEncryptionKey();
        const iv = crypto.getRandomValues(new Uint8Array(12));
        
        const encrypted = await crypto.subtle.encrypt(
            { name: 'AES-GCM', iv },
            key,
            dataBuffer
        );
        
        return {
            iv: Array.from(iv),
            data: Array.from(new Uint8Array(encrypted))
        };
    }
    
    async decrypt(encrypted) {
        const key = await this.getEncryptionKey();
        const iv = new Uint8Array(encrypted.iv);
        const data = new Uint8Array(encrypted.data);
        
        const decrypted = await crypto.subtle.decrypt(
            { name: 'AES-GCM', iv },
            key,
            data
        );
        
        const decoder = new TextDecoder();
        return JSON.parse(decoder.decode(decrypted));
    }
    
    async getEncryptionKey() {
        if (this.encryptionKey) return this.encryptionKey;
        
        // 从安全存储获取或生成密钥
        this.encryptionKey = await crypto.subtle.generateKey(
            { name: 'AES-GCM', length: 256 },
            true,
            ['encrypt', 'decrypt']
        );
        
        return this.encryptionKey;
    }
}

10.5 代码审查要点

10.5.1 审查清单

  1. 安全性:是否存在XSS、注入、信息泄露风险?
  2. 性能:是否存在性能瓶颈、内存泄漏?
  3. 可靠性:错误处理是否完善?
  4. 兼容性:是否兼容不同Typora版本?
  5. 可维护性:代码是否清晰、有注释?
  6. 国际化:是否支持多语言?
  7. 文档:是否有完整的README和使用说明?

10.5.2 代码质量检查

javascript 复制代码
// ESLint配置示例
module.exports = {
    env: {
        browser: true,
        node: true,
        es2021: true
    },
    extends: ['eslint:recommended'],
    parserOptions: {
        ecmaVersion: 2021,
        sourceType: 'module'
    },
    rules: {
        'no-console': 'warn',
        'no-unused-vars': 'error',
        'no-eval': 'error',
        'no-implied-eval': 'error',
        'no-new-func': 'error',
        'no-script-url': 'error',
        'no-unsafe-finally': 'error',
        'require-await': 'warn',
        'max-lines': ['warn', 300],
        'complexity': ['warn', 10]
    }
};

十一、真实案例分析

11.1 代码片段管理器插件

11.1.1 功能需求

  • 管理常用代码片段
  • 支持分类和标签
  • 快速插入到文档
  • 支持自定义片段

11.1.2 技术实现

javascript 复制代码
// 完整代码片段管理器(见6.2节)
// 核心功能:
// 1. 片段存储 - 使用JSON文件持久化
// 2. 片段搜索 - 支持名称、描述、语言搜索
// 3. 片段插入 - 支持占位符和Tab跳转
// 4. 片段管理 - 增删改查操作

11.2 实时协作编辑插件

11.2.1 架构设计

javascript 复制代码
// 协作编辑基础框架
class CollaborationPlugin extends BasePlugin {
    constructor(fixedName, setting, i18n) {
        super(fixedName, setting, i18n);
        this.roomId = null;
        this.peers = new Map();
        this.operations = [];
        this.websocket = null;
    }
    
    process() {
        this.setupWebSocket();
        this.setupOTEngine();
        this.setupPresence();
    }
    
    setupWebSocket() {
        // WebSocket连接
        const wsUrl = this.config.WS_URL || 'ws://localhost:8080';
        this.websocket = new WebSocket(wsUrl);
        
        this.websocket.onmessage = (event) => {
            const data = JSON.parse(event.data);
            this.handleRemoteOperation(data);
        };
    }
    
    setupOTEngine() {
        // 操作变换引擎
        // 实现OT算法处理并发编辑
    }
    
    setupPresence() {
        // 用户在线状态
        // 光标位置同步
        // 选区和高亮
    }
}

11.3 智能目录生成插件

11.3.1 功能实现

javascript 复制代码
// 智能目录生成(见5.5节侧边栏面板)
// 核心功能:
// 1. 自动提取标题层级
// 2. 生成可点击的目录树
// 3. 支持滚动跟随
// 4. 支持折叠/展开

11.4 图片优化处理插件

11.4.1 功能实现

javascript 复制代码
// 图片优化插件
class ImageOptimizerPlugin extends BaseCustomPlugin {
    selector = () => 'img';
    
    hint = () => '🖼️ 优化图片';
    
    callback = (anchorNode) => {
        this.optimizeImage(anchorNode);
    }
    
    async optimizeImage(imgElement) {
        const src = imgElement.src;
        
        // 检查是否为本地图片
        if (src.startsWith('file://')) {
            // 压缩图片
            const compressed = await this.compressImage(src);
            // 更新图片
            imgElement.src = compressed;
            // 显示优化结果
            this.utils.showNotification('图片已优化');
        }
    }
    
    async compressImage(filePath) {
        // 使用sharp或类似库压缩图片
        // 返回压缩后的base64或路径
    }
}

11.5 多语言翻译插件

11.5.1 功能实现

javascript 复制代码
// 翻译插件
class TranslationPlugin extends BaseCustomPlugin {
    selector = () => 'p, h1, h2, h3, h4, h5, h6, blockquote';
    
    hint = () => '🌐 翻译选中内容';
    
    callback = (anchorNode) => {
        const text = anchorNode.textContent;
        this.translateText(text);
    }
    
    async translateText(text) {
        // 调用翻译API
        const targetLang = this.config.TARGET_LANG || 'zh-CN';
        const result = await this.callTranslationAPI(text, targetLang);
        
        // 显示翻译结果
        this.showTranslationPopup(result);
    }
    
    showTranslationPopup(translation) {
        // 创建浮动显示翻译结果
        const popup = document.createElement('div');
        popup.style.cssText = `
            position: fixed;
            background: white;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 15px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.2);
            z-index: 10000;
            max-width: 400px;
        `;
        popup.textContent = translation;
        document.body.appendChild(popup);
        
        // 定位到鼠标位置
        // 添加关闭按钮
        // 支持替换原文
    }
}

十二、未来展望与生态

12.1 Typora插件生态现状

当前Typora插件生态主要围绕obgnail/typora_plugin项目展开,该项目提供了:

  • 60+内置插件:涵盖标签页管理、多关键词搜索、代码增强、图表支持等功能
  • 活跃的社区:GitHub上有持续更新和贡献
  • 丰富的扩展点:快捷键、菜单、按钮、JSON-RPC等多种扩展方式

12.2 新版本API展望

随着Typora版本更新,插件API可能的发展方向:

  1. 官方API支持:Typora官方可能提供正式插件API
  2. TypeScript支持:更好的类型定义和开发体验
  3. 更丰富的UI组件:标准化的UI组件库
  4. 性能优化:更高效的插件加载和运行机制

12.3 跨平台兼容性

Typora基于Electron,天然支持跨平台:

  • Windows:支持Windows 7及以上
  • macOS:支持macOS 10.12及以上
  • Linux:支持主流Linux发行版

插件开发时需注意:

  • 路径分隔符差异(/ vs \
  • 快捷键差异(Ctrl vs Cmd
  • 文件系统权限差异

12.4 社区贡献指南

12.4.1 如何贡献

  1. 报告问题:在GitHub提交Issue
  2. 提交代码:Fork仓库,创建PR
  3. 编写文档:完善README和API文档
  4. 分享经验:撰写博客和教程

12.4.2 贡献规范

  • 遵循代码风格
  • 添加单元测试
  • 更新文档
  • 保持向后兼容

12.5 企业级应用前景

Typora插件在企业级场景中的应用:

  1. 技术文档平台:集成企业文档管理系统
  2. 知识库建设:团队协作写作工具
  3. 自动化出版:集成CI/CD流水线
  4. 数据报告生成:自动化报告生成工具

十三、总结

13.1 核心要点回顾

  1. 架构理解:Typora基于Electron,插件系统采用三层架构
  2. 开发基础:使用JavaScript/HTML/CSS扩展Typora功能
  3. 插件类型:BasePlugin(复杂核心插件)和BaseCustomPlugin(上下文感知插件)
  4. 生命周期:七阶段生命周期模型
  5. 核心API:IPlugin、Utils框架、事件系统
  6. 扩展方式:快捷键、菜单、按钮、JSON-RPC等多种方式

13.2 学习路径建议

第一阶段:基础入门(1-2周)

  • 了解Typora插件系统架构
  • 掌握IPlugin和BaseCustomPlugin基类
  • 完成Hello World插件

第二阶段:实践进阶(2-4周)

  • 开发自定义菜单和快捷键插件【5.2、5.3节】
  • 实现UI扩展和状态栏插件【5.4、5.5节】
  • 学习Utils框架和事件系统

第三阶段:高级开发(4-8周)

  • 开发AI辅助写作插件【6.5节】
  • 集成JSON-RPC外部API【6.6节】
  • 性能优化和调试【第八章】

第四阶段:生态贡献(持续)

  • 发布插件到社区【第九章】
  • 参与开源项目贡献【12.4节】
  • 分享经验和最佳实践

13.3 开发者成长路线

复制代码
初学者 → 了解Electron和Typora架构
       ↓
插件开发者 → 掌握BaseCustomPlugin开发
       ↓
高级开发者 → 掌握BasePlugin和系统级扩展
       ↓
核心贡献者 → 参与框架开发和生态建设
       ↓
技术专家 → 架构设计和最佳实践推广

十四、附录

14.1 API参考手册

14.1.1 IPlugin生命周期方法

方法 返回值 说明
beforeProcess() Promise/void 预处理,可返回stopLoadPluginError阻止加载
style() string 返回CSS样式字符串
styleTemplate() object 返回样式模板参数
html() string 返回HTML字符串
hotkey() object 返回快捷键映射
init() void 初始化插件
process() void 主处理逻辑
afterProcess() void 清理资源

14.1.2 Utils框架核心方法

方法 说明
getFilePath() 获取当前文件路径
getHomeDir() 获取用户主目录
getBasePlugin(name) 获取基础插件实例
getCustomPlugin(name) 获取自定义插件实例
callPluginFunction(name, func, ...args) 调用其他插件方法
registerStyle(name, css) 注入样式
showNotification(msg) 显示通知

14.1.3 事件列表

事件 说明
typora:ready Typora加载完成
file:opened 文件打开
file:saved 文件保存
content:changed 内容变化
allPluginsHadInjected 所有插件注入完成

14.2 快捷键大全

14.2.1 常用快捷键

快捷键 功能
Ctrl+Shift+I 打开开发者工具
Ctrl+Shift+F 格式化文档
Ctrl+Shift+S 保存并导出
Ctrl+Alt+T 插入表格

14.2.2 自定义快捷键配置

javascript 复制代码
// 在hotkey()方法中注册
hotkey() {
    return {
        'Ctrl+Shift+M': this.myFunction.bind(this),
        'Ctrl+Alt+R': this.runScript.bind(this)
    };
}

14.3 常见问题解答

Q1: 插件安装后不生效?

解决方案

  1. 确认插件目录位置正确
  2. 检查Typora版本是否支持插件系统(需要0.9.86以上版本)
  3. 彻底退出Typora,清缓存后重启
  4. 检查配置文件是否正确启用插件

Q2: 如何调试插件?

  1. 开启调试模式
  2. 使用Ctrl+Shift+I打开开发者工具
  3. 在Console中查看日志输出
  4. 使用console.log()输出调试信息

Q3: 插件更新后不生效?

  1. 彻底退出Typora
  2. 清缓存
  3. 重启Typora

Q4: 如何获取当前文档内容?

javascript 复制代码
const content = this.utils.getEditorContent();

Q5: 如何插入文本到编辑器?

javascript 复制代码
this.utils.insertText('要插入的文本');

14.4 学习资源推荐

14.4.1 官方资源

14.4.2 社区资源

  • CSDN Typora专栏
  • DeepWiki Typora插件文档
  • V2EX Typora讨论区

14.4.3 推荐阅读

  • 《Electron实战》
  • 《JavaScript高级程序设计》
  • 《Node.js设计模式》

14.5 术语表

术语 说明
Electron 跨平台桌面应用框架
渲染进程 负责渲染UI的进程
主进程 负责管理应用生命周期的进程
IPlugin 插件基类接口
BasePlugin 基础插件类
BaseCustomPlugin 自定义插件类
Utils 工具框架
生命周期 插件从加载到卸载的过程
Hook 钩子函数,在特定时机执行
JSON-RPC JSON远程过程调用协议

十五、详细资料

15.1 官方文档索引

  1. Typora官方文档https://support.typora.io/
  2. 插件框架文档https://github.com/obgnail/typora_plugin
  3. DeepWiki文档https://deepwiki.com/obgnail/typora_plugin
  4. Custom Plugin Developmenthttps://deepwiki.com/obgnail/typora_plugin/17-custom-plugin-development

15.2 第三方库推荐

库名 用途 说明
lodash 工具函数 数据处理和操作
axios HTTP请求 API调用
marked Markdown解析 文档处理
highlight.js 语法高亮 代码高亮
mermaid 图表绘制 流程图、时序图

15.3 示例代码仓库

  1. typora_plugin主仓库https://github.com/obgnail/typora_plugin
  2. 插件示例集合:plugin/custom/plugins/目录下的示例
  3. 社区插件:GitHub上搜索"typora plugin"

15.4 社区论坛资源

  1. GitHub Issueshttps://github.com/obgnail/typora_plugin/issues
  2. CSDN Typora专栏https://blog.csdn.net/
  3. V2EXhttps://www.v2ex.com/
  4. Typora官方论坛https://support.typora.io/Community/

15.5 进阶学习路径

15.5.1 前端技术栈

  1. JavaScript ES6+:现代JavaScript语法
  2. DOM操作:文档对象模型操作
  3. 事件系统:事件捕获和冒泡
  4. 异步编程:Promise、async/await
  5. CSS3:布局、动画、响应式

15.5.2 桌面应用开发

  1. Electron基础:主进程和渲染进程
  2. IPC通信:进程间通信
  3. Node.js集成:文件系统、网络
  4. 打包发布:electron-builder

15.5.3 插件工程化

  1. 模块化:ES Module、CommonJS
  2. 测试:单元测试、集成测试
  3. CI/CD:持续集成和部署
  4. 文档生成:API文档自动化

本文完,全文约40000字,包含100+代码示例和50+最佳实践建议。

最后更新:2026年7月