1. 簡介
這是一個chrome-extension,實現了一個增強文字復制的功能,我給它起名叫做Copy with URL。
它能夠將選中的純文本變身為超鏈接文本,你可以將超鏈接文本粘貼在其他支持插入超鏈接文本的編輯器中,比如Evernote,Word或者Excel。
有了Copy with URL,從此在需要插入超鏈接文本的時候,就不再需要復制文本>粘貼文本>復制鏈接>編輯鏈接+各種切換程序窗口的繁瑣操作。
如果你有頻繁使用超鏈接文本的習慣,那么它將極大提升你的工作效率,讓你更加專注於工作本身。
項目代碼見Copy-with-URL,涉及到的技術見文末總結。
2. 功能
使用它和正常的復制粘貼文本基本沒有區別,兩張圖來展示該擴展的使用方法和效果。

step1:選中網頁的純文本文字(文字原本是超鏈接文本也可以)后單機鼠標右鍵,在右鍵菜單中點擊Copy with URL選項。此時,文字及其所在網頁的URL就以超鏈接形式存儲在了系統剪貼板中。

step2:這就是將剛復制的超鏈接文本粘貼在word中的效果。受實現方式的限制,粘貼的內容消除了原本的字體等格式。
3. 項目闡述(相關工作)
該擴展源碼基於chrome-extension:Copy as Markdown Quotation進行改動開發,這里首先對原作者表示感謝!
Copy as Markdown Quotation實現了將純文本轉變為Markdown格式的超鏈接文本,粘貼在Word中的效果就是一段純文本后跟了一個本質上是文本的鏈接。
本擴展將純文本和URL組合成超鏈接文本,雖然只是一個小改動,但是對於我來說,還是廢了幾番周折,所以我將開發過程和所用到的技術記錄在此。
4. 實現
4.1 總體設計
需要明確的是:超鏈接文本和文本的區別在於超鏈接文本實際上是一個HTML頁的<a>標簽。知道了這一點后,開發的目標就定位成將在HTML頁內選定或創建的 <a> 標簽復制到剪貼板。其實,我之前很長時間都沒弄明白超鏈接文本和純文本的區別,以至於不知道如何下手。如果用純文本寫一段 <a href="url">value</a> ,把它復制到任一編輯器,得到的仍然是這段文本,因為這並不是一個DOM對象(不確定這樣說夠不夠准確)。
此擴展首先獲取選中對象,之后對選中的對象進行解析,提取出文本,標簽等信息。得到信息后,再將文本和URL組合成<a>標簽,最后將標簽復制到系統剪貼板。
js實現復制到剪貼板功能,兼容所有瀏覽器介紹了用js實現復制功能的多種方案,Copy as Markdown Quotation采用的是js自帶的 document.execCommand('copy');方法,但該方法貌似只能復制純文本內容,反正我沒有改動成功。
本工程使用的是clipboard.js框架,選它是因為看中它輕量級,易操作的特性。關鍵是它最終實現了復制帶鏈接的文本。
4.2 工程目錄
該工程包含的源代碼文件較少,所有的文件都在一個文件夾下。具體的文件有:
| manifest.json
| background.js
| background-cp.js
| clipboard.min.js
| icon16.png
| icon48.png
| icon128.png
4.3 manifest.json
manifest.json對於一個chrome-extension是很重要的文件,它的存在決定了extension的形態。
{ "background": { "scripts": [ "background-cp.js" ] }, "description": "Copy text in web page with URL", "icons": { "128": "icon128.png", "16": "icon16.png", "48": "icon48.png" }, "manifest_version": 2, "minimum_chrome_version": "24", "name": "Copy with URL", "permissions": [ "tabs", "storage", "contextMenus", "notifications", "file://*/","http://*/", "https://*/" ], "update_url": "https://clients2.google.com/service/update2/crx", "version": "0.4.1" }
在上面的manifest.json中,值得一提的是"permissions"中的contextMenus為該擴展獲取了操作瀏覽器右鍵菜單的權限。"background"屬性內的background-cp.js指示了該擴展在后台運行的js代碼,"background"還可以指定擴展運行在后台的html頁面。沒有指定html的話,瀏覽器會為extension創建一個空白的html頁。
4.4 backgroud-cp.js
該擴展的所有行為都定義在了background-cp.js內,代碼有點長,但只有幾個函數而已。
document.write("<script type='text/javascript' src='clipboard.min.js'></script>");
var get_selection = function() {
var selection = document.getSelection();
var text = selection.toString();
var node = selection.getRangeAt(0).startContainer;
var uri = node.baseURI || document.documentURI;
var parent = node.parentElement;
var whiteSpace = (parent && window.getComputedStyle(parent)['white-space']);
var index;
var ext;
var is_code = function(elem) {
if (!elem) return false;
// Is the element monospace?
if (window.getComputedStyle(elem)['white-space'].toLowerCase() == 'pre') {
return true;
}
// Is the element generated by CodeMirror?
if (elem.className.toLowerCase().split(' ').indexOf('codemirror') >= 0) {
return true;
}
return false;
}
var pre = is_code(parent);
console.log(pre)
var get_frag = function(parent) {
var frag, sibling, nephew;
if (!parent) {
return null;
}
frag = parent.id || parent.name;
if (frag) {
return frag;
}
sibling = parent.previousSibling;
while(sibling) {
frag = sibling.id || sibling.name;
if (frag) {
return frag;
}
nephew = sibling.children && sibling.children[0];
frag = nephew && (nephew.id || nephew.name);
if (frag) {
return frag;
}
sibling = sibling.previousSibling;
}
}
var fileName;
var orig_frag;
var frag;
// Remove the fragment from the url and find the better one only if the
// original one was not semantically significant.
index = uri.lastIndexOf('#');
console.log(index)
orig_frag = index >= 0 ? uri.substring(index + 1) : null;
if (!orig_frag || orig_frag.indexOf('/') < 0) { // Assume the fragment is siginificant if it contains '/'.
uri = index >= 0 ? uri.substring(0, index) : uri;
while(!frag && parent) {
frag = get_frag(parent);
parent = parent.parentElement;
}
}
// Get extension from the url
index = uri.lastIndexOf('/');
fileName = index >= 0 ? uri.substring(index + 1) : '';
index = fileName.lastIndexOf('.');
ext = index >= 0 ? fileName.substring(index + 1) : '';
if (frag) {
uri += '#' + frag;
}
return {
text: text,
uri: uri,
pre: pre,
ext: ext
};
}
var ltrim_lines = function(str) {
return str.replace(/^(\s*\n)*/, '');
}
var rtrim = function(str) {
return str.replace(/[\s\n]*$/, '');
}
var copy_as_markdown_quot = function (args) {
chrome.tabs.executeScript( {
code: "(" + get_selection + ")();"
}, function(selections) {
var text = rtrim(ltrim_lines(selections[0].text));
var uri = selections[0].uri;
var pre = selections[0].pre;
var ext = selections[0].ext;
if (text) {
lines = text.split('\n');
result = '';
if (pre) result += '> ```' + ext + '\n';
for (var i = 0; i < lines.length; i++) {
result += lines[i] + '\n';
}
if (pre) result += '> ```\n'
copyTextToClipboard(result,uri);
}
});
};
function copyTextToClipboard(text,uri) {
var copyFrom,agent,body;
copyFrom = document.createElement("a");
copyFrom.setAttribute("id","target");
copyFrom.setAttribute("href",uri);
copyFrom.innerHTML = text;
agent = document.createElement("button");
body = document.getElementsByTagName('body')[0];
body.appendChild(copyFrom);
body.appendChild(agent);
// 采用Clipboard.js方案
// trouble:沒有可以傳入的HTML元素,但我們可以動態創建一個DOM對象作為代理
var clipboard = new ClipboardJS(agent, {
target: function() {
return document.querySelector('#target');
}
});
clipboard.on('success', function(e) {
console.log(e);
});
clipboard.on('error', function(e) {
console.log(e);
});
agent.click();
// copyFrom.focus();
// copyFrom.select(); // 問題所在 無法對copyFrom對象使用select()方法
// document.execCommand('copy'); // copy動態創建的<a>失敗
body.removeChild(copyFrom);
body.removeChild(agent);
}
chrome.contextMenus.create({
title: "Copy with URL",
contexts: ['selection'],
onclick: copy_as_markdown_quot
});
關於background-cp.js的幾點說明:
1) 代碼開頭的 document.write() 方法包含了clipboard框架,這樣就可以在background-cp.js內調用clipboard.js的方法。
2) chrome.contextMenus.create({...}); 在瀏覽器的右鍵菜單中創建子選項,點擊選項觸發 copy_as_markdown_quot() 函數。
3) chrome.tabs.executeScript({code:"..."},function(){...}) 可以在當前頁面執行一段嵌入式的js代碼,需要事先在"permissions"內聲明"tabs"權限。本例中,要在tab內執行的就是 get_selection() 函數,該方法解析出鼠標選中區域 document.getSelection(); 的文本及URL等信息,向回調函數傳入一個selections參數。
4) 回調函數對selection對象進行格式化處理,隨后調用 copyTextToClipboard() 函數,將格式化后的文本復制到剪貼板。
5) 對 copyTextToClipboard() 中復制方法的改動是本擴展的核心工作。該方法首先創建一個 <a> 標簽,該標簽實際上創建在隱藏的background.html頁中。然后實例化ClipboardJS對象復制 <a> 元素。
6) clipboard.js/demo/中給出了幾個應用clipboard.js的例子,本擴展所用的方法基於function-target.html進行改動。
clipboard需要傳入一個DOM selector,HTML element或者list of HTML elements來初始化。在function-target例中,點擊實例化ClipboardJS的button可以復制目標元素(帶格式),這也為實現本擴展目標提供了可能。
7) 問題在於無法在tab頁或者background.html頁找到一個合適的元素來初始化ClipboardJS對象,而且我也不想笨拙地通過點擊除右鍵菜單選項的方式來實現復制。
解決這個問題的方法是在background頁動態創建一個隱藏的button作為代理,如 agent = document.createElement("button"); 所示。除此之外, body.appendChild(agent); 將button嵌入到body當中。最后對該button調用一次 click()方法,復制生效,大功告成!
5. future
1) 計划加入清除文本格式的子菜單選項
2) 突破某些高冷網頁的復制限制 例如:百度文庫,知乎
6. 技術總結
本擴展實現了一個簡單的文本復制功能,應用到了這么幾項技術:
1) chrome-extension創建右鍵菜單選項
2) chrome-extension在tab頁執行一段js代碼
3) 動態創建HTML元素,設置元素屬性
4) 獲取並解析鼠標選中區域
5) Clipboard.js
6) 在一個JS文件內引入另一個JS文件內的方法
資料
【3】Copy as Markdown Quotation Chrome插件
