202510
web

构建 GitCasso:利用 Prism.js 实现 GitHub 评论动态语法高亮与 localStorage 草稿恢复

通过 Chrome 扩展,使用 Prism.js 为 GitHub 评论提供实时语法高亮,并结合 localStorage 实现草稿自动保存,提升编辑无缝性。

在 GitHub 的 issue 和 pull request 评论编辑过程中,开发者常常面临代码块缺乏实时语法高亮的困扰,这不仅影响代码的可读性,还容易因意外关闭页面而丢失宝贵的草稿内容。构建一个如 GitCasso 般的 Chrome 扩展,可以有效解决这些痛点:通过注入 Prism.js 实现动态语法高亮,让 markdown 中的代码块即时呈现彩色高亮效果;同时,利用 localStorage 机制自动保存编辑内容,支持断线续传和历史恢复,确保工作不被中断。这种方案的核心观点在于,将浏览器原生存储与轻量级高亮库相结合,提供无缝的编辑体验,而非依赖 GitHub 的原生功能,从而避免平台更新的不确定性。

Prism.js 作为一种高效的语法高亮工具,其优势在于体积小巧(核心库仅 2KB 左右)和易于扩展,支持超过 100 种编程语言。通过在扩展的内容脚本中加载 Prism.js,我们可以针对 GitHub 评论的 textarea 或 contenteditable 元素进行实时渲染。高亮过程的证据在于,Prism.js 通过 tokenize 机制解析代码片段,并应用 CSS 类来实现颜色区分,例如将 JavaScript 的关键字如 'function' 标记为蓝色,这大大提高了代码审查的准确率。根据 Prism.js 官方文档,“Prism is a lightweight, robust, and elegant syntax highlighter.” 这确保了在动态输入场景下的性能稳定,不会显著拖慢页面加载。

对于草稿恢复,localStorage 的 setItem 和 getItem API 提供了简单可靠的持久化方式。观点是,定期序列化整个评论内容并以 URL + 时间戳为键存储,能有效防止数据丢失。证据显示,在多标签页环境下,localStorage 的同步特性允许跨会话恢复,而其 5MB 容量上限足以容纳典型评论(平均 1-2KB)。实现时,需要监听 input 事件,每 5 秒触发一次保存,避免频繁 I/O 操作导致的性能瓶颈。同时,引入版本控制,如存储时附加时间戳,便于用户从弹出菜单中选择恢复特定版本。

要落地这个扩展,首先配置 manifest.json 文件。使用 Manifest V3 版本,确保兼容性。关键参数包括:

  • "manifest_version": 3,

  • "name": "GitCasso",

  • "version": "1.0",

  • "permissions": ["storage"], // 仅需 localStorage 权限,减少安全风险

  • "content_scripts": [{

    "matches": ["https://github.com/*"],

    "js": ["prism.js", "content.js"],

    "css": ["prism.css"],

    "run_at": "document_end"

}]

这限制了扩展仅在 GitHub 域名激活,注入 Prism 的 JS 和 CSS 文件。内容脚本 content.js 中,首先检测评论编辑器元素:使用 querySelector('.js-comment-field') 或类似选择器定位 textarea。如果检测到,初始化 Prism 高亮:为 markdown 代码块添加 Prism 的类,如 Prism.highlightElement(codeElement),但针对实时输入,需要一个 overlay div 镜像 textarea 内容,并应用高亮。参数建议:高亮延迟 300ms,使用 debounce 函数防抖动输入事件;支持语言默认为 'markdown',自动检测 fenced code blocks 如 ```javascript。

草稿保存逻辑在 content.js 中实现一个 AutoSave 类:

class AutoSave {

constructor(textarea, url) {

this.textarea = textarea;

this.url = url;

this.saveInterval = 5000;  // 5 秒间隔

this.draftKey = `gitcasso-draft-${encodeURIComponent(url)}-${Date.now()}`;

this.init();

}

init() {

this.loadDraft();

this.textarea.addEventListener('input', debounce(this.save.bind(this), this.saveInterval));

// 添加恢复按钮到 UI

this.addRestoreButton();

}

save() {

const content = this.textarea.value;

if (content.trim()) {

  localStorage.setItem(this.draftKey, content);

  // 保留最近 5 个版本

  this.cleanupOldDrafts();

}

}

loadDraft() {

const drafts = this.getDrafts();

if (drafts.length > 0) {

  const latest = drafts[drafts.length - 1];

  this.textarea.value = localStorage.getItem(latest) || '';

}

}

getDrafts() {

const prefix = `gitcasso-draft-${encodeURIComponent(this.url)}`;

const keys = [];

for (let i = 0; i < localStorage.length; i++) {

  const key = localStorage.key(i);

  if (key.startsWith(prefix)) keys.push(key);

}

return keys.sort().slice(-5);  // 最近 5 个

}

cleanupOldDrafts() {

const drafts = this.getDrafts();

if (drafts.length > 5) {

  for (let i = 0; i < drafts.length - 5; i++) {

    localStorage.removeItem(drafts[i]);

  }

}

}

addRestoreButton() {

// 创建弹出菜单,列出 drafts,点击恢复

const button = document.createElement('button');

button.textContent = '恢复草稿';

button.onclick = () => {

  const drafts = this.getDrafts();

  // 显示简单下拉或 alert 选择

  if (drafts.length > 0) {

    const choice = prompt('选择恢复的草稿索引 (0 为最新):', '0');

    if (choice !== null) {

      const index = parseInt(choice);

      if (index >= 0 && index < drafts.length) {

        this.textarea.value = localStorage.getItem(drafts[index]) || '';

      }

    }

  }

};

this.textarea.parentNode.appendChild(button);

}

}

实例化时:new AutoSave(textarea, window.location.href);

对于高亮集成,引入一个实时高亮函数:

function applyRealTimeHighlight(textarea) {

const overlay = document.createElement('div');

overlay.className = 'prism-overlay';

overlay.style.cssText = `

position: absolute;

top: 0; left: 0;

background: white;

pointer-events: none;

white-space: pre-wrap;

font-family: inherit;

font-size: inherit;

line-height: inherit;

padding: inherit;

border: inherit;

`;

textarea.parentNode.style.position = 'relative';

textarea.parentNode.appendChild(overlay);

function updateOverlay() {

const value = textarea.value;

// 简单解析 markdown 代码块并高亮

const highlighted = Prism.highlight(value, Prism.languages.markdown, 'markdown');

overlay.innerHTML = highlighted;

}

textarea.addEventListener('input', debounce(updateOverlay, 300));

updateOverlay();

}

这模拟了 overtype 的效果,但使用 Prism。注意,Prism 默认不支持实时 overlay,需要自定义 CSS 匹配 textarea 尺寸:overlay.width = textarea.scrollWidth; 等。

风险与限制:GitHub 的选择器可能因 A/B 测试变化,建议使用 MutationObserver 监听 DOM 变动,每 2 秒轮询一次元素存在。localStorage 在 incognito 模式下不可用,可 fallback 到 sessionStorage。权限最小化,避免 "activeTab" 除非必要。

监控要点清单:

  1. 保存成功率:日志每次 save 的结果,目标 >99%。

  2. 高亮性能:测量 input 后渲染时间 <100ms。

  3. 存储使用:定期检查 localStorage.usedQuota,警报 >80%。

  4. 兼容性测试:针对 GitHub 桌面/移动视图,确保高亮不干扰提交按钮。

回滚策略:如果高亮冲突,提供 toggle 开关 via chrome.storage.sync.set({enabled: false})。

通过以上参数和清单,这个扩展可以快速迭代部署。Gitcasso 的开源实现证明了这种方法的实用性,“Syntax highlighting and autosave for comments on GitHub (and other markdown-friendly websites)。” 开发者可 fork 该 repo,替换 highlight.js 为 Prism.js,进一步定制。最终,这种工具不仅提升个人效率,还能推广到团队协作中,减少因编辑挫败导致的沟通障碍。

(字数约 1250)