Using the nsIFind Interface

October 3, 2011

In a recent update to Googlebar Lite, I made a number of improvements to the search term highlighting feature, fixing several bugs along the way. This feature uses the nsIFind interface available in Firefox, which is poorly documented in my opinion. Unable to find any decent examples, I picked apart another extension I found that uses this interface, and I now better understand how it works. As such, I thought I’d provide an example of how to use this interface so that future developers won’t have to dig down in the source like I did.

The following example will be simple, but we’ll do some DOM modification along the way to spice things up. Note that this interface really only has one function that we care about: Find. Here’s what it looks like:

Note that both the startPoint and endPoint parameters are ranges. Technically, these ranges can have different start and end points themselves, which complicates things depending on which direction you are searching (forwards or backwards). However, this example will avoid that scenario to keep things simple. I will also not discuss how to handle searching within frames on a website, something I’ll leave as an exercise to the reader (hint: the solution involves recursion). Let’s dive right in to the code:

// TODO: Note that in practice, we should do some error checking on
// the following three variables to make sure they are really available
var win = window.content; // Get a reference to the window's content
var doc = win.document; // Get a reference to the document
var body = doc.body; // Get a reference to the body element
var term = "Firefox"; // The term to find, hard-coded for this example
// Create a highlighted span element which we will clone
var span = doc.createElement("span");
span.setAttribute("style", "background: #FF0; color: #000; " +
"display: inline !important; font-size: inherit !important;");
// Create our search range
var searchRange = doc.createRange();
searchRange.selectNodeContents(body);
// Create the start point
var start = searchRange.cloneRange();
start.collapse(true); // Collapse to the beginning
// Create the end point
var end = searchRange.cloneRange();
end.collapse(false); // Collapse to the end
// Create the finder instance
var finder = Components.classes['@mozilla.org/embedcomp/rangefind;1']
.createInstance(Components.interfaces.nsIFind);
// Perform the find operation
while((start = finder.Find(term, searchRange, start, end)))
{
// Clone the highlighter node and surround our search results with it
var hilitenode = span.cloneNode(true);
start.surroundContents(hilitenode);
// Collapse the starting range to its end point, so we don't find this
// instance again the next time around the loop
start.collapse(false);
// Workaround for Firefox bug #488427
body.offsetWidth;
}

There are a few sections of this code that are worth expanding on. As the “TODO” comment suggests, you should test to make sure that your references to the window’s content actually exist. A simple if(!variable) test will suffice. Next, when creating our highlighting span, you’ll note that I set a few interesting style rules: display:inline and font-size:inherit, both of which have the !important modifier applied to them. These rules help ensure that our inserted spans don’t interfere too much with the existing page layout. I’m sure that additional rules could be added to make this even more battle hardened, but these are what I use in Googlebar Lite.

Next, when we create the search range, we use the selectNodeContents function to populate the range object. In our example, we select the contents of the “body” tag, which is essentially all of the page’s content. Our start and end points are created by cloning our search range, then using the collapse function. Passing true to this function will collapse the range to its start point, while passing false will collapse to the end point.

Everything else should be fairly straightforward: we create an instance of the nsIFind interface, call its Find method, assigning our starting point to the result (so we don’t repeatedly find the same instance of our search string). For each instance, we surround it with our “highlighted” span and we collapse the start point, again so we skip this instance the next time around the loop.

The last line of code in this function deserves some explanation. As you can see, we simply do a read on the offsetWidth property of the body element. This read essentially forces a reflow of the page, which flushes the changes we made to the DOM (inserting the our highlighted span). If we don’t flush the changes by reflowing the page, the Find method will skip to the next sibling element in the DOM. Bug 488427 has all the details, though (as of this writing) its currently closed and marked as “worksforme.” Nevertheless, this problem still persists as of Firefox 7.0.1, and this simple fix acts as a nice workaround. Note that this read must appear between the time you insert your element and the time you call nsIFind.

That may work, but it seems to me that you’re introducing a lot of memory overhead that isn’t needed. My workaround simply reads a value which triggers a reflow. Your method is cloning the search range on each hit. Those are objects that the garbage collector now has to deal with, not to mention the overhead in memory allocation.