# # # add_file "json.py" # content [8011edc26270fc6ae7e34887279cb08479418368] # # add_file "viewmtn.js" # content [d81eac728f091a04c0de4f00de2a9edbf0171e19] # # patch "common.py" # from [d2ea6fa516f36b77496097ec0efbc5844339b44d] # to [6fb10f24e4921c4e9dfa6baec980b8f927a218cd] # # patch "html.py" # from [e671830a6dbe86055c8422412c55806f36e8b2d2] # to [e9fa4f4925ed2d6898b2346b003c51dcf7e24bf1] # # patch "viewmtn.css" # from [a1b37d9cb0fa5fddaf650dccc25af95bd9597a3f] # to [191628c6def3f60fb8cdb729ede26c3420f4c5d5] # # patch "wrapper.py" # from [1cdf75ce5311b1fd168aff335e1de92049f8174d] # to [225e4f37f55a61876170a8da4cc1e35d4f792cab] # ============================================================ --- json.py 8011edc26270fc6ae7e34887279cb08479418368 +++ json.py 8011edc26270fc6ae7e34887279cb08479418368 @@ -0,0 +1,310 @@ +import string +import types + +## json.py implements a JSON (http://json.org) reader and writer. +## Copyright (C) 2005 Patrick D. Logan +## Contact mailto:address@hidden +## +## This library is free software; you can redistribute it and/or +## modify it under the terms of the GNU Lesser General Public +## License as published by the Free Software Foundation; either +## version 2.1 of the License, or (at your option) any later version. +## +## This library is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public +## License along with this library; if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +class _StringGenerator(object): + def __init__(self, string): + self.string = string + self.index = -1 + def peek(self): + i = self.index + 1 + if i < len(self.string): + return self.string[i] + else: + return None + def next(self): + self.index += 1 + if self.index < len(self.string): + return self.string[self.index] + else: + raise StopIteration + def all(self): + return self.string + +class WriteException(Exception): + pass + +class ReadException(Exception): + pass + +class JsonReader(object): + hex_digits = {'A': 10,'B': 11,'C': 12,'D': 13,'E': 14,'F':15} + escapes = {'t':'\t','n':'\n','f':'\f','r':'\r','b':'\b'} + + def read(self, s): + self._generator = _StringGenerator(s) + result = self._read() + return result + + def _read(self): + self._eatWhitespace() + peek = self._peek() + if peek is None: + raise ReadException, "Nothing to read: '%s'" % self._generator.all() + if peek == '{': + return self._readObject() + elif peek == '[': + return self._readArray() + elif peek == '"': + return self._readString() + elif peek == '-' or peek.isdigit(): + return self._readNumber() + elif peek == 't': + return self._readTrue() + elif peek == 'f': + return self._readFalse() + elif peek == 'n': + return self._readNull() + elif peek == '/': + self._readComment() + return self._read() + else: + raise ReadException, "Input is not valid JSON: '%s'" % self._generator.all() + + def _readTrue(self): + self._assertNext('t', "true") + self._assertNext('r', "true") + self._assertNext('u', "true") + self._assertNext('e', "true") + return True + + def _readFalse(self): + self._assertNext('f', "false") + self._assertNext('a', "false") + self._assertNext('l', "false") + self._assertNext('s', "false") + self._assertNext('e', "false") + return False + + def _readNull(self): + self._assertNext('n', "null") + self._assertNext('u', "null") + self._assertNext('l', "null") + self._assertNext('l', "null") + return None + + def _assertNext(self, ch, target): + if self._next() != ch: + raise ReadException, "Trying to read %s: '%s'" % (target, self._generator.all()) + + def _readNumber(self): + isfloat = False + result = self._next() + peek = self._peek() + while peek is not None and (peek.isdigit() or peek == "."): + isfloat = isfloat or peek == "." + result = result + self._next() + peek = self._peek() + try: + if isfloat: + return float(result) + else: + return int(result) + except ValueError: + raise ReadException, "Not a valid JSON number: '%s'" % result + + def _readString(self): + result = "" + assert self._next() == '"' + try: + while self._peek() != '"': + ch = self._next() + if ch == "\\": + ch = self._next() + if ch in 'brnft': + ch = self.escapes[ch] + elif ch == "u": + ch4096 = self._next() + ch256 = self._next() + ch16 = self._next() + ch1 = self._next() + n = 4096 * self._hexDigitToInt(ch4096) + n += 256 * self._hexDigitToInt(ch256) + n += 16 * self._hexDigitToInt(ch16) + n += self._hexDigitToInt(ch1) + ch = unichr(n) + elif ch not in '"/\\': + raise ReadException, "Not a valid escaped JSON character: '%s' in %s" % (ch, self._generator.all()) + result = result + ch + except StopIteration: + raise ReadException, "Not a valid JSON string: '%s'" % self._generator.all() + assert self._next() == '"' + return result + + def _hexDigitToInt(self, ch): + try: + result = self.hex_digits[ch.upper()] + except KeyError: + try: + result = int(ch) + except ValueError: + raise ReadException, "The character %s is not a hex digit." % ch + return result + + def _readComment(self): + assert self._next() == "/" + second = self._next() + if second == "/": + self._readDoubleSolidusComment() + elif second == '*': + self._readCStyleComment() + else: + raise ReadException, "Not a valid JSON comment: %s" % self._generator.all() + + def _readCStyleComment(self): + try: + done = False + while not done: + ch = self._next() + done = (ch == "*" and self._peek() == "/") + if not done and ch == "/" and self._peek() == "*": + raise ReadException, "Not a valid JSON comment: %s, '/*' cannot be embedded in the comment." % self._generator.all() + self._next() + except StopIteration: + raise ReadException, "Not a valid JSON comment: %s, expected */" % self._generator.all() + + def _readDoubleSolidusComment(self): + try: + ch = self._next() + while ch != "\r" and ch != "\n": + ch = self._next() + except StopIteration: + pass + + def _readArray(self): + result = [] + assert self._next() == '[' + done = self._peek() == ']' + while not done: + item = self._read() + result.append(item) + self._eatWhitespace() + done = self._peek() == ']' + if not done: + ch = self._next() + if ch != ",": + raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) + assert ']' == self._next() + return result + + def _readObject(self): + result = {} + assert self._next() == '{' + done = self._peek() == '}' + while not done: + key = self._read() + if type(key) is not types.StringType: + raise ReadException, "Not a valid JSON object key (should be a string): %s" % key + self._eatWhitespace() + ch = self._next() + if ch != ":": + raise ReadException, "Not a valid JSON object: '%s' due to: '%s'" % (self._generator.all(), ch) + self._eatWhitespace() + val = self._read() + result[key] = val + self._eatWhitespace() + done = self._peek() == '}' + if not done: + ch = self._next() + if ch != ",": + raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) + assert self._next() == "}" + return result + + def _eatWhitespace(self): + p = self._peek() + while p is not None and p in string.whitespace or p == '/': + if p == '/': + self._readComment() + else: + self._next() + p = self._peek() + + def _peek(self): + return self._generator.peek() + + def _next(self): + return self._generator.next() + +class JsonWriter(object): + + def _append(self, s): + self._results.append(s) + + def write(self, obj, escaped_forward_slash=False): + self._escaped_forward_slash = escaped_forward_slash + self._results = [] + self._write(obj) + return "".join(self._results) + + def _write(self, obj): + ty = type(obj) + if ty is types.DictType: + n = len(obj) + self._append("{") + for k, v in obj.items(): + self._write(k) + self._append(":") + self._write(v) + n = n - 1 + if n > 0: + self._append(",") + self._append("}") + elif ty is types.ListType or ty is types.TupleType: + n = len(obj) + self._append("[") + for item in obj: + self._write(item) + n = n - 1 + if n > 0: + self._append(",") + self._append("]") + elif ty is types.StringType or ty is types.UnicodeType: + self._append('"') + obj = obj.replace('\\', r'\\') + if self._escaped_forward_slash: + obj = obj.replace('/', r'\/') + obj = obj.replace('"', r'\"') + obj = obj.replace('\b', r'\b') + obj = obj.replace('\f', r'\f') + obj = obj.replace('\n', r'\n') + obj = obj.replace('\r', r'\r') + obj = obj.replace('\t', r'\t') + self._append(obj) + self._append('"') + elif ty is types.IntType or ty is types.LongType: + self._append(str(obj)) + elif ty is types.FloatType: + self._append("%f" % obj) + elif obj is True: + self._append("true") + elif obj is False: + self._append("false") + elif obj is None: + self._append("null") + else: + raise WriteException, "Cannot write in JSON: %s" % repr(obj) + +def write(obj, escaped_forward_slash=False): + return JsonWriter().write(obj, escaped_forward_slash) + +def read(s): + return JsonReader().read(s) ============================================================ --- viewmtn.js d81eac728f091a04c0de4f00de2a9edbf0171e19 +++ viewmtn.js d81eac728f091a04c0de4f00de2a9edbf0171e19 @@ -0,0 +1,95 @@ + +var theBox; +var callbacksInstalled = false; +var pendingDeferred = null; + +function installCallbacks() +{ + if (callbacksInstalled != false) { + return; + } + callbacksInstalled = true; + + cbinst = function (e) { + updateNodeAttributes(e, { "onmouseover" : partial(mouseOverHandler, e), + 'onmouseout' : partial(mouseOutHandler, e) } ); + } + + var elems = getElementsByTagAndClassName(null, "branchLink"); + map(cbinst, elems); + + var elems = getElementsByTagAndClassName(null, "revisionLink"); + map(cbinst, elems); + + theBox = $("popupBox"); +} + +function updatePopup(boundTo, className) +{ + jsonData = boundTo.jsonData; + + var pos = elementPosition(boundTo); + var newBox; + + info = null; + if (jsonData.type == "branch") { + info = "branch: "; + } else if (jsonData.type == "revision") { + info = "revision: "; + } + + if (info == null) { + info = ""; + } + + newBox = DIV({ 'id' : 'popupBox'}, + info); + + if (boundTo.offsetHeight) { + offset_height = boundTo.offsetHeight; + } else { + offset_height = 24; // yick + } + + newY = pos.y + offset_height + 20; + newX = pos.x + 20; + + newBox.style.top = newY + 'px'; + newBox.style.left = newX + 'px'; + swapDOM(theBox, newBox); + theBox = newBox; +} + +function jsonLoadComplete(boundTo, className, jsonData) +{ + boundTo.jsonData = jsonData; + updatePopup(boundTo, className); + pendingDeferred = null; +} + +function mouseOverHandler(boundTo, evt) +{ + var className = getNodeAttribute(boundTo, "class"); + + if (boundTo.jsonData) { + return updatePopup(boundTo, className); + } + + // var uri = "META.json"; + var uri = "getjson.py?className=" + className; + var d = loadJSONDoc(uri); + + d.addCallback(jsonLoadComplete, boundTo, className); + pendingDeferred = d; +} + +function mouseOutHandler(boundTo, evt) +{ + if (pendingDeferred != null) { + pendingDeferred.cancel(); + pendingDeferred = null; + } + var newBox = DIV({'id' : 'popupBox', 'class' : 'invisible'}); + swapDOM(theBox, newBox); + theBox = newBox; +} ============================================================ --- common.py d2ea6fa516f36b77496097ec0efbc5844339b44d +++ common.py 6fb10f24e4921c4e9dfa6baec980b8f927a218cd @@ -55,70 +55,62 @@ hq = html_escape() if not no_quote and description != None: description = hq(description) if link_type == "revision": - rv = '' % (urllib.quote(link_to)) - if description != None: rv += description - else: rv += hq(link_to[:8]) + ".." - rv += '' - if description == None: rv = '[' + rv + ']' - return rv + rv = '' % (urllib.quote(link_to)) + if description != None: rv += description + else: rv += hq(link_to[:8]) + ".." + rv += '' + if description == None: rv = '[' + rv + ']' elif link_type == "diff" or link_type == "download_diff": - link_to = map(urllib.quote, filter(lambda x: x != None, link_to)) - if link_type == "diff": + link_to = map(urllib.quote, filter(lambda x: x != None, link_to)) + if link_type == "diff": handler = "diff.psp" - else: + else: handler = "getdiff.py" uri = '%s?id1=%s&id2=%s' % (handler, link_to[0], link_to[1]) - if len(link_to) == 3: - uri += '&fname=%s' % (link_to[2]) - rv = '' - if description != None: rv += description - else: rv += "diff" - rv += '' - return rv + if len(link_to) == 3: + uri += '&fname=%s' % (link_to[2]) + rv = '' + if description != None: rv += description + else: rv += "diff" + rv += '' elif link_type == "download": - if type(link_to) == type([]): - rv = '' % (urllib.quote(link_to[0]), urllib.quote(link_to[1])) - link_id = link_to[0] - else: - rv = '' % (urllib.quote(link_to)) - link_id = link_to - if description != None: rv += description + "" - else: rv = "[" + rv + hq(link_id[:8]) + ".." + "]" - return rv + if type(link_to) == type([]): + rv = '' % (urllib.quote(link_to[0]), urllib.quote(link_to[1])) + link_id = link_to[0] + else: + rv = '' % (urllib.quote(link_to)) + link_id = link_to + if description != None: rv += description + "" + else: rv = "[" + rv + hq(link_id[:8]) + ".." + "]" elif link_type == "file": - revision_id, path = link_to - rv = '' % (urllib.quote(revision_id), urllib.quote(path)) - if description != None: rv += description + "" - else: rv = "[" + rv + hq(path + '@' + revision_id[:8]) + ".." + "]" - return rv + revision_id, path = link_to + rv = '' % (urllib.quote(revision_id), urllib.quote(path)) + if description != None: rv += description + "" + else: rv = "[" + rv + hq(path + '@' + revision_id[:8]) + ".." + "]" elif link_type == "branch": - rv = '' % (urllib.quote(link_to)) - if description != None: rv += description - else: rv += hq(link_to) - rv += '' - return rv + rv = '' % (urllib.quote(link_to)) + if description != None: rv += description + else: rv += hq(link_to) + rv += '' elif link_type == "tar": - rv = '' % (urllib.quote(link_to)) - if description != None: rv += description - else: rv = "tar of [" + rv + hq(link_to[:8]) + "..]" + "]" - rv += '' - return rv + rv = '' % (urllib.quote(link_to)) + if description != None: rv += description + else: rv = "tar of [" + rv + hq(link_to[:8]) + "..]" + "]" + rv += '' elif link_type == "headofbranch": - rv = '' % (urllib.quote(link_to)) - if description != None: rv += description - else: rv += "head of " + hq(link_to) - rv += '' - return rv + rv = '' % (urllib.quote(link_to)) + if description != None: rv += description + else: rv += "head of " + hq(link_to) + rv += '' elif link_type == "manifest": - rv = '' % (urllib.quote(link_to)) - if description != None: rv += description - else: rv += hq(link_to[:8]) + ".." - rv += '' - if description == None: rv = '[' + rv + ']' - return rv + rv = '' % (urllib.quote(link_to)) + if description != None: rv += description + else: rv += hq(link_to[:8]) + ".." + rv += '' + if description == None: rv = '[' + rv + ']' else: - rv = 'Unknown link type: %s' % (hq(link_type)) - return rv + rv = 'Unknown link type: %s' % (hq(link_type)) + return '%s' % (hq(link_type+'Link'), rv) def html_escape(): "returns a function stolen from pydoc that can be used to escape HTML" ============================================================ --- html.py e671830a6dbe86055c8422412c55806f36e8b2d2 +++ html.py e9fa4f4925ed2d6898b2346b003c51dcf7e24bf1 @@ -58,10 +58,13 @@ ViewMTN: %(title)s + + %(extra_header)s + @@ -76,5 +79,8 @@ rv += '''
Output generated by ViewMTN %s on %s. + ''' % (release, time.strftime("%a, %d %b %Y %H:%M:%S %Z", time.localtime())) return rv ============================================================ --- viewmtn.css a1b37d9cb0fa5fddaf650dccc25af95bd9597a3f +++ viewmtn.css 191628c6def3f60fb8cdb729ede26c3420f4c5d5 @@ -70,3 +70,16 @@ font-size: 120%; font-family: sans-serif; } + +.invisible { + display: none; +} + +DIV#popupBox { + position: absolute; + border-width: 1px; + border-style: solid; + border-color: black; + background-color: #d0d0f0; + padding: 2px; +} ============================================================ --- wrapper.py 1cdf75ce5311b1fd168aff335e1de92049f8174d +++ wrapper.py 225e4f37f55a61876170a8da4cc1e35d4f792cab @@ -7,6 +7,7 @@ import common import config import urllib +import json import os import re @@ -50,6 +51,18 @@ req.write(mt.diff(id1, id2, files)) return apache.OK +def get_json(req, vars): + mt = vars['mt'] + form = util.FieldStorage(req) + #if not form.has_key('class') or not form.has_key('for'): + # return apache.HTTP_BAD_REQUEST + req.content_type = "text/plain" + writer = json.JsonWriter() + rv = {} + rv['type'] = 'branch' + req.write(writer.write(rv)) + return apache.OK + def get_tar(req, vars): "make a tar file out of a given manifest ID" class DummyFile: @@ -92,7 +105,8 @@ handlers = { 'getfile.py' : get_file, 'getdiff.py' : get_diff, - 'gettar.py' : get_tar + 'gettar.py' : get_tar, + 'getjson.py' : get_json } def cleanup(req, vars):