diff --git a/kanso.json b/kanso.json index 5aab509..5a0398b 100644 --- a/kanso.json +++ b/kanso.json @@ -23,6 +23,7 @@ "duality-handlebars": null, "duality": null, "duality-contrib-session-hb": null, - "less-precompiler": null + "less-precompiler": null, + "sanitize": null } } diff --git a/lib/lists.js b/lib/lists.js index 30a7205..ea2f7da 100644 --- a/lib/lists.js +++ b/lib/lists.js @@ -1,6 +1,8 @@ var templates = require('duality/templates'), utils = require('./utils'), - shows = require('./shows'); + shows = require('./shows'), + ui = require('./ui'), + events = require('duality/events'); exports.app_list = function (head, req) { @@ -33,11 +35,20 @@ exports.app_details = function (head, req) { return shows.not_found(null, req); } var doc = rows[0].doc; + var screenshot = doc.kanso.screenshots ? doc.kanso.screenshots[0]: null; + + events.once('afterResponse', function (info, req, res) { + if (doc.kanso.readme) { + ui.fetchREADME(doc, doc.kanso.readme); + } + }); return { title: 'App: ' + doc._id, content: templates.render('app_details.html', req, { doc: doc, + screenshot: screenshot, + build_time: new Date(doc.kanso._build_time), install_script_url: utils.install_script_url(req, doc._id) }) }; diff --git a/lib/showdown-wiki.js b/lib/showdown-wiki.js new file mode 100644 index 0000000..5173404 --- /dev/null +++ b/lib/showdown-wiki.js @@ -0,0 +1,1420 @@ +/****************************** + * NOTE: + * + * This has been edited to avoid embedded HTML and add support for fenced code + * blocks, wiki links etc + * + *****************************/ + +var duality_utils = require('duality/utils'), + sanitize = require('sanitize'), + _ = require('underscore')._; + + +// +// showdown.js -- A javascript port of Markdown. +// +// Copyright (c) 2007 John Fraser. +// +// Original Markdown Copyright (c) 2004-2005 John Gruber +// +// +// Redistributable under a BSD-style open source license. +// See license.txt for more information. +// +// The full source distribution is at: +// +// A A L +// T C A +// T K B +// +// +// + +// +// Wherever possible, Showdown is a straight, line-by-line port +// of the Perl version of Markdown. +// +// This is not a normal parser design; it's basically just a +// series of string substitutions. It's hard to read and +// maintain this way, but keeping Showdown close to the original +// design makes it easier to port new features. +// +// More importantly, Showdown behaves like markdown.pl in most +// edge cases. So web applications can do client-side preview +// in Javascript, and then build identical HTML on the server. +// +// This port needs the new RegExp functionality of ECMA 262, +// 3rd Edition (i.e. Javascript 1.5). Most modern web browsers +// should do fine. Even with the new regular expression features, +// We do a lot of work to emulate Perl's regex functionality. +// The tricky changes in this file mostly have the "attacklab:" +// label. Major or self-explanatory changes don't. +// +// Smart diff tools like Araxis Merge will be able to match up +// this file with markdown.pl in a useful way. A little tweaking +// helps: in a copy of markdown.pl, replace "#" with "//" and +// replace "$text" with "text". Be sure to ignore whitespace +// and line endings. +// + + +// +// Showdown usage: +// +// var text = "Markdown *rocks*."; +// +// var converter = new Showdown.converter(); +// var html = converter.makeHtml(text); +// +// alert(html); +// +// Note: move the sample code to the bottom of this +// file before uncommenting it. +// + + +// +// Showdown namespace +// +var Showdown = {}; + +// +// converter +// +// Wraps all "globals" so that the only thing +// exposed is makeHtml(). +// +Showdown.converter = function() { + +// +// Globals: +// + +// Global hashes, used by various utility routines +var g_urls; +var g_titles; +var g_html_blocks; + +// Used to track when we're inside an ordered or unordered list +// (see _ProcessListItems() for details): +var g_list_level = 0; + + +this.makeHtml = function(text) { +// +// Main function. The order in which other subs are called here is +// essential. Link and image substitutions need to happen before +// _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the +// and tags get encoded. +// + + // Clear the global hashes. If we don't clear these, you get conflicts + // from other articles when generating a page which contains more than + // one article (e.g. an index page that shows the N most recent + // articles): + g_urls = new Array(); + g_titles = new Array(); + g_html_blocks = new Array(); + + // attacklab: Replace ~ with ~T + // This lets us use tilde as an escape char to avoid md5 hashes + // The choice of character is arbitray; anything that isn't + // magic in Markdown will work. + text = text.replace(/~/g,"~T"); + + // attacklab: Replace $ with ~D + // RegExp interprets $ as a special character + // when it's in a replacement string + text = text.replace(/\$/g,"~D"); + + // Standardize line endings + text = text.replace(/\r\n/g,"\n"); // DOS to Unix + text = text.replace(/\r/g,"\n"); // Mac to Unix + + // Make sure text begins and ends with a couple of newlines: + text = "\n\n" + text + "\n\n"; + + // Convert all tabs to spaces. + text = _Detab(text); + + // Strip any lines consisting only of spaces and tabs. + // This makes subsequent regexen easier to write, because we can + // match consecutive blank lines with /\n+/ instead of something + // contorted like /[ \t]*\n+/ . + text = text.replace(/^[ \t]+$/mg,""); + + // Turn block-level HTML blocks into hash entries + text = _HashHTMLBlocks(text); + + // Strip link definitions, store in hashes. + text = _StripLinkDefinitions(text); + + text = _RunBlockGamut(text); + + text = _UnescapeSpecialChars(text); + + // attacklab: Restore dollar signs + text = text.replace(/~D/g,"$$"); + + // attacklab: Restore tildes + text = text.replace(/~T/g,"~"); + + // ** GFM ** Auto-link URLs and emails + text = text.replace(/https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/g, function(wholeMatch,matchIndex){ + var left = text.slice(0, matchIndex), right = text.slice(matchIndex) + if (left.match(/<[^>]+$/) && right.match(/^[^>]*>/)) {return wholeMatch} + //href = wholeMatch.replace(/^http:\/\/github.com\//, "https://github.com/") + //return "" + wholeMatch + ""; + return "" + wholeMatch + ""; + }); + text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/ig, function(wholeMatch){return "" + wholeMatch + "";}); + + return text; +} + + +var _StripLinkDefinitions = function(text) { +// +// Strips link definitions from text, stores the URLs and titles in +// hash references. +// + + // Link defs are in the form: ^[id]: url "optional title" + + /* + var text = text.replace(/ + ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 + [ \t]* + \n? // maybe *one* newline + [ \t]* + ? // url = $2 + [ \t]* + \n? // maybe one newline + [ \t]* + (?: + (\n*) // any lines skipped = $3 attacklab: lookbehind removed + ["(] + (.+?) // title = $4 + [")] + [ \t]* + )? // title is optional + (?:\n+|$) + /gm, + function(){...}); + */ + var text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|\Z)/gm, + function (wholeMatch,m1,m2,m3,m4) { + m1 = m1.toLowerCase(); + g_urls[m1] = _EncodeAmpsAndAngles(m2); // Link IDs are case-insensitive + if (m3) { + // Oops, found blank lines, so it's not a title. + // Put back the parenthetical statement we stole. + return m3+m4; + } else if (m4) { + g_titles[m1] = m4.replace(/"/g,"""); + } + + // Completely remove the definition from the text + return ""; + } + ); + + return text; +} + + +var _HashHTMLBlocks = function(text) { + // attacklab: Double up blank lines to reduce lookaround + text = text.replace(/\n/g,"\n\n"); + + // Hashify HTML blocks: + // We only want to do this for block-level HTML tags, such as headers, + // lists, and tables. That's because we still want to wrap

s around + // "paragraphs" that are wrapped in non-block-level tags, such as anchors, + // phrase emphasis, and spans. The list of tags we're looking for is + // hard-coded: + var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" + var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" + + // First, look for nested blocks, e.g.: + //

+ //
+ // tags for inner block must be indented. + //
+ //
+ // + // The outermost tags must start at the left margin for this to match, and + // the inner nested divs must be indented. + // We need to do this before the next, more liberal match, because the next + // match will start at the first `
` and stop at the first `
`. + + // attacklab: This regex can be expensive when it fails. + /* + var text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_a) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*?\n // any number of lines, minimally matching + // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm,hashElement); + + // + // Now match more liberally, simply from `\n` to `\n` + // + + /* + var text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_b) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*? // any number of lines, minimally matching + .* // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm,hashElement); + + // Special case just for
. It was easier to make a special case than + // to make the other regex more complicated. + + /* + text = text.replace(/ + ( // save in $1 + \n\n // Starting after a blank line + [ ]{0,3} + (<(hr) // start tag = $2 + \b // word break + ([^<>])*? // + \/?>) // the matching end tag + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,hashElement); + + // Special case for standalone HTML comments: + + /* + text = text.replace(/ + ( // save in $1 + \n\n // Starting after a blank line + [ ]{0,3} // attacklab: g_tab_width - 1 + + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,hashElement); + + // PHP and ASP-style processor instructions ( and <%...%>) + + /* + text = text.replace(/ + (?: + \n\n // Starting after a blank line + ) + ( // save in $1 + [ ]{0,3} // attacklab: g_tab_width - 1 + (?: + <([?%]) // $2 + [^\r]*? + \2> + ) + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,hashElement); + + // attacklab: Undo double lines (see comment at top of this function) + text = text.replace(/\n\n/g,"\n"); + return text; +} + +var hashElement = function(wholeMatch,m1) { + var blockText = m1; + + // Undo double lines + blockText = blockText.replace(/\n\n/g,"\n"); + blockText = blockText.replace(/^\n/,""); + + // strip trailing blank lines + blockText = blockText.replace(/\n+$/g,""); + + // Replace the element text with a marker ("~KxK" where x is its key) + blockText = "\n\n~K" + (g_html_blocks.push(blockText)-1) + "K\n\n"; + + return blockText; +}; + +var _RunBlockGamut = function(text) { +// +// These are all the transformations that form block-level +// tags like paragraphs, headers, and list items. +// + text = _DoHeaders(text); + + // Do Horizontal Rules: + var key = hashBlock("
"); + text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,key); + text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,key); + text = text.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,key); + + text = _DoLists(text); + text = _DoCodeBlocks(text); + text = _DoBlockQuotes(text); + + // We already ran _HashHTMLBlocks() before, in Markdown(), but that + // was to escape raw HTML in the original Markdown source. This time, + // we're escaping the markup we've just created, so that we don't wrap + //

tags around block-level tags. + text = _HashHTMLBlocks(text); + text = _FormParagraphs(text); + + return text; +} + + +var _RunSpanGamut = function(text) { +// +// These are all the transformations that occur *within* block-level +// tags like paragraphs, headers, and list items. +// + + text = _DoCodeSpans(text); + text = _EscapeSpecialCharsWithinTagAttributes(text); + text = _EncodeBackslashEscapes(text); + + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + text = _DoImages(text); + text = _DoAnchors(text); + + // Make links out of things like `` + // Must come after _DoAnchors(), because you can use < and > + // delimiters in inline links like [this](). + text = _DoAutoLinks(text); + text = _EncodeAmpsAndAngles(text); + text = _DoItalicsAndBold(text); + + // Do hard breaks: + text = text.replace(/ +\n/g,"
\n"); + + return text; +} + +var _EscapeSpecialCharsWithinTagAttributes = function(text) { +// +// Within tags -- meaning between < and > -- encode [\ ` * _] so they +// don't conflict with their use in Markdown for code, italics and strong. +// + + // Build a regex to find HTML tags and comments. See Friedl's + // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. + var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi; + + text = text.replace(regex, function(wholeMatch) { + var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g,"$1`"); + tag = escapeCharacters(tag,"\\`*_"); + return tag; + }); + + return text; +} + +var _DoAnchors = function(text) { +// +// Turn Markdown link shortcuts into XHTML tags. +// + // + // First, handle reference-style links: [link text] [id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[] // or anything else + )* + ) + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + )()()()() // pad remaining backreferences + /g,_DoAnchors_callback); + */ + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeAnchorTag); + + // + // Next, inline-style links: [link text](url "optional title") + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[\]] // or anything else + ) + ) + \] + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? // href = $4 + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // Title = $7 + \6 // matching quote + [ \t]* // ignore any spaces/tabs between closing quote and ) + )? // title is optional + \) + ) + /g,writeAnchorTag); + */ + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag); + + // + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link test][1] + // or [link test](/foo) + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ([^\[\]]+) // link text = $2; can't contain '[' or ']' + \] + )()()()()() // pad rest of backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); + + return text; +} + +var writeAnchorTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { + if (m7 == undefined) m7 = ""; + var whole_match = m1; + var link_text = m2; + var link_id = m3.toLowerCase(); + var url = m4; + var title = m7; + + if (url == "") { + if (link_id == "") { + // lower-case and turn embedded newlines into spaces + link_id = link_text.toLowerCase().replace(/ ?\n/g," "); + } + url = "#"+link_id; + + if (g_urls[link_id] != undefined) { + url = g_urls[link_id]; + if (g_titles[link_id] != undefined) { + title = g_titles[link_id]; + } + } + else { + if (whole_match.search(/\(\s*\)$/m)>-1) { + // Special case for explicit empty url + url = ""; + } else { + return whole_match; + } + } + } + + url = escapeCharacters(url,"*_"); + var result = ""; + + return result; +} + + +var _DoImages = function(text) { +// +// Turn Markdown image shortcuts into tags. +// + + // + // First, handle reference-style labeled images: ![alt text][id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + )()()()() // pad rest of backreferences + /g,writeImageTag); + */ + text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeImageTag); + + // + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + \s? // One optional whitespace character + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? // src url = $4 + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // title = $7 + \6 // matching quote + [ \t]* + )? // title is optional + \) + ) + /g,writeImageTag); + */ + text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeImageTag); + + return text; +} + +var writeImageTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { + var whole_match = m1; + var alt_text = m2; + var link_id = m3.toLowerCase(); + var url = m4; + var title = m7; + + if (!title) title = ""; + + if (url == "") { + if (link_id == "") { + // lower-case and turn embedded newlines into spaces + link_id = alt_text.toLowerCase().replace(/ ?\n/g," "); + } + url = "#"+link_id; + + if (g_urls[link_id] != undefined) { + url = g_urls[link_id]; + if (g_titles[link_id] != undefined) { + title = g_titles[link_id]; + } + } + else { + return whole_match; + } + } + + alt_text = alt_text.replace(/"/g,"""); + url = escapeCharacters(url,"*_"); + var result = "\""' + _RunSpanGamut(m1) + "");}); + + text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, + function(matchFound,m1){return hashBlock('

' + _RunSpanGamut(m1) + "

");}); + + // atx-style headers: + // # Header 1 + // ## Header 2 + // ## Header 2 with closing hashes ## + // ... + // ###### Header 6 + // + + /* + text = text.replace(/ + ^(\#{1,6}) // $1 = string of #'s + [ \t]* + (.+?) // $2 = Header text + [ \t]* + \#* // optional closing #'s (not counted) + \n+ + /gm, function() {...}); + */ + + text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, + function(wholeMatch,m1,m2) { + var h_level = m1.length; + return hashBlock("' + _RunSpanGamut(m2) + ""); + }); + + function headerId(m) { + return m.replace(/[^\w]/g, '').toLowerCase(); + } + return text; +} + +// This declaration keeps Dojo compressor from outputting garbage: +var _ProcessListItems; + +var _DoLists = function(text) { +// +// Form HTML ordered (numbered) and unordered (bulleted) lists. +// + + // attacklab: add sentinel to hack around khtml/safari bug: + // http://bugs.webkit.org/show_bug.cgi?id=11231 + text += "~0"; + + // Re-usable pattern to match any entirel ul or ol list: + + /* + var whole_list = / + ( // $1 = whole list + ( // $2 + [ ]{0,3} // attacklab: g_tab_width - 1 + ([*+-]|\d+[.]) // $3 = first list item marker + [ \t]+ + ) + [^\r]+? + ( // $4 + ~0 // sentinel for workaround; should be $ + | + \n{2,} + (?=\S) + (?! // Negative lookahead for another list item marker + [ \t]* + (?:[*+-]|\d+[.])[ \t]+ + ) + ) + )/g + */ + var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; + + if (g_list_level) { + text = text.replace(whole_list,function(wholeMatch,m1,m2) { + var list = m1; + var list_type = (m2.search(/[*+-]/g)>-1) ? "ul" : "ol"; + + // Turn double returns into triple returns, so that we can make a + // paragraph for the last item in a list, if necessary: + list = list.replace(/\n{2,}/g,"\n\n\n");; + var result = _ProcessListItems(list); + + // Trim any trailing whitespace, to put the closing `` + // up on the preceding line, to get it past the current stupid + // HTML block parser. This is a hack to work around the terrible + // hack that is the HTML block parser. + result = result.replace(/\s+$/,""); + result = "<"+list_type+">" + result + "\n"; + return result; + }); + } else { + whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; + text = text.replace(whole_list,function(wholeMatch,m1,m2,m3) { + var runup = m1; + var list = m2; + + var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol"; + // Turn double returns into triple returns, so that we can make a + // paragraph for the last item in a list, if necessary: + var list = list.replace(/\n{2,}/g,"\n\n\n");; + var result = _ProcessListItems(list); + result = runup + "<"+list_type+">\n" + result + "\n"; + return result; + }); + } + + // attacklab: strip sentinel + text = text.replace(/~0/,""); + + return text; +} + +_ProcessListItems = function(list_str) { +// +// Process the contents of a single ordered or unordered list, splitting it +// into individual list items. +// + // The $g_list_level global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. + // + // We do this because when we're not inside a list, we want to treat + // something like this: + // + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. + // + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. + // + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". + + g_list_level++; + + // trim trailing blank lines: + list_str = list_str.replace(/\n{2,}$/,"\n"); + + // attacklab: add sentinel to emulate \z + list_str += "~0"; + + /* + list_str = list_str.replace(/ + (\n)? // leading line = $1 + (^[ \t]*) // leading whitespace = $2 + ([*+-]|\d+[.]) [ \t]+ // list marker = $3 + ([^\r]+? // list item text = $4 + (\n{1,2})) + (?= \n* (~0 | \2 ([*+-]|\d+[.]) [ \t]+)) + /gm, function(){...}); + */ + list_str = list_str.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm, + function(wholeMatch,m1,m2,m3,m4){ + var item = m4; + var leading_line = m1; + var leading_space = m2; + + if (leading_line || (item.search(/\n{2,}/)>-1)) { + item = _RunBlockGamut(_Outdent(item)); + } + else { + // Recursion for sub-lists: + item = _DoLists(_Outdent(item)); + item = item.replace(/\n$/,""); // chomp(item) + item = _RunSpanGamut(item); + } + + return "
  • " + item + "
  • \n"; + } + ); + + // attacklab: strip sentinel + list_str = list_str.replace(/~0/g,""); + + g_list_level--; + return list_str; +} + + +var _DoCodeBlocks = function(text) { +// +// Process Markdown `
    ` blocks.
    +//  
    +
    +    /*
    +        text = text.replace(text,
    +            /(?:\n\n|^)
    +            (                               // $1 = the code block -- one or more lines, starting with a space/tab
    +                (?:
    +                    (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
    +                    .*\n+
    +                )+
    +            )
    +            (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
    +        /g,function(){...});
    +    */
    +
    +    // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
    +    text += "~0";
    +    
    +    text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
    +        function(wholeMatch,m1,m2) {
    +            var codeblock = m1;
    +            var nextChar = m2;
    +        
    +            codeblock = _EncodeCode( _Outdent(codeblock));
    +            codeblock = _Detab(codeblock);
    +            codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines
    +            codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace
    +
    +            codeblock = "
    " + codeblock + "\n
    "; + + return hashBlock(codeblock) + nextChar; + } + ); + + // attacklab: strip sentinel + text = text.replace(/~0/,""); + + return text; +} + +var hashBlock = function(text) { + text = text.replace(/(^\n+|\n+$)/g,""); + return "\n\n~K" + (g_html_blocks.push(text)-1) + "K\n\n"; +} + + +var _DoCodeSpans = function(text) { +// +// * Backtick quotes are used for spans. +// +// * You can use multiple backticks as the delimiters if you want to +// include literal backticks in the code span. So, this input: +// +// Just type ``foo `bar` baz`` at the prompt. +// +// Will translate to: +// +//

    Just type foo `bar` baz at the prompt.

    +// +// There's no arbitrary limit to the number of backticks you +// can use as delimters. If you need three consecutive backticks +// in your code, use four for delimiters, etc. +// +// * You can use spaces to get literal backticks at the edges: +// +// ... type `` `bar` `` ... +// +// Turns to: +// +// ... type `bar` ... +// + + /* + text = text.replace(/ + (^|[^\\]) // Character before opening ` can't be a backslash + (`+) // $2 = Opening run of ` + ( // $3 = The code block + [^\r]*? + [^`] // attacklab: work around lack of lookbehind + ) + \2 // Matching closer + (?!`) + /gm, function(){...}); + */ + + text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, + function(wholeMatch,m1,m2,m3,m4) { + var c = m3; + c = c.replace(/^([ \t]*)/g,""); // leading whitespace + c = c.replace(/[ \t]*$/g,""); // trailing whitespace + c = _EncodeCode(c); + return m1+""+c+""; + }); + + return text; +} + + +// THIS HAS BEEN EDITED FOR THE WIKI +var _EncodeCode = function(text) { +// +// Encode/escape certain characters inside Markdown code runs. +// The point is that in code, these characters are literals, +// and lose their special Markdown meanings. +// + // Encode all ampersands; HTML entities are not + // entities within a Markdown code span. + //text = text.replace(/&/g,"&"); + + // Do the angle bracket song and dance: + //text = text.replace(//g,">"); + + // Now, escape characters that are magic in Markdown: + text = escapeCharacters(text,"\*_{}[]\\",false); + +// jj the line above breaks this: +//--- + +//* Item + +// 1. Subitem + +// special char: * +//--- + + return text; +} + + +var _DoItalicsAndBold = function(text) { + + // must go first: + text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, + "$2"); + + text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, + "$2"); + + return text; +} + + +var _DoBlockQuotes = function(text) { + + /* + text = text.replace(/ + ( // Wrap whole match in $1 + ( + ^[ \t]*>[ \t]? // '>' at the start of a line + .+\n // rest of the first line + (.+\n)* // subsequent consecutive lines + \n* // blanks + )+ + ) + /gm, function(){...}); + */ + + text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, + function(wholeMatch,m1) { + var bq = m1; + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + bq = bq.replace(/^[ \t]*>[ \t]?/gm,"~0"); // trim one level of quoting + + // attacklab: clean up hack + bq = bq.replace(/~0/g,""); + + bq = bq.replace(/^[ \t]+$/gm,""); // trim whitespace-only lines + bq = _RunBlockGamut(bq); // recurse + + bq = bq.replace(/(^|\n)/g,"$1 "); + // These leading spaces screw with
     content, so we need to fix that:
    +            bq = bq.replace(
    +                    /(\s*
    [^\r]+?<\/pre>)/gm,
    +                function(wholeMatch,m1) {
    +                    var pre = m1;
    +                    // attacklab: hack around Konqueror 3.5.4 bug:
    +                    pre = pre.replace(/^  /mg,"~0");
    +                    pre = pre.replace(/~0/g,"");
    +                    return pre;
    +                });
    +            
    +            return hashBlock("
    \n" + bq + "\n
    "); + }); + return text; +} + + +var _FormParagraphs = function(text) { +// +// Params: +// $text - string to process with html

    tags +// + + // Strip leading and trailing lines: + text = text.replace(/^\n+/g,""); + text = text.replace(/\n+$/g,""); + + var grafs = text.split(/\n{2,}/g); + var grafsOut = new Array(); + + // + // Wrap

    tags. + // + var end = grafs.length; + for (var i=0; i= 0) { + grafsOut.push(str); + } + else if (str.search(/\S/) >= 0) { + str = _RunSpanGamut(str); + str = str.replace(/^([ \t]*)/g,"

    "); + str += "

    " + grafsOut.push(str); + } + + } + + // + // Unhashify HTML blocks + // + end = grafsOut.length; + for (var i=0; i= 0) { + var blockText = g_html_blocks[RegExp.$1]; + blockText = blockText.replace(/\$/g,"$$$$"); // Escape any dollar signs + grafsOut[i] = grafsOut[i].replace(/~K\d+K/,blockText); + } + } + + return grafsOut.join("\n\n"); +} + + +var _EncodeAmpsAndAngles = function(text) { +// Smart processing for ampersands and angle brackets that need to be encoded. + + // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + // http://bumppo.net/projects/amputator/ + //text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&"); + + // Encode naked <'s + //text = text.replace(/<(?![a-z\/?\$!])/gi,"<"); + + return text; +} + + +var _EncodeBackslashEscapes = function(text) { +// +// Parameter: String. +// Returns: The string, with after processing the following backslash +// escape sequences. +// + + // attacklab: The polite way to do this is with the new + // escapeCharacters() function: + // + // text = escapeCharacters(text,"\\",true); + // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); + // + // ...but we're sidestepping its use of the (slow) RegExp constructor + // as an optimization for Firefox. This function gets called a LOT. + + text = text.replace(/\\(\\)/g,escapeCharacters_callback); + text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g,escapeCharacters_callback); + return text; +} + + +var _DoAutoLinks = function(text) { + + text = text.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,"
    $1"); + + // Email addresses: + + /* + text = text.replace(/ + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + > + /gi, _DoAutoLinks_callback()); + */ + text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, + function(wholeMatch,m1) { + return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); + } + ); + + return text; +} + + +var _EncodeEmailAddress = function(addr) { +// +// Input: an email address, e.g. "foo@example.com" +// +// Output: the email address as a mailto link, with each character +// of the address encoded as either a decimal or hex entity, in +// the hopes of foiling most address harvesting spam bots. E.g.: +// +// foo +// @example.com +// +// Based on a filter by Matthew Wickline, posted to the BBEdit-Talk +// mailing list: +// + + // attacklab: why can't javascript speak hex? + function char2hex(ch) { + var hexDigits = '0123456789ABCDEF'; + var dec = ch.charCodeAt(0); + return(hexDigits.charAt(dec>>4) + hexDigits.charAt(dec&15)); + } + + var encode = [ + function(ch){return "&#"+ch.charCodeAt(0)+";";}, + function(ch){return "&#x"+char2hex(ch)+";";}, + function(ch){return ch;} + ]; + + addr = "mailto:" + addr; + + addr = addr.replace(/./g, function(ch) { + if (ch == "@") { + // this *must* be encoded. I insist. + ch = encode[Math.floor(Math.random()*2)](ch); + } else if (ch !=":") { + // leave ':' alone (to spot mailto: later) + var r = Math.random(); + // roughly 10% raw, 45% hex, 45% dec + ch = ( + r > .9 ? encode[2](ch) : + r > .45 ? encode[1](ch) : + encode[0](ch) + ); + } + return ch; + }); + + addr = "" + addr + ""; + addr = addr.replace(/">.+:/g,"\">"); // strip the mailto: from the visible part + + return addr; +} + + +var _UnescapeSpecialChars = function(text) { +// +// Swap back in all the special characters we've hidden. +// + text = text.replace(/~E(\d+)E/g, + function(wholeMatch,m1) { + var charCodeToReplace = parseInt(m1); + return String.fromCharCode(charCodeToReplace); + } + ); + return text; +} + + +var _Outdent = function(text) { +// +// Remove one level of line-leading tabs or spaces +// + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + text = text.replace(/^(\t|[ ]{1,4})/gm,"~0"); // attacklab: g_tab_width + + // attacklab: clean up hack + text = text.replace(/~0/g,"") + + return text; +} + +var _Detab = function(text) { +// attacklab: Detab's completely rewritten for speed. +// In perl we could fix it by anchoring the regexp with \G. +// In javascript we're less fortunate. + + // expand first n-1 tabs + text = text.replace(/\t(?=\t)/g," "); // attacklab: g_tab_width + + // replace the nth with two sentinels + text = text.replace(/\t/g,"~A~B"); + + // use the sentinel to anchor our regex so it doesn't explode + text = text.replace(/~B(.+?)~A/g, + function(wholeMatch,m1,m2) { + var leadingText = m1; + var numSpaces = 4 - leadingText.length % 4; // attacklab: g_tab_width + + // there *must* be a better way to do this: + for (var i=0; i 1 ? + '#' + encodeURIComponent(target.slice(1).join('#')): + ''; + str = before + + '[' + exports.escapeMarkdown(sanitize.h(match[1])) + ']' + + '(' + baseURL + '/' + pageid + anchor + ')' + + after; + } + // links without custom text + var match, re = new RegExp('\\[\\[([^\\]]+)\\]\\]'); + while (match = re.exec(str)) { + var before = str.substr(0, match.index) + var after = str.substr(match.index + match[0].length) + var text = match[1].replace(/_/g, ' '); + var target = match[1].split('#'); + var pageid = encodeURIComponent(target[0]); + var anchor = target.length > 1 ? + '#' + encodeURIComponent(target.slice(1).join('#')): + ''; + str = before + + '[' + exports.escapeMarkdown(sanitize.h(text)) + ']' + + '(' + baseURL + '/' + pageid + anchor + ')' + + after; + } + + // escape html + str = sanitize.h(str); + + // fix blockquotes + str = _.map(str.split('\n'), function (l) { + var m = /(^(?:>\s)+)/.exec(l); + if (m) { + return l.substr(0, m.index) + + m[1].replace(/>/g, '>') + + l.substr(m.index + m[1].length); + } + return l; + }).join('\n'); + + // process fenced code blocks + var re1 = /```([A-Za-z]+)\s*([\s\S]+?)```/; // with syntax highlighting + var re2 = /```\s*([\s\S]+?)```/; // without syntax highlighting + var block; + while (block = re1.exec(str) || re2.exec(str)) { + var pre; + if (block.length === 3) { + // we have a code format + pre = '
    ' +
    +                block[2] + '
    '; + } + else { + // no syntax highlighting, use basic pre tag + pre = '
    ' + block[1] + '
    '; + } + str = str.substr(0, block.index) + + pre + + str.substr(block.index + block[0].length); + } + + var c = new Showdown.converter(); + return c.makeHtml(str); +}; diff --git a/lib/ui.js b/lib/ui.js new file mode 100644 index 0000000..58aeeef --- /dev/null +++ b/lib/ui.js @@ -0,0 +1,44 @@ +var duality = require('duality/core'), + sanitize = require('sanitize'), + showdown = require('./showdown-wiki'), + path = require('path'); + + +exports.fetchREADME = function (doc, att) { + var pre = $('
    ');
    +    if (att) {
    +        var dbURL = duality.getDBURL();
    +        $.get(dbURL + '/' + encodeURIComponent(doc._id) + '/' + att,
    +            function (data) {
    +                var ext = path.extname(att);
    +                if (ext === '.md' || ext === '.markdown') {
    +                    var html = showdown.convert(data);
    +                    $('#readme').append('
    ' + html + '
    '); + exports.syntaxHighlight(); + } + else { + $('#readme').append(pre.text(data)); + } + } + ); + } + else { + $('#readme').append( + pre.text('This package has no README') + ); + } +}; + + +/** + * Add syntax highlighting to the page using highlight.js (hljs) + */ + +exports.syntaxHighlight = function () { + $('pre > code').each(function () { + if (this.className) { + // has a code class + $(this).html(hljs.highlight(this.className, $(this).text()).value); + } + }); +}; diff --git a/packages/sanitize/kanso.json b/packages/sanitize/kanso.json new file mode 100644 index 0000000..d974d29 --- /dev/null +++ b/packages/sanitize/kanso.json @@ -0,0 +1,11 @@ +{ + "name": "sanitize", + "version": "0.0.8", + "categories": ["utils"], + "description": "Functions for santizing user input or other untrusted data", + "modules": "sanitize.js", + "dependencies": { + "modules": null, + "underscore": null + } +} diff --git a/packages/sanitize/sanitize.js b/packages/sanitize/sanitize.js new file mode 100644 index 0000000..e62331e --- /dev/null +++ b/packages/sanitize/sanitize.js @@ -0,0 +1,199 @@ +/** + * Input sanitization, escaping, and construction functions + * covering security-sensitive areas. + * + * @module + */ + +/** + * Module dependencies + */ + +var _ = require('underscore')._; + +/** + * + * @name escapeUrlParams(s) + * @param {Object} obj An object containing url parameters, with + * parameter names stored as property names (or keys). + * @returns {String} + * @api public + */ + +exports.escapeUrlParams = exports.url = function (obj) +{ + var rv = [ ]; + + for (var key in obj) { + rv.push( + encodeURIComponent(key) + + '=' + encodeURIComponent(obj[key]) + ); + } + + return (rv.length > 0 ? ('?' + rv.join('&')) : ''); +}; + + +/** + * Encodes required characters as HTML entities so a string + * can be included in a page. This function must be used to + * avoid Cross-site Scripting attacks. + * + * @name escapeHtml(s) + * @param {String} s + * @returns {String} + * @api public + */ + +exports.escapeHtml = exports.h = function (s) +{ + s = ('' + s); /* Coerce to string */ + s = s.replace(/&/g, '&'); + s = s.replace(//g, '>'); + s = s.replace(/"/g, '"'); + s = s.replace(/'/g, '''); + return s; +}; + + +/** + * Encodes selected characters in a string, so that the string + * can be safely used within a Javascript string constant. This + * function must be used to avoid cross-site scripting attacks + * (or, in some cases, arbitrary server-side code execution). + * + * @name escapeJavascriptString(s) + * @param {String} s + * @returns {String} + * @api public + */ + +exports.escapeJavascriptString = exports.js = function (s) +{ + s = ('' + s); /* Coerce to string */ + s = s.replace(/'/g, "\\'"); + s = s.replace(/"/g, '\\"'); + return s; +}; + + +/** + * Encodes selected characters in a string, so that the string + * can be safely used within an XML character data section. + * This function must be used to avoid cross-site scripting attacks. + * + * @name escapeXmlCharacterData(s) + * @param {String} s + * @returns {String} + * @api public + */ + +exports.escapeXmlCharacterData = exports.cdata = function (s) +{ + s = ('' + s); /* Coerce to string */ + s = s.replace(/\]\]>/g, ''); + return s; +}; + + +/** + * Encodes selected characters in a string, so that the string + * can be safely used on the right side of a CSS attribute selector's + * equal sign. This function should be used when using a user-modifiable + * string to match an element by attribute -- either in jQuery, or as part + * of a dynamically-generated CSS selector. + * + * @name escapeAttributeSelectorValue(s) + * @param {String} s + * @returns {String} + * @api public + */ + +exports.escapeAttributeSelectorValue = exports.css = function (s) +{ + s = ('' + s); /* Coerce to string */ + s = s.replace(/['"\\=\[\]]/, '\\$1'); + return s; +}; + + +/** + * Takes any number of arguments, and combines them together + * to safely form a string that's suitable for use as an + * identifier or name. + * + * @name generateAbstractIdentifier(rv, args, esc_fn, final_fn) + * @param {Array} rv An accumulator array, used to store results. + * @param {Array} args An array of arguments to the id-generating function. + * @param {Array} esc_fn An input-sanitizing function, to be applied to + * each component of the identifier before it's emitted. + * @param {Array} join_fn A function that accepts an array of sanitized + * identifier components, and produces a string. + * @returns {String} + */ + +exports.generateAbstractIdentifier = function (rv, args, esc_fn, join_fn) { + if (args.length <= 0) { + return null; + } + for (var i = 0, len = args.length; i < len; ++i) { + var arg = args[i]; + if (arg !== undefined && arg !== null) { + if (_.isArray(arg)) { + /* Avoid recursion; limit to one level deep */ + for (var j = 0, lenj = arg.length; j < lenj; ++j) { + if (arg[j] !== undefined && arg[j] !== null) { + rv.push(esc_fn(arg[j])); + } + } + } else { + rv.push(esc_fn(arg)); + } + } + } + return join_fn(rv); +}; + + +/** + * Takes any number of arguments, and combines them together + * to safely form a string that's suitable for use as a DOM + * element identifier. + * + * @name generateDomIdentifier() + * @returns {String} + * @api public + */ + +exports.generateDomIdentifier = exports.id = function (/* ... */) { + return exports.generateAbstractIdentifier( + [ 'id' ], arguments, function (x) { + return ('' + x).replace(/[^A-Za-z0-9_]+/, '_'); + }, function (x) { + return x.join('_'); + } + ); +}; + + +/** + * Takes any number of arguments, and combines them together + * to safely form a string that's suitable for use in a DOM + * element's name attribute. + * + * @name generateDomName() + * @returns {String} + * @api public + */ + +exports.generateDomName = exports.name = function (/* ... */) { + return exports.generateAbstractIdentifier( + [ ], arguments, function (x) { + return ('' + x).replace(/[\'\"]+/, '_'); + }, function (x) { + return x.join('.'); + } + ); +}; diff --git a/static/css/garden.less b/static/css/garden.less index 46d2700..e5894f2 100644 --- a/static/css/garden.less +++ b/static/css/garden.less @@ -16,3 +16,29 @@ font-weight: bold; color: #31d055; } + +#details_sidebar { + margin-top: 1em; + width: 200px; + margin-right: 20px; +} + +.no_screenshot { + width: 200px; + height: 125px; + background: #ccc; + position: relative; +} +.no_screenshot div { + background: #999; + position: absolute; + bottom: 0; + left: 0; + color: white; + text-align: center; + width: 200px; +} + +.install_actions { + margin: 2em 0 1em 0; +} diff --git a/templates/app_details.html b/templates/app_details.html index 21190fa..2e7029f 100644 --- a/templates/app_details.html +++ b/templates/app_details.html @@ -1,11 +1,20 @@
    +
    + {{#if screenshot}} + + {{else}} +
    No screenshot
    + {{/if}} +

    {{doc.kanso.name}}

    {{doc.kanso.description}}

    -
    -
    @@ -15,13 +24,17 @@

    {{doc.kanso.name}}

    +
    +

    Version: {{doc.kanso.version}}

    +

    Built: {{build_time}}

    +
    +

    Manual install

    curl {{install_script_url}} | bash

    You may want to read the script first.

    -
    - -
    diff --git a/templates/base.html b/templates/base.html index 5be7450..f76f9b8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,8 +11,9 @@ {{{include "duality-contrib-session/session.html"}}}