Source code line numbers (again)

This is embarrassing. Back in December, I made some changes to the site to allow the line numbers in the source code I post here to be toggled on and off. The idea, as I described here, was to have line numbers that I could refer to when describing the code, but for readers to be able to turn them off if they want to copy the code and use it in their own projects. I thought the combination of JavaScript and CSS worked perfectly in Safari, Firefox, and Camino (IE’s JavaScript had a buggy replace command, so I decided to give up on it).

Today, Adam Dexter emailed me with the surprising news that the toggling of line numbers didn’t work in Firefox. Oh, it hid the line numbers, all right, but when he copied code with the line numbers hidden and pasted it into another document, the line numbers appeared in the pasted text. I checked for myself and found that he was right. And the same problem shows up in Camino. Since copying and pasting without line numbers was the whole point of the code, this bug was mortifying.

How could this slip through? Apparently, after testing thoroughly in Safari—where the copy-and-pasting works as I intended—my tests with Firefox and Camino only verified that the line numbers got hidden; I didn’t check whether they got carried along with copied text.

(In my [weak] defense, I think this is a bug in Gecko. Copying should not collect text that’s hidden. But still, I should have tested.)

So here’s the improved code that really really really does work in Safari, Firefox, and Camino.

 1:  function styleLN() {
 2:    var isIE = navigator.appName.indexOf('Microsoft') != -1;
 3:    if (isIE) return;
 4:    var preElems = document.getElementsByTagName('pre');
 5:    if (0 == preElems.length) {   // no pre elements; stop
 6:       return;
 7:    }
 8:    for (var i = 0; i < preElems.length; i++) {
 9:      var pre = preElems[i];
10:      var code = pre.getElementsByTagName('code')[0];
11:      if (null == code) {        // no code; move on
12:        continue;
13:      }
14:      var oldContent = code.innerHTML;
15:      var newContent = oldContent.replace(/^( *)(\d+):(  )/mg, 
16:                 '<span class="ln">$1$2$3<' + '/span>');
17:      if (oldContent.match(/^( *)(\d+):(  )/mg)) {
18:        newContent += "\n" + '<button onclick="showCode(this.parentNode)">Without line numbers</button>';
19:      }
20:      code.innerHTML = newContent;
21:    }
22:  }
23:  
24:  function showCode(code) {
25:    var oldCode = code.cloneNode(true);
26:    for (var i=0; i<oldCode.childNodes.length; i++){
27:      node = oldCode.childNodes[i];
28:      if (node.nodeName == 'SPAN' || node.nodeName == 'BUTTON'){
29:        oldCode.removeChild(node);
30:      }
31:    }
32:    var w = window.open("", "", "width=800,height=500,resizable=yes,scrollbars=yes");
33:    var d = w.document;
34:    d.open();
35:    d.write("<html><head><title>Code</title></head><body><pre><code>", oldCode.innerHTML, "</code></pre></body></html>");
36:    d.close();
37:  }

The styleLN function is the same as before; it looks for code with line numbers and wraps <span> tags around the numbers so my style sheet can give them a smaller font size and a lighter color.

The showCode function, which is called when the reader clicks the “Without line numbers” button at the bottom of the block of code, is new. When clicked, it pops up a new window with the raw code. The reader can copy the code from the new window.

showCode works by going through the code block and removing all the <span> elements and the <button> element at the end of the block. I think the logic of the function is pretty clear, but Line 25 may need some further explanation. The <code> element has to be “cloned” because objects in JavaScript are assigned by reference, not by value. So if that line were

25:    var oldCode = code;

it would simply assign the reference, and oldCode would point to the same chunk of data as code does. The removeChild calls to oldCode would then remove the child elements of the original data and eliminate the line numbers from the initial browser window as well as the popup window. The cloneNode(true) call makes a copy of the data so that all subsequent changes are to the copy rather than the original. Yes, I learned this by doing it the wrong way first.

My thanks to Adam Dexter for finding the bug.

Tags: