Re: Automatic line numbering on PRE elements - Solved, sort of



Wayne wrote:
Anthony Levensalor wrote:
Wayne said:
[snip]
Would it be reasonable to post the code here, and have
some of you experts fix it up so it works in all
browsers?

I expect that something like this is much better done on the server - use a suitably styled ol element and you should be able to overcome most browser differences.

Doing this on the client just doesn't make sense.


[...]


<script>
// numberLines.js version 0.1
// Written 12/29/2007 by Wayne Pollock, Tampa Florida USA
//
// Number the lines of PRE elements with the CSS class "numbered".
// This only makes sense for PRE and other elements where white
// space is preserved. (CSS3 "content" module draft has ::line-marker
// for this, but that's not available yet.)
//
// === Hack to handle non-TEXT nodes in the PRE: ===
// Suppose the PRE element contains ("B" is the bold tag):
// before B bold-text /B after\nNext-line
// After the "obvious" way of adding numbers you get:
// 1: before
// bold-text2:
// 3: after
// 4: Next-line
// To fix, you must remove the trailing newline from the before TEXT
// node (before adding line numbers), and treat the next TEXT node
// as a continuation of the previous line, to get:
// 1: before bold-text after
// 2: Next-line
// See why I call it a hack? This is ugly code!

Now you see why this would be simple on a serve but difficult in a browser.


// TODO: It looks nicer to calulate the number of lines in a PRE
// to determine how much padding the line numbers need.

Use an ol element and you don't have to worry about that.


function numberLines ()
{
var preNodes = getElements( "numbered", "pre" );
if ( preNodes.length == 0 ) return true;

That seems odd - why return true if nothing happend? It's also redundant - the following loop will only run if preNodes.length > zero anyway.


// Find each text node within each preNode (a PRE with
// class of "numbered") and add to array to be processed:
for ( var i = 0; i < preNodes.length; ++i )
{
var lineNum = 1; // Reset the line num for each PRE element
var preNode = preNodes[i];
var newLineHackNeeded = false;
var previousTextNode = null; // May need to remove trailing newline

for ( var node = preNode.firstChild; node != null; node = node.nextSibling )
{
if ( newLineHackNeeded && previousTextNode != null )
{
var data = previousTextNode.data;
if ( data.charAt(data.length-1) == '\n' )
data = data.slice(0,-1);
previousTextNode.data = data;
}

if ( node.nodeType == 3 /* Node.TEXT_NODE */ )
{
node.data = addLineNumbers( node.data, newLineHackNeeded );
newLineHackNeeded = false;
previousTextNode = node;
} else {
newLineHackNeeded = true;
}
}
}

What if each text node is not on its own line?


// Nested function (a closure):

It should be indented to indicate that it is inside the outer function. But there doesn't seem to be any need for it anyway, why not just do it in sequence?


function addLineNumbers ( text, newLineHackNeeded )
{
if ( text == null || text.length == 0 )
return text;

var lines = text.split( '\n' );

A better pattern is /\r\n?|\n/ but you will also find that IE tends to discard empty lines.

[...]

Anyhow, here's a much shorter version that should do what you want - you may need to play with the CSS a little to get it to look consistent between browsers. It will not deal with inline elements that go over multiple lines such as span or b.


<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd";>
<title>Number pre lines</title>

<style type="text/css">
ol.numbered {
font-family: monospace;
font-size: 100%;
}
ol.numbered li {
margin-left: 1em;
}
ol.numbered li pre {
margin: 0;
}
</style>

<script type="text/javascript">

function numberPre() {
// Regular expression to test for className 'numbered'
var re = /(^|\s)numbered(\s|$)/;
var h, ol;

// Get all pre elements
var el, els = document.getElementsByTagName('pre');

// Loop over them...
for (var i=0, len=els.length; i<len; i++) {
el = els[i];

// If it has a class 'numbered', process it
if (re.test(el.className)) {

// Put a single space at the start of all lines
// to help fix browser differences
h = el.innerHTML.replace( /(\r\n|\n)/g, '$1&nbsp;');

// Split lines into an array
h = h.split(/\r\n|\n/);

// Remove last line if blank - IE removes all trailing
// blank lines before it gets to here
if (h.length && /^&nbsp;$/.test(h[h.length-1]) ) {
h.pop();
}

// Create a new ol element
ol = document.createElement('ol');
ol.className = 'numbered';

// Insert the HTML as li elements with original
// text wrapped in pre elements
ol.innerHTML = '<li><pre>'
+ h.join('</pre><li><pre>')
+ '</pre>';
el.parentNode.replaceChild(ol, el);
};
}
}

</script>

<body>
<div>
<input type="button" value="Number" onclick="numberPre()">
<pre class="numbered">
Some test lines

and some more

</pre>
<pre class="notNumbered">
not numbered
</pre>
</div>
</body>


--
Rob
"We shall not cease from exploration, and the end of all our
exploring will be to arrive where we started and know the
place for the first time." -- T. S. Eliot
.