/*
* Code Highlighter
*
* A rewrite of Dan Webb's original Unobtrusive Code Highlighter. Refactored to
* take advantage of Prototype idioms and to clean out the cruft of legacy
* browser support.
*
* Tested on:
* Firefox 3+
* Safari 3+
*
*
* Usage:
* (1) Include this script on your page.
* (2) Include any number of language parsing definitions. Sample definitions
* are included.
* (3) Assign a class name to any code block you want highlighted. The class
* name should be identical to the language definition name passed to
* `CodeHighlighter.addStyle`.
* (4) Include a stylesheet that highlights your code. You can target parsed
* tokens by name: for example, `pre code .string` will highlight anything
* with a style rule named "string."
*
* Dan's script (and therefore _this_ script) was inspired by star-light,
* written by the cunning Dean Edwards (http://dean.edwards.name/star-light/).
*
*/
(function() {
var _styleSets = [];
var _codeElements;
var _rules = _makeRules();
var DEFAULT_TEMPLATE = new Template(
'<span class="#{className}">#{text}</span>');
var $Codes = Class.create(Enumerable, {
initialize: function(nodes) {
this.nodes = $A(nodes);
},
_each: function() {
return this.nodes._each.apply(this.nodes, arguments);
}
});
function _makeStyleSet(name, rules, ignoreCase, options) {
return {
name: name,
rules: rules,
ignoreCase: ignoreCase || false
};
}
function _makeRules(styleSet) {
var rules = [];
rules.toString = function() {
return this.pluck('exp').join('|');
};
if (styleSet) {
var exp, rule;
for (var ruleName in styleSet.rules) {
rule = styleSet.rules[ruleName];
if (!Object.isString(rule.exp)) {
exp = String(rule.exp).substr(1, String(rule.exp).length - 2);
} else {
exp = rule.exp;
}
rules.push({
className: ruleName,
exp: "(" + exp + ")",
length: (exp.match(/(^|[^\\])\([^?]/g) || "").length + 1,
replacement: rule.replacement || null
});
}
}
return rules;
}
function _parse(text, rules, ignoreCase) {
var re = new RegExp(rules, ignoreCase ? "gi" : "g");
return text.replace(re, function() {
var i = 0, j = 1, rule;
while (rule = rules[i++]) {
if (arguments[j]) {
// If no custom replacement is defined, do the simple replacement.
if (!rule.replacement) {
return DEFAULT_TEMPLATE.evaluate({
className: rule.className, text: arguments[0] });
} else {
// Replace $0 with the className; then do normal replaces.
var str = rule.replacement.replace("$0", rule.className);
for (var k = 1; k <= rule.length - 1; k++) {
str = str.replace("$" + k, arguments[j + k]);
}
return str;
}
} else {
j += rule.length;
}
}
});
}
function _highlightCode(styleSet) {
var parsed;
var stylableElements = _codeElements.select( function(code) {
return code.className.include(styleSet.name);
});
var rules = _makeRules(styleSet);
for (var i = 0, element; element = $(stylableElements[i]); i++) {
text = element.innerHTML;
// EVIL hack to fix IE whitespace badness if it's inside a <pre>
if (Prototype.Browser.IE && element.parentNode.nodeName === 'PRE') {
element = element.parentNode;
text = element.innerHTML;
parsed = text.replace(/(<code[^>]*>)([^<]*)<\/code>/i,
function() {
return arguments[1] + _parse(arguments[2], rules, styleSet.ignoreCase)
+ "</code>";
});
parsed = parsed.replace(/\n(\s*)/g, function() {
return "\n" + (" ".times(arguments[1].length));
});
parsed = parsed.replace(/\t/g,
" ".times(CodeHighlighter.TAB_SIZE));
parsed = parsed.replace(/\n(<\/\w+>)?/g, "<br />$1");
parsed = parsed.replace(/<br \/>[\n\r\s]*<br \/>/g, "<p><br /></p>");
} else {
parsed = _parse(text, rules, styleSet);
}
element.update(parsed);
}
}
function _initialize() {
_codeElements = new $Codes(document.getElementsByTagName('code'));
_styleSets.each(_highlightCode);
}
window.CodeHighlighter = {
TAB_SIZE: 2,
addStyle: function(name, rules, ignoreCase, options) {
_styleSets.push(_makeStyleSet(name, rules, ignoreCase, options));
if (_styleSets.length == 1) {
document.observe("dom:loaded", _initialize);
}
}
};
})();