started on documentation site
[god.git] / site / javascripts / code_highlighter.js
blob4ed72e1692ecee005501fd90d9d66f923ffce471
1 /* Unobtrustive Code Highlighter By Dan Webb 11/2005\r
2    Version: 0.4\r
3         \r
4         Usage:\r
5                 Add a script tag for this script and any stylesets you need to use\r
6                 to the page in question, add correct class names to CODE elements, \r
7                 define CSS styles for elements. That's it! \r
8         \r
9         Known to work on:\r
10                 IE 5.5+ PC\r
11                 Firefox/Mozilla PC/Mac\r
12                 Opera 7.23 + PC\r
13                 Safari 2\r
14                 \r
15         Known to degrade gracefully on:\r
16                 IE5.0 PC\r
17         \r
18         Note: IE5.0 fails due to the use of lookahead in some stylesets.  To avoid script errors\r
19         in older browsers use expressions that use lookahead in string format when defining stylesets.\r
20         \r
21         This script is inspired by star-light by entirely cunning Dean Edwards\r
22         http://dean.edwards.name/star-light/.  \r
23 */\r
25 // replace callback support for safari.\r
26 if ("a".replace(/a/, function() {return "b"}) != "b") (function(){\r
27   var default_replace = String.prototype.replace;\r
28   String.prototype.replace = function(search,replace){\r
29         // replace is not function\r
30         if(typeof replace != "function"){\r
31                 return default_replace.apply(this,arguments)\r
32         }\r
33         var str = "" + this;\r
34         var callback = replace;\r
35         // search string is not RegExp\r
36         if(!(search instanceof RegExp)){\r
37                 var idx = str.indexOf(search);\r
38                 return (\r
39                         idx == -1 ? str :\r
40                         default_replace.apply(str,[search,callback(search, idx, str)])\r
41                 )\r
42         }\r
43         var reg = search;\r
44         var result = [];\r
45         var lastidx = reg.lastIndex;\r
46         var re;\r
47         while((re = reg.exec(str)) != null){\r
48                 var idx  = re.index;\r
49                 var args = re.concat(idx, str);\r
50                 result.push(\r
51                         str.slice(lastidx,idx),\r
52                         callback.apply(null,args).toString()\r
53                 );\r
54                 if(!reg.global){\r
55                         lastidx += RegExp.lastMatch.length;\r
56                         break\r
57                 }else{\r
58                         lastidx = reg.lastIndex;\r
59                 }\r
60         }\r
61         result.push(str.slice(lastidx));\r
62         return result.join("")\r
63   }\r
64 })();\r
66 var CodeHighlighter = { styleSets : new Array };\r
68 CodeHighlighter.addStyle = function(name, rules) {\r
69         // using push test to disallow older browsers from adding styleSets\r
70         if ([].push) this.styleSets.push({\r
71                 name : name, \r
72                 rules : rules,\r
73                 ignoreCase : arguments[2] || false\r
74         })\r
75         \r
76         function setEvent() {\r
77                 // set highlighter to run on load (use LowPro if present)\r
78                 if (typeof Event != 'undefined' && typeof Event.onReady == 'function') \r
79                   return Event.onReady(CodeHighlighter.init.bind(CodeHighlighter));\r
80                 \r
81                 var old = window.onload;\r
82                 \r
83                 if (typeof window.onload != 'function') {\r
84                         window.onload = function() { CodeHighlighter.init() };\r
85                 } else {\r
86                         window.onload = function() {\r
87                                 old();\r
88                                 CodeHighlighter.init();\r
89                         }\r
90                 }\r
91         }\r
92         \r
93         // only set the event when the first style is added\r
94         if (this.styleSets.length==1) setEvent();\r
95 }\r
97 CodeHighlighter.init = function() {\r
98         if (!document.getElementsByTagName) return; \r
99         if ("a".replace(/a/, function() {return "b"}) != "b") return; // throw out Safari versions that don't support replace function\r
100         // throw out older browsers\r
101         \r
102         var codeEls = document.getElementsByTagName("CODE");\r
103         // collect array of all pre elements\r
104         codeEls.filter = function(f) {\r
105                 var a =  new Array;\r
106                 for (var i = 0; i < this.length; i++) if (f(this[i])) a[a.length] = this[i];\r
107                 return a;\r
108         } \r
109         \r
110         var rules = new Array;\r
111         rules.toString = function() {\r
112                 // joins regexes into one big parallel regex\r
113                 var exps = new Array;\r
114                 for (var i = 0; i < this.length; i++) exps.push(this[i].exp);\r
115                 return exps.join("|");\r
116         }\r
117         \r
118         function addRule(className, rule) {\r
119                 // add a replace rule\r
120                 var exp = (typeof rule.exp != "string")?String(rule.exp).substr(1, String(rule.exp).length-2):rule.exp;\r
121                 // converts regex rules to strings and chops of the slashes\r
122                 rules.push({\r
123                         className : className,\r
124                         exp : "(" + exp + ")",\r
125                         length : (exp.match(/(^|[^\\])\([^?]/g) || "").length + 1, // number of subexps in rule\r
126                         replacement : rule.replacement || null \r
127                 });\r
128         }\r
129         \r
130         function parse(text, ignoreCase) {\r
131                 // main text parsing and replacement\r
132                 return text.replace(new RegExp(rules, (ignoreCase)?"gi":"g"), function() {\r
133                         var i = 0, j = 1, rule;\r
134                         while (rule = rules[i++]) {\r
135                                 if (arguments[j]) {\r
136                                         // if no custom replacement defined do the simple replacement\r
137                                         if (!rule.replacement) return "<span class=\"" + rule.className + "\">" + arguments[0] + "</span>";\r
138                                         else {\r
139                                                 // replace $0 with the className then do normal replaces\r
140                                                 var str = rule.replacement.replace("$0", rule.className);\r
141                                                 for (var k = 1; k <= rule.length - 1; k++) str = str.replace("$" + k, arguments[j + k]);\r
142                                                 return str;\r
143                                         }\r
144                                 } else j+= rule.length;\r
145                         }\r
146                 });\r
147         }\r
148         \r
149         function highlightCode(styleSet) {\r
150                 // clear rules array\r
151                 var parsed, clsRx = new RegExp("(\\s|^)" + styleSet.name + "(\\s|$)");\r
152                 rules.length = 0;\r
153                 \r
154                 // get stylable elements by filtering out all code elements without the correct className       \r
155                 var stylableEls = codeEls.filter(function(item) { return clsRx.test(item.className) });\r
156                 \r
157                 // add style rules to parser\r
158                 for (var className in styleSet.rules) addRule(className, styleSet.rules[className]);\r
159                 \r
160                         \r
161                 // replace for all elements\r
162                 for (var i = 0; i < stylableEls.length; i++) {\r
163                         // EVIL hack to fix IE whitespace badness if it's inside a <pre>\r
164                         if (/MSIE/.test(navigator.appVersion) && stylableEls[i].parentNode.nodeName == 'PRE') {\r
165                                 stylableEls[i] = stylableEls[i].parentNode;\r
166                                 \r
167                                 parsed = stylableEls[i].innerHTML.replace(/(<code[^>]*>)([^<]*)<\/code>/i, function() {\r
168                                         return arguments[1] + parse(arguments[2], styleSet.ignoreCase) + "</code>"\r
169                                 });\r
170                                 parsed = parsed.replace(/\n( *)/g, function() { \r
171                                         var spaces = "";\r
172                                         for (var i = 0; i < arguments[1].length; i++) spaces+= "&nbsp;";\r
173                                         return "\n" + spaces;  \r
174                                 });\r
175                                 parsed = parsed.replace(/\t/g, "&nbsp;&nbsp;&nbsp;&nbsp;");\r
176                                 parsed = parsed.replace(/\n(<\/\w+>)?/g, "<br />$1").replace(/<br \/>[\n\r\s]*<br \/>/g, "<p><br></p>");\r
177                                 \r
178                         } else parsed = parse(stylableEls[i].innerHTML, styleSet.ignoreCase);\r
179                         \r
180                         stylableEls[i].innerHTML = parsed;\r
181                 }\r
182         }\r
183         \r
184         // run highlighter on all stylesets\r
185         for (var i=0; i < this.styleSets.length; i++) {\r
186                 highlightCode(this.styleSets[i]);  \r
187         }\r