diff --git a/src/annotator.coffee b/src/annotator.coffee
index 766d4cb30..9f934d894 100644
--- a/src/annotator.coffee
+++ b/src/annotator.coffee
@@ -411,15 +411,19 @@ class Annotator extends Delegator
   highlightRange: (normedRange, cssClass='annotator-hl') ->
     white = /^\s*$/
 
-    hl = $("<span class='#{cssClass}'></span>")
-
+    results = []
     # Ignore text nodes that contain only whitespace characters. This prevents
     # spans being injected between elements that can only contain a restricted
     # subset of nodes such as table rows and lists. This does mean that there
     # may be the odd abandoned whitespace node in a paragraph that is skipped
     # but better than breaking table layouts.
     for node in normedRange.textNodes() when not white.test(node.nodeValue)
-      $(node).wrapAll(hl).parent().show()[0]
+      hl = document.createElement('span')
+      hl.className = cssClass
+      node.parentNode.replaceChild(hl, node)
+      hl.appendChild(node)
+      results.push(hl)
+    return results
 
   # Public: highlight a list of ranges
   #
diff --git a/src/range.coffee b/src/range.coffee
index e51214e18..833cba6f9 100644
--- a/src/range.coffee
+++ b/src/range.coffee
@@ -252,13 +252,22 @@ class Range.NormalizedRange
   #
   # Returns updated self or null.
   limit: (bounds) ->
-    nodes = $.grep this.textNodes(), (node) ->
-      node.parentNode == bounds or $.contains(bounds, node.parentNode)
+    if @commonAncestor == bounds or $.contains(bounds, @commonAncestor)
+      return this
 
-    return null unless nodes.length
+    if not $.contains(@commonAncestor, bounds)
+      return null
+    document = bounds.ownerDocument
 
-    @start = nodes[0]
-    @end   = nodes[nodes.length - 1]
+    if not $.contains(bounds, @start)
+      walker = document.createTreeWalker(bounds, NodeFilter.SHOW_TEXT)
+      @start = walker.firstChild()
+
+    if not $.contains(bounds, @end)
+      walker = document.createTreeWalker(bounds, NodeFilter.SHOW_TEXT)
+      @end = walker.lastChild()
+
+    return null unless @start and @end
 
     startParents = $(@start).parents()
     for parent in $(@end).parents()
diff --git a/src/util.coffee b/src/util.coffee
index 4921516d6..34e0d0006 100644
--- a/src/util.coffee
+++ b/src/util.coffee
@@ -50,27 +50,13 @@ Util.contains = (parent, child) ->
 #
 # Returns a new jQuery collection of text nodes.
 Util.getTextNodes = (jq) ->
-  getTextNodes = (node) ->
-    if node and node.nodeType != Node.TEXT_NODE
-      nodes = []
-
-      # If not a comment then traverse children collecting text nodes.
-      # We traverse the child nodes manually rather than using the .childNodes
-      # property because IE9 does not update the .childNodes property after
-      # .splitText() is called on a child text node.
-      if node.nodeType != Node.COMMENT_NODE
-        # Start at the last child and walk backwards through siblings.
-        node = node.lastChild
-        while node
-          nodes.push getTextNodes(node)
-          node = node.previousSibling
-
-      # Finally reverse the array so that nodes are in the correct order.
-      return nodes.reverse()
-    else
-      return node
+  getTextNodes = (root) ->
+    document = root.ownerDocument
+    walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
+    nodes = (node while node = walker.nextNode())
+    return nodes
 
-  jq.map -> Util.flatten(getTextNodes(this))
+  jq.map -> getTextNodes(this)
 
 # Public: determine the last text node inside or before the given node
 Util.getLastTextNodeUpTo = (n) ->