typing: improve reliability of `is_typed`.
[nit.git] / share / nitdoc / js / lib / highlight.js
1 /* Copyright (c) 2006, Ivan Sagalaev
2 All rights reserved.
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are met:
5
6 * Redistributions of source code must retain the above copyright
7 notice, this list of conditions and the following disclaimer.
8 * Redistributions in binary form must reproduce the above copyright
9 notice, this list of conditions and the following disclaimer in the
10 documentation and/or other materials provided with the distribution.
11 * Neither the name of highlight.js nor the names of its contributors
12 may be used to endorse or promote products derived from this software
13 without specific prior written permission.
14
15 THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
16 EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
19 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27 define([], function () {
28 var hljs = new function() {
29
30 /* Utility functions */
31
32 function escape(value) {
33 return value.replace(/&/gm, '&amp;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;');
34 }
35
36 function tag(node) {
37 return node.nodeName.toLowerCase();
38 }
39
40 function testRe(re, lexeme) {
41 var match = re && re.exec(lexeme);
42 return match && match.index == 0;
43 }
44
45 function blockLanguage(block) {
46 var classes = (block.className + ' ' + (block.parentNode ? block.parentNode.className : '')).split(/\s+/);
47 classes = classes.map(function(c) {return c.replace(/^lang(uage)?-/, '');});
48 return classes.filter(function(c) {return getLanguage(c) || c == 'no-highlight';})[0];
49 }
50
51 function inherit(parent, obj) {
52 var result = {};
53 for (var key in parent)
54 result[key] = parent[key];
55 if (obj)
56 for (var key in obj)
57 result[key] = obj[key];
58 return result;
59 };
60
61 /* Stream merging */
62
63 function nodeStream(node) {
64 var result = [];
65 (function _nodeStream(node, offset) {
66 for (var child = node.firstChild; child; child = child.nextSibling) {
67 if (child.nodeType == 3)
68 offset += child.nodeValue.length;
69 else if (tag(child) == 'br')
70 offset += 1;
71 else if (child.nodeType == 1) {
72 result.push({
73 event: 'start',
74 offset: offset,
75 node: child
76 });
77 offset = _nodeStream(child, offset);
78 result.push({
79 event: 'stop',
80 offset: offset,
81 node: child
82 });
83 }
84 }
85 return offset;
86 })(node, 0);
87 return result;
88 }
89
90 function mergeStreams(original, highlighted, value) {
91 var processed = 0;
92 var result = '';
93 var nodeStack = [];
94
95 function selectStream() {
96 if (!original.length || !highlighted.length) {
97 return original.length ? original : highlighted;
98 }
99 if (original[0].offset != highlighted[0].offset) {
100 return (original[0].offset < highlighted[0].offset) ? original : highlighted;
101 }
102
103 /*
104 To avoid starting the stream just before it should stop the order is
105 ensured that original always starts first and closes last:
106
107 if (event1 == 'start' && event2 == 'start')
108 return original;
109 if (event1 == 'start' && event2 == 'stop')
110 return highlighted;
111 if (event1 == 'stop' && event2 == 'start')
112 return original;
113 if (event1 == 'stop' && event2 == 'stop')
114 return highlighted;
115
116 ... which is collapsed to:
117 */
118 return highlighted[0].event == 'start' ? original : highlighted;
119 }
120
121 function open(node) {
122 function attr_str(a) {return ' ' + a.nodeName + '="' + escape(a.value) + '"';}
123 result += '<' + tag(node) + Array.prototype.map.call(node.attributes, attr_str).join('') + '>';
124 }
125
126 function close(node) {
127 result += '</' + tag(node) + '>';
128 }
129
130 function render(event) {
131 (event.event == 'start' ? open : close)(event.node);
132 }
133
134 while (original.length || highlighted.length) {
135 var stream = selectStream();
136 result += escape(value.substr(processed, stream[0].offset - processed));
137 processed = stream[0].offset;
138 if (stream == original) {
139 /*
140 On any opening or closing tag of the original markup we first close
141 the entire highlighted node stack, then render the original tag along
142 with all the following original tags at the same offset and then
143 reopen all the tags on the highlighted stack.
144 */
145 nodeStack.reverse().forEach(close);
146 do {
147 render(stream.splice(0, 1)[0]);
148 stream = selectStream();
149 } while (stream == original && stream.length && stream[0].offset == processed);
150 nodeStack.reverse().forEach(open);
151 } else {
152 if (stream[0].event == 'start') {
153 nodeStack.push(stream[0].node);
154 } else {
155 nodeStack.pop();
156 }
157 render(stream.splice(0, 1)[0]);
158 }
159 }
160 return result + escape(value.substr(processed));
161 }
162
163 /* Initialization */
164
165 function compileLanguage(language) {
166
167 function reStr(re) {
168 return (re && re.source) || re;
169 }
170
171 function langRe(value, global) {
172 return RegExp(
173 reStr(value),
174 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '')
175 );
176 }
177
178 function compileMode(mode, parent) {
179 if (mode.compiled)
180 return;
181 mode.compiled = true;
182
183 mode.keywords = mode.keywords || mode.beginKeywords;
184 if (mode.keywords) {
185 var compiled_keywords = {};
186
187 function flatten(className, str) {
188 if (language.case_insensitive) {
189 str = str.toLowerCase();
190 }
191 str.split(' ').forEach(function(kw) {
192 var pair = kw.split('|');
193 compiled_keywords[pair[0]] = [className, pair[1] ? Number(pair[1]) : 1];
194 });
195 }
196
197 if (typeof mode.keywords == 'string') { // string
198 flatten('keyword', mode.keywords);
199 } else {
200 Object.keys(mode.keywords).forEach(function (className) {
201 flatten(className, mode.keywords[className]);
202 });
203 }
204 mode.keywords = compiled_keywords;
205 }
206 mode.lexemesRe = langRe(mode.lexemes || /\b[A-Za-z0-9_]+\b/, true);
207
208 if (parent) {
209 if (mode.beginKeywords) {
210 mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')\\b';
211 }
212 if (!mode.begin)
213 mode.begin = /\B|\b/;
214 mode.beginRe = langRe(mode.begin);
215 if (!mode.end && !mode.endsWithParent)
216 mode.end = /\B|\b/;
217 if (mode.end)
218 mode.endRe = langRe(mode.end);
219 mode.terminator_end = reStr(mode.end) || '';
220 if (mode.endsWithParent && parent.terminator_end)
221 mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end;
222 }
223 if (mode.illegal)
224 mode.illegalRe = langRe(mode.illegal);
225 if (mode.relevance === undefined)
226 mode.relevance = 1;
227 if (!mode.contains) {
228 mode.contains = [];
229 }
230 var expanded_contains = [];
231 mode.contains.forEach(function(c) {
232 if (c.variants) {
233 c.variants.forEach(function(v) {expanded_contains.push(inherit(c, v));});
234 } else {
235 expanded_contains.push(c == 'self' ? mode : c);
236 }
237 });
238 mode.contains = expanded_contains;
239 mode.contains.forEach(function(c) {compileMode(c, mode);});
240
241 if (mode.starts) {
242 compileMode(mode.starts, parent);
243 }
244
245 var terminators =
246 mode.contains.map(function(c) {
247 return c.beginKeywords ? '\\.?(' + c.begin + ')\\.?' : c.begin;
248 })
249 .concat([mode.terminator_end, mode.illegal])
250 .map(reStr)
251 .filter(Boolean);
252 mode.terminators = terminators.length ? langRe(terminators.join('|'), true) : {exec: function(s) {return null;}};
253
254 mode.continuation = {};
255 }
256
257 compileMode(language);
258 }
259
260 /*
261 Core highlighting function. Accepts a language name, or an alias, and a
262 string with the code to highlight. Returns an object with the following
263 properties:
264
265 - relevance (int)
266 - value (an HTML string with highlighting markup)
267
268 */
269 function highlight(name, value, ignore_illegals, continuation) {
270
271 function subMode(lexeme, mode) {
272 for (var i = 0; i < mode.contains.length; i++) {
273 if (testRe(mode.contains[i].beginRe, lexeme)) {
274 return mode.contains[i];
275 }
276 }
277 }
278
279 function endOfMode(mode, lexeme) {
280 if (testRe(mode.endRe, lexeme)) {
281 return mode;
282 }
283 if (mode.endsWithParent) {
284 return endOfMode(mode.parent, lexeme);
285 }
286 }
287
288 function isIllegal(lexeme, mode) {
289 return !ignore_illegals && testRe(mode.illegalRe, lexeme);
290 }
291
292 function keywordMatch(mode, match) {
293 var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0];
294 return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str];
295 }
296
297 function buildSpan(classname, insideSpan, leaveOpen, noPrefix) {
298 var classPrefix = noPrefix ? '' : options.classPrefix,
299 openSpan = '<span class="' + classPrefix,
300 closeSpan = leaveOpen ? '' : '</span>';
301
302 openSpan += classname + '">';
303
304 return openSpan + insideSpan + closeSpan;
305 }
306
307 function processKeywords() {
308 if (!top.keywords)
309 return escape(mode_buffer);
310 var result = '';
311 var last_index = 0;
312 top.lexemesRe.lastIndex = 0;
313 var match = top.lexemesRe.exec(mode_buffer);
314 while (match) {
315 result += escape(mode_buffer.substr(last_index, match.index - last_index));
316 var keyword_match = keywordMatch(top, match);
317 if (keyword_match) {
318 relevance += keyword_match[1];
319 result += buildSpan(keyword_match[0], escape(match[0]));
320 } else {
321 result += escape(match[0]);
322 }
323 last_index = top.lexemesRe.lastIndex;
324 match = top.lexemesRe.exec(mode_buffer);
325 }
326 return result + escape(mode_buffer.substr(last_index));
327 }
328
329 function processSubLanguage() {
330 if (top.subLanguage && !languages[top.subLanguage]) {
331 return escape(mode_buffer);
332 }
333 var result = top.subLanguage ? highlight(top.subLanguage, mode_buffer, true, top.continuation.top) : highlightAuto(mode_buffer);
334 // Counting embedded language score towards the host language may be disabled
335 // with zeroing the containing mode relevance. Usecase in point is Markdown that
336 // allows XML everywhere and makes every XML snippet to have a much larger Markdown
337 // score.
338 if (top.relevance > 0) {
339 relevance += result.relevance;
340 }
341 if (top.subLanguageMode == 'continuous') {
342 top.continuation.top = result.top;
343 }
344 return buildSpan(result.language, result.value, false, true);
345 }
346
347 function processBuffer() {
348 return top.subLanguage !== undefined ? processSubLanguage() : processKeywords();
349 }
350
351 function startNewMode(mode, lexeme) {
352 var markup = mode.className? buildSpan(mode.className, '', true): '';
353 if (mode.returnBegin) {
354 result += markup;
355 mode_buffer = '';
356 } else if (mode.excludeBegin) {
357 result += escape(lexeme) + markup;
358 mode_buffer = '';
359 } else {
360 result += markup;
361 mode_buffer = lexeme;
362 }
363 top = Object.create(mode, {parent: {value: top}});
364 }
365
366 function processLexeme(buffer, lexeme) {
367
368 mode_buffer += buffer;
369 if (lexeme === undefined) {
370 result += processBuffer();
371 return 0;
372 }
373
374 var new_mode = subMode(lexeme, top);
375 if (new_mode) {
376 result += processBuffer();
377 startNewMode(new_mode, lexeme);
378 return new_mode.returnBegin ? 0 : lexeme.length;
379 }
380
381 var end_mode = endOfMode(top, lexeme);
382 if (end_mode) {
383 var origin = top;
384 if (!(origin.returnEnd || origin.excludeEnd)) {
385 mode_buffer += lexeme;
386 }
387 result += processBuffer();
388 do {
389 if (top.className) {
390 result += '</span>';
391 }
392 relevance += top.relevance;
393 top = top.parent;
394 } while (top != end_mode.parent);
395 if (origin.excludeEnd) {
396 result += escape(lexeme);
397 }
398 mode_buffer = '';
399 if (end_mode.starts) {
400 startNewMode(end_mode.starts, '');
401 }
402 return origin.returnEnd ? 0 : lexeme.length;
403 }
404
405 if (isIllegal(lexeme, top))
406 throw new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '<unnamed>') + '"');
407
408 /*
409 Parser should not reach this point as all types of lexemes should be caught
410 earlier, but if it does due to some bug make sure it advances at least one
411 character forward to prevent infinite looping.
412 */
413 mode_buffer += lexeme;
414 return lexeme.length || 1;
415 }
416
417 var language = getLanguage(name);
418 if (!language) {
419 throw new Error('Unknown language: "' + name + '"');
420 }
421
422 compileLanguage(language);
423 var top = continuation || language;
424 var result = '';
425 for(var current = top; current != language; current = current.parent) {
426 if (current.className) {
427 result += buildSpan(current.className, result, true);
428 }
429 }
430 var mode_buffer = '';
431 var relevance = 0;
432 try {
433 var match, count, index = 0;
434 while (true) {
435 top.terminators.lastIndex = index;
436 match = top.terminators.exec(value);
437 if (!match)
438 break;
439 count = processLexeme(value.substr(index, match.index - index), match[0]);
440 index = match.index + count;
441 }
442 processLexeme(value.substr(index));
443 for(var current = top; current.parent; current = current.parent) { // close dangling modes
444 if (current.className) {
445 result += '</span>';
446 }
447 };
448 return {
449 relevance: relevance,
450 value: result,
451 language: name,
452 top: top
453 };
454 } catch (e) {
455 if (e.message.indexOf('Illegal') != -1) {
456 return {
457 relevance: 0,
458 value: escape(value)
459 };
460 } else {
461 throw e;
462 }
463 }
464 }
465
466 /*
467 Highlighting with language detection. Accepts a string with the code to
468 highlight. Returns an object with the following properties:
469
470 - language (detected language)
471 - relevance (int)
472 - value (an HTML string with highlighting markup)
473 - second_best (object with the same structure for second-best heuristically
474 detected language, may be absent)
475
476 */
477 function highlightAuto(text, languageSubset) {
478 languageSubset = languageSubset || options.languages || Object.keys(languages);
479 var result = {
480 relevance: 0,
481 value: escape(text)
482 };
483 var second_best = result;
484 languageSubset.forEach(function(name) {
485 if (!getLanguage(name)) {
486 return;
487 }
488 var current = highlight(name, text, false);
489 current.language = name;
490 if (current.relevance > second_best.relevance) {
491 second_best = current;
492 }
493 if (current.relevance > result.relevance) {
494 second_best = result;
495 result = current;
496 }
497 });
498 if (second_best.language) {
499 result.second_best = second_best;
500 }
501 return result;
502 }
503
504 /*
505 Post-processing of the highlighted markup:
506
507 - replace TABs with something more useful
508 - replace real line-breaks with '<br>' for non-pre containers
509
510 */
511 function fixMarkup(value) {
512 if (options.tabReplace) {
513 value = value.replace(/^((<[^>]+>|\t)+)/gm, function(match, p1, offset, s) {
514 return p1.replace(/\t/g, options.tabReplace);
515 });
516 }
517 if (options.useBR) {
518 value = value.replace(/\n/g, '<br>');
519 }
520 return value;
521 }
522
523 /*
524 Applies highlighting to a DOM node containing code. Accepts a DOM node and
525 two optional parameters for fixMarkup.
526 */
527 function highlightBlock(block) {
528 var text = options.useBR ? block.innerHTML
529 .replace(/\n/g,'').replace(/<br>|<br [^>]*>/g, '\n').replace(/<[^>]*>/g,'')
530 : block.textContent;
531 var language = blockLanguage(block);
532 if (language == 'no-highlight')
533 return;
534 var result = language ? highlight(language, text, true) : highlightAuto(text);
535 var original = nodeStream(block);
536 if (original.length) {
537 var pre = document.createElementNS('http://www.w3.org/1999/xhtml', 'pre');
538 pre.innerHTML = result.value;
539 result.value = mergeStreams(original, nodeStream(pre), text);
540 }
541 result.value = fixMarkup(result.value);
542
543 block.innerHTML = result.value;
544 block.className += ' hljs ' + (!language && result.language || '');
545 block.result = {
546 language: result.language,
547 re: result.relevance
548 };
549 if (result.second_best) {
550 block.second_best = {
551 language: result.second_best.language,
552 re: result.second_best.relevance
553 };
554 }
555 }
556
557 var options = {
558 classPrefix: 'hljs-',
559 tabReplace: null,
560 useBR: false,
561 languages: undefined
562 };
563
564 /*
565 Updates highlight.js global options with values passed in the form of an object
566 */
567 function configure(user_options) {
568 options = inherit(options, user_options);
569 }
570
571 /*
572 Applies highlighting to all <pre><code>..</code></pre> blocks on a page.
573 */
574 function initHighlighting() {
575 if (initHighlighting.called)
576 return;
577 initHighlighting.called = true;
578
579 var blocks = document.querySelectorAll('pre code');
580 Array.prototype.forEach.call(blocks, highlightBlock);
581 }
582
583 /*
584 Attaches highlighting to the page load event.
585 */
586 function initHighlightingOnLoad() {
587 addEventListener('DOMContentLoaded', initHighlighting, false);
588 addEventListener('load', initHighlighting, false);
589 }
590
591 var languages = {};
592 var aliases = {};
593
594 function registerLanguage(name, language) {
595 var lang = languages[name] = language(this);
596 if (lang.aliases) {
597 lang.aliases.forEach(function(alias) {aliases[alias] = name;});
598 }
599 }
600
601 function listLanguages() {
602 return Object.keys(languages);
603 }
604
605 function getLanguage(name) {
606 return languages[name] || languages[aliases[name]];
607 }
608
609 /* Interface definition */
610
611 this.highlight = highlight;
612 this.highlightAuto = highlightAuto;
613 this.fixMarkup = fixMarkup;
614 this.highlightBlock = highlightBlock;
615 this.configure = configure;
616 this.initHighlighting = initHighlighting;
617 this.initHighlightingOnLoad = initHighlightingOnLoad;
618 this.registerLanguage = registerLanguage;
619 this.listLanguages = listLanguages;
620 this.getLanguage = getLanguage;
621 this.inherit = inherit;
622
623 // Common regexps
624 this.IDENT_RE = '[a-zA-Z][a-zA-Z0-9_]*';
625 this.UNDERSCORE_IDENT_RE = '[a-zA-Z_][a-zA-Z0-9_]*';
626 this.NUMBER_RE = '\\b\\d+(\\.\\d+)?';
627 this.C_NUMBER_RE = '(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float
628 this.BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b...
629 this.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~';
630
631 // Common modes
632 this.BACKSLASH_ESCAPE = {
633 begin: '\\\\[\\s\\S]', relevance: 0
634 };
635 this.APOS_STRING_MODE = {
636 className: 'string',
637 begin: '\'', end: '\'',
638 illegal: '\\n',
639 contains: [this.BACKSLASH_ESCAPE]
640 };
641 this.QUOTE_STRING_MODE = {
642 className: 'string',
643 begin: '"', end: '"',
644 illegal: '\\n',
645 contains: [this.BACKSLASH_ESCAPE]
646 };
647 this.PHRASAL_WORDS_MODE = {
648 begin: /\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/
649 };
650 this.C_LINE_COMMENT_MODE = {
651 className: 'comment',
652 begin: '//', end: '$',
653 contains: [this.PHRASAL_WORDS_MODE]
654 };
655 this.C_BLOCK_COMMENT_MODE = {
656 className: 'comment',
657 begin: '/\\*', end: '\\*/',
658 contains: [this.PHRASAL_WORDS_MODE]
659 };
660 this.HASH_COMMENT_MODE = {
661 className: 'comment',
662 begin: '#', end: '$',
663 contains: [this.PHRASAL_WORDS_MODE]
664 };
665 this.NUMBER_MODE = {
666 className: 'number',
667 begin: this.NUMBER_RE,
668 relevance: 0
669 };
670 this.C_NUMBER_MODE = {
671 className: 'number',
672 begin: this.C_NUMBER_RE,
673 relevance: 0
674 };
675 this.BINARY_NUMBER_MODE = {
676 className: 'number',
677 begin: this.BINARY_NUMBER_RE,
678 relevance: 0
679 };
680 this.CSS_NUMBER_MODE = {
681 className: 'number',
682 begin: this.NUMBER_RE + '(' +
683 '%|em|ex|ch|rem' +
684 '|vw|vh|vmin|vmax' +
685 '|cm|mm|in|pt|pc|px' +
686 '|deg|grad|rad|turn' +
687 '|s|ms' +
688 '|Hz|kHz' +
689 '|dpi|dpcm|dppx' +
690 ')?',
691 relevance: 0
692 };
693 this.REGEXP_MODE = {
694 className: 'regexp',
695 begin: /\//, end: /\/[gim]*/,
696 illegal: /\n/,
697 contains: [
698 this.BACKSLASH_ESCAPE,
699 {
700 begin: /\[/, end: /\]/,
701 relevance: 0,
702 contains: [this.BACKSLASH_ESCAPE]
703 }
704 ]
705 };
706 this.TITLE_MODE = {
707 className: 'title',
708 begin: this.IDENT_RE,
709 relevance: 0
710 };
711 this.UNDERSCORE_TITLE_MODE = {
712 className: 'title',
713 begin: this.UNDERSCORE_IDENT_RE,
714 relevance: 0
715 };
716 };
717 return hljs;
718 });
719