From edc38b4134e362138a505f2999d8049b735bd9f2 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 18 May 2009 19:33:05 +0200 Subject: [PATCH] Use parseUri() for req.uri. Update docs. --- src/http.js | 303 ++++++++++++---------- test-http_simple.js | 2 +- test_http.js | 4 +- website/node.html | 143 +++++++---- website/sh_main.js | 545 ++++++++++++++++++++++++++++++++++++++++ website/sh_vim-dark.css | 4 +- 6 files changed, 816 insertions(+), 185 deletions(-) create mode 100644 website/sh_main.js diff --git a/src/http.js b/src/http.js index ac2de941ff..5e9c3b9e37 100644 --- a/src/http.js +++ b/src/http.js @@ -38,6 +38,56 @@ node.http.STATUS_CODES = { 100 : 'Continue' , 505 : 'HTTP Version not supported' }; +/* + parseUri 1.2.1 + (c) 2007 Steven Levithan + MIT License +*/ + +function parseUri (str) { + var o = parseUri.options, + m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), + uri = {}, + i = 14; + + while (i--) uri[o.key[i]] = m[i] || ""; + + uri[o.q.name] = {}; + uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { + if ($1) uri[o.q.name][$1] = $2; + }); + uri.toString = function () { return str; }; + + return uri; +}; + +parseUri.options = { + strictMode: false, + key: [ "source" + , "protocol" + , "authority" + , "userInfo" + , "user" + , "password" + , "host" + , "port" + , "relative" + , "path" + , "directory" + , "file" + , "query" + , "anchor" + ], + q: { + name: "queryKey", + parser: /(?:^|&)([^&=]*)=?([^&]*)/g + }, + parser: { + strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, + loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ + } +}; + var connection_expression = /Connection/i; var transfer_encoding_expression = /Transfer-Encoding/i; var close_expression = /close/i; @@ -51,154 +101,154 @@ function toRaw(string) { return a; } -/* This is a wrapper around the LowLevelServer interface. It provides - * connection handling, overflow checking, and some data buffering. - */ -node.http.Server = function (RequestHandler, options) { - if (!(this instanceof node.http.Server)) - throw Error("Constructor called as a function"); +node.http.ServerResponse = function (connection, responses) { + responses.push(this); + this.connection = connection; + var output = []; + + // The send method appends data onto the output array. The deal is, + // the data is either an array of integer, representing binary or it + // is a string in which case it's UTF8 encoded. + // Two things to considered: + // - we should be able to send mixed encodings. + // - we don't want to call connection.send("smallstring") because that + // is wasteful. *I think* its rather faster to concat inside of JS + // Thus I attempt to concat as much as possible. + function send (data) { + if (connection.readyState === "closed" || connection.readyState === "readOnly") + { + responses = []; + return; + } - function Response (connection, responses) { - responses.push(this); - this.connection = connection; - var output = []; - - // The send method appends data onto the output array. The deal is, - // the data is either an array of integer, representing binary or it - // is a string in which case it's UTF8 encoded. - // Two things to considered: - // - we should be able to send mixed encodings. - // - we don't want to call connection.send("smallstring") because that - // is wasteful. *I think* its rather faster to concat inside of JS - // Thus I attempt to concat as much as possible. - function send (data) { - if (connection.readyState === "closed" || connection.readyState === "readOnly") - { - responses = []; - return; - } + if (output.length == 0) { + output.push(data); + return; + } - if (output.length == 0) { - output.push(data); - return; - } + var li = output.length-1; - var li = output.length-1; + if (data.constructor == String && output[li].constructor == String) { + output[li] += data; + return; + } - if (data.constructor == String && output[li].constructor == String) { - output[li] += data; - return; - } + if (data.constructor == Array && output[li].constructor == Array) { + output[li] = output[li].concat(data); + return; + } - if (data.constructor == Array && output[li].constructor == Array) { - output[li] = output[li].concat(data); - return; - } + // If the string is small enough, just convert it to binary + if (data.constructor == String + && data.length < 128 + && output[li].constructor == Array) + { + output[li] = output[li].concat(toRaw(data)); + return; + } - // If the string is small enough, just convert it to binary - if (data.constructor == String - && data.length < 128 - && output[li].constructor == Array) - { - output[li] = output[li].concat(toRaw(data)); - return; - } + output.push(data); + }; - output.push(data); - }; + this.flush = function () { + if (responses.length > 0 && responses[0] === this) + while (output.length > 0) + connection.send(output.shift()); + }; - this.flush = function () { - if (responses.length > 0 && responses[0] === this) - while (output.length > 0) - connection.send(output.shift()); - }; + var chunked_encoding = false; + var connection_close = false; - var chunked_encoding = false; - var connection_close = false; + this.sendHeader = function (status_code, headers) { + var sent_connection_header = false; + var sent_transfer_encoding_header = false; + var sent_content_length_header = false; - this.sendHeader = function (status_code, headers) { - var sent_connection_header = false; - var sent_transfer_encoding_header = false; - var sent_content_length_header = false; - - var reason = node.http.STATUS_CODES[status_code] || "unknown"; - var header = "HTTP/1.1 " - + status_code.toString() - + " " - + reason - + "\r\n" - ; - - for (var i = 0; i < headers.length; i++) { - var field = headers[i][0]; - var value = headers[i][1]; - - header += field + ": " + value + "\r\n"; - - if (connection_expression.exec(field)) { - sent_connection_header = true; - if (close_expression.exec(value)) - connection_close = true; - } else if (transfer_encoding_expression.exec(field)) { - sent_transfer_encoding_header = true; - if (chunk_expression.exec(value)) - chunked_encoding = true; - } else if (content_length_expression.exec(field)) { - sent_content_length_header = true; - } - } + var reason = node.http.STATUS_CODES[status_code] || "unknown"; + var header = "HTTP/1.1 " + + status_code.toString() + + " " + + reason + + "\r\n" + ; - // keep-alive logic - if (sent_connection_header == false) { - if (this.should_keep_alive) { - header += "Connection: keep-alive\r\n"; - } else { + for (var i = 0; i < headers.length; i++) { + var field = headers[i][0]; + var value = headers[i][1]; + + header += field + ": " + value + "\r\n"; + + if (connection_expression.exec(field)) { + sent_connection_header = true; + if (close_expression.exec(value)) connection_close = true; - header += "Connection: close\r\n"; - } + } else if (transfer_encoding_expression.exec(field)) { + sent_transfer_encoding_header = true; + if (chunk_expression.exec(value)) + chunked_encoding = true; + } else if (content_length_expression.exec(field)) { + sent_content_length_header = true; } + } - if (sent_content_length_header == false && sent_transfer_encoding_header == false) { - header += "Transfer-Encoding: chunked\r\n"; - chunked_encoding = true; + // keep-alive logic + if (sent_connection_header == false) { + if (this.should_keep_alive) { + header += "Connection: keep-alive\r\n"; + } else { + connection_close = true; + header += "Connection: close\r\n"; } + } - header += "\r\n"; + if (sent_content_length_header == false && sent_transfer_encoding_header == false) { + header += "Transfer-Encoding: chunked\r\n"; + chunked_encoding = true; + } - send(header); - }; + header += "\r\n"; - this.sendBody = function (chunk) { - if (chunked_encoding) { - send(chunk.length.toString(16)); - send("\r\n"); - send(chunk); - send("\r\n"); - } else { - send(chunk); - } + send(header); + }; - this.flush(); - }; + this.sendBody = function (chunk) { + if (chunked_encoding) { + send(chunk.length.toString(16)); + send("\r\n"); + send(chunk); + send("\r\n"); + } else { + send(chunk); + } - this.finished = false; - this.finish = function () { - if (chunked_encoding) - send("0\r\n\r\n"); // last chunk + this.flush(); + }; - this.finished = true; + this.finished = false; + this.finish = function () { + if (chunked_encoding) + send("0\r\n\r\n"); // last chunk - while (responses.length > 0 && responses[0].finished) { - var res = responses[0]; - res.flush(); - responses.shift(); - } + this.finished = true; - if (responses.length == 0 && connection_close) { - connection.fullClose(); - } - }; - } + while (responses.length > 0 && responses[0].finished) { + var res = responses[0]; + res.flush(); + responses.shift(); + } + + if (responses.length == 0 && connection_close) { + connection.fullClose(); + } + }; +}; + +/* This is a wrapper around the LowLevelServer interface. It provides + * connection handling, overflow checking, and some data buffering. + */ +node.http.Server = function (RequestHandler, options) { + if (!(this instanceof node.http.Server)) + throw Error("Constructor called as a function"); function ConnectionHandler (connection) { // An array of responses for each connection. In pipelined connections @@ -214,7 +264,7 @@ node.http.Server = function (RequestHandler, options) { , onBody : null // by user , onBodyComplete : null // by user } - var res = new Response(connection, responses); + var res = new node.http.ServerResponse(connection, responses); this.onURI = function (data) { req.uri += data; @@ -246,6 +296,7 @@ node.http.Server = function (RequestHandler, options) { this.onHeadersComplete = function () { req.http_version = this.http_version; req.method = this.method; + req.uri = parseUri(req.uri); res.should_keep_alive = this.should_keep_alive; diff --git a/test-http_simple.js b/test-http_simple.js index fa86ed0b8c..61f21f0191 100644 --- a/test-http_simple.js +++ b/test-http_simple.js @@ -10,7 +10,7 @@ new node.http.Server(function (req, res) { var arg = commands[2]; var status = 200; - //p(req.headers); + p(req.headers); if (command == "bytes") { var n = parseInt(arg, 10) diff --git a/test_http.js b/test_http.js index a7c234524e..4cb22396a8 100644 --- a/test_http.js +++ b/test_http.js @@ -1,7 +1,7 @@ new node.http.Server(function (req, res) { setTimeout(function () { res.sendHeader(200, [["Content-Type", "text/plain"]]); - res.sendBody("Hello World"); + res.sendBody(JSON.stringify(req.uri)); res.finish(); - }, 1000); + }, 1); }).listen(8000, "localhost"); diff --git a/website/node.html b/website/node.html index 4c9ff9d7b0..71cfa866ba 100644 --- a/website/node.html +++ b/website/node.html @@ -68,7 +68,7 @@ a:hover { text-decoration: underline; } } - + @@ -84,10 +84,12 @@ a:hover { text-decoration: underline; }
  1. Timers
  2. File System -
  3. TCP -
  4. HTTP +
  5. tcp +
  6. http
    1. Server +
    2. ServerRequest +
    3. ServerResponse
    4. Client
  7. Modules @@ -99,11 +101,12 @@ a:hover { text-decoration: underline; }

    Node

    -

    Node is a purely asynchronous I/O framework for Purely asynchronous I/O for V8 javascript. -

    This is an example of a web server written with Node which listens on -port 8000 and responds with "Hello World" after waiting two seconds: +

    This is an example of a web server written with Node which responds with +"Hello World" after waiting two seconds: +

    new node.http.Server(function (req, res) {
       setTimeout(function () {
         res.sendHeader(200, [["Content-Type", "text/plain"]]);
    @@ -111,7 +114,20 @@ port 8000 and responds with "Hello World" after waiting two seconds:
         res.finish();
       }, 2000);
     }).listen(8000);
    -
    +puts("Server running at http://127.0.0.1:8000/"); + +

    Execution does not block on setTimeout() +nor +listen(8000). +File I/O is also preformed without blocking. +In fact, not a single function in Node blocks execution. +There isn't even a call to run the event loop. + +

    Programmers using this environment will find it difficult to design +their systems ineffiencely. It's impossible to make a database call from a +web server and block other requests.

    Check out the API documentation for more examples. @@ -224,26 +240,35 @@ See File System -

    TCP

    +

    node.tcp

    -

    HTTP (node.http)

    +

    node.http

    Node provides a web server and client interface. The interface is rather low-level but complete. For example, it does not parse application/x-www-form-urlencoded message bodies. The interface does abstract the Transfer-Encoding (i.e. chuncked or identity), Message -boundarys, and Keep-Alive connections. +boundaries, and Keep-Alive connections. + +

    node.http.Server

    -

    HTTP Server (node.http.Server)

    -
    new Server(request_handler, options)
    +
    var server = new node.http.Server(request_handler, options);
    -

    Creates a new web server. The options argument accepts - the same values as the options argument for - node.tcp.Server does. The options argument is optional. +

    Creates a new web server. + +

    + The options argument is optional. + The options argument accepts the same values + as the options argument for node.tcp.Server does. + +

    The request_handler is a + callback which is made on each request with a + ServerRequest and + ServerResponse arguments. -

    The request_handler is a function which is called on - each request with a Message object argument.

    server.listen(port, hostname) @@ -261,26 +286,37 @@ boundarys, and Keep-Alive connections.
    -

    HTTP Request Message (node.http.Message)

    +

    node.http.ServerRequest

    -

    This object is only created internally—not by the user. It is passed -as an argument to the request_handler callback in a web server. +

    This object is created internally by a HTTP server—not by the user. +It is passed as the first argument to the request_handler callback. -

    This object, unlike in other HTTP APIs, is used as an interface for both -the request and response. Members and callbacks reference request data, like -msg.method and msg.onBody. The methods are for -sending a response to this message. Like msg.sendHeader() and -msg.sendBody().

    -
    msg.method +
    req.method
    The request method as a string. Read only. Example: "GET", "DELETE".
    -
    msg.uri -
    The request URI as a string. Read only. - Example: "/index.html?hello=world".
    - -
    msg.headers +
    req.uri +
    URI object. Has many fields. +
    req.uri.toString() +
    The original URI found in the status line. +
    req.uri.anchor +
    req.uri.query +
    req.uri.file +
    req.uri.directory +
    req.uri.path +
    req.uri.relative +
    req.uri.port +
    req.uri.host +
    req.uri.password +
    req.uri.user +
    req.uri.authority +
    req.uri.protocol +
    req.uri.source +
    req.uri.queryKey + +
    req.headers
    The request headers expressed as an array of 2-element arrays. Read only. Example:
    @@ -291,40 +327,41 @@ sending a response to this message. Like msg.sendHea
     ]
     
    -
    msg.http_version
    +
    req.http_version
    The HTTP protocol version as a string. Read only. Examples: "1.1", "1.0" -
    msg.connection
    -
    A reference to the node.tcp.Connection object. Read - only. Note that multiple messages can be sent on a single connection. -
    - -
    msg.onBody
    +
    req.onBody
    Callback. Should be set by the user to be informed of when a piece of the message body is received. Example:
    -msg.onBody = function (chunk) {
    +req.onBody = function (chunk) {
       puts("part of the body: " + chunk);
    -}
    +};
     
    A chunk of the body is given as the single argument. The transfer-encoding has been removed. -

    The body chunk is either a String in the case of utf8 encoding or an - array of numbers in the case of raw encoding. -

    msg.onBodyComplete
    +

    The body chunk is either a String in the case of UTF-8 encoding or an + array of numbers in the case of raw encoding. The body encoding is set with + req.setBodyEncoding(). + +

    req.onBodyComplete
    Callback. Made exactly once for each message. No arguments. After onBodyComplete is executed onBody will no longer be called.
    -
    msg.setBodyEncoding(encoding)
    +
    req.setBodyEncoding(encoding)
    Set the encoding for the request body. Either "utf8" or "raw". Defaults to raw. TODO +
    -
    msg.sendHeader(status_code, headers)
    +

    node.http.ServerResponse

    + +
    +
    res.sendHeader(status_code, headers)
    Sends a response header to the request. The status code is a 3-digit HTTP status code, like 404. The second argument, @@ -334,28 +371,26 @@ msg.onBody = function (chunk) {

    Example:

     var body = "hello world";
    -msg.sendHeader( 200
    -              , [ ["Content-Length", body.length] 
    -                , ["Content-Type", "text/plain"] 
    -                ]
    -              );
    +res.sendHeader(200, [ ["Content-Length", body.length]
    +                    , ["Content-Type", "text/plain"]
    +                    ]);
     
    This method must only be called once on a message and it must be called - before msg.finish() is called. + before res.finish() is called.
    -
    msg.sendBody(chunk)
    +
    res.sendBody(chunk)
    This method must be called after sendHeader was called. It sends a chunk of the response body. This method may be called multiple times to provide successive parts of the body.
    -
    msg.finish()
    +
    res.finish()
    This method signals that all of the response headers and body has been sent; that server should consider this message complete. - The method, msg.finish(), MUST be called on each response. + The method, res.finish(), MUST be called on each response.
    diff --git a/website/sh_main.js b/website/sh_main.js new file mode 100644 index 0000000000..87182662c2 --- /dev/null +++ b/website/sh_main.js @@ -0,0 +1,545 @@ +/* +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 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
     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);
    +    for (var j = 0; j < htmlClasses.length; j++) {
    +      var htmlClass = htmlClasses[j].toLowerCase();
    +      if (htmlClass === 'sh_sourcecode') {
    +        continue;
    +      }
    +      if (htmlClass.substr(0, 3) === 'sh_') {
    +        var language = htmlClass.substring(3);
    +        if (language in sh_languages) {
    +          sh_highlightElement(element, sh_languages[language]);
    +        }
    +        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;
    +      }
    +    }
    +  }
    +}
    +
    +
    +
    +function sh_highlightDocument(prefix, suffix) {
    +  highlight(prefix, suffix, 'code');
    +  highlight(prefix, suffix, 'pre');
    +}
    diff --git a/website/sh_vim-dark.css b/website/sh_vim-dark.css
    index 74d7cefb7b..ae1d75c4ad 100644
    --- a/website/sh_vim-dark.css
    +++ b/website/sh_vim-dark.css
    @@ -4,7 +4,7 @@
       font-style: normal;
     }
     
    -.sh_sourceCode .sh_symbol , pre.sh_sourceCode .sh_cbracket {
    +.sh_sourceCode .sh_symbol , .sh_sourceCode .sh_cbracket {
       color: #bbd;
     }
     
    @@ -12,7 +12,7 @@
       font-style: italic;
     }
     
    -.sh_sourceCode .sh_string, pre.sh_sourceCode .sh_regexp, pre.sh_sourceCode .sh_number 
    +.sh_sourceCode .sh_string, .sh_sourceCode .sh_regexp, .sh_sourceCode .sh_number 
     {
       color: #daa;
     }