{"id":2524,"date":"2012-08-05T20:51:08","date_gmt":"2012-08-06T02:51:08","guid":{"rendered":"http:\/\/bililite.com\/blog\/?p=2524"},"modified":"2012-08-16T22:29:52","modified_gmt":"2012-08-17T04:29:52","slug":"line-numbering-in-pre-elements","status":"publish","type":"post","link":"https:\/\/bililite.com\/blog\/2012\/08\/05\/line-numbering-in-pre-elements\/","title":{"rendered":"Line Numbering in <code>&lt;pre&gt;<\/code> Elements"},"content":{"rendered":"<p>I've thought about creating <a href=\"\/blog\/2011\/01\/28\/playing-with-syntax-highlighters\/\" title=\"Playing with Syntax Highlighters\">my own syntax highlighter<\/a>. I've been using <a href=\"https:\/\/github.com\/aercolino\/Chili\">Chili<\/a>, but there are <a href=\"\/blog\/2011\/10\/25\/weird-bug-with-chili-and-firefox\/\" title=\"Weird bug with chili and Firefox\">some odd bugs<\/a> that pop up here and there and it doesn't seem to play well with Chrome. And it hasn't been updated in 2 years.<\/p>\r\n<p>One thing I did want was line numbering, but that's been a bugaboo of syntax highighlighters for a long time&mdash;you want the numbers but do not want them copied when code is selected. Firefox copies the numbers when using <code class=\"language-html\">&lt;li&gt;<\/code> elements, and tables or inserted text will also copy everything. The <a href=\"http:\/\/www.manuel-strehl.de\/dev\/markup_for_syntax_highlighting.en.html\">answer<\/a> seems to be using <code class=\"language-css\">:before<\/code> to insert the line numbers, since that text won't be copied in any modern browser (IE 8 and below don't support <code class=\"language-css\">:before<\/code>, but we won't worry about that).<\/p>\r\n<p>The issue then is how to tell CSS about the lines. We want to wrap them in <code class=\"language-html\">&lt;span&gt;<\/code>s, as so:<\/p>\r\n<pre class=test1><code class=\"language-html\">&lt;pre&gt;\r\n&lt;span class=line&gt;This is a &lt;em&gt;text&lt;\/em&gt;&lt;\/span&gt;\r\n&lt;span class=line&gt;This is the second line&lt;\/span&gt;\r\n&lt;\/pre&gt;<\/code><\/pre>\r\n<p>And number everything with CSS:<\/p>\r\n<pre><code class=\"language-css demo\">pre.test1 {\r\n\tcounter-reset: linecounter;\r\n}\r\npre.test1 span.line{\r\n\tcounter-increment: linecounter;\r\n}\r\npre.test1 span.line:before{\r\n\tcontent: counter(linecounter);\r\n\twidth: 2em;\r\n\tdisplay: inline-block;\r\n\tborder-right: 1px solid black;\r\n}<\/code><\/pre>\r\n<p>And this is the result, exactly as desired.<\/p>\r\n<pre class=test1>\r\n<span class=line>This is a <em>text<\/em><\/span>\r\n<span class=line>This is the second line<\/span>\r\n<\/pre>\r\n<p>The keys in the CSS are lines 1 and 4 that set up the counter (change line 1 to <code class=\"language-css\">linecounter 4<\/code> to start the numbering at 5 (<code class=\"language-css\">counter-increment<\/code> increments <em>before<\/em> displaying)) (change <code class=\"language-css\">linecounter<\/code> to anything you want as long as its consistent). Line 7 displays the value of the counter in the <code class=\"language-css\">:before<\/code> pseudoelement, and lines 8-10 are just old-fashioned styling to make it prettier. You of course would want to add some padding, margin, odd\/even backgrounds etc., but that's old hat.<\/p>\r\n<!--more-->\r\n<p>But how do we get the <code class=\"language-html\">&lt;span&gt;<\/code>s to wrap the lines? We could just take the text and split it on <code class=\"language-javascript\">'\\n'<\/code> and use string processing to wrap them: <code class=\"language-javasscript\">element.innerHTML = element.textContent.replace(\/.+\/g, '<span>$&<\/span>')<\/code> but that loses all internal markup. Luckily, browsers that implement <code><a href=\"http:\/\/html5demos.com\/contenteditable\">contentEditable<\/a><\/code> know how to insert stuff without messing up the structure by using <a href=\"http:\/\/www.w3.org\/TR\/DOM-Level-2-Traversal-Range\/ranges.html\">ranges<\/a>, and we know <a href=\"\/blog\/2011\/01\/17\/cross-browser-text-ranges-and-selections\/\" title=\"Cross-Browser Text Ranges and Selections\">how to manipulate ranges<\/a>.<\/p>\r\n<p>Rather than including the whole <code>bililiteRange<\/code> class, since I know we're only going to be dealing with standards-compliant browsers, I can just take out the relevant code:<\/p>\r\n<pre><code class=\"language-javascript demo\">function wrapLines (el){\r\n\tvar text = el.textContent.split('\\n');\r\n\tvar range = document.createRange();\r\n\tvar pointer = 0; \/\/ start of text\r\n\tel.textContent.split('\\n').forEach(function(line, i){\r\n\t\tvar len = line.length;\r\n\t\tsetBounds (pointer, pointer+len); \/\/ sets range to the characters of the line\r\n\t\tvar wrapper = document.createElement('span');\r\n\t\twrapper.setAttribute('class', 'line');\r\n\t\twrapper.appendChild(range.extractContents()); \/\/ pulls the contents of the range out of the document and into wrapper\r\n\t\trange.insertNode(wrapper); \/\/ and put back the wrapped line\r\n\t\tpointer += len+1; \/\/ skip the newline\r\n\t});\r\n\t\/\/ now, we're left with a bunch of empty spans\/other elements that were split across lines and the browser divided them into three parts (first line, newline character, second line)\r\n\t\/\/ those mess up the odd\/even calculations. Replace them with plain text.\r\n\tfor (var node = el.firstChild; node; node = node.nextSibling){\r\n\t\tif (node.nodeType != 3 &amp;&amp; node.getAttribute('class') != 'line'){\r\n\t\t\tvar replacement = document.createTextNode(node.textContent);\r\n\t\t\tel.replaceChild(replacement, node);\r\n\t\t\tnode = replacement;\r\n\t\t}\r\n\t}\r\n\t\r\n\tfunction setBounds (start, end){\r\n\t\t\/\/ since the browser throws an error if we try to move the beginning past the end (unlike IE, which just adusts the end)\r\n\t\t\/\/ we have to reset the range to cover the entire element, then move the start, then move the end to the start, then move the end\r\n\t\trange.selectNodeContents(el);\r\n\t\tmoveBoundary (start, 'start');\r\n\t\trange.collapse (true);\r\n\t\tmoveBoundary (end-start, 'end');\r\n\t}\r\n\tfunction moveBoundary (n, start){\r\n\t\t\/\/ move the boundary n characters forward, up to the end of the element. Forward only!\r\n\t\t\/\/  start is 'start' or 'end', and is used to create the appropriate method names ('startContainer' or 'endContainer' etc.)\r\n\t\t\/\/ if the start is moved after the end, then an exception is raised\r\n\t\tif (n &lt;= 0) return;\r\n\t\tvar startNode = range[start+'Container'];\r\n\t\t\/\/ we may be starting somewhere into the text\r\n\t\tif (startNode.nodeType == 3) n += range[start+'Offset'];\r\n\t\t\/\/ nodeIterators from http:\/\/www.w3.org\/TR\/DOM-Level-2-Traversal-Range\/traversal.html\r\n\t\tvar iter = document.createNodeIterator(el, 4 \/* SHOW_TEXT *\/), node;\r\n\t\twhile (node = iter.nextNode()){\r\n\t\t\tif (startNode.compareDocumentPosition(node) &amp; 2 \/* DOCUMENT_POSITION_PRECEDING *\/ ) continue;\r\n\t\t\tif (n &lt;= node.nodeValue.length){\r\n\t\t\t\t\/\/ we found the last character!\r\n\t\t\t\trange[start == 'start' ? 'setStart' :'setEnd'](node, n);\r\n\t\t\t\treturn;\r\n\t\t\t}else{\r\n\t\t\t\tn -= node.nodeValue.length; \/\/ eat these characters\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}<\/code><\/pre>\r\n<p>And now it works (note the original markup has no line-wrapping spans; that's added with javascript):<\/p>\r\n<pre><code class=\"language-html demo\">&lt;pre class=\"test1 numbered\"&gt;\r\nThis is a &lt;em&gt;text&lt;\/em&gt;\r\nThis is the second line\r\nThis is the third line; this text should have line numbers.&lt;\/pre&gt;<\/code><\/pre>\r\n<pre><code class=\"language-javascript demo\">wrapLines($('.numbered')[0])<\/code><\/pre>\r\n","protected":false},"excerpt":{"rendered":"I've thought about creating my own syntax highlighter. I've been using Chili, but there are some odd bugs that pop up here and there and it doesn't seem to play well with Chrome. And it hasn't been updated in 2 years. One thing I did want was line numbering, but that's been a bugaboo of [&hellip;]","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10],"tags":[],"_links":{"self":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/2524"}],"collection":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/comments?post=2524"}],"version-history":[{"count":30,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/2524\/revisions"}],"predecessor-version":[{"id":2579,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/2524\/revisions\/2579"}],"wp:attachment":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/media?parent=2524"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/categories?post=2524"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/tags?post=2524"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}