In a previous post, I mentioned using ordered lists to display block-level code snippets. The solution worked, but had disadvantages, most notably the inefficiency during initially getting the code on the site. So I decided to revisit displaying block-level code snippets, with a solution that accomplished the following goals:
- Efficiency during initial writing / copy & paste
- Extensibility
- Cross-browser compatibility
- Minimal footprint
- Usability (line numbers, ability to copy & paste, etc.)
The new solution acts on any code in the following format:
<pre><code>
<?php echo 'Hello World'; ?>
</code></pre>
… and turns it into this:
<php echo 'Hello World'; ?>
Here’s the code:
// Handles dynamic beautifying of code blocks
var codeHandler = {
codeBlocks: '',
codeBlock: '',
lineCount: '',
init: function() {
// Store code blocks
this.codeBlocks = $("pre code");
// Loop through each code block
(this.codeBlocks).each(function() {
// Store code block reference
codeHandler.codeBlock = $(this);
// Make any adjustments to actual code text
codeHandler.codeHTML(codeHandler.codeBlock);
// Get line count
codeHandler.lineCount = 1; codeHandler.getLineCount(codeHandler.codeBlock);
// Display the line numbers
codeHandler.printLineNumbers();
});
},
// Edit text in given elm
codeHTML: function() {
// Remove initial extra line breaks
this.codeBlock.firstChild.nodeValue = this.codeBlock.firstChild.nodeValue.replace(/^[\s]*[\n\r]/g,'');
// Remove trailing extra line breaks
this.codeBlock.lastChild.nodeValue = this.codeBlock.lastChild.nodeValue.replace(/[\n\r]+[\s]*$/g,'');
},
// Count the line breaks in a given element
getLineCount : function(elm) {
// Loop through code block elements recursively, counting line breaks
for (var i in elm.childNodes) {
var kid = elm.childNodes[i];
if (kid.nodeName) {
if (kid.hasChildNodes()) codeHandler.getLineCount(kid);
if (kid.nodeType == 3) {
if (kid.nodeValue.match(/[\n\r]/g)) {
codeHandler.lineCount += kid.nodeValue.match(/[\n\r]/g).length;
}
}
}
}
},
// Print line number list in code block
printLineNumbers : function() {
// Create a list for the line numbers
var lineWrapper = document.createElement('ul');
lineWrapper.className = 'lineNumbers';
(this.codeBlock).appendChild(lineWrapper);
// Add a list item for every line
for (var i = 1; i < (this.lineCount+1); i++) {
var line = document.createElement('li');
line.appendChild(document.createTextNode(i));
lineWrapper.appendChild(line);
}
}
};
// Call the initialization function as soon as the DOM is loaded
DOMAssistant.DOMReady(codeHandler.init);
I wanted to start simple, so for the time being the only thing the script really does is remove beginning and ending carriage returns, and adds line numbers. The line numbers are specified and positioned in a different block, so the user should be able to highlight and copy the code without touching them.
The CSS does the majority of the beautifying:
pre code {
display: block;
padding: 10px 10px 10px 30px;
line-height: 22px;
position: relative;
border: 1px solid #ccc;
overflow: auto;
width: 91%;
white-space: pre;
}
ul.lineNumbers {
position: absolute;
top: 8px;
left: 0;
margin: 0;
padding: 0;
}
ul.lineNumbers li {
border-right: 1px solid;
list-style-type: none;
line-height: 18px;
margin: 4px 0;
padding: 0 4px;
}
The CSS does several things:
- Handles the overflow so lines don’t wrap or extend outside of the code block. (Handling overflow in IE is extremely annoying… Read the expanding box problem, rendering scrollbars inside vs. outside the box, other google results.)
- Positions the line numbers absolutely at the top left of the code block and pads the left of the code so there is no overlap.
- Ensures equivalent line heights for the line numbers and the code so they line up.
- Background, borders, etc.
I’ll probably revisit block-level code snippets again in the future, but for the time being this solution makes it extremely easy to add code to the site, and should be useful enough for the user.