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); }