mirror of
https://github.com/YunoHost/doc.git
synced 2024-09-03 20:06:26 +02:00
633 lines
19 KiB
JavaScript
633 lines
19 KiB
JavaScript
/*
|
|
Syntax highlighting with language autodetection.
|
|
http://highlightjs.org/
|
|
*/
|
|
|
|
function() {
|
|
|
|
/* Utility functions */
|
|
|
|
function escape(value) {
|
|
return value.replace(/&/gm, '&').replace(/</gm, '<').replace(/>/gm, '>');
|
|
}
|
|
|
|
function findCode(pre) {
|
|
for (var node = pre.firstChild; node; node = node.nextSibling) {
|
|
if (node.nodeName.toUpperCase () == 'CODE')
|
|
return node;
|
|
if (!(node.nodeType == 3 && node.nodeValue.match(/\s+/)))
|
|
break;
|
|
}
|
|
}
|
|
|
|
function blockText(block, ignoreNewLines) {
|
|
return Array.prototype.map.call(block.childNodes, function(node) {
|
|
if (node.nodeType == 3) {
|
|
return ignoreNewLines ? node.nodeValue.replace(/\n/g, '') : node.nodeValue;
|
|
}
|
|
if (node.nodeName.toUpperCase () == 'BR') {
|
|
return '\n';
|
|
}
|
|
return blockText(node, ignoreNewLines);
|
|
}).join('');
|
|
}
|
|
|
|
function blockLanguage(block) {
|
|
var classes = (block.className + ' ' + (block.parentNode ? block.parentNode.className : '')).split(/\s+/);
|
|
classes = classes.map(function(c) {return c.replace(/^language-/, '');});
|
|
for (var i = 0; i < classes.length; i++) {
|
|
if (languages[classes[i]] || classes[i] == 'no-highlight') {
|
|
return classes[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Stream merging */
|
|
|
|
function nodeStream(node) {
|
|
var result = [];
|
|
(function _nodeStream(node, offset) {
|
|
for (var child = node.firstChild; child; child = child.nextSibling) {
|
|
if (child.nodeType == 3)
|
|
offset += child.nodeValue.length;
|
|
else if (child.nodeName.toUpperCase() == 'BR')
|
|
offset += 1;
|
|
else if (child.nodeType == 1) {
|
|
result.push({
|
|
event: 'start',
|
|
offset: offset,
|
|
node: child
|
|
});
|
|
offset = _nodeStream(child, offset);
|
|
result.push({
|
|
event: 'stop',
|
|
offset: offset,
|
|
node: child
|
|
});
|
|
}
|
|
}
|
|
return offset;
|
|
})(node, 0);
|
|
return result;
|
|
}
|
|
|
|
function mergeStreams(stream1, stream2, value) {
|
|
var processed = 0;
|
|
var result = '';
|
|
var nodeStack = [];
|
|
|
|
function selectStream() {
|
|
if (stream1.length && stream2.length) {
|
|
if (stream1[0].offset != stream2[0].offset)
|
|
return (stream1[0].offset < stream2[0].offset) ? stream1 : stream2;
|
|
else {
|
|
/*
|
|
To avoid starting the stream just before it should stop the order is
|
|
ensured that stream1 always starts first and closes last:
|
|
|
|
if (event1 == 'start' && event2 == 'start')
|
|
return stream1;
|
|
if (event1 == 'start' && event2 == 'stop')
|
|
return stream2;
|
|
if (event1 == 'stop' && event2 == 'start')
|
|
return stream1;
|
|
if (event1 == 'stop' && event2 == 'stop')
|
|
return stream2;
|
|
|
|
... which is collapsed to:
|
|
*/
|
|
return stream2[0].event == 'start' ? stream1 : stream2;
|
|
}
|
|
} else {
|
|
return stream1.length ? stream1 : stream2;
|
|
}
|
|
}
|
|
|
|
function open(node) {
|
|
function attr_str(a) {return ' ' + a.nodeName + '="' + escape(a.value) + '"';}
|
|
return '<' + node.nodeName + Array.prototype.map.call(node.attributes, attr_str).join('') + '>';
|
|
}
|
|
|
|
while (stream1.length || stream2.length) {
|
|
var current = selectStream().splice(0, 1)[0];
|
|
result += escape(value.substr(processed, current.offset - processed));
|
|
processed = current.offset;
|
|
if ( current.event == 'start') {
|
|
result += open(current.node);
|
|
nodeStack.push(current.node);
|
|
} else if (current.event == 'stop') {
|
|
var node, i = nodeStack.length;
|
|
do {
|
|
i--;
|
|
node = nodeStack[i];
|
|
result += ('</' + node.nodeName.toLowerCase() + '>');
|
|
} while (node != current.node);
|
|
nodeStack.splice(i, 1);
|
|
while (i < nodeStack.length) {
|
|
result += open(nodeStack[i]);
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
return result + escape(value.substr(processed));
|
|
}
|
|
|
|
/* Initialization */
|
|
|
|
function compileLanguage(language) {
|
|
|
|
function reStr(re) {
|
|
return (re && re.source) || re;
|
|
}
|
|
|
|
function langRe(value, global) {
|
|
return RegExp(
|
|
reStr(value),
|
|
'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '')
|
|
);
|
|
}
|
|
|
|
function compileMode(mode, parent) {
|
|
if (mode.compiled)
|
|
return;
|
|
mode.compiled = true;
|
|
|
|
var keywords = []; // used later with beginWithKeyword but filled as a side-effect of keywords compilation
|
|
if (mode.keywords) {
|
|
var compiled_keywords = {};
|
|
|
|
function flatten(className, str) {
|
|
if (language.case_insensitive) {
|
|
str = str.toLowerCase();
|
|
}
|
|
str.split(' ').forEach(function(kw) {
|
|
var pair = kw.split('|');
|
|
compiled_keywords[pair[0]] = [className, pair[1] ? Number(pair[1]) : 1];
|
|
keywords.push(pair[0]);
|
|
});
|
|
}
|
|
|
|
mode.lexemsRe = langRe(mode.lexems || '\\b' + hljs.IDENT_RE + '\\b(?!\\.)', true);
|
|
if (typeof mode.keywords == 'string') { // string
|
|
flatten('keyword', mode.keywords);
|
|
} else {
|
|
for (var className in mode.keywords) {
|
|
if (!mode.keywords.hasOwnProperty(className))
|
|
continue;
|
|
flatten(className, mode.keywords[className]);
|
|
}
|
|
}
|
|
mode.keywords = compiled_keywords;
|
|
}
|
|
if (parent) {
|
|
if (mode.beginWithKeyword) {
|
|
mode.begin = '\\b(' + keywords.join('|') + ')\\b(?!\\.)\\s*';
|
|
}
|
|
mode.beginRe = langRe(mode.begin ? mode.begin : '\\B|\\b');
|
|
if (!mode.end && !mode.endsWithParent)
|
|
mode.end = '\\B|\\b';
|
|
if (mode.end)
|
|
mode.endRe = langRe(mode.end);
|
|
mode.terminator_end = reStr(mode.end) || '';
|
|
if (mode.endsWithParent && parent.terminator_end)
|
|
mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end;
|
|
}
|
|
if (mode.illegal)
|
|
mode.illegalRe = langRe(mode.illegal);
|
|
if (mode.relevance === undefined)
|
|
mode.relevance = 1;
|
|
if (!mode.contains) {
|
|
mode.contains = [];
|
|
}
|
|
for (var i = 0; i < mode.contains.length; i++) {
|
|
if (mode.contains[i] == 'self') {
|
|
mode.contains[i] = mode;
|
|
}
|
|
compileMode(mode.contains[i], mode);
|
|
}
|
|
if (mode.starts) {
|
|
compileMode(mode.starts, parent);
|
|
}
|
|
|
|
var terminators = [];
|
|
for (var i = 0; i < mode.contains.length; i++) {
|
|
terminators.push(reStr(mode.contains[i].begin));
|
|
}
|
|
if (mode.terminator_end) {
|
|
terminators.push(reStr(mode.terminator_end));
|
|
}
|
|
if (mode.illegal) {
|
|
terminators.push(reStr(mode.illegal));
|
|
}
|
|
mode.terminators = terminators.length ? langRe(terminators.join('|'), true) : {exec: function(s) {return null;}};
|
|
}
|
|
|
|
compileMode(language);
|
|
}
|
|
|
|
/*
|
|
Core highlighting function. Accepts a language name and a string with the
|
|
code to highlight. Returns an object with the following properties:
|
|
|
|
- relevance (int)
|
|
- keyword_count (int)
|
|
- value (an HTML string with highlighting markup)
|
|
|
|
*/
|
|
function highlight(language_name, value, ignore_illegals, continuation) {
|
|
|
|
function subMode(lexem, mode) {
|
|
for (var i = 0; i < mode.contains.length; i++) {
|
|
var match = mode.contains[i].beginRe.exec(lexem);
|
|
if (match && match.index == 0) {
|
|
return mode.contains[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
function endOfMode(mode, lexem) {
|
|
if (mode.end && mode.endRe.test(lexem)) {
|
|
return mode;
|
|
}
|
|
if (mode.endsWithParent) {
|
|
return endOfMode(mode.parent, lexem);
|
|
}
|
|
}
|
|
|
|
function isIllegal(lexem, mode) {
|
|
return !ignore_illegals && mode.illegal && mode.illegalRe.test(lexem);
|
|
}
|
|
|
|
function keywordMatch(mode, match) {
|
|
var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0];
|
|
return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str];
|
|
}
|
|
|
|
function processKeywords() {
|
|
var buffer = escape(mode_buffer);
|
|
if (!top.keywords)
|
|
return buffer;
|
|
var result = '';
|
|
var last_index = 0;
|
|
top.lexemsRe.lastIndex = 0;
|
|
var match = top.lexemsRe.exec(buffer);
|
|
while (match) {
|
|
result += buffer.substr(last_index, match.index - last_index);
|
|
var keyword_match = keywordMatch(top, match);
|
|
if (keyword_match) {
|
|
keyword_count += keyword_match[1];
|
|
result += '<span class="'+ keyword_match[0] +'">' + match[0] + '</span>';
|
|
} else {
|
|
result += match[0];
|
|
}
|
|
last_index = top.lexemsRe.lastIndex;
|
|
match = top.lexemsRe.exec(buffer);
|
|
}
|
|
return result + buffer.substr(last_index);
|
|
}
|
|
|
|
function processSubLanguage() {
|
|
if (top.subLanguage && !languages[top.subLanguage]) {
|
|
return escape(mode_buffer);
|
|
}
|
|
var continuation = top.subLanguageMode == 'continuous' ? top.top : undefined;
|
|
var result = top.subLanguage ? highlight(top.subLanguage, mode_buffer, true, continuation) : highlightAuto(mode_buffer);
|
|
// Counting embedded language score towards the host language may be disabled
|
|
// with zeroing the containing mode relevance. Usecase in point is Markdown that
|
|
// allows XML everywhere and makes every XML snippet to have a much larger Markdown
|
|
// score.
|
|
if (top.relevance > 0) {
|
|
keyword_count += result.keyword_count;
|
|
relevance += result.relevance;
|
|
}
|
|
top.top = result.top;
|
|
return '<span class="' + result.language + '">' + result.value + '</span>';
|
|
}
|
|
|
|
function processBuffer() {
|
|
return top.subLanguage !== undefined ? processSubLanguage() : processKeywords();
|
|
}
|
|
|
|
function startNewMode(mode, lexem) {
|
|
var markup = mode.className? '<span class="' + mode.className + '">': '';
|
|
if (mode.returnBegin) {
|
|
result += markup;
|
|
mode_buffer = '';
|
|
} else if (mode.excludeBegin) {
|
|
result += escape(lexem) + markup;
|
|
mode_buffer = '';
|
|
} else {
|
|
result += markup;
|
|
mode_buffer = lexem;
|
|
}
|
|
top = Object.create(mode, {parent: {value: top}});
|
|
}
|
|
|
|
function processLexem(buffer, lexem) {
|
|
mode_buffer += buffer;
|
|
if (lexem === undefined) {
|
|
result += processBuffer();
|
|
return 0;
|
|
}
|
|
|
|
var new_mode = subMode(lexem, top);
|
|
if (new_mode) {
|
|
result += processBuffer();
|
|
startNewMode(new_mode, lexem);
|
|
return new_mode.returnBegin ? 0 : lexem.length;
|
|
}
|
|
|
|
var end_mode = endOfMode(top, lexem);
|
|
if (end_mode) {
|
|
var origin = top;
|
|
if (!(origin.returnEnd || origin.excludeEnd)) {
|
|
mode_buffer += lexem;
|
|
}
|
|
result += processBuffer();
|
|
do {
|
|
if (top.className) {
|
|
result += '</span>';
|
|
}
|
|
relevance += top.relevance;
|
|
top = top.parent;
|
|
} while (top != end_mode.parent);
|
|
if (origin.excludeEnd) {
|
|
result += escape(lexem);
|
|
}
|
|
mode_buffer = '';
|
|
if (end_mode.starts) {
|
|
startNewMode(end_mode.starts, '');
|
|
}
|
|
return origin.returnEnd ? 0 : lexem.length;
|
|
}
|
|
|
|
if (isIllegal(lexem, top))
|
|
throw new Error('Illegal lexem "' + lexem + '" for mode "' + (top.className || '<unnamed>') + '"');
|
|
|
|
/*
|
|
Parser should not reach this point as all types of lexems should be caught
|
|
earlier, but if it does due to some bug make sure it advances at least one
|
|
character forward to prevent infinite looping.
|
|
*/
|
|
mode_buffer += lexem;
|
|
return lexem.length || 1;
|
|
}
|
|
|
|
var language = languages[language_name];
|
|
if (!language) {
|
|
throw new Error('Unknown language: "' + language_name + '"');
|
|
}
|
|
|
|
compileLanguage(language);
|
|
var top = continuation || language;
|
|
var result = '';
|
|
for(var current = top; current != language; current = current.parent) {
|
|
if (current.className) {
|
|
result = '<span class="' + current.className +'">' + result;
|
|
}
|
|
}
|
|
var mode_buffer = '';
|
|
var relevance = 0;
|
|
var keyword_count = 0;
|
|
try {
|
|
var match, count, index = 0;
|
|
while (true) {
|
|
top.terminators.lastIndex = index;
|
|
match = top.terminators.exec(value);
|
|
if (!match)
|
|
break;
|
|
count = processLexem(value.substr(index, match.index - index), match[0]);
|
|
index = match.index + count;
|
|
}
|
|
processLexem(value.substr(index));
|
|
for(var current = top; current.parent; current = current.parent) { // close dangling modes
|
|
if (current.className) {
|
|
result += '</span>';
|
|
}
|
|
};
|
|
return {
|
|
relevance: relevance,
|
|
keyword_count: keyword_count,
|
|
value: result,
|
|
language: language_name,
|
|
top: top
|
|
};
|
|
} catch (e) {
|
|
if (e.message.indexOf('Illegal') != -1) {
|
|
return {
|
|
relevance: 0,
|
|
keyword_count: 0,
|
|
value: escape(value)
|
|
};
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Highlighting with language detection. Accepts a string with the code to
|
|
highlight. Returns an object with the following properties:
|
|
|
|
- language (detected language)
|
|
- relevance (int)
|
|
- keyword_count (int)
|
|
- value (an HTML string with highlighting markup)
|
|
- second_best (object with the same structure for second-best heuristically
|
|
detected language, may be absent)
|
|
|
|
*/
|
|
function highlightAuto(text) {
|
|
var result = {
|
|
keyword_count: 0,
|
|
relevance: 0,
|
|
value: escape(text)
|
|
};
|
|
var second_best = result;
|
|
for (var key in languages) {
|
|
if (!languages.hasOwnProperty(key))
|
|
continue;
|
|
var current = highlight(key, text, false);
|
|
current.language = key;
|
|
if (current.keyword_count + current.relevance > second_best.keyword_count + second_best.relevance) {
|
|
second_best = current;
|
|
}
|
|
if (current.keyword_count + current.relevance > result.keyword_count + result.relevance) {
|
|
second_best = result;
|
|
result = current;
|
|
}
|
|
}
|
|
if (second_best.language) {
|
|
result.second_best = second_best;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
Post-processing of the highlighted markup:
|
|
|
|
- replace TABs with something more useful
|
|
- replace real line-breaks with '<br>' for non-pre containers
|
|
|
|
*/
|
|
function fixMarkup(value, tabReplace, useBR) {
|
|
if (tabReplace) {
|
|
value = value.replace(/^((<[^>]+>|\t)+)/gm, function(match, p1, offset, s) {
|
|
return p1.replace(/\t/g, tabReplace);
|
|
});
|
|
}
|
|
if (useBR) {
|
|
value = value.replace(/\n/g, '<br>');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/*
|
|
Applies highlighting to a DOM node containing code. Accepts a DOM node and
|
|
two optional parameters for fixMarkup.
|
|
*/
|
|
function highlightBlock(block, tabReplace, useBR) {
|
|
var text = blockText(block, useBR);
|
|
var language = blockLanguage(block);
|
|
if (language == 'no-highlight')
|
|
return;
|
|
var result = language ? highlight(language, text, true) : highlightAuto(text);
|
|
language = result.language;
|
|
var original = nodeStream(block);
|
|
if (original.length) {
|
|
var pre = document.createElementNS('http://www.w3.org/1999/xhtml', 'pre');
|
|
pre.innerHTML = result.value;
|
|
result.value = mergeStreams(original, nodeStream(pre), text);
|
|
}
|
|
result.value = fixMarkup(result.value, tabReplace, useBR);
|
|
|
|
var class_name = block.className;
|
|
if (!class_name.match('(\\s|^)(language-)?' + language + '(\\s|$)')) {
|
|
class_name = class_name ? (class_name + ' ' + language) : language;
|
|
}
|
|
block.innerHTML = result.value;
|
|
block.className = class_name;
|
|
block.result = {
|
|
language: language,
|
|
kw: result.keyword_count,
|
|
re: result.relevance
|
|
};
|
|
if (result.second_best) {
|
|
block.second_best = {
|
|
language: result.second_best.language,
|
|
kw: result.second_best.keyword_count,
|
|
re: result.second_best.relevance
|
|
};
|
|
}
|
|
}
|
|
|
|
/*
|
|
Applies highlighting to all <pre><code>..</code></pre> blocks on a page.
|
|
*/
|
|
function initHighlighting() {
|
|
if (initHighlighting.called)
|
|
return;
|
|
initHighlighting.called = true;
|
|
Array.prototype.map.call(document.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', 'pre'), findCode).
|
|
filter(Boolean).
|
|
forEach(function(code){highlightBlock(code, hljs.tabReplace);});
|
|
}
|
|
|
|
/*
|
|
Attaches highlighting to the page load event.
|
|
*/
|
|
function initHighlightingOnLoad() {
|
|
window.addEventListener('DOMContentLoaded', initHighlighting, false);
|
|
window.addEventListener('load', initHighlighting, false);
|
|
}
|
|
|
|
var languages = {}; // a shortcut to avoid writing "this." everywhere
|
|
|
|
/* Interface definition */
|
|
|
|
this.LANGUAGES = languages;
|
|
this.highlight = highlight;
|
|
this.highlightAuto = highlightAuto;
|
|
this.fixMarkup = fixMarkup;
|
|
this.highlightBlock = highlightBlock;
|
|
this.initHighlighting = initHighlighting;
|
|
this.initHighlightingOnLoad = initHighlightingOnLoad;
|
|
|
|
// Common regexps
|
|
this.IDENT_RE = '[a-zA-Z][a-zA-Z0-9_]*';
|
|
this.UNDERSCORE_IDENT_RE = '[a-zA-Z_][a-zA-Z0-9_]*';
|
|
this.NUMBER_RE = '\\b\\d+(\\.\\d+)?';
|
|
this.C_NUMBER_RE = '(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float
|
|
this.BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b...
|
|
this.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|\\.|-|-=|/|/=|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~';
|
|
|
|
// Common modes
|
|
this.BACKSLASH_ESCAPE = {
|
|
begin: '\\\\[\\s\\S]', relevance: 0
|
|
};
|
|
this.APOS_STRING_MODE = {
|
|
className: 'string',
|
|
begin: '\'', end: '\'',
|
|
illegal: '\\n',
|
|
contains: [this.BACKSLASH_ESCAPE],
|
|
relevance: 0
|
|
};
|
|
this.QUOTE_STRING_MODE = {
|
|
className: 'string',
|
|
begin: '"', end: '"',
|
|
illegal: '\\n',
|
|
contains: [this.BACKSLASH_ESCAPE],
|
|
relevance: 0
|
|
};
|
|
this.C_LINE_COMMENT_MODE = {
|
|
className: 'comment',
|
|
begin: '//', end: '$'
|
|
};
|
|
this.C_BLOCK_COMMENT_MODE = {
|
|
className: 'comment',
|
|
begin: '/\\*', end: '\\*/'
|
|
};
|
|
this.HASH_COMMENT_MODE = {
|
|
className: 'comment',
|
|
begin: '#', end: '$'
|
|
};
|
|
this.NUMBER_MODE = {
|
|
className: 'number',
|
|
begin: this.NUMBER_RE,
|
|
relevance: 0
|
|
};
|
|
this.C_NUMBER_MODE = {
|
|
className: 'number',
|
|
begin: this.C_NUMBER_RE,
|
|
relevance: 0
|
|
};
|
|
this.BINARY_NUMBER_MODE = {
|
|
className: 'number',
|
|
begin: this.BINARY_NUMBER_RE,
|
|
relevance: 0
|
|
};
|
|
this.REGEXP_MODE = {
|
|
className: 'regexp',
|
|
begin: /\//, end: /\/[gim]*/,
|
|
illegal: /\n/,
|
|
contains: [
|
|
this.BACKSLASH_ESCAPE,
|
|
{
|
|
begin: /\[/, end: /\]/,
|
|
relevance: 0,
|
|
contains: [this.BACKSLASH_ESCAPE]
|
|
}
|
|
]
|
|
};
|
|
|
|
// Utility functions
|
|
this.inherit = function(parent, obj) {
|
|
var result = {};
|
|
for (var key in parent)
|
|
result[key] = parent[key];
|
|
if (obj)
|
|
for (var key in obj)
|
|
result[key] = obj[key];
|
|
return result;
|
|
};
|
|
}
|