构建 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" 除非必要。
监控要点清单:
-
保存成功率:日志每次 save 的结果,目标 >99%。
-
高亮性能:测量 input 后渲染时间 <100ms。
-
存储使用:定期检查 localStorage.usedQuota,警报 >80%。
-
兼容性测试:针对 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)