Search This Blog

为 Blogger HTML 编辑页面添加代码标签的快捷方式

动机

在blogger中写博客的时候经常需要插入代码,通过在HTML编辑模式下为代码段添加<code>...</code>标签实现,一旦插入代码较多这个过程就显得比较繁琐了;因此考虑开发一个chrome的插件实现这个功能。主要的需求描述如下:选中需要添加代码HTML标签的代码段,右键菜单中显示需要添加代码段的模式,点击后将添加了HTML标签的代码段替换选中的原代码段

Demo


设计思路

预期实现的功能:为所选的可编辑内容弹出右键菜单,根据相应的标签选择替换所选内容。
为了实现这个功能,首先要对chrome插件的架构有一定的了解。下图给出了Chrome插件的插件的一个基本框架图。(由来源作者总结,感觉很清晰)
值得注意的是:Chrome的插件是有一个独立的运行环境的,可以是popup或者background,均可以是由HTML/CSS/JavaScript编写,用于呈现插件的显示或者功能。而此外Chrome还提供了content script的方式,用于在“匹配的”网页中注入(injection)content script脚本,从而获取或者操作页面上的DOM(Document Object Model)对象。注意:处于安全性的考虑,content script只对页面上的DOM对象有操作的权限,而对页面中的JavaScript脚本或其中出现的变量等均无访问的权限。
Message机制:目前看来,content script与插件之间是相互隔离的,无法交互;而实际上Chrome还提供了一个Message的方法为content script与插件之间提供了通信的方式。例如:可以在content script中发送消息(sendMessage),而在background script中注册该消息的监听(onMessage.addListener),如此便可以实现content script通知background script的需求了;反之类似。
来源: Chrome插件(Extensions)开发攻略
插件框架:有了以上对Chrome插件架构的基本了解以后,就可以设计本插件的框架图了,如下图所示。在extension的background页面调用了contextMenus这个API将插件功能添加到右键菜单中,为“selection”类型的内容(DOM对象)创建了该插件的右键菜单,并且在创建时绑定了“onclick”方法,在其中调用了sendMessage方法,以此通知content script当前选择了需要替换的内容以及需要添加的标签类型。另一方面,在content script中,注册了onMessage方法,从而可以监听background中发送的消息。在onMessage方法中,实现了对“selection”内容的添加标签后替换的功能。
设计思路:插件框架图

实现

根据以上的设计思路,完成代码如下。(完整项目地址:CodeTag)
Chrome 插件下载地址: Code Tag
  • manifest.json
  • {
        "name": "Code Tag",
        "description": "This extension helps add html tag for editable selected context",
        "version": "0.2",
        "permissions": [
            "contextMenus"
        ],
        "content_scripts": [{
                "matches": ["<all_urls>"],
                "js": ["addTag.js"]
            }
        ],
        "background": {
            "scripts": [
                "background.js"
            ]
        },
        "icons": {
            "128": "icon.png"
        },
        "manifest_version": 2
    }
  • background.js
  • // parent menu
    var parent = chrome.contextMenus.create({
            "title": "Code Tag",
            "contexts": ["selection"]
        });
    
    // sub-menu for block style
    chrome.contextMenus.create({
        "title": "Block",
        "parentId": parent,
        "contexts": ["selection"],
        "onclick": function (info, tab) {
            if (info.editable) {
                chrome.tabs.query({
                    "active": true,
                    "currentWindow": true
                }, function (tabs) {
                    chrome.tabs.sendMessage(tabs[0].id, {
                        // codetag message, indicating block style
                        "codetag": "block"
                    });
                });
            }
        }
    });
    
    // sub-menu for inline style
    chrome.contextMenus.create({
        "title": "Inline",
        "parentId": parent,
        "contexts": ["selection"],
        "onclick": function (info, tab) {
            if (info.editable) {
                chrome.tabs.query({
                    "active": true,
                    "currentWindow": true
                }, function (tabs) {
                    chrome.tabs.sendMessage(tabs[0].id, {
                        // codetag message, indicating inline style
                        "codetag": "inline"
                    });
                });
            }
        }
    });
  • addTag.js (content script)
  • // trancode "<" and ">" in code snippet into html coding
    function transCode(text) {
        var newStr;
        newStr = text.replace(/</g, "<");
        newStr = newStr.replace(/>/g, ">");
        return newStr;
    }
    
    // register listener on message, fired when a sendMessage called in background
    // in this function, selection is recognized and replaced with tag added context
    // different tags are determined by the message sent from background
    chrome.extension.onMessage.addListener(function (message, sender, callback) {
        // get selection
        var sel = window.getSelection();
        var codeWithTag;
    
        if (message.codetag == "block") {
            codeWithTag = "<pre><code>" + transCode(sel.toString()) + "</code></pre>";
        }
        if (message.codetag == "inline") {
            codeWithTag = "<code>" + transCode(sel.toString()) + "</code>";
        }
        var elem = document.activeElement;
        var start = elem.selectionStart;
        var end = elem.selectionEnd;
        elem.value = elem.value.slice(0, start) + codeWithTag + elem.value.substr(end);
        // Set cursor after selected text
        elem.selectionStart = start + codeWithTag.length;
        elem.selectionEnd = elem.selectionStart;
    });
目前实现的功能是两个,分别可以添加<code>...</code> 或者<pre><code>...</code></pre>标签,从而实现Inline(嵌入行内)和Block(代码块)模式。(:为了实现语法高亮,可以参考之前的文章:使用 highlight.js 高亮博文中的代码

遇到的问题

1. getSelection无法获取预期的内容
起初,因为没有搞清楚Chrome插件的架构,导致踩了不少雷,其中就包括遇到getSelection方法无法获取预期内容的问题;最初设计插件的时候只考虑了background,而未添加content script,这样一来实际上是无法通过JavaScript代码访问到页面内容的。而getSelection是属于JavaScript中的方法。准确来说,在background中调用getSelection也只是在background的页面中进行操作,也就是说如果访问DOM对象,例如document,指代的是background页面,而我当时以为是当前的网页,这也就造成了getSelection无法获取预期内容的现象。

2. info.selectionText 将换行符替换为空格
接上一个问题,由于无法获取getSelection的预期内容,那么后续的操作也就无从进行下去。改变思路找到Chrome的contextMenus中的info.selectionText属性,可以获取在菜单创建时所选择的文本。看上去可以解决需求。但是当选中的文本中出现换行符时,就不能如愿了,换行符被替换为空格。这个方案再次失效。其中selectionText将换行符替换为空格的原因可能在于Chrome在菜单的显示中提供了“%s”直接转换选中文本的方式,如果保留换行符那么在菜单显示的时候就不方便了。

3. 插件的Debug窗口无法看到content script
最终找到了 Chrome插件(Extensions)开发攻略 这篇文章,对Chrome插件的架构有了重新的认识,意识到可以通过content script实现需求。与此同时也了解了Chrome的Debug工具。但是在调试的过程中又遇到了新的问题。原本是在插件的背景页启动了Debug页面,而在其中的content script部分并未发现所写的content script代码,无从调试。原因在于content script代码是注入到匹配的页面中,而不是插件的背景页,所以要调试content script需要在网页中进行审查(Inspector),启动Debug。

4. contextMenus同时满足两个条件
需求中是要对可编辑editable)的选中内容selection)进行替换,contextMenus可以根据点击发生的对象类型弹出相应的右键菜单,并且可以对不同的对象类型进行“OR”操作,但是并不支持“AND”操作,因此仅对于可编辑的选中内容右键弹出该插件菜单的操作是不能“直接”实现的。简单地变通方式可以如下:在contextMenus中只关注“selection”类型的对象,并且检查对象的“info.editable”属性进行判断,如果为真则触发sendMessage方法,否则不进行处理。

参考

1 comment:

  1. 如果只是想简单实现:与正文不同的字体(等宽字体)、字号, 与正文的显示有所区分。
    可以利用blockquote的CSS自定义:https://zelikk.blogspot.com/2018/12/blogger-css-block-quote-code.html

    ReplyDelete