UNPKG

33.6 kBJavaScriptView Raw
1/**
2 * Sunlight
3 * Intelligent syntax highlighting
4 *
5 * http://sunlightjs.com/
6 *
7 * by Tommy Montgomery <http://tmont.com>
8 * Licensed under WTFPL <http://sam.zoy.org/wtfpl/>
9 */
10(function(window, document, undefined){
11
12 var
13 //http://webreflection.blogspot.com/2009/01/32-bytes-to-know-if-your-browser-is-ie.html
14 //we have to sniff this because IE requires \r
15 isIe = !+"\v1",
16 EOL = isIe ? "\r" : "\n",
17 EMPTY = function() { return null; },
18 HIGHLIGHTED_NODE_COUNT = 0,
19 DEFAULT_LANGUAGE = "plaintext",
20 DEFAULT_CLASS_PREFIX = "sunlight-",
21
22 //global sunlight variables
23 defaultAnalyzer,
24 getComputedStyle,
25 globalOptions = {
26 tabWidth: 4,
27 classPrefix: DEFAULT_CLASS_PREFIX,
28 showWhitespace: false,
29 maxHeight: false
30 },
31 languages = {},
32 languageDefaults = {},
33 events = {
34 beforeHighlightNode: [],
35 beforeHighlight: [],
36 beforeTokenize: [],
37 afterTokenize: [],
38 beforeAnalyze: [],
39 afterAnalyze: [],
40 afterHighlight: [],
41 afterHighlightNode: []
42 };
43
44 defaultAnalyzer = (function() {
45 function defaultHandleToken(suffix) {
46 return function(context) {
47 var element = document.createElement("span");
48 element.className = context.options.classPrefix + suffix;
49 element.appendChild(context.createTextNode(context.tokens[context.index]));
50 return context.addNode(element) || true;
51 };
52 }
53
54 return {
55 handleToken: function(context) {
56 return defaultHandleToken(context.tokens[context.index].name)(context);
57 },
58
59 //just append default content as a text node
60 handle_default: function(context) {
61 return context.addNode(context.createTextNode(context.tokens[context.index]));
62 },
63
64 //this handles the named ident mayhem
65 handle_ident: function(context) {
66 var evaluate = function(rules, createRule) {
67 var i;
68 rules = rules || [];
69 for (i = 0; i < rules.length; i++) {
70 if (typeof(rules[i]) === "function") {
71 if (rules[i](context)) {
72 return defaultHandleToken("named-ident")(context);
73 }
74 } else if (createRule && createRule(rules[i])(context.tokens)) {
75 return defaultHandleToken("named-ident")(context);
76 }
77 }
78
79 return false;
80 };
81
82 return evaluate(context.language.namedIdentRules.custom)
83 || evaluate(context.language.namedIdentRules.follows, function(ruleData) { return createProceduralRule(context.index - 1, -1, ruleData, context.language.caseInsensitive); })
84 || evaluate(context.language.namedIdentRules.precedes, function(ruleData) { return createProceduralRule(context.index + 1, 1, ruleData, context.language.caseInsensitive); })
85 || evaluate(context.language.namedIdentRules.between, function(ruleData) { return createBetweenRule(context.index, ruleData.opener, ruleData.closer, context.language.caseInsensitive); })
86 || defaultHandleToken("ident")(context);
87 }
88 };
89 }());
90
91 languageDefaults = {
92 analyzer: create(defaultAnalyzer),
93 customTokens: [],
94 namedIdentRules: {},
95 punctuation: /[^\w\s]/,
96 numberParser: defaultNumberParser,
97 caseInsensitive: false,
98 doNotParse: /\s/,
99 contextItems: {},
100 embeddedLanguages: {}
101 };
102
103 //adapted from http://blargh.tommymontgomery.com/2010/04/get-computed-style-in-javascript/
104 getComputedStyle = (function() {
105 var func = null;
106 if (document.defaultView && document.defaultView.getComputedStyle) {
107 func = document.defaultView.getComputedStyle;
108 } else {
109 func = function(element, anything) {
110 return element["currentStyle"] || {};
111 };
112 }
113
114 return function(element, style) {
115 return func(element, null)[style];
116 }
117 }());
118
119 //-----------
120 //FUNCTIONS
121 //-----------
122
123 function createCodeReader(text) {
124 var index = 0,
125 line = 1,
126 column = 1,
127 length,
128 EOF = undefined,
129 currentChar,
130 nextReadBeginsLine;
131
132 text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); //normalize line endings to unix
133
134 length = text.length;
135 currentChar = length > 0 ? text.charAt(0) : EOF;
136
137 function getCharacters(count) {
138 var value;
139 if (count === 0) {
140 return "";
141 }
142
143 count = count || 1;
144
145 value = text.substring(index + 1, index + count + 1);
146 return value === "" ? EOF : value;
147 }
148
149 return {
150 toString: function() {
151 return "length: " + length + ", index: " + index + ", line: " + line + ", column: " + column + ", current: [" + currentChar + "]";
152 },
153
154 peek: function(count) {
155 return getCharacters(count);
156 },
157
158 substring: function() {
159 return text.substring(index);
160 },
161
162 peekSubstring: function() {
163 return text.substring(index + 1);
164 },
165
166 read: function(count) {
167 var value = getCharacters(count),
168 newlineCount,
169 lastChar;
170
171 if (value === "") {
172 //this is a result of reading/peeking/doing nothing
173 return value;
174 }
175
176 if (value !== EOF) {
177 //advance index
178 index += value.length;
179 column += value.length;
180
181 //update line count
182 if (nextReadBeginsLine) {
183 line++;
184 column = 1;
185 nextReadBeginsLine = false;
186 }
187
188 newlineCount = value.substring(0, value.length - 1).replace(/[^\n]/g, "").length;
189 if (newlineCount > 0) {
190 line += newlineCount;
191 column = 1;
192 }
193
194 lastChar = last(value);
195 if (lastChar === "\n") {
196 nextReadBeginsLine = true;
197 }
198
199 currentChar = lastChar;
200 } else {
201 index = length;
202 currentChar = EOF;
203 }
204
205 return value;
206 },
207
208 text: function() { return text; },
209
210 getLine: function() { return line; },
211 getColumn: function() { return column; },
212 isEof: function() { return index >= length; },
213 isSol: function() { return column === 1; },
214 isSolWs: function() {
215 var temp = index,
216 c;
217 if (column === 1) {
218 return true;
219 }
220
221 //look backward until we find a newline or a non-whitespace character
222 while ((c = text.charAt(--temp)) !== "") {
223 if (c === "\n") {
224 return true;
225 }
226 if (!/\s/.test(c)) {
227 return false;
228 }
229 }
230
231 return true;
232 },
233 isEol: function() { return nextReadBeginsLine; },
234 EOF: EOF,
235 current: function() { return currentChar; }
236 };
237 }
238
239 //http://javascript.crockford.com/prototypal.html
240 function create(o) {
241 function F() {}
242 F.prototype = o;
243 return new F();
244 }
245
246 function appendAll(parent, children) {
247 var i;
248 for (i = 0; i < children.length; i++) {
249 parent.appendChild(children[i]);
250 }
251 }
252
253 //gets the last character in a string or the last element in an array
254 function last(thing) {
255 return thing.charAt ? thing.charAt(thing.length - 1) : thing[thing.length - 1];
256 }
257
258 //array.contains()
259 function contains(arr, value, caseInsensitive) {
260 var i;
261 if (arr.indexOf && !caseInsensitive) {
262 return arr.indexOf(value) >= 0;
263 }
264
265 for (i = 0; i < arr.length; i++) {
266 if (arr[i] === value) {
267 return true;
268 }
269
270 if (caseInsensitive && typeof(arr[i]) === "string" && typeof(value) === "string" && arr[i].toUpperCase() === value.toUpperCase()) {
271 return true;
272 }
273 }
274
275 return false;
276 }
277
278 //non-recursively merges one object into the other
279 function merge(defaultObject, objectToMerge) {
280 var key;
281 if (!objectToMerge) {
282 return defaultObject;
283 }
284
285 for (key in objectToMerge) {
286 defaultObject[key] = objectToMerge[key];
287 }
288
289 return defaultObject;
290 }
291
292 function clone(object) {
293 return merge({}, object);
294 }
295
296 //http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript/3561711#3561711
297 function regexEscape(s) {
298 return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
299 }
300
301 function createProceduralRule(startIndex, direction, tokenRequirements, caseInsensitive) {
302 tokenRequirements = tokenRequirements.slice(0);
303 return function(tokens) {
304 var tokenIndexStart = startIndex,
305 j,
306 expected,
307 actual;
308
309 if (direction === 1) {
310 tokenRequirements.reverse();
311 }
312
313 for (j = 0; j < tokenRequirements.length; j++) {
314 actual = tokens[tokenIndexStart + (j * direction)];
315 expected = tokenRequirements[tokenRequirements.length - 1 - j];
316
317 if (actual === undefined) {
318 if (expected["optional"] !== undefined && expected.optional) {
319 tokenIndexStart -= direction;
320 } else {
321 return false;
322 }
323 } else if (actual.name === expected.token && (expected["values"] === undefined || contains(expected.values, actual.value, caseInsensitive))) {
324 //derp
325 continue;
326 } else if (expected["optional"] !== undefined && expected.optional) {
327 tokenIndexStart -= direction; //we need to reevaluate against this token again
328 } else {
329 return false;
330 }
331 }
332
333 return true;
334 };
335 }
336
337 function createBetweenRule(startIndex, opener, closer, caseInsensitive) {
338 return function(tokens) {
339 var index = startIndex,
340 token,
341 success = false;
342
343 //check to the left: if we run into a closer or never run into an opener, fail
344 while ((token = tokens[--index]) !== undefined) {
345 if (token.name === closer.token && contains(closer.values, token.value)) {
346 if (token.name === opener.token && contains(opener.values, token.value, caseInsensitive)) {
347 //if the closer is the same as the opener that's okay
348 success = true;
349 break;
350 }
351
352 return false;
353 }
354
355 if (token.name === opener.token && contains(opener.values, token.value, caseInsensitive)) {
356 success = true;
357 break;
358 }
359 }
360
361 if (!success) {
362 return false;
363 }
364
365 //check to the right for the closer
366 index = startIndex;
367 while ((token = tokens[++index]) !== undefined) {
368 if (token.name === opener.token && contains(opener.values, token.value, caseInsensitive)) {
369 if (token.name === closer.token && contains(closer.values, token.value, caseInsensitive)) {
370 //if the closer is the same as the opener that's okay
371 success = true;
372 break;
373 }
374
375 return false;
376 }
377
378 if (token.name === closer.token && contains(closer.values, token.value, caseInsensitive)) {
379 success = true;
380 break;
381 }
382 }
383
384 return success;
385 };
386 }
387
388 function matchWord(context, wordMap, tokenName, doNotRead) {
389 var current = context.reader.current(),
390 i,
391 word,
392 peek,
393 line = context.reader.getLine(),
394 column = context.reader.getColumn();
395
396 wordMap = wordMap || [];
397 if (context.language.caseInsensitive) {
398 current = current.toUpperCase();
399 }
400
401 if (!wordMap[current]) {
402 return null;
403 }
404
405 wordMap = wordMap[current];
406 for (i = 0; i < wordMap.length; i++) {
407 word = wordMap[i].value;
408
409 peek = current + context.reader.peek(word.length);
410 if (word === peek || wordMap[i].regex.test(peek)) {
411 return context.createToken(
412 tokenName,
413 context.reader.current() + context.reader[doNotRead ? "peek" : "read"](word.length - 1),
414 line,
415 column
416 );
417 }
418 }
419
420 return null;
421 }
422
423 //gets the next token in the specified direction while matcher matches the current token
424 function getNextWhile(tokens, index, direction, matcher) {
425 var count = 1,
426 token;
427
428 direction = direction || 1;
429 while (token = tokens[index + (direction * count++)]) {
430 if (!matcher(token)) {
431 return token;
432 }
433 }
434
435 return undefined;
436 }
437
438 //this is crucial for performance
439 function createHashMap(wordMap, boundary, caseInsensitive) {
440 //creates a hash table where the hash is the first character of the word
441 var newMap = { },
442 i,
443 word,
444 firstChar;
445
446 for (i = 0; i < wordMap.length; i++) {
447 word = caseInsensitive ? wordMap[i].toUpperCase() : wordMap[i];
448 firstChar = word.charAt(0);
449 if (!newMap[firstChar]) {
450 newMap[firstChar] = [];
451 }
452
453 newMap[firstChar].push({ value: word, regex: new RegExp("^" + regexEscape(word) + boundary, caseInsensitive ? "i" : "") });
454 }
455
456 return newMap;
457 }
458
459 function defaultNumberParser(context) {
460 var current = context.reader.current(),
461 number,
462 line = context.reader.getLine(),
463 column = context.reader.getColumn(),
464 allowDecimal = true,
465 peek;
466
467 if (!/\d/.test(current)) {
468 //is it a decimal followed by a number?
469 if (current !== "." || !/\d/.test(context.reader.peek())) {
470 return null;
471 }
472
473 //decimal without leading zero
474 number = current + context.reader.read();
475 allowDecimal = false;
476 } else {
477 number = current;
478 if (current === "0" && context.reader.peek() !== ".") {
479 //hex or octal
480 allowDecimal = false;
481 }
482 }
483
484 //easy way out: read until it's not a number or letter
485 //this will work for hex (0xef), octal (012), decimal and scientific notation (1e3)
486 //anything else and you're on your own
487
488 while ((peek = context.reader.peek()) !== context.reader.EOF) {
489 if (!/[A-Za-z0-9]/.test(peek)) {
490 if (peek === "." && allowDecimal && /\d$/.test(context.reader.peek(2))) {
491 number += context.reader.read();
492 allowDecimal = false;
493 continue;
494 }
495
496 break;
497 }
498
499 number += context.reader.read();
500 }
501
502 return context.createToken("number", number, line, column);
503 }
504
505 function fireEvent(eventName, highlighter, eventContext) {
506 var delegates = events[eventName] || [],
507 i;
508
509 for (i = 0; i < delegates.length; i++) {
510 delegates[i].call(highlighter, eventContext);
511 }
512 }
513
514 function Highlighter(options) {
515 this.options = merge(clone(globalOptions), options);
516 }
517
518 Highlighter.prototype = (function() {
519 var parseNextToken = (function() {
520 function isIdentMatch(context) {
521 return context.language.identFirstLetter && context.language.identFirstLetter.test(context.reader.current());
522 }
523
524 //token parsing functions
525 function parseKeyword(context) {
526 return matchWord(context, context.language.keywords, "keyword");
527 }
528
529 function parseCustomTokens(context) {
530 var tokenName,
531 token;
532 if (context.language.customTokens === undefined) {
533 return null;
534 }
535
536 for (tokenName in context.language.customTokens) {
537 token = matchWord(context, context.language.customTokens[tokenName], tokenName);
538 if (token !== null) {
539 return token;
540 }
541 }
542
543 return null;
544 }
545
546 function parseOperator(context) {
547 return matchWord(context, context.language.operators, "operator");
548 }
549
550 function parsePunctuation(context) {
551 var current = context.reader.current();
552 if (context.language.punctuation.test(regexEscape(current))) {
553 return context.createToken("punctuation", current, context.reader.getLine(), context.reader.getColumn());
554 }
555
556 return null;
557 }
558
559 function parseIdent(context) {
560 var ident,
561 peek,
562 line = context.reader.getLine(),
563 column = context.reader.getColumn();
564
565 if (!isIdentMatch(context)) {
566 return null;
567 }
568
569 ident = context.reader.current();
570 while ((peek = context.reader.peek()) !== context.reader.EOF) {
571 if (!context.language.identAfterFirstLetter.test(peek)) {
572 break;
573 }
574
575 ident += context.reader.read();
576 }
577
578 return context.createToken("ident", ident, line, column);
579 }
580
581 function parseDefault(context) {
582 if (context.defaultData.text === "") {
583 //new default token
584 context.defaultData.line = context.reader.getLine();
585 context.defaultData.column = context.reader.getColumn();
586 }
587
588 context.defaultData.text += context.reader.current();
589 return null;
590 }
591
592 function parseScopes(context) {
593 var current = context.reader.current(),
594 tokenName,
595 specificScopes,
596 j,
597 opener,
598 line,
599 column,
600 continuation,
601 value;
602
603 for (tokenName in context.language.scopes) {
604 specificScopes = context.language.scopes[tokenName];
605 for (j = 0; j < specificScopes.length; j++) {
606 opener = specificScopes[j][0];
607
608 value = current + context.reader.peek(opener.length - 1);
609
610 if (opener !== value && (!context.language.caseInsensitive || value.toUpperCase() !== opener.toUpperCase())) {
611 continue;
612 }
613
614 line = context.reader.getLine(), column = context.reader.getColumn();
615 context.reader.read(opener.length - 1);
616 continuation = getScopeReaderFunction(specificScopes[j], tokenName);
617 return continuation(context, continuation, value, line, column);
618 }
619 }
620
621 return null;
622 }
623
624 function parseNumber(context) {
625 return context.language.numberParser(context);
626 }
627
628 function parseCustomRules(context) {
629 var customRules = context.language.customParseRules,
630 i,
631 token;
632
633 if (customRules === undefined) {
634 return null;
635 }
636
637 for (i = 0; i < customRules.length; i++) {
638 token = customRules[i](context);
639 if (token) {
640 return token;
641 }
642 }
643
644 return null;
645 }
646
647 return function(context) {
648 if (context.language.doNotParse.test(context.reader.current())) {
649 return parseDefault(context);
650 }
651
652 return parseCustomRules(context)
653 || parseCustomTokens(context)
654 || parseKeyword(context)
655 || parseScopes(context)
656 || parseIdent(context)
657 || parseNumber(context)
658 || parseOperator(context)
659 || parsePunctuation(context)
660 || parseDefault(context);
661 }
662 }());
663
664 function getScopeReaderFunction(scope, tokenName) {
665 var escapeSequences = scope[2] || [],
666 closerLength = scope[1].length,
667 closer = typeof(scope[1]) === "string" ? new RegExp(regexEscape(scope[1])) : scope[1].regex,
668 zeroWidth = scope[3] || false;
669
670 //processCurrent indicates that this is being called from a continuation
671 //which means that we need to process the current char, rather than peeking at the next
672 return function(context, continuation, buffer, line, column, processCurrent) {
673 var foundCloser = false;
674 buffer = buffer || "";
675
676 processCurrent = processCurrent ? 1 : 0;
677
678 function process(processCurrent) {
679 //check for escape sequences
680 var peekValue,
681 current = context.reader.current(),
682 i;
683
684 for (i = 0; i < escapeSequences.length; i++) {
685 peekValue = (processCurrent ? current : "") + context.reader.peek(escapeSequences[i].length - processCurrent);
686 if (peekValue === escapeSequences[i]) {
687 buffer += context.reader.read(peekValue.length - processCurrent);
688 return true;
689 }
690 }
691
692 peekValue = (processCurrent ? current : "") + context.reader.peek(closerLength - processCurrent);
693 if (closer.test(peekValue)) {
694 foundCloser = true;
695 return false;
696 }
697
698 buffer += processCurrent ? current : context.reader.read();
699 return true;
700 };
701
702 if (!processCurrent || process(true)) {
703 while (context.reader.peek() !== context.reader.EOF && process(false)) { }
704 }
705
706 if (processCurrent) {
707 buffer += context.reader.current();
708 context.reader.read();
709 } else {
710 buffer += zeroWidth || context.reader.peek() === context.reader.EOF ? "" : context.reader.read(closerLength);
711 }
712
713 if (!foundCloser) {
714 //we need to signal to the context that this scope was never properly closed
715 //this has significance for partial parses (e.g. for nested languages)
716 context.continuation = continuation;
717 }
718
719 return context.createToken(tokenName, buffer, line, column);
720 };
721 }
722
723 //called before processing the current
724 function switchToEmbeddedLanguageIfNecessary(context) {
725 var i,
726 embeddedLanguage;
727
728 for (i = 0; i < context.language.embeddedLanguages.length; i++) {
729 if (!languages[context.language.embeddedLanguages[i].language]) {
730 //unregistered language
731 continue;
732 }
733
734 embeddedLanguage = clone(context.language.embeddedLanguages[i]);
735
736 if (embeddedLanguage.switchTo(context)) {
737 embeddedLanguage.oldItems = clone(context.items);
738 context.embeddedLanguageStack.push(embeddedLanguage);
739 context.language = languages[embeddedLanguage.language];
740 context.items = merge(context.items, clone(context.language.contextItems));
741 break;
742 }
743 }
744 }
745
746 //called after processing the current
747 function switchBackFromEmbeddedLanguageIfNecessary(context) {
748 var current = last(context.embeddedLanguageStack),
749 lang;
750
751 if (current && current.switchBack(context)) {
752 context.language = languages[current.parentLanguage];
753 lang = context.embeddedLanguageStack.pop();
754
755 //restore old items
756 context.items = clone(lang.oldItems);
757 lang.oldItems = {};
758 }
759 }
760
761 function tokenize(unhighlightedCode, language, partialContext, options) {
762 var tokens = [],
763 context,
764 continuation,
765 token;
766
767 fireEvent("beforeTokenize", this, { code: unhighlightedCode, language: language });
768 context = {
769 reader: createCodeReader(unhighlightedCode),
770 language: language,
771 items: clone(language.contextItems),
772 token: function(index) { return tokens[index]; },
773 getAllTokens: function() { return tokens.slice(0); },
774 count: function() { return tokens.length; },
775 options: options,
776 embeddedLanguageStack: [],
777
778 defaultData: {
779 text: "",
780 line: 1,
781 column: 1
782 },
783 createToken: function(name, value, line, column) {
784 return {
785 name: name,
786 line: line,
787 value: isIe ? value.replace(/\n/g, "\r") : value,
788 column: column,
789 language: this.language.name
790 };
791 }
792 };
793
794 //if continuation is given, then we need to pick up where we left off from a previous parse
795 //basically it indicates that a scope was never closed, so we need to continue that scope
796 if (partialContext.continuation) {
797 continuation = partialContext.continuation;
798 partialContext.continuation = null;
799 tokens.push(continuation(context, continuation, "", context.reader.getLine(), context.reader.getColumn(), true));
800 }
801
802 while (!context.reader.isEof()) {
803 switchToEmbeddedLanguageIfNecessary(context);
804 token = parseNextToken(context);
805
806 //flush default data if needed (in pretty much all languages this is just whitespace)
807 if (token !== null) {
808 if (context.defaultData.text !== "") {
809 tokens.push(context.createToken("default", context.defaultData.text, context.defaultData.line, context.defaultData.column));
810 context.defaultData.text = "";
811 }
812
813 if (token[0] !== undefined) {
814 //multiple tokens
815 tokens = tokens.concat(token);
816 } else {
817 //single token
818 tokens.push(token);
819 }
820 }
821
822 switchBackFromEmbeddedLanguageIfNecessary(context);
823 context.reader.read();
824 }
825
826 //append the last default token, if necessary
827 if (context.defaultData.text !== "") {
828 tokens.push(context.createToken("default", context.defaultData.text, context.defaultData.line, context.defaultData.column));
829 }
830
831 fireEvent("afterTokenize", this, { code: unhighlightedCode, parserContext: context });
832 return context;
833 }
834
835 function createAnalyzerContext(parserContext, partialContext, options) {
836 var nodes = [],
837 prepareText = function() {
838 var nbsp, tab;
839 if (options.showWhitespace) {
840 nbsp = String.fromCharCode(0xB7);
841 tab = new Array(options.tabWidth).join(String.fromCharCode(0x2014)) + String.fromCharCode(0x2192);
842 } else {
843 nbsp = String.fromCharCode(0xA0);
844 tab = new Array(options.tabWidth + 1).join(nbsp);
845 }
846
847 return function(token) {
848 var value = token.value.split(" ").join(nbsp),
849 tabIndex,
850 lastNewlineColumn,
851 actualColumn,
852 tabLength;
853
854 //tabstop madness: replace \t with the appropriate number of characters, depending on the tabWidth option and its relative position in the line
855 while ((tabIndex = value.indexOf("\t")) >= 0) {
856 lastNewlineColumn = value.lastIndexOf(EOL, tabIndex);
857 actualColumn = lastNewlineColumn === -1 ? tabIndex : tabIndex - lastNewlineColumn - 1;
858 tabLength = options.tabWidth - (actualColumn % options.tabWidth); //actual length of the TAB character
859
860 value = value.substring(0, tabIndex) + tab.substring(options.tabWidth - tabLength) + value.substring(tabIndex + 1);
861 }
862
863 return value;
864 };
865 }();
866
867 return {
868 tokens: (partialContext.tokens || []).concat(parserContext.getAllTokens()),
869 index: partialContext.index ? partialContext.index + 1 : 0,
870 language: null,
871 getAnalyzer: EMPTY,
872 options: options,
873 continuation: parserContext.continuation,
874 addNode: function(node) { nodes.push(node); },
875 createTextNode: function(token) { return document.createTextNode(prepareText(token)); },
876 getNodes: function() { return nodes; },
877 resetNodes: function() { nodes = []; },
878 items: parserContext.items
879 };
880 }
881
882 //partialContext allows us to perform a partial parse, and then pick up where we left off at a later time
883 //this functionality enables nested highlights (language within a language, e.g. PHP within HTML followed by more PHP)
884 function highlightText(unhighlightedCode, languageId, partialContext) {
885 var language = languages[languageId],
886 analyzerContext;
887
888 partialContext = partialContext || { };
889 if (language === undefined) {
890 //use default language if one wasn't specified or hasn't been registered
891 language = languages[DEFAULT_LANGUAGE];
892 }
893
894 fireEvent("beforeHighlight", this, { code: unhighlightedCode, language: language, previousContext: partialContext });
895
896 analyzerContext = createAnalyzerContext(
897 tokenize.call(this, unhighlightedCode, language, partialContext, this.options),
898 partialContext,
899 this.options
900 );
901
902 analyze.call(this, analyzerContext, partialContext.index ? partialContext.index + 1 : 0);
903
904 fireEvent("afterHighlight", this, { analyzerContext: analyzerContext });
905
906 return analyzerContext;
907 }
908
909 function createContainer(ctx) {
910 var container = document.createElement("span");
911 container.className = ctx.options.classPrefix + ctx.language.name;
912 return container;
913 }
914
915 function analyze(analyzerContext, startIndex) {
916 var nodes,
917 lastIndex,
918 container,
919 i,
920 tokenName,
921 func,
922 language,
923 analyzer;
924
925 fireEvent("beforeAnalyze", this, { analyzerContext: analyzerContext });
926
927 if (analyzerContext.tokens.length > 0) {
928 analyzerContext.language = languages[analyzerContext.tokens[0].language] || languages[DEFAULT_LANGUAGE];;
929 nodes = [];
930 lastIndex = 0;
931 container = createContainer(analyzerContext);
932
933 for (i = startIndex; i < analyzerContext.tokens.length; i++) {
934 language = languages[analyzerContext.tokens[i].language] || languages[DEFAULT_LANGUAGE];
935 if (language.name !== analyzerContext.language.name) {
936 appendAll(container, analyzerContext.getNodes());
937 analyzerContext.resetNodes();
938
939 nodes.push(container);
940 analyzerContext.language = language;
941 container = createContainer(analyzerContext);
942 }
943
944 analyzerContext.index = i;
945 tokenName = analyzerContext.tokens[i].name;
946 func = "handle_" + tokenName;
947
948 analyzer = analyzerContext.getAnalyzer.call(analyzerContext) || analyzerContext.language.analyzer;
949 analyzer[func] ? analyzer[func](analyzerContext) : analyzer.handleToken(analyzerContext);
950 }
951
952 //append the last nodes, and add the final nodes to the context
953 appendAll(container, analyzerContext.getNodes());
954 nodes.push(container);
955 analyzerContext.resetNodes();
956 for (i = 0; i < nodes.length; i++) {
957 analyzerContext.addNode(nodes[i]);
958 }
959 }
960
961 fireEvent("afterAnalyze", this, { analyzerContext: analyzerContext });
962 }
963
964 return {
965 //matches the language of the node to highlight
966 matchSunlightNode: function() {
967 var regex;
968
969 return function(node) {
970 if (!regex) {
971 regex = new RegExp("(?:\\s|^)" + this.options.classPrefix + "highlight-(\\S+)(?:\\s|$)");
972 }
973
974 return regex.exec(node.className);
975 };
976 }(),
977
978 //determines if the node has already been highlighted
979 isAlreadyHighlighted: function() {
980 var regex;
981 return function(node) {
982 if (!regex) {
983 regex = new RegExp("(?:\\s|^)" + this.options.classPrefix + "highlighted(?:\\s|$)");
984 }
985
986 return regex.test(node.className);
987 };
988 }(),
989
990 //highlights a block of text
991 highlight: function(code, languageId) { return highlightText.call(this, code, languageId); },
992
993 //recursively highlights a DOM node
994 highlightNode: function highlightRecursive(node) {
995 var match,
996 languageId,
997 currentNodeCount,
998 j,
999 nodes,
1000 k,
1001 partialContext,
1002 container,
1003 codeContainer;
1004
1005 if (this.isAlreadyHighlighted(node) || (match = this.matchSunlightNode(node)) === null) {
1006 return;
1007 }
1008
1009 languageId = match[1];
1010 currentNodeCount = 0;
1011 fireEvent("beforeHighlightNode", this, { node: node });
1012 for (j = 0; j < node.childNodes.length; j++) {
1013 if (node.childNodes[j].nodeType === 3) {
1014 //text nodes
1015 partialContext = highlightText.call(this, node.childNodes[j].nodeValue, languageId, partialContext);
1016 HIGHLIGHTED_NODE_COUNT++;
1017 currentNodeCount = currentNodeCount || HIGHLIGHTED_NODE_COUNT;
1018 nodes = partialContext.getNodes();
1019
1020 node.replaceChild(nodes[0], node.childNodes[j]);
1021 for (k = 1; k < nodes.length; k++) {
1022 node.insertBefore(nodes[k], nodes[k - 1].nextSibling);
1023 }
1024 } else if (node.childNodes[j].nodeType === 1) {
1025 //element nodes
1026 highlightRecursive.call(this, node.childNodes[j]);
1027 }
1028 }
1029
1030 //indicate that this node has been highlighted
1031 node.className += " " + this.options.classPrefix + "highlighted";
1032
1033 //if the node is block level, we put it in a container, otherwise we just leave it alone
1034 if (getComputedStyle(node, "display") === "block") {
1035 container = document.createElement("div");
1036 container.className = this.options.classPrefix + "container";
1037
1038 codeContainer = document.createElement("div");
1039 codeContainer.className = this.options.classPrefix + "code-container";
1040
1041 //apply max height if specified in options
1042 if (this.options.maxHeight !== false) {
1043 codeContainer.style.overflowY = "auto";
1044 codeContainer.style.maxHeight = this.options.maxHeight + (/^\d+$/.test(this.options.maxHeight) ? "px" : "");
1045 }
1046
1047 container.appendChild(codeContainer);
1048
1049 node.parentNode.insertBefore(codeContainer, node);
1050 node.parentNode.removeChild(node);
1051 codeContainer.appendChild(node);
1052
1053 codeContainer.parentNode.insertBefore(container, codeContainer);
1054 codeContainer.parentNode.removeChild(codeContainer);
1055 container.appendChild(codeContainer);
1056 }
1057
1058 fireEvent("afterHighlightNode", this, {
1059 container: container,
1060 codeContainer: codeContainer,
1061 node: node,
1062 count: currentNodeCount
1063 });
1064 }
1065 };
1066 }());
1067
1068 //public facing object
1069 window.Sunlight = {
1070 version: "1.18",
1071 Highlighter: Highlighter,
1072 createAnalyzer: function() { return create(defaultAnalyzer); },
1073 globalOptions: globalOptions,
1074
1075 highlightAll: function(options) {
1076 var highlighter = new Highlighter(options),
1077 tags = document.getElementsByTagName("*"),
1078 i;
1079
1080 for (i = 0; i < tags.length; i++) {
1081 highlighter.highlightNode(tags[i]);
1082 }
1083 },
1084
1085 registerLanguage: function(languageId, languageData) {
1086 var tokenName,
1087 embeddedLanguages,
1088 languageName;
1089
1090 if (!languageId) {
1091 throw "Languages must be registered with an identifier, e.g. \"php\" for PHP";
1092 }
1093
1094 languageData = merge(merge({}, languageDefaults), languageData);
1095 languageData.name = languageId;
1096
1097 //transform keywords, operators and custom tokens into a hash map
1098 languageData.keywords = createHashMap(languageData.keywords || [], "\\b", languageData.caseInsensitive);
1099 languageData.operators = createHashMap(languageData.operators || [], "", languageData.caseInsensitive);
1100 for (tokenName in languageData.customTokens) {
1101 languageData.customTokens[tokenName] = createHashMap(
1102 languageData.customTokens[tokenName].values,
1103 languageData.customTokens[tokenName].boundary,
1104 languageData.caseInsensitive
1105 );
1106 }
1107
1108 //convert the embedded language object to an easier-to-use array
1109 embeddedLanguages = [];
1110 for (languageName in languageData.embeddedLanguages) {
1111 embeddedLanguages.push({
1112 parentLanguage: languageData.name,
1113 language: languageName,
1114 switchTo: languageData.embeddedLanguages[languageName].switchTo,
1115 switchBack: languageData.embeddedLanguages[languageName].switchBack
1116 });
1117 }
1118
1119 languageData.embeddedLanguages = embeddedLanguages;
1120
1121 languages[languageData.name] = languageData;
1122 },
1123
1124 isRegistered: function(languageId) { return languages[languageId] !== undefined; },
1125
1126 bind: function(event, callback) {
1127 if (!events[event]) {
1128 throw "Unknown event \"" + event + "\"";
1129 }
1130
1131 events[event].push(callback);
1132 },
1133
1134 util: {
1135 last: last,
1136 regexEscape: regexEscape,
1137 eol: EOL,
1138 clone: clone,
1139 escapeSequences: ["\\n", "\\t", "\\r", "\\\\", "\\v", "\\f"],
1140 contains: contains,
1141 matchWord: matchWord,
1142 createHashMap: createHashMap,
1143 createBetweenRule: createBetweenRule,
1144 createProceduralRule: createProceduralRule,
1145 getNextNonWsToken: function(tokens, index) { return getNextWhile(tokens, index, 1, function(token) { return token.name === "default"; }); },
1146 getPreviousNonWsToken: function(tokens, index) { return getNextWhile(tokens, index, -1, function(token) { return token.name === "default"; }); },
1147 getNextWhile: function(tokens, index, matcher) { return getNextWhile(tokens, index, 1, matcher); },
1148 getPreviousWhile: function(tokens, index, matcher) { return getNextWhile(tokens, index, -1, matcher); },
1149 whitespace: { token: "default", optional: true },
1150 getComputedStyle: getComputedStyle
1151 }
1152 };
1153
1154 //register the default language
1155 window.Sunlight.registerLanguage(DEFAULT_LANGUAGE, { punctuation: /(?!x)x/, numberParser: EMPTY });
1156
1157}(this, document));
\No newline at end of file