Mercurial > browser-couch
view js/ext/docs.js @ 57:147016e199d0
Added code-illuminated documentation.
| author | Atul Varma <varmaa@toolness.com> |
|---|---|
| date | Wed, 15 Apr 2009 08:24:32 -0700 |
| parents | |
| children |
line wrap: on
line source
/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Ubiquity. * * The Initial Developer of the Original Code is Mozilla. * Portions created by the Initial Developer are Copyright (C) 2007 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Atul Varma <atul@mozilla.com> * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ // == Code Illuminated Source Documentation == // // Everything is contained in the {{{App}}} namespace. var App = { }; // ** {{{ App.TrivialParser }}} ** // // This is a trivial parser implementation, which can be used for any file // that the application doesn't know how to properly parse and render. It just // outputs the full contents of the file as the code, and the documentation // states that there's no documentation for the file. App.TrivialParser = function TrivialParser(pattern) { this.pattern = pattern; }; App.TrivialParser.prototype = { // ** {{{ App.TrivialParser.blockify() }}} ** // // Given a string containing the contents of a file, chops the file // up into an array of segments containing documentation-code pairs. blockify: function TrivialParser_blockify(code) { return [{text: "No documentation exists for this file.", lineno: 0, numLines: 0, code: code}]; }, // ** {{{ App.TrivialParser.renderDocText() }}} ** // // Given a jQuery and a string containing documentation text, renders // the documentation into the jQuery. renderDocText: function TrivialParser_renderDocText(jQuery, text) { jQuery.text(text); }, // ** {{{ App.TrivialParser.renderCode() }}} ** // // Given a jQuery and a string containing code, renders the code // into the jQuery. renderCode: function TrivialParser_renderCode(jQuery, code) { jQuery.text(code); } }; // ** {{{ App.JsParser }}} ** // // This is a parser implementation for parsing and rendering JavaScript with // [[http://wikicreole.org/|WikiCreole]] formatted comments as documentation. App.JsParser = function JsParser(creole) { if (creole) this._creole = creole; }; App.JsParser.prototype = { pattern: /.*\.js$/, blockify: function JsParser_blockify(code) { var lines = code.split('\n'); var blocks = []; var blockText = ""; var codeText = ""; var firstCommentLine; var lastCommentLine; function maybeAppendBlock() { if (blockText) blocks.push({text: blockText, lineno: firstCommentLine, numLines: lastCommentLine - firstCommentLine + 1, code: codeText}); } jQuery.each( lines, function(lineNum) { var line = this; var isCode = true; var isComment = (App.trim(line).indexOf("//") == 0); if (isComment) { var startIndex = line.indexOf("//"); var text = line.slice(startIndex + 3); if (lineNum == lastCommentLine + 1) { blockText += text + "\n"; lastCommentLine += 1; isCode = false; } else if (text[0] == "=" || text[0] == "*") { maybeAppendBlock(); firstCommentLine = lineNum; lastCommentLine = lineNum; blockText = text + "\n"; codeText = ""; isCode = false; } } if (isCode) codeText += line + "\n"; }); maybeAppendBlock(); if (blocks.length) return blocks; else return [{text: "No documentation exists for this file.", lineno: 0, numLines: 0, code: code}]; }, renderDocText: function JsParser_renderDocText(jQuery, text) { if (!this._creole) this._creole = new Parse.Simple.Creole( {interwiki: { WikiCreole: 'http://www.wikicreole.org/wiki/', Wikipedia: 'http://en.wikipedia.org/wiki/' }, linkFormat: '' }); var self = this; jQuery.each(function() { self._creole.parse(this, text); }); }, renderCode: function JsParser_renderCode(jQuery, code) { var self = this; jQuery.text(code); } }; // ** {{{ App.parsers }}} ** // // An array of parser interfaces, from least-specific to // most-specific. That is, the "default" parser that is used if no // more specialized parser can be found is the first item in the // array, followed by the next least specific one, and so on. App.parsers = [new App.TrivialParser(/.*/), new App.JsParser()]; // ** {{{ App.trim() }}} ** // // Simple utility function to trim whitespace from both sides of a string. App.trim = function trim(str) { return str.replace(/^\s+|\s+$/g,""); }; // ** {{{ App.processors }}} ** // // An array of user-defined processor functions. They should take one // argument, the DOM node containing the documentation. User-defined // processor functions are called after standard processing is done. App.processors = []; // Has a {label, urlOrCallback} dict for each keyword. App.menuItems = {}; // ** {{{ App.getParserForFile() }}} ** // // Given a filename, attempts to find the best parser for it and // returns it. App.getParserForFile = function getParserForFile(filename) { for (var i = App.parsers.length - 1; i >= 0; i--) if (filename.match(App.parsers[i].pattern)) return App.parsers[i]; throw new Error("Parser not found for " + filename); }; // ** {{{ App.layout() }}} ** // // Given a parser implementation, a body of text, and a DOM element, // splits the code from the documentation and lays them out // side-by-side into the DOM element. App.layout = function layout(parser, code, div) { jQuery.each( parser.blockify(code), function() { var docs = $('<div class="documentation">'); docs.css(App.columnCss); parser.renderDocText(docs, this.text); $(div).append(docs); var code = $('<div class="code">'); code.css(App.columnCss); parser.renderCode(code, this.code); $(div).append(code); var docsSurplus = docs.height() - code.height() + 1; if (docsSurplus > 0) code.css({paddingBottom: docsSurplus + "px"}); $(div).append('<div class="divider">'); }); // Run the user-defined processors. jQuery.each( App.processors, function() { this($(div).find(".documentation")); }); }; // ** {{{ App.addMenuItem() }}} ** // // Adds a menu item to the {{{element}}} DOM node showing the {{{label}}} // text. If {{{urlOrCallback}}} is an URL, choosing the item causes a new // window to be opened with that URL. If it's a function, it will be called // when choosing the item. // // If the node does not have a menu yet, one will be created. App.addMenuItem = function addMenuItem(element, label, urlOrCallback) { var text = $(element).text(); if (!$(element).parent().hasClass("popup-enabled")) { App.menuItems[text] = []; $(element).wrap('<span class="popup-enabled"></span>'); $(element).mousedown( function(evt) { evt.preventDefault(); var popup = $('<div class="popup"></div>'); function addItemToPopup(label, urlOrCallback) { var callback; var menuItem = $('<div class="item"></div>'); menuItem.text(label); function onOverOrOut() { $(this).toggleClass("selected"); } menuItem.mouseover(onOverOrOut); menuItem.mouseout(onOverOrOut); if (typeof(urlOrCallback) == "string") callback = function() { window.open(urlOrCallback); }; else callback = urlOrCallback; menuItem.mouseup(callback); popup.append(menuItem); } jQuery.each( App.menuItems[text], function() { addItemToPopup(this.label, this.urlOrCallback); }); popup.find(".item:last").addClass("bottom"); popup.css({left: evt.pageX + "px"}); $(window).mouseup( function mouseup() { popup.remove(); $(window).unbind("mouseup", mouseup); }); $(this).append(popup); }); } App.menuItems[text].push({ label: label, urlOrCallback: urlOrCallback }); }; // The current page we're on. App.currentPage = null; // The current section of the current page we're on. App.currentSection = null; // Maps filenames to DOM elements containing the rendered // documentation and source code for the filename. App.pages = {}; // ** {{{ App.navigate() }}} ** // // Navigates to the code/documentation of a different file if // needed. The appropriate view is fetched from the URL hash. If that // is empty, the overview is shown. App.navigate = function navigate() { var newPage; var section; if (window.location.hash) newPage = window.location.hash.slice(1); else newPage = "overview"; var hashIndex = newPage.indexOf("#"); if (hashIndex != -1) { section = newPage.slice(hashIndex + 1).replace(/_/g, " "); newPage = newPage.slice(0, hashIndex); } function scrollToAnchor() { if (section) { var anchor; $(":header").each( function() { if ($(this).text() == section && !anchor) anchor = this; }); if (anchor) window.scroll(0, $(anchor).offset().top); } else window.scroll(0, 0); } function onNewPageLoaded() { $(App.pages[newPage]).show(); scrollToAnchor(); } if (App.currentPage != newPage) { if (App.currentPage) $(App.pages[App.currentPage]).hide(); App.currentPage = newPage; App.currentSection = section; if (!App.pages[newPage]) { var parser = App.getParserForFile(newPage); var newDiv = $("<div>"); newDiv.attr("name", newPage); $("#content").append(newDiv); App.pages[newPage] = newDiv; jQuery.ajax( {url: newPage, success: function onCodeLoaded(code) { App.layout(parser, code, newDiv); onNewPageLoaded(); }, error: function onError() { newDiv.text("Sorry, couldn't load " + newPage + "."); }, dataType: "text"} ); } else onNewPageLoaded(); } else if (App.currentSection != section) { App.currentSection = section; scrollToAnchor(); } }; // ** {{{ App.CHARS_PER_ROW }}} ** // // Maximum number of characters per row to display on each column. By // default, this is typographically enforced: any lines that exceed // this number of characters per row will look bad because of // overflow, and intentionally so. App.CHARS_PER_ROW = 80; // ** {{{ App.initColumnSizes() }}} ** // // Dynamically initializes the widths of the code and documentation // columns. App.initColumnSizes = function initSizes() { // Get the width of a single monospaced character of code. var oneCodeCharacter = $('<div class="code">M</div>'); $("#content").append(oneCodeCharacter); App.charWidth = oneCodeCharacter.width(); App.columnWidth = App.charWidth * App.CHARS_PER_ROW; $(oneCodeCharacter).remove(); // Dynamically determine the column widths and padding based on // the font size. var padding = App.charWidth * 2; App.columnCss = {width: App.columnWidth, paddingLeft: padding, paddingRight: padding}; $("#content").css({width: (App.columnWidth + padding*2) * 2}); $(".documentation").css(App.columnCss); $(".code").css(App.columnCss); }; // == Initialization == $(window).ready( function() { App.pages["overview"] = $("#overview").get(0); App.initColumnSizes(); window.setInterval( function() { App.navigate(); }, 100 ); App.navigate(); });
