From 96d7ae0c19bb739276588957e7587a52ef58a5da Mon Sep 17 00:00:00 2001 From: Joel Ray Holveck Date: Thu, 19 May 2016 18:25:46 -0700 Subject: [PATCH 1/3] Add an option to change the font to the XKCD font. This also will add mouseovers with the original text. --- substitutions/html/config.html | 13 ++++ substitutions/js/background.js | 10 ++- substitutions/js/options.js | 13 +++- substitutions/js/substitutions.js | 108 ++++++++++++++++++++++-------- substitutions/manifest.json | 3 + 5 files changed, 115 insertions(+), 32 deletions(-) diff --git a/substitutions/html/config.html b/substitutions/html/config.html index 69ecacf..c2f5c78 100644 --- a/substitutions/html/config.html +++ b/substitutions/html/config.html @@ -48,6 +48,19 @@

XKCD Substitutions

+
+

Use XKCD Font

+

To help you remember that the page is more awesome, this extension can write its substitutions in Randall's handwriting, just like the font on this page.

+
+
+ +
+
+
+

Website Blacklist

While this extension tries not to affect things like login forms or emails, due to the complex nature of websites this cannot be guaranteed. So there is a way to stop this extension working on specific sites.

diff --git a/substitutions/js/background.js b/substitutions/js/background.js index 541b7c0..855ff3c 100644 --- a/substitutions/js/background.js +++ b/substitutions/js/background.js @@ -79,6 +79,7 @@ var default_blacklisted_sites = [ "outlook.com", "xkcd.com" ]; +var default_use_font = true; var debug = false; @@ -113,7 +114,9 @@ function addMessage(request, sender, sendResponse) { if (debug) { console.log("message fire"); } chrome.storage.sync.get(null, function(result) { if (request === "config" && result["replacements"]) { - sendResponse(result["replacements"]); + sendResponse({"replacements": result["replacements"], + "useFont": result["useFont"], + "fontUrl": chrome.extension.getURL("fonts/xkcd.otf")}); } }); return true; @@ -137,6 +140,11 @@ function fixDataCorruption() { "blacklist": default_blacklisted_sites }); } + if (!result["useFont"]) { + chrome.storage.sync.set({ + "useFont": default_use_font + }); + } }); } diff --git a/substitutions/js/options.js b/substitutions/js/options.js index f249e6c..028fe01 100644 --- a/substitutions/js/options.js +++ b/substitutions/js/options.js @@ -16,6 +16,7 @@ function makeClean() { function saveOptions(e) { e.preventDefault; console.log("ping"); + var useFont = $("#use-font").prop("checked"); var blacklist = $("#website-blacklist").val().replace(/\s+/g, "").toLowerCase().split(","); var replacements = []; var originals = $('#replacements [name="origin"]'); @@ -30,7 +31,8 @@ function saveOptions(e) { }; chrome.storage.sync.set({ "blacklist": blacklist, - "replacements": replacements + "replacements": replacements, + "useFont": useFont }, function() { makeClean(); @@ -41,7 +43,8 @@ function saveOptions(e) { function reset(){ chrome.storage.sync.set({ "blacklist": default_blacklisted_sites, - "replacements": default_replacements + "replacements": default_replacements, + "useFont": default_use_font }, function() { makeClean(); @@ -53,7 +56,9 @@ function reset(){ function populateSettings() { $("#replacements").empty(); $("blacklist input").val(""); + $("#use-font").prop("checked", false); chrome.storage.sync.get(null, function(result) { + $("#use-font").prop("checked", result["useFont"]); $("#blacklist input").val(result["blacklist"].join(", ")); var replacements = result['replacements']; for (var i = 0; i < replacements.length; i++) { @@ -94,7 +99,9 @@ $(document).ready(function() { $("#refresh").on('click', populateSettings); - $("#reset").on('click', reset) + $("#reset").on('click', reset); + + $("#use-font").on('click', makeDirty); $("#blacklist").keypress(function(e) { if (e.which == 13) { diff --git a/substitutions/js/substitutions.js b/substitutions/js/substitutions.js index bd8023a..5b47a8e 100644 --- a/substitutions/js/substitutions.js +++ b/substitutions/js/substitutions.js @@ -2,7 +2,8 @@ // Icon and idea are from www.xkcd.com/1288 chrome.runtime.sendMessage("config", function(response) { "use strict"; - // taken from http://stackoverflow.com/questions/17264639/replace-text-but-keep-case + + // Taken from http://stackoverflow.com/questions/17264639/replace-text-but-keep-case function matchCase(text, pattern) { var result = ''; for (var i = 0; i < text.length; i++) { @@ -16,37 +17,88 @@ chrome.runtime.sendMessage("config", function(response) { } return result; } - var substitute = (function() { + + // Taken from Google's Closure library, goog.string.regExpEscape + function regExpEscape(s) { + return String(s) + .replace(/([-()\[\]{}+?*.$\^|,:#= 0; i--) { + replacementsMap[replacements[i][0].toLowerCase()] = replacements[i][1]; + } + var ignore = { + "STYLE": 0, + "SCRIPT": 0, + "NOSCRIPT": 0, + "IFRAME": 0, + "OBJECT": 0, + "INPUT": 0, + "FORM": 0, + "TEXTAREA": 0 + }; + + function substitute(node) { "use strict"; - var replacements, ignore, i, replacementsObject, original; - replacements = response; - replacementsObject = []; - for (i = replacements.length - 1; i >= 0; i--) { - original = new RegExp("\\b" + replacements[i][0] + "\\b", "gi"); - replacementsObject.push([original, replacements[i][1]]); - } - return function(node) { - var i; - var ignore = { - "STYLE": 0, - "SCRIPT": 0, - "NOSCRIPT": 0, - "IFRAME": 0, - "OBJECT": 0, - "INPUT": 0, - "FORM": 0, - "TEXTAREA": 0 - }; - if (node.parentElement.tagName in ignore) { + var replacementIdx; + var splitIdx; + var parent = node.parentElement; + if (parent) { + if (parent.tagName in ignore) { + return; + } + var cls = parent.getAttribute("class"); + if (cls && cls.indexOf("xkcdSubstitutionsExtensionSubbed") != -1) { return; } - for (i = replacementsObject.length - 1; i >= 0; i--) { - node.nodeValue = node.nodeValue.replace(replacementsObject[i][0], function(match) { - return matchCase(replacementsObject[i][1], match); - }); + } + if (!node.nodeValue.match(originalsRegexp)) { + return; + } + var splits = node.nodeValue.split(originalsRegexp); + var docFrag = document.createDocumentFragment(); + for (splitIdx = 0; splitIdx < splits.length; splitIdx++) { + var splitString = splits[splitIdx]; + var splitStringLower = splitString.toLowerCase(); + var newNode; + if (splitStringLower in replacementsMap) { + newNode = document.createElement("span"); + newNode.setAttribute("class", "xkcdSubstitutionsExtensionSubbed"); + newNode.setAttribute("title", splitString); + newNode.textContent = matchCase(replacementsMap[splitStringLower], + splitString); + } else { + newNode = document.createTextNode(splitString); } - }; - })(); + docFrag.appendChild(newNode); + } + node.parentNode.replaceChild(docFrag, node); + } var node, iter; var iter = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT); diff --git a/substitutions/manifest.json b/substitutions/manifest.json index 2684325..d2d177e 100644 --- a/substitutions/manifest.json +++ b/substitutions/manifest.json @@ -16,6 +16,9 @@ ], "persistent": false }, + "web_accessible_resources": [ + "fonts/xkcd.otf" + ], "permissions": [ "tabs", "storage", From 8912c053d5fcc0cebffccbe0c3a182449d9a80bd Mon Sep 17 00:00:00 2001 From: Joel Ray Holveck Date: Fri, 20 May 2016 22:40:16 -0700 Subject: [PATCH 2/3] Ignore entire subtrees if needed. We already ignore the immediate children of script, form, etc. elements, as well as spans that we've inserted ourselves. This change makes us skip the entire subtree of such an element. One important effect is that if another extension changes one of our spans using a similar algorithm to ours (inserting a new subtree) we previously could end up reprocessing that node in a future document_end event. Now, we prevent that. --- substitutions/js/substitutions.js | 70 ++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/substitutions/js/substitutions.js b/substitutions/js/substitutions.js index 5b47a8e..1f9970e 100644 --- a/substitutions/js/substitutions.js +++ b/substitutions/js/substitutions.js @@ -63,45 +63,87 @@ chrome.runtime.sendMessage("config", function(response) { "TEXTAREA": 0 }; + // Set up the functions we'll need to perform the iteration. + var node; + var iter; + + var filter = { + acceptNode: function(node) { + if (node.nodeType == Node.TEXT_NODE) { + // This is a text node that we should show to the + // filter. + return NodeFilter.FILTER_ACCEPT; + } + if (node.nodeType == Node.ELEMENT_NODE) { + // Fold to upper case before checking the tag name, + // since this may be XHTML etc. + if (node.tagName.toUpperCase() in ignore) { + // Ignore this element, and all its children. + return NodeFilter.FILTER_REJECT; + } + if (node.classList.contains("xkcdSubstitutionsExtensionSubbed")) { + // We've already changed this text. Note that some + // other extension or the page's own scripts may + // have made more changes to what we did, so don't + // fight back and forth, constantly changing the + // DOM. Instead, just skip this subtree entirely. + return NodeFilter.FILTER_REJECT; + } + } + // This is not a node we're interested in. Skip this node, + // but process its children. + return NodeFilter.FILTER_SKIP; + } + }; + function substitute(node) { - "use strict"; var replacementIdx; var splitIdx; - var parent = node.parentElement; - if (parent) { - if (parent.tagName in ignore) { - return; - } - var cls = parent.getAttribute("class"); - if (cls && cls.indexOf("xkcdSubstitutionsExtensionSubbed") != -1) { - return; - } - } + + // Before starting, make sure there's something to substitute. + // Otherwise, we end up doing a lot of expensive tree modification + // for no reason. if (!node.nodeValue.match(originalsRegexp)) { return; } - var splits = node.nodeValue.split(originalsRegexp); + + // Prepare a document fragment to hold the result. var docFrag = document.createDocumentFragment(); + + // Split the string into substring, where each substring either contains + // something we'll substitute, or something that we won't. We do this + // by using the capturing parentheses in originalsRegexp. + var splits = node.nodeValue.split(originalsRegexp); for (splitIdx = 0; splitIdx < splits.length; splitIdx++) { var splitString = splits[splitIdx]; var splitStringLower = splitString.toLowerCase(); var newNode; if (splitStringLower in replacementsMap) { + // This is something that needs to be changed. newNode = document.createElement("span"); newNode.setAttribute("class", "xkcdSubstitutionsExtensionSubbed"); newNode.setAttribute("title", splitString); newNode.textContent = matchCase(replacementsMap[splitStringLower], splitString); } else { + // This is a stretch between stuff that needs changing. newNode = document.createTextNode(splitString); } docFrag.appendChild(newNode); } + + // Let the tree walker know that its place has changed: the old + // node it sent us is gone, and so we'll update its current place + // to refer to the last node we've processed. + iter.currentNode = docFrag.lastChild; + // Make the changes. node.parentNode.replaceChild(docFrag, node); } - var node, iter; - var iter = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT); + iter = document.createTreeWalker(document.body, + NodeFilter.SHOW_ELEMENT | + NodeFilter.SHOW_TEXT, + filter); while ((node = iter.nextNode())) { substitute(node); } From ce9cfb726cd16d0049677a6580fbd5e46ef9470a Mon Sep 17 00:00:00 2001 From: Stephen Brown II Date: Sat, 18 Mar 2017 22:00:09 -0500 Subject: [PATCH 3/3] Use small-caps font-variant to fit in better with text on page --- substitutions/js/substitutions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/substitutions/js/substitutions.js b/substitutions/js/substitutions.js index 1f9970e..3471942 100644 --- a/substitutions/js/substitutions.js +++ b/substitutions/js/substitutions.js @@ -36,6 +36,7 @@ chrome.runtime.sendMessage("config", function(response) { '}' + '.xkcdSubstitutionsExtensionSubbed {' + ' font-family: xkcdSubstitutionsFont !important;' + + ' font-variant: small-caps;' + '}'; document.head.appendChild(stylesheet); }