554 строки
16 KiB
JavaScript
554 строки
16 KiB
JavaScript
/*
|
|
SHJS - Syntax Highlighting in JavaScript
|
|
Copyright (C) 2007, 2008 gnombat@users.sourceforge.net
|
|
License: http://shjs.sourceforge.net/doc/gplv3.html
|
|
*/
|
|
|
|
if (! this.sh_languages) {
|
|
this.sh_languages = {};
|
|
}
|
|
var sh_requests = {};
|
|
|
|
function sh_isEmailAddress(url) {
|
|
if (/^mailto:/.test(url)) {
|
|
return false;
|
|
}
|
|
return url.indexOf('@') !== -1;
|
|
}
|
|
|
|
function sh_setHref(tags, numTags, inputString) {
|
|
var url = inputString.substring(tags[numTags - 2].pos, tags[numTags - 1].pos);
|
|
if (url.length >= 2 && url.charAt(0) === '<' && url.charAt(url.length - 1) === '>') {
|
|
url = url.substr(1, url.length - 2);
|
|
}
|
|
if (sh_isEmailAddress(url)) {
|
|
url = 'mailto:' + url;
|
|
}
|
|
tags[numTags - 2].node.href = url;
|
|
}
|
|
|
|
/*
|
|
Konqueror has a bug where the regular expression /$/g will not match at the end
|
|
of a line more than once:
|
|
|
|
var regex = /$/g;
|
|
var match;
|
|
|
|
var line = '1234567890';
|
|
regex.lastIndex = 10;
|
|
match = regex.exec(line);
|
|
|
|
var line2 = 'abcde';
|
|
regex.lastIndex = 5;
|
|
match = regex.exec(line2); // fails
|
|
*/
|
|
function sh_konquerorExec(s) {
|
|
var result = [''];
|
|
result.index = s.length;
|
|
result.input = s;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
Highlights all elements containing source code in a text string. The return
|
|
value is an array of objects, each representing an HTML start or end tag. Each
|
|
object has a property named pos, which is an integer representing the text
|
|
offset of the tag. Every start tag also has a property named node, which is the
|
|
DOM element started by the tag. End tags do not have this property.
|
|
@param inputString a text string
|
|
@param language a language definition object
|
|
@return an array of tag objects
|
|
*/
|
|
function sh_highlightString(inputString, language) {
|
|
if (/Konqueror/.test(navigator.userAgent)) {
|
|
if (! language.konquered) {
|
|
for (var s = 0; s < language.length; s++) {
|
|
for (var p = 0; p < language[s].length; p++) {
|
|
var r = language[s][p][0];
|
|
if (r.source === '$') {
|
|
r.exec = sh_konquerorExec;
|
|
}
|
|
}
|
|
}
|
|
language.konquered = true;
|
|
}
|
|
}
|
|
|
|
var a = document.createElement('a');
|
|
var span = document.createElement('span');
|
|
|
|
// the result
|
|
var tags = [];
|
|
var numTags = 0;
|
|
|
|
// each element is a pattern object from language
|
|
var patternStack = [];
|
|
|
|
// the current position within inputString
|
|
var pos = 0;
|
|
|
|
// the name of the current style, or null if there is no current style
|
|
var currentStyle = null;
|
|
|
|
var output = function(s, style) {
|
|
var length = s.length;
|
|
// this is more than just an optimization - we don't want to output empty <span></span> elements
|
|
if (length === 0) {
|
|
return;
|
|
}
|
|
if (! style) {
|
|
var stackLength = patternStack.length;
|
|
if (stackLength !== 0) {
|
|
var pattern = patternStack[stackLength - 1];
|
|
// check whether this is a state or an environment
|
|
if (! pattern[3]) {
|
|
// it's not a state - it's an environment; use the style for this environment
|
|
style = pattern[1];
|
|
}
|
|
}
|
|
}
|
|
if (currentStyle !== style) {
|
|
if (currentStyle) {
|
|
tags[numTags++] = {pos: pos};
|
|
if (currentStyle === 'sh_url') {
|
|
sh_setHref(tags, numTags, inputString);
|
|
}
|
|
}
|
|
if (style) {
|
|
var clone;
|
|
if (style === 'sh_url') {
|
|
clone = a.cloneNode(false);
|
|
}
|
|
else {
|
|
clone = span.cloneNode(false);
|
|
}
|
|
clone.className = style;
|
|
tags[numTags++] = {node: clone, pos: pos};
|
|
}
|
|
}
|
|
pos += length;
|
|
currentStyle = style;
|
|
};
|
|
|
|
var endOfLinePattern = /\r\n|\r|\n/g;
|
|
endOfLinePattern.lastIndex = 0;
|
|
var inputStringLength = inputString.length;
|
|
while (pos < inputStringLength) {
|
|
var start = pos;
|
|
var end;
|
|
var startOfNextLine;
|
|
var endOfLineMatch = endOfLinePattern.exec(inputString);
|
|
if (endOfLineMatch === null) {
|
|
end = inputStringLength;
|
|
startOfNextLine = inputStringLength;
|
|
}
|
|
else {
|
|
end = endOfLineMatch.index;
|
|
startOfNextLine = endOfLinePattern.lastIndex;
|
|
}
|
|
|
|
var line = inputString.substring(start, end);
|
|
|
|
var matchCache = [];
|
|
for (;;) {
|
|
var posWithinLine = pos - start;
|
|
|
|
var stateIndex;
|
|
var stackLength = patternStack.length;
|
|
if (stackLength === 0) {
|
|
stateIndex = 0;
|
|
}
|
|
else {
|
|
// get the next state
|
|
stateIndex = patternStack[stackLength - 1][2];
|
|
}
|
|
|
|
var state = language[stateIndex];
|
|
var numPatterns = state.length;
|
|
var mc = matchCache[stateIndex];
|
|
if (! mc) {
|
|
mc = matchCache[stateIndex] = [];
|
|
}
|
|
var bestMatch = null;
|
|
var bestPatternIndex = -1;
|
|
for (var i = 0; i < numPatterns; i++) {
|
|
var match;
|
|
if (i < mc.length && (mc[i] === null || posWithinLine <= mc[i].index)) {
|
|
match = mc[i];
|
|
}
|
|
else {
|
|
var regex = state[i][0];
|
|
regex.lastIndex = posWithinLine;
|
|
match = regex.exec(line);
|
|
mc[i] = match;
|
|
}
|
|
if (match !== null && (bestMatch === null || match.index < bestMatch.index)) {
|
|
bestMatch = match;
|
|
bestPatternIndex = i;
|
|
if (match.index === posWithinLine) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestMatch === null) {
|
|
output(line.substring(posWithinLine), null);
|
|
break;
|
|
}
|
|
else {
|
|
// got a match
|
|
if (bestMatch.index > posWithinLine) {
|
|
output(line.substring(posWithinLine, bestMatch.index), null);
|
|
}
|
|
|
|
var pattern = state[bestPatternIndex];
|
|
|
|
var newStyle = pattern[1];
|
|
var matchedString;
|
|
if (newStyle instanceof Array) {
|
|
for (var subexpression = 0; subexpression < newStyle.length; subexpression++) {
|
|
matchedString = bestMatch[subexpression + 1];
|
|
output(matchedString, newStyle[subexpression]);
|
|
}
|
|
}
|
|
else {
|
|
matchedString = bestMatch[0];
|
|
output(matchedString, newStyle);
|
|
}
|
|
|
|
switch (pattern[2]) {
|
|
case -1:
|
|
// do nothing
|
|
break;
|
|
case -2:
|
|
// exit
|
|
patternStack.pop();
|
|
break;
|
|
case -3:
|
|
// exitall
|
|
patternStack.length = 0;
|
|
break;
|
|
default:
|
|
// this was the start of a delimited pattern or a state/environment
|
|
patternStack.push(pattern);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// end of the line
|
|
if (currentStyle) {
|
|
tags[numTags++] = {pos: pos};
|
|
if (currentStyle === 'sh_url') {
|
|
sh_setHref(tags, numTags, inputString);
|
|
}
|
|
currentStyle = null;
|
|
}
|
|
pos = startOfNextLine;
|
|
}
|
|
|
|
return tags;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// DOM-dependent functions
|
|
|
|
function sh_getClasses(element) {
|
|
var result = [];
|
|
var htmlClass = element.className;
|
|
if (htmlClass && htmlClass.length > 0) {
|
|
var htmlClasses = htmlClass.split(' ');
|
|
for (var i = 0; i < htmlClasses.length; i++) {
|
|
if (htmlClasses[i].length > 0) {
|
|
result.push(htmlClasses[i]);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function sh_addClass(element, name) {
|
|
var htmlClasses = sh_getClasses(element);
|
|
for (var i = 0; i < htmlClasses.length; i++) {
|
|
if (name.toLowerCase() === htmlClasses[i].toLowerCase()) {
|
|
return;
|
|
}
|
|
}
|
|
htmlClasses.push(name);
|
|
element.className = htmlClasses.join(' ');
|
|
}
|
|
|
|
/**
|
|
Extracts the tags from an HTML DOM NodeList.
|
|
@param nodeList a DOM NodeList
|
|
@param result an object with text, tags and pos properties
|
|
*/
|
|
function sh_extractTagsFromNodeList(nodeList, result) {
|
|
var length = nodeList.length;
|
|
for (var i = 0; i < length; i++) {
|
|
var node = nodeList.item(i);
|
|
switch (node.nodeType) {
|
|
case 1:
|
|
if (node.nodeName.toLowerCase() === 'br') {
|
|
var terminator;
|
|
if (/MSIE/.test(navigator.userAgent)) {
|
|
terminator = '\r';
|
|
}
|
|
else {
|
|
terminator = '\n';
|
|
}
|
|
result.text.push(terminator);
|
|
result.pos++;
|
|
}
|
|
else {
|
|
result.tags.push({node: node.cloneNode(false), pos: result.pos});
|
|
sh_extractTagsFromNodeList(node.childNodes, result);
|
|
result.tags.push({pos: result.pos});
|
|
}
|
|
break;
|
|
case 3:
|
|
case 4:
|
|
result.text.push(node.data);
|
|
result.pos += node.length;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Extracts the tags from the text of an HTML element. The extracted tags will be
|
|
returned as an array of tag objects. See sh_highlightString for the format of
|
|
the tag objects.
|
|
@param element a DOM element
|
|
@param tags an empty array; the extracted tag objects will be returned in it
|
|
@return the text of the element
|
|
@see sh_highlightString
|
|
*/
|
|
function sh_extractTags(element, tags) {
|
|
var result = {};
|
|
result.text = [];
|
|
result.tags = tags;
|
|
result.pos = 0;
|
|
sh_extractTagsFromNodeList(element.childNodes, result);
|
|
return result.text.join('');
|
|
}
|
|
|
|
/**
|
|
Merges the original tags from an element with the tags produced by highlighting.
|
|
@param originalTags an array containing the original tags
|
|
@param highlightTags an array containing the highlighting tags - these must not overlap
|
|
@result an array containing the merged tags
|
|
*/
|
|
function sh_mergeTags(originalTags, highlightTags) {
|
|
var numOriginalTags = originalTags.length;
|
|
if (numOriginalTags === 0) {
|
|
return highlightTags;
|
|
}
|
|
|
|
var numHighlightTags = highlightTags.length;
|
|
if (numHighlightTags === 0) {
|
|
return originalTags;
|
|
}
|
|
|
|
var result = [];
|
|
var originalIndex = 0;
|
|
var highlightIndex = 0;
|
|
|
|
while (originalIndex < numOriginalTags && highlightIndex < numHighlightTags) {
|
|
var originalTag = originalTags[originalIndex];
|
|
var highlightTag = highlightTags[highlightIndex];
|
|
|
|
if (originalTag.pos <= highlightTag.pos) {
|
|
result.push(originalTag);
|
|
originalIndex++;
|
|
}
|
|
else {
|
|
result.push(highlightTag);
|
|
if (highlightTags[highlightIndex + 1].pos <= originalTag.pos) {
|
|
highlightIndex++;
|
|
result.push(highlightTags[highlightIndex]);
|
|
highlightIndex++;
|
|
}
|
|
else {
|
|
// new end tag
|
|
result.push({pos: originalTag.pos});
|
|
|
|
// new start tag
|
|
highlightTags[highlightIndex] = {node: highlightTag.node.cloneNode(false), pos: originalTag.pos};
|
|
}
|
|
}
|
|
}
|
|
|
|
while (originalIndex < numOriginalTags) {
|
|
result.push(originalTags[originalIndex]);
|
|
originalIndex++;
|
|
}
|
|
|
|
while (highlightIndex < numHighlightTags) {
|
|
result.push(highlightTags[highlightIndex]);
|
|
highlightIndex++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
Inserts tags into text.
|
|
@param tags an array of tag objects
|
|
@param text a string representing the text
|
|
@return a DOM DocumentFragment representing the resulting HTML
|
|
*/
|
|
function sh_insertTags(tags, text) {
|
|
var doc = document;
|
|
|
|
var result = document.createDocumentFragment();
|
|
var tagIndex = 0;
|
|
var numTags = tags.length;
|
|
var textPos = 0;
|
|
var textLength = text.length;
|
|
var currentNode = result;
|
|
|
|
// output one tag or text node every iteration
|
|
while (textPos < textLength || tagIndex < numTags) {
|
|
var tag;
|
|
var tagPos;
|
|
if (tagIndex < numTags) {
|
|
tag = tags[tagIndex];
|
|
tagPos = tag.pos;
|
|
}
|
|
else {
|
|
tagPos = textLength;
|
|
}
|
|
|
|
if (tagPos <= textPos) {
|
|
// output the tag
|
|
if (tag.node) {
|
|
// start tag
|
|
var newNode = tag.node;
|
|
currentNode.appendChild(newNode);
|
|
currentNode = newNode;
|
|
}
|
|
else {
|
|
// end tag
|
|
currentNode = currentNode.parentNode;
|
|
}
|
|
tagIndex++;
|
|
}
|
|
else {
|
|
// output text
|
|
currentNode.appendChild(doc.createTextNode(text.substring(textPos, tagPos)));
|
|
textPos = tagPos;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
Highlights an element containing source code. Upon completion of this function,
|
|
the element will have been placed in the "sh_sourceCode" class.
|
|
@param element a DOM <pre> element containing the source code to be highlighted
|
|
@param language a language definition object
|
|
*/
|
|
function sh_highlightElement(element, language) {
|
|
sh_addClass(element, 'sh_sourceCode');
|
|
var originalTags = [];
|
|
var inputString = sh_extractTags(element, originalTags);
|
|
var highlightTags = sh_highlightString(inputString, language);
|
|
var tags = sh_mergeTags(originalTags, highlightTags);
|
|
var documentFragment = sh_insertTags(tags, inputString);
|
|
while (element.hasChildNodes()) {
|
|
element.removeChild(element.firstChild);
|
|
}
|
|
element.appendChild(documentFragment);
|
|
}
|
|
|
|
function sh_getXMLHttpRequest() {
|
|
if (window.ActiveXObject) {
|
|
return new ActiveXObject('Msxml2.XMLHTTP');
|
|
}
|
|
else if (window.XMLHttpRequest) {
|
|
return new XMLHttpRequest();
|
|
}
|
|
throw 'No XMLHttpRequest implementation available';
|
|
}
|
|
|
|
function sh_load(language, element, prefix, suffix) {
|
|
if (language in sh_requests) {
|
|
sh_requests[language].push(element);
|
|
return;
|
|
}
|
|
sh_requests[language] = [element];
|
|
var request = sh_getXMLHttpRequest();
|
|
var url = prefix + 'sh_' + language + suffix;
|
|
request.open('GET', url, true);
|
|
request.onreadystatechange = function () {
|
|
if (request.readyState === 4) {
|
|
try {
|
|
if (! request.status || request.status === 200) {
|
|
eval(request.responseText);
|
|
var elements = sh_requests[language];
|
|
for (var i = 0; i < elements.length; i++) {
|
|
sh_highlightElement(elements[i], sh_languages[language]);
|
|
}
|
|
}
|
|
else {
|
|
throw 'HTTP error: status ' + request.status;
|
|
}
|
|
}
|
|
finally {
|
|
request = null;
|
|
}
|
|
}
|
|
};
|
|
request.send(null);
|
|
}
|
|
|
|
/**
|
|
Highlights all elements containing source code on the current page. Elements
|
|
containing source code must be "pre" elements with a "class" attribute of
|
|
"sh_LANGUAGE", where LANGUAGE is a valid language identifier; e.g., "sh_java"
|
|
identifies the element as containing "java" language source code.
|
|
*/
|
|
function highlight(prefix, suffix, tag) {
|
|
var nodeList = document.getElementsByTagName(tag);
|
|
for (var i = 0; i < nodeList.length; i++) {
|
|
var element = nodeList.item(i);
|
|
var htmlClasses = sh_getClasses(element);
|
|
var highlighted = false;
|
|
var donthighlight = false;
|
|
for (var j = 0; j < htmlClasses.length; j++) {
|
|
var htmlClass = htmlClasses[j].toLowerCase();
|
|
if (htmlClass === 'sh_none') {
|
|
donthighlight = true
|
|
continue;
|
|
}
|
|
if (htmlClass.substr(0, 3) === 'sh_') {
|
|
var language = htmlClass.substring(3);
|
|
if (language in sh_languages) {
|
|
sh_highlightElement(element, sh_languages[language]);
|
|
highlighted = true;
|
|
}
|
|
else if (typeof(prefix) === 'string' && typeof(suffix) === 'string') {
|
|
sh_load(language, element, prefix, suffix);
|
|
}
|
|
else {
|
|
throw 'Found <' + tag + '> element with class="' + htmlClass + '", but no such language exists';
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (highlighted === false && donthighlight == false) {
|
|
sh_highlightElement(element, sh_languages["javascript"]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function sh_highlightDocument(prefix, suffix) {
|
|
highlight(prefix, suffix, 'tt');
|
|
highlight(prefix, suffix, 'code');
|
|
highlight(prefix, suffix, 'pre');
|
|
}
|