freecog.net

syntaxhighlighter.js

Also see Raw version, Minified version.

   1 //!debugon
   2 // Copyright 2006 Tom W.M. (http://freecog.net/)
   3 // 11:27 p.m. Tuesday, April 11, 2006
   4 
   5 // Use CodeProcessor.process(els) to add line numbers and syntax 
   6 // highlighting.  The node must be identifiable as acceptable to each
   7 // appropriate processor module.  For the moment, this means it must
   8 // have a certain class name--"ln-con" for line numbering, "js-con" for
   9 // JavaScript.
  10 
  11 // Dependencies:
  12 //  * Dean Edwards' Base class
  13 //  * My AsyncQueue class
  14 //  * MochiKit.Base
  15 //  * MochiKit.Async
  16 
  17 // Known bugs:
  18 //  * Multiple division symbols on a line could potentially confuse
  19 //    the JavaScript highlighter.
  20 
  21 // Note regarding mixing of object and regular expression literals:
  22 //    There is a bug in both my editor's syntax higlighting 
  23 //    (Notepad2) and JSMin.  Neither recognizes RE literals that
  24 //    follow a colon (JSMin's code states that it only recognizes
  25 //    those following "(", ",", or "=").  To work around this, all
  26 //    RE literals that would be effected have been wrapped in
  27 //    parenthesis.  Unfortunately, this makes them even more
  28 //    illegible (though not as much as converting them to strings
  29 //    would).
  30 
  31 
  32 CodeProcessor = {
  33     // List of processors.
  34     processors: [],
  35     // Registers a processor constructor.  Processor.test() should be a 
  36     // function that returns true if the processor will accept a DOM
  37     // node.
  38     add: function(name, processor) {
  39         CodeProcessor.processors.push({
  40             name: name,
  41             processor: processor,
  42             test: processor.test
  43         });
  44         return processor;
  45     },
  46     // Applies each of the processors to the passed element(s).
  47     // Accepts a string ID, element, or array-like object of the same.
  48     process: function(els) {
  49         // Put non-array-like objects into an array.
  50         if (!MochiKit.Base.isArrayLike(els)) {
  51             els = [els];
  52         }
  53         var results = [];
  54         for (var x = 0, y = els.length; x < y; x++) {
  55             var dom_node = els[x];
  56             if (typeof dom_node == 'string')
  57                 dom_node = document.getElementById(dom_node);
  58             results.push(CodeProcessor._process_element(dom_node));
  59         }
  60         return results;
  61     },
  62     // Processes an individual element.
  63     _process_element: function(dom_node) {
  64         var CP = CodeProcessor;
  65         
  66         var filter_classes = ['ann-bracket', 'ann-default', 'ann-ellip'];
  67         var filter_ids = [];
  68         
  69         var className = dom_node.className;
  70         var m = className.match(this.SELECT_BY_CLASS_RE);
  71         if (m) { 
  72             var classes = m[1].substring(3).split('---');
  73             filter_classes = filter_classes.concat(classes);
  74         }
  75         
  76         m = className.match(this.SELECT_BY_ID_RE);
  77         if (m) filter_ids = m[1].substring(3).split('---');
  78         
  79         var filterCount = 0;
  80         // The filter function:
  81         var filter = function(node) {
  82             var id = node.id;
  83             for (var i = 0; i < filter_ids.length; i++) {
  84                 if (id == filter_ids[i]) return true;
  85             }
  86             var cls = " " + node.className.replace(/\s+/g, " ") + " ";
  87             for (var i = 0; i < filter_classes.length; i++) {
  88                 var test = " " + filter_classes[i] + " ";
  89                 if (cls.indexOf(test) > -1) return true;
  90             }
  91             filterCount++;
  92             return false;
  93         };
  94         
  95         // Processors that accepted dom_node
  96         var processors = [];
  97         // Deferred to chain the deferred properties to.
  98         var chain = MochiKit.Async.succeed();
  99         
 100         for (var i = 0, j = CP.processors.length; i < j; i++) {
 101             chain.addCallback(function(proc) {
 102                 if (proc.test(dom_node)) {
 103                     var p = new proc.processor(filter, dom_node);
 104                     processors.push(p);
 105                     if (p.filter_classes)
 106                         filter_classes = filter_classes.concat(p.filter_classes);
 107                     if (p.filter_ids)
 108                         filter_ids = filter_ids.concat(p.filter_ids);
 109                     filter = p.node_filter || filter;
 110                     return p.deferred;
 111                 }
 112             }, CP.processors[i]);
 113             chain.addErrback(log); // debug
 114         }
 115         
 116         var deferred = new MochiKit.Async.Deferred();
 117         chain.addCallback(function() {
 118             deferred.callback(dom_node, filter);
 119         });
 120         
 121         return {
 122             processors: processors,
 123             deferred: deferred
 124         };
 125     },
 126     SELECT_BY_CLASS_RE: (/filterByClass(---[a-zA-Z][-a-zA-Z0-9]+)*/),
 127     SELECT_BY_ID_RE: (/filterByID(---[a-zA-Z][-a-zA-Z0-9\.]+)*/),
 128     match_class: function(re, el) {
 129         return el.className.match(new RegExp("(^|\\s)" + re + "(\\s|$)"));
 130     },
 131     
 132     PARENTHESIS_RE: ( /^([\(\)])/ ),
 133     BRACKETS_RE: ( /^([\[\]])/ ),
 134     BRACES_RE: ( /^([\{\}])/ ),
 135     STRING_SQUOT_RE: ( /^(''|'(\\\\|\\'|[^'])+')/ ),
 136     STRING_DQUOT_RE: ( /^(""|"(\\\\|\\"|[^"])+")/ ),
 137     
 138     // Generally useful constants for use in processor modules
 139     C_LINEEND_COMMENT_CTX: { // Single line comment
 140         start: ( /^(\/\/[^\r\n]*)/ ),
 141         end: ( /^(.*?)[\r\n]/ ),
 142         cls: 'c-sl-com'
 143     },
 144     APOS_LINEEND_COMMENT_CTX: { // Single line comment
 145         start: ( /^('[^\r\n]*)/ ),
 146         end: ( /^(.*?)[\r\n]/ ),
 147         cls: 'apos-com'
 148     },
 149     C_MULTILINE_COMMENT_CTX: { // Multi-line comment
 150         start: ( /^(\/\*[\s\S]*)/ ),
 151         end: ( /^([\s\S]*?\*\/)/ ),
 152         cls: 'c-ml-com'
 153     },
 154     DQUOTE_CTX: { // Double quote string
 155         start: ( /^(".*)/ ),
 156         end: ( /^("|(\\"|[^"])")/ ),
 157         cls: 'dquote-str'
 158     },
 159     SQUOTE_CTX: { // Single quote string
 160         start: ( /^('.*)/ ),
 161         end: ( /^('|(\\\\\|\\'|[^'])+')/ ),
 162         cls: 'squote-str'
 163     },
 164     
 165     with_other_class: function(ctx, new_cls) {
 166         var o = {};
 167         MochiKit.Base.update(o, ctx);
 168         o.cls = new_cls;
 169         return o;
 170     },
 171     
 172     toString: function(){ return '[object CodeProcessor]'; }
 173 };
 174 
 175 
 176 /**********************************************************************/
 177 
 178 
 179 /**
 180 * Allows for iterable-like processing of text nodes in a DOM tree,
 181 * traversing depth-first.  This is intended to be subclassed for
 182 * use.
 183 *
 184 * @constructor
 185 * @id NodeQueue
 186 */
 187 NodeQueue = Base.extend({
 188     /**
 189     * Initializes the queue.  The children of the (optional)
 190     * DOM node passed as the second argument will be inserted
 191     * into the queue.
 192     * @method
 193     * @id NodeQueue.prototype.init_node_queue
 194     */
 195     init_node_queue: function(dom_node) {
 196         this._current = null;
 197         this._queue = []; // FIFO
 198         switch (arguments.length) {
 199             case 0:
 200                 break;
 201             case 1:
 202                 this.prepend_children(dom_node);
 203                 break;
 204             default:
 205                 throw "Invalid arguments to init_node_queue()";
 206         }
 207     },
 208     /**
 209     * Dispatches the node currently at the top of the queue to a 
 210     * processing function.
 211     * @method
 212     * @id NodeQueue.prototype.dispatch_node
 213     */
 214     dispatch_node: function() {
 215         this._current = this._current || this._queue.shift();
 216         if (!this._current) this.end_node_queue();
 217         
 218         //log("@@@    Loop. this._current = " + repr(this._current));
 219         
 220         switch (this._current.nodeType) {
 221             case 1: // ELEMENT_NODE
 222                 this.process_element_node(this._current);
 223                 break;
 224             case 3: // TEXT_NODE
 225                 this.process_text_node(this._current);
 226                 break;
 227             default:
 228                 // Just skip anything else.
 229                 this.advance_queue();
 230                 break;
 231         }
 232     },
 233     /**
 234     * Discards anything currently being processed, so that
 235     * `dispatch_node` fetches the next node from the queue.
 236     * This must be called once each time `process_element_node`
 237     * and `process_text_node` are finished with a node, or the
 238     * queue will not advance.
 239     * @method
 240     * @id NodeQueue.prototype.advance_queue
 241     */
 242     advance_queue: function() {
 243         this._current = null;
 244     },
 245     /**
 246     * Prepends children into the queue for processing.
 247     * This must be called with every element that is 
 248     * passed to `process_element_node`, or its children
 249     * will not be processed.
 250     * @method
 251     * @id NodeQueue.prototype.prepend_children
 252     */
 253     prepend_children: function(element) {
 254         for (var i = element.childNodes.length - 1; i >= 0; i--) {
 255             var n = element.childNodes[i];
 256             var num = this._queue.unshift(n);
 257         }
 258         //log("###    " + element.childNodes.length + " childNodes unshift'd.");
 259     },
 260     /**
 261     * Gets called with each element that passes through
 262     * the queue, placing their child nodes into the queue.
 263     * Override this in a subclass for different behavior.
 264     * @method
 265     * @id NodeQueue.prototype.process_element_node
 266     */
 267     process_element_node: function(node) {
 268         this.prepend_children(node);
 269         this.advance_queue();
 270     },
 271     /**
 272     * Gets called with each text node that passes through
 273     * the queue.  Override this in a subclass.
 274     * @method
 275     * @id NodeQueue.prototype.process_text_node
 276     */
 277     process_text_node: function(node) {
 278         // override in subclass
 279         this.advance_queue();
 280     },
 281     /**
 282     * Gets called when the queue is exhausted.  Override this
 283     * in a subclass if you need it to do something.
 284     * @method
 285     * @id NodeQueue.prototype.end_node_queue
 286     */
 287     end_node_queue: function() {},
 288     toString: function() { return '[object NodeQueue instance]'; }
 289 });
 290 
 291 
 292 /**
 293 * Extends `NodeQueue` to allow for filtering of the elements it
 294 * processes.  Created to allow simple processing of all the text
 295 * nodes in a tree.
 296 *
 297 * @id FilteredNodeQueue
 298 * @constructor
 299 */
 300 FilteredNodeQueue = NodeQueue.extend({
 301     /**
 302     * Initializes the queue.  Pass a predicate that identifies
 303     * acceptable nodes (required) and a DOM node (optional) to 
 304     * seed the queue with (placing its child nodes in the queue).
 305     * @method
 306     * @id FilteredNodeQueue.prototype.init_node_queue
 307     */
 308     init_node_queue: function(node_filter, dom_node) {
 309         this.__node_filter = node_filter;
 310         if (this.__node_filter(dom_node))
 311             this.base();
 312         else
 313             this.base(dom_node);
 314     },
 315     /**
 316     * Places the child nodes of every element that passes
 317     * through the queue in the queue unless the element
 318     * is filtered.
 319     * @method
 320     * @id FilteredNodeQueue.prototype.process_element_node
 321     */
 322     process_element_node: function(node) {
 323         if (!this.__node_filter(node))
 324             this.prepend_children(node);
 325         this.advance_queue();
 326     },
 327     toString: function() { return '[object FilteredNodeQueue instance]'; }
 328 });
 329 
 330 
 331 /**********************************************************************/
 332 
 333 /**
 334 * Adds line numbers to source code.
 335 *
 336 * @id CodeProcessor.LineNumProc
 337 * @constructor
 338 */
 339 CodeProcessor.LineNumProc = FilteredNodeQueue.extend({
 340     constructor: function(node_filter, dom_node) {
 341         this.dom_node = dom_node;
 342         this.node_filter = node_filter;
 343         this.init_node_queue(node_filter, dom_node);
 344         this.number_nodes = [];
 345         this.filter_classes = [this.NUMBER_CLASS];
 346         this._counter = 1;
 347         
 348         this.deferred = (new AsyncQueue(this))
 349             // Create containers:
 350             .next(function() {
 351                 // Line 1:
 352                 dom_node.insertBefore(this.create_number_container(),
 353                                      dom_node.firstChild);
 354             })
 355                 // The rest of the lines:
 356             .loop(this.dispatch_node)
 357             // Calculate how many characters wide
 358             // the numbering shoudl be:
 359             .next(this.init_number_insertion)
 360             // Insert the text into the containers:
 361             .loop(this.insert_number)
 362             // Done!
 363             //.next(partial(log, "LineNumProc done."))
 364             .go();
 365     },
 366     /**
 367     * Creates the <span> that contains the line number, incrementing
 368     * the counter, appending it to the `number_nodes` array and 
 369     * returning it.
 370     * @method
 371     * @id CodeProcessor.LineNumProc.prototype.create_number_container
 372     */
 373     create_number_container: function() {
 374         this._counter++;
 375         var el = document.createElement('span');
 376         el.className = this.NUMBER_CLASS;
 377         this.number_nodes.push(el);
 378         return el;
 379     },
 380     /**
 381     * Searches through text nodes for line breaks.  The text node is
 382     * split after the break, and the line number container inserted.
 383     * @method
 384     * @id CodeProcessor.LineNumProc.prototype.process_text_node
 385     */
 386     process_text_node: function(node) {
 387         var text = node.nodeValue;
 388         var m = text.match(/[^\r\n]*(\r\n|[\r\n])/);
 389         if (m) {
 390             var t = document.createTextNode(m[0]);
 391             node.parentNode.insertBefore(t, node);
 392             node.parentNode.insertBefore(
 393                 this.create_number_container(), node);
 394             text = text.substring(m[0].length);
 395             if (!text) {
 396                 node.parentNode.removeChild(node);
 397                 this.advance_queue();
 398             } else {
 399                 node.nodeValue = text;
 400             }
 401         } else {
 402             this.advance_queue();
 403         }
 404     },
 405     /**
 406     * Stop the `AsyncQueue` instance's loop when the node queue
 407     * is exhausted.
 408     * @method
 409     * @id CodeProcessor.LineNumProc.prototype.end_node_queue
 410     */
 411     end_node_queue: AsyncQueue.stop_loop,
 412     /**
 413     * Sets the value this._precision to the correct number of places
 414     * for the value of `this._counter`.
 415     * @method
 416     * @id CodeProcessor.LineNumProc.prototype.init_number_insertion
 417     */
 418     init_number_insertion: function() {
 419         var p = 1, c = this._counter;
 420         while (Math.pow(10, p) < c) {
 421             p++;
 422         }
 423         // Precision is 2 at minimum, so that multiple short code 
 424         // examples on a single page will match up.
 425         this._precision = Math.max(2, p);
 426         this._index = 0;
 427     },
 428     /**
 429     * Pads a number's string representation to the number of places
 430     * in `this._precision`
 431     * @method
 432     * @id CodeProcessor.LineNumProc.prototype.
 433     */
 434     pad: function(num) {
 435         num = "" + num;
 436         while (num.length < this._precision) {
 437             num = "\xA0" + num;
 438         }
 439         return num;
 440     },
 441     /**
 442     * Insert 
 443     * @method
 444     * @id CodeProcessor.LineNumProc.prototype.insert_number
 445     */
 446     insert_number: function() {
 447         var node = this.number_nodes[this._index++];
 448         if (!node) {
 449             throw AsyncQueue.StopLoop;
 450         }
 451         var text = this.pad(this._index) + '.\xA0';
 452         var num = document.createTextNode(text);
 453         node.appendChild(num);
 454     },
 455     CONTAINER_CLASS: 'ln-con',
 456     NUMBER_CLASS: 'ln-num',
 457     toString: function(){ return '[object CodeProcessor.LineNumProc instance]'; }
 458 }, {
 459     /**
 460     * Predicate that accepts DOMElements with classes of "linenumbers"
 461     * "ln-con".
 462     * @method
 463     * @id CodeProcessor.LineNumProc.prototype.insert_number
 464     */
 465     test: MochiKit.Base.partial(CodeProcessor.match_class, "(linenumbers|ln-con)")
 466 });
 467 
 468 CodeProcessor.add("LineNumProc", CodeProcessor.LineNumProc);
 469 
 470 
 471 /**********************************************************************/
 472 
 473 // JavaScript Highlighter module for SyntaxHighlighter
 474 // Copyright 2006 Tom W.M. (http://freecog.net/)
 475 // 12:15 p.m. Monday, July 03, 2006
 476 
 477 CodeProcessor.JSHighlighter = FilteredNodeQueue.extend({
 478     constructor: function(node_filter, dom_node) {
 479         //log("Instantiating JSHighlighter()");
 480         this.dom_node = dom_node;
 481         this.node_filter = node_filter;
 482         this.init_node_queue(node_filter, dom_node);
 483         
 484         this.deferred = (new AsyncQueue(this))
 485             .loop(this.dispatch_node)
 486             //.next(partial(log, "JSHighlighter done."))
 487             .go();
 488     },
 489     end_node_queue: AsyncQueue.stop_loop,
 490     process_text_node: function(node) {
 491         var text = node.nodeValue;
 492         
 493         // Empty text nodes trigger an infinite loop.
 494         if (!text) {
 495             //log("<<<    Empty text node discarded.");
 496             node.parentNode.removeChild(node);
 497             this.advance_queue();
 498             return;
 499         }
 500         
 501         var ctx = this._context;
 502         if (ctx) { // Is there an open context?
 503             var m = text.match(ctx.end);
 504             if (m) { // Is the end of the context?
 505                 if (m[1]) 
 506                     this._process_match(ctx, m[1], node, text);
 507                 if (ctx.postprocessor) {
 508                     ctx.postprocessor(this._context_contents);
 509                 }
 510                 this._context = null;
 511                 //log("XXX    Context closed. m = " + repr(m));
 512             } else {
 513                 // We haven't reached the end yet.  Devour the
 514                 // entire text node.
 515                 this._process_match(ctx, text, node, text);
 516                 //log("---    Context devoured " + repr(text));
 517             }
 518             return;
 519             
 520         } else { // Process normally.
 521             var REs = this._get_chunks();
 522             for (var i = 0; i < REs.length; i++) {
 523                 var o = REs[i];
 524                 var m = text.match(o.re);
 525                 if (m) {
 526                     this._process_match(o, m[1], node, text);
 527                     //log(">>>    RE " + i + " matched " + repr(m) + ". cls = " + repr(o.cls));
 528                     return;
 529                 }
 530             }
 531         }
 532         
 533         // Hmm... looks like none of the REs matched.
 534         // We could be dealing with a comment or string
 535         // that has been broken up.
 536         var cs = this._get_contexts();
 537         for (var i = 0; i < cs.length; i++) {
 538             var o = cs[i];
 539             var m = text.match(o.start);
 540             if (m) {
 541                 this._context = o;
 542                 this._context_contents = [];
 543                 this._process_match(o, m[1], node, text);
 544                 //log("+++    Context opened. m = " + repr(m));
 545                 return;
 546             }
 547         }
 548         
 549         // If all else fails, move forward a character.
 550         var t = document.createTextNode(text.charAt(0));
 551         text = text.substring(1);
 552         node.nodeValue = text;
 553         node.parentNode.insertBefore(t, node);
 554     },
 555     // Turns a regular expression match into the appropriate DOM nodes.
 556     _process_match: function(o, m, node, text) {
 557         var t = document.createTextNode(m);
 558         if (o.cls) {
 559             var e = document.createElement('span');
 560             e.className = o.cls;
 561             e.appendChild(t);
 562         }
 563         node.parentNode.insertBefore(e || t, node);
 564         if (this._context) {
 565             this._context_contents.push(e || t);
 566         }
 567         text = text.substring(m.length);
 568         if (text) {
 569             node.nodeValue = text; // Probably slow.
 570         } else { // We're done with the text node.
 571             node.parentNode.removeChild(node);
 572             this.advance_queue();
 573         }
 574     },
 575     
 576     // Maps regular expressions to classes.
 577     _get_chunks: function() {
 578         var C = CodeProcessor;
 579         var J = C.JSHighlighter;
 580         return this.__chunks_cache || (
 581             this.__chunks_cache = [
 582                 {re: J.SKIP_RE,         cls: null},
 583                 {re: C.PARENTHESIS_RE,  cls: "js-par"},
 584                 {re: C.BRACKETS_RE,     cls: "js-bra"},
 585                 {re: C.BRACES_RE,       cls: "js-br"},
 586                 {re: J.KEYWORD_RE,      cls: "js-kwd"},
 587                 {re: J.BUILTIN_RE,      cls: "js-id js-builtin"},
 588                 {re: J.BASE_RE,         cls: "js-id js-base"},
 589                 {re: J.OPERATOR_W_RE,   cls: "js-op"},
 590                 {re: J.NUMBER_RE,       cls: "js-num"},
 591                 {re: J.NUMBER_W_RE,     cls: "js-num"},
 592                 //{re: J.LONE_DOT,        cls: null},
 593                 {re: J.KEY_RE,          cls: "js-id js-key"},
 594                 {re: J.CONSTANT_RE,     cls: "js-id js-const"},
 595                 {re: J.ID_RE,           cls: "js-id"},
 596                 {re: J.COMMENT_M_RE,    cls: "js-com"},
 597                 {re: J.RE_RE,           cls: "js-re"},
 598                 {re: J.COMMENT_S_RE,    cls: "js-com"},
 599                 {re: J.OPERATOR_S_RE,   cls: "js-op"},
 600                 {re: C.STRING_SQUOT_RE, cls: "js-str"},
 601                 {re: C.STRING_DQUOT_RE, cls: "js-str"}
 602             ]);
 603     },
 604     _get_contexts: function() {
 605         var C = CodeProcessor;
 606         return this.__context_cache || (
 607             this.__context_cache = [
 608                 C.with_other_class(C.C_LINEEND_COMMENT_CTX, 'js-com'),
 609                 C.with_other_class(C.C_MULTILINE_COMMENT_CTX, 'js-com'),
 610                 C.with_other_class(C.DQUOTE_CTX, 'js-str'),
 611                 C.with_other_class(C.SQUOTE_CTX, 'js-str')
 612             ]);
 613     },
 614     toString: function(){ return '[object CodeProcessor.JSHighlighter instance]'; }
 615 }, {
 616     // All regular expressions are constructed so that the first group 
 617     // is the relevant chunk of text.
 618     
 619     // Should ? and : be operators?  Something else?
 620     SKIP_RE: ( /^(([\s,;\?:]|\.(?![$_a-zA-Z]))+)/ ),
 621     //LONE_DOT: ( /^(\.)/ ),
 622     
 623     // There are far too many of these silly keywords.
 624     // Should this list include "true" and "false"?
 625     KEYWORD_RE: new RegExp("^(" +
 626         "abstract|boolean|break|byte|case|catch|char|class|const|" +
 627         "continue|debugger|default|delete|double|do|else|enum|export|" +
 628         "extends|final|finally|float|for|function|goto|if|implements|" +
 629         "import|interface|int|in|long|native|new|package|private|" +
 630         "protected|public|return|short|static|super|synchronized|" +
 631         "switch|this|throws|throw|transient|try|var|volatile|void|" +
 632         "while|with)(?![0-9a-zA-Z\\$_])"),
 633     
 634     NUMBER_RE: ( /^([+-]?(0x[0-9a-f]+|[0-9]+\.[0-9]*|\.?[0-9]+)(e[+-]?[0-9]+)?)/i ),
 635     
 636     // Should NaN and Infitity be styled as numbers, or keywords?
 637     NUMBER_W_RE: ( /^([+-]?Infinity|NaN)(?![0-9a-zA-Z\$_])/ ),
 638     
 639     RE_RE: ( /^(\/(\\\\|\\\/|[^\/\n\r])*[^\\]\/g?i?m?)[ \t]*([\)\}\],;.\n\t]|$)/ ),
 640     
 641     // Matches the keyword operators.  Must go *before* the identifier RE,
 642     // because these are also all valid identifiers.
 643     OPERATOR_W_RE: ( /^(delete|instanceof|typeof)(?![0-9a-zA-Z\$_])/ ),
 644     
 645     // Matches symbol operators.  Must go after the regular expression RE
 646     // and the single and multiline comment REs, or it will swallow the 
 647     // beginning of those constructs.
 648     OPERATOR_S_RE: new RegExp(
 649         "^(\\+=|-=|>=|<=|>{1,3}|<{1,2}|\\+{1,2}|-{1,2}|={1,3}|" + 
 650         "\\*=?|\\\/=?(?![\\\/\\*])|%=?|!=|&&?|\\|\\|?|!!?|\\^|~)"),
 651     
 652     ID_RE: ( /^([$_A-Z][A-Z0-9$_]*)/i ),
 653     // A constant is an uppercase identifier of only letters, numbers
 654     // and underscores.
 655     CONSTANT_RE: ( /^([_A-Z][A-Z0-9_]+)([^a-zA-Z0-9_$]|$)/ ),
 656     
 657     COMMENT_S_RE: ( /^(\/\/.*?)[\r\n]/ ),
 658     COMMENT_M_RE: ( /^(\/\*[\s\S]*?\*\/)/ ),
 659     BUILTIN_RE: ( /^(true|false|null|undefined)(?!([0-9a-zA-Z\$_]))/ ),
 660     
 661     // This is intended to match a key in an object literal, though it
 662     // will also match case statement.
 663     KEY_RE: ( /^([$_a-zA-Z][a-zA-Z0-9$_]*):/ ),
 664     
 665     // Special methods in Dean Edwards' Base class.
 666     // I'd include "Base", but it conflicts with MochiKit.Base
 667     BASE_RE: ( /^(base|extend|implement)(?![0-9a-zA-Z\$_])/ ),
 668     
 669     
 670     // Checks for the presence of the class 'js-con'.
 671     //test: function(n) {
 672     //  return (" " + n.className.replace(/\s+/g, " ") + " ")
 673     //           .indexOf(' js-con ') > -1;
 674     //}
 675     test: MochiKit.Base.partial(CodeProcessor.match_class, "(js-con|javascript)")
 676 });
 677 
 678 // Register
 679 CodeProcessor.add("JSHighlighter", CodeProcessor.JSHighlighter);
 680 
 681 
 682 /**********************************************************************/
 683 
 684 CodeProcessor.PBasic2Highlighter = CodeProcessor.JSHighlighter.extend({
 685     _get_chunks: function() {
 686         var C = CodeProcessor;
 687         var P = C.PBasic2Highlighter;
 688         return this.__chunks_cache || (
 689             this.__chunks_cache = [
 690                 {re: /\s+\.\\,/,                cls: null},
 691                 {re: C.PARENTHESIS_RE,          cls: 'pb2-par'},
 692                 {re: C.BRACKETS_RE,             cls: 'pb2-bra'},
 693                 {re: P.NUMBER_RE,               cls: 'pb2-num'},
 694                 {re: P.OPERATOR_RE,             cls: 'pb2-op'},
 695                 {re: P.RESERVED_WORD_RE,        cls: 'pb2-kwd'},
 696                 {re: P.WORD_OPERATOR_RE,        cls: 'pb2-op'},
 697                 {re: P.TYPE_RE,                 cls: 'pb2-type'},
 698                 {re: P.CONVERSION_FORMATTER_RE, cls: 'pb2-conv'},
 699                 {re: P.LABEL_RE,                cls: 'pb2-id pb2-label'},
 700                 {re: P.SYMBOL_TYPE_RE,          cls: 'pb2-symb'},
 701                 {re: P.BUILTIN_RE,              cls: 'pb2-id pb2-builtin'},
 702                 {re: P.ALIAS_MODIFIER_RE,       cls: 'pb2-aliasmod'},
 703                 {re: P.DEBUG_CONTROL_RE,        cls: 'pb2-ctrl'},
 704                 {re: P.ID_RE,                   cls: 'pb2-id'},
 705                 {re: C.STRING_DQUOT_RE,         cls: 'pb2-str'},
 706                 {re: P.COND_COMPILATION_RE,     cls: 'pb2-cc'}
 707             ]);
 708     },
 709     _get_contexts: function() {
 710         var C = CodeProcessor;
 711         return this.__context_cache || (
 712             this.__context_cache = [
 713                 C.with_other_class(C.APOS_LINEEND_COMMENT_CTX , 'pb2-com'),
 714                 C.with_other_class(C.DQUOTE_CTX, 'pb2-str')
 715             ]);
 716     },
 717     toString: function(){ return '[object CodeProcessor.PBasic2Highlighter instance]'; }
 718 }, {
 719     LABEL_RE: ( /^([A-Z][A-Za-z0-9_]*):/ ),
 720     
 721     NUMBER_RE: ( /^(-?(\d+|%[10]+|$[a-zA-Z0-9]+))/ ),
 722     
 723     ID_RE: ( /^([a-z][a-z0-9]*)/i ),
 724     
 725     // Symbol include for BS1 compatability:
 726     SYMBOL_TYPE_RE: ( /^(PIN|CON|VAR|SYMBOL)([^a-zA-Z0-9_]|$)/ ),
 727     TYPE_RE: ( /^(Word|Byte|Nib|Bit)([^a-zA-Z0-9_]|$)/ ),
 728     BUILTIN_RE: new RegExp('^(' +
 729         '(IN|OUT|DIR)([SLHA-D]|[0-9]|1[0-5])|' + // Pins
 730         'W([0-9]|1[0-2])' + // Words
 731         'B(1?[0-9]|2[0-5])' + // Bytes
 732         ')([^a-zA-Z0-9_]|$)', 'i'
 733     ),
 734     CONVERSION_FORMATTER_RE: new RegExp('^((' +
 735         'S?DEC|([SI]|IS)?HEX|([SI]|IS)?BIN|S?NUM|(WAIT|SP)?STR|SKIP|' +
 736         'WAIT' +
 737         ')\d*)([^a-zA-Z0-9_]|$)', 'i'
 738     ),
 739     ALIAS_MODIFIER_RE: new RegExp('^(' + 
 740         '(HIGH|LOW)(BYTE|NIB|BIT)|BYTE[01]|NIB[0-3]|BIT([0-9]|1[0-5])' +
 741         ')([^a-zA-Z0-9_]|$)', 'i'
 742     ),
 743     DEBUG_CONTROL_RE: new RegExp('^(' + 
 744         'LF|CLS|CRS(RXY|RLF|RRT|RUP|RDN|RX|RY)|CLR(EOL|DN)|CR' +
 745         ')([^a-zA-Z0-9_]|$)', 'i'
 746     ),
 747     RESERVED_WORD_RE: new RegExp('^(' +
 748         'AUXIO|BRANCH|BUTTON|COMPARE|CONFIGPIN|COUNT|DATA|DEBUG(IN)?|' +
 749         'DO|LOOP|WHILE|UNTIL|DTMFOUT|EEPROM|EXIT|FOR|NEXT|TO|STEP|' + 
 750         'FREQOUT|GET|GO(SUB|TO)|HIGH|I2C(IN|OUT)|(END)?IF|THEN|ELSE(IF)?|' +
 751         'INPUT|IOTERM|LCD(CMD|IN|OUT)|LOOK(DOWN|UP)|LOW|MAINIO|NAP|ON|' +
 752         'OUTPUT|OW(IN|OUT)|PAUSE|POLL(IN|MODE|OUT|RUN|WAIT)|POT|' +
 753         'PULS(IN|OUT)|PUT|PWM|RANDOM|RCTIME|READ|RETURN|REVERSE|RUN|' + 
 754         '(END)?SELECT|CASE|SER(IN|OUT)|SHIFT(IN|OUT)|SLEEP|SOUND|STOP|' +
 755         'STORE|TOGGLE|THRESOLD|WRITE|XOUT|END)([^a-zA-Z0-9_]|$)', 'i'
 756     ),
 757     OPERATOR_RE: ( /^(=|<[><=]?|>[>=]?|\+|-|\*[\*\/]?|\/\/?|&\/?|\|\/?|\^\/?|~|\?)/ ),
 758     WORD_OPERATOR_RE: new RegExp('^(' + 
 759         'MIN|MAX|DIG|REV|AND|X?OR|ATN|HYP|ABS|[DN]CD|SIN|COS|SQR|NOT' +
 760         ')([^a-zA-Z0-9_]|$)', 'i'
 761     ),
 762     COND_COMPILATION_RE: ( /^(#[A-Z]+)[^A-Z]/ ),
 763     test: MochiKit.Base.partial(CodeProcessor.match_class, "(pbasic2?)")
 764 });
 765 
 766 // Register
 767 CodeProcessor.add("PBasic2Highlighter", CodeProcessor.PBasic2Highlighter);
 768 
 769 
 770 /**********************************************************************/
 771 
 772 // JavaScript Highlighter module for SyntaxHighlighter
 773 // Copyright 2006 Tom W.M. (http://freecog.net/)
 774 // 12:15 p.m. Monday, July 03, 2006
 775 
 776 // A good syntax overview: http://www.blooberry.com/indexdot/css/syntax/syntax.htm
 777 
 778 /*
 779 Units:
 780 %|em|ex|px|in|cm|mm|pt|pc|deg|grad|rad|s|ms|Hz|kHz
 781 
 782 Parsing outline
 783 
 784 1: General Block
 785     1.1: Whitespace
 786     1.2: Comments
 787     1.3: At-Rule
 788         /^@[^\s]+(?=(\s|$))/
 789         ... optional arguments (url, string, chunks) ... 
 790         ... block (parse contents) or ;
 791     1.4: Rule-Set
 792         selector (chunks)
 793         ... declaration block (parse contents, see 2) ...
 794     
 795 2: Declaration Block
 796     2.1: Whitespace
 797     2.2: Comments
 798     2.3: Declarations -- the following in order
 799         2.3.1: /^[^\s:]+/     property
 800              If it begins with a _, mark it as a hack.
 801         2.3.2: /^:/           colon
 802         2.3.3: chunks (3)
 803         2.3.4: /^!important/  optionally
 804         2.3.5: /^;|(?=})|     termination
 805 
 806 3: Chunks -- general whitespace-separated values.
 807    Used to represent selectors, @rule arguments and property values.
 808    The series of chunks contintues until {, }, or ; are encountered
 809    outside of a string or URL.
 810     3.1: whitespace -- skip
 811     3.2: URLs /^url\([^\)]+?\)/
 812     3.3: string -- open string context
 813     3.4: colors
 814          Should this include the currentColor keyword?
 815          A swatch of the appropriate color should be generated following
 816          the color.
 817     3.5: possible color /^[a-z]{3,}(?=[\s;!}]|$)/i
 818          Test if it is actually a color by setting and getting it as a 
 819          color property--if it works, generate a color swatch.
 820     3.6: other stuff -- skip
 821 */
 822 
 823 /**
 824 * @constructor
 825 * @id CodeProcessor.CSSHighlighter
 826 */
 827 CodeProcessor.CSSHighlighter = FilteredNodeQueue.extend({
 828     constructor: function(dom_node, node_filter) {
 829         //log("Instantiating JSHighlighter()");
 830         this.dom_node = dom_node;
 831         this.node_filter = node_filter;
 832         this.init_node_queue(node_filter, dom_node);
 833         
 834         this._stack = CSSHighlighter._get_base_stack();
 835         
 836         this.deferred = (new AsyncQueue(this))
 837             .loop(this.dispatch_node)
 838             .next(partial(log, "CSSHighlighter done."))
 839             .go();
 840     },
 841     end_node_queue: AsyncQueue.stop_loop,
 842     process_text_node: function(node) {
 843         
 844     },
 845     toString: function(){ return '[object CodeProcessor.CSSHighlighter instance]'; }
 846 }, (function(){
 847     // Regular expression fragments:
 848     
 849     // Characters that cannot be part of a token.
 850     var token_break = ':;\s';
 851     
 852     // Hex, RGB(A) and HSL(A) colors
 853     var value_colors = '#([0-9a-f]{3}){1,2}|' + 
 854         '(rgb|hsl)a?\((\s*\d+%?\s*,){2}\s*\d+%?\s*' + 
 855         '(,\s*(1|0|0?.\d+)\s*))';
 856     
 857     // CSS2.1 named colors:
 858     var named_colors = 'maroon|red|orange|yellow|olive|' +
 859         'purple|fuchsia|white|lime|green|navy|blue|aqua|' +
 860         'teal|black|silver|gray';
 861     // CSS3 named colors from MochiKit, if available
 862     if (MochiKit.Color && MochiKit.Color.Color &&
 863             MochiKit.Color.Color._namedColors) {
 864         named_colors = MochiKit.Base.keys(MochiKit.Color.Color._namedColors);
 865         named_colors.sort();
 866         named_colors.reverse();
 867         named_colors = MochiKit.Base.keys().join('|');
 868     }
 869     
 870     var system_colors = 'Active(Border|Caption)|' + 
 871         'AppWorkspace|Background|Button(Face|' + 
 872         'Highlight|Shadow|Text)|Caption|GrayText|' + 
 873         'Highlight(Text)?|Inactive(Border|Caption' + 
 874         '(Text)?)|Info(Background|Text)|Menu(Text)?|' + 
 875         'Scrollbar|ThreeD(DarkShadow|Face|Highlight|' + 
 876         'LightShadow|Shadow)|Window(Frame|Text)';
 877     
 878     // All valid colors
 879     var colors = [value_colors, named_colors, system_colors].join('|');
 880     
 881     var vendor_prefixes = '-(ms|moz|o|atsc|wap)-';
 882     var vendor_properties = 'filter|'
 883     
 884     // Select CSS 2.1 properties (plus some from CSS 3).
 885     var props = 'azimuth|background(-(attachment|' + 
 886         'color|image|position|repeat))?|border(-(' + 
 887         'bottom|left|right|top)(-(color|style|width' + 
 888         '))?|(collapse|color|spacing|style|width))?|' + 
 889         'bottom|box-sizing|caption-side|clear|clip|' + 
 890         'color|content(-(increment|reset))?|cue(-(' + 
 891         'after|before))?|cursor|direction|display|' + 
 892         'elevation|empty-cells|float|font(-(family|' + 
 893         'size|style|variant|weight))?|height|left|' + 
 894         'letter-spacing|line-height|list-style(-(' + 
 895         'image|position|type))?|margin(-(bottom|left|' + 
 896         'right|top))?|(max|min)-(height|width)|orphans' + 
 897         '|outline(-(color|style|width))?|overflow(-x|' + 
 898         '-y)?|padding(-(bottom|left|right|top))?|page' + 
 899         '-break-(after|before|inside)|pause(-(after|' + 
 900         'before))?|pitch(-range)?|play-during|position' + 
 901         '|quotes|richness|right|speak(-(header|numeral' + 
 902         '|punctuation))?speech-rate|stress|table-layou' + 
 903         't|text-(align|decoration|indent|transform)top' + 
 904         '|unicode-bidi|vertical-align|visibility|voice' +
 905         '-family|volume|white-space|widows|width|word-' +
 906         'spacing|z-index';
 907     var string = '(""|"(\\\\|\\"|[^"])+")';
 908     
 909     var url = 'url\(\s*(' + string + '|' +
 910             '([\w!#$%&*\-~\/])*' + ')\s*\)';
 911     
 912     function re(base, follows, flags) {
 913         var re = '^(' + base + ')' + (follows || '');
 914         return (flags) ? new RegExp(re, flags) : new RegExp(re);
 915     }
 916     
 917     return {
 918         test: MochiKit.Base.partial(CodeProcessor.match_class, "(css-con|css)"),
 919         STRING: re(string),
 920         URL: re(url),
 921         COLOR: re(colors, token_break, 'i')
 922     };
 923 })());
 924 
 925 
 926 // Register
 927 // Not implemented yet.
 928 //CodeProcessor.add("CSSHighlighter", CodeProcessor.CSSHighlighter);