diff --git a/.gitignore b/.gitignore index 89ab5bb..cb1d476 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.tmproj .DS_Store _blog +assets/*.min.js discussd/discuss.dirty diff --git a/Makefile b/Makefile index 2dea411..00fd2c2 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,45 @@ -build: +JAVASCRIPTS=assets/blog.js assets/gitter.js assets/jquery-serializeObject.js assets/proj.js \ + assets/request.js assets/showdown.js assets/storage-polyfill.js assets/store.js \ + assets/strftime.js assets/tmpl.js + +MIN_JAVASCRIPTS=assets/blog.min.js assets/gitter.min.js assets/jquery-serializeObject.min.js assets/proj.min.js \ + assets/request.min.js assets/showdown.min.js assets/storage-polyfill.min.js assets/store.min.js \ + assets/strftime.min.js assets/tmpl.min.js + +POSTS=$(shell echo _blog/*.html) + +all: proj blog combine + +proj: projects.json templates/proj/index.html templates/proj/proj/index.html ./build.js + +blog: _blog/posts.json templates/blog/index.html templates/blog/post.html $(POSTS) + @echo + ./blog.rb _blog blog + +minify: $(JAVASCRIPTS) + @echo + ./minify.sh + +combine: minify $(MIN_JAVASCRIPTS) + @echo + ./combine.sh + +publish_blog: blog combine + publish blog + +publish_proj: proj combine + publish proj + +publish: publish_blog publish_proj index.html + publish index.html + publish assets + publish blog + publish proj + +clean: + rm -rf proj/* + rm -rf blog/* + rm assets/*.min.js + +.PHONY: blog diff --git a/assets/blog.css b/assets/blog.css index f85005b..c4873b8 100644 --- a/assets/blog.css +++ b/assets/blog.css @@ -48,9 +48,9 @@ a.img { border: none } } #index { display: none - ; width: 60% + ; width: 80% ; min-width: 200px - ; max-width: 600px + ; max-width: 800px ; border: solid 1px #999 ; -moz-border-radius: 10px ; -webkit-border-radius: 10px @@ -68,22 +68,22 @@ a.img { border: none } .date { float: right } -#posts { border-left: solid 0.2em #ddd +#posts { border-left: solid 0.15em #999 ; width: 80% ; min-width: 400px ; max-width: 800px - ; margin: 4em auto + ; margin: 4em auto 2em ; padding: 0 3em ; font-size: 1.2em } article { color: #222 - ; padding-bottom: 5em + ; padding-bottom: 1em ; line-height: 1.2em } article:last-child { padding-bottom: 0 - ; margin-bottom: 2em + ; margin-bottom: 1em } article h1 { text-align: left @@ -116,7 +116,9 @@ article h3 { font-size: 1.6em ; font-weight: normal } -time { color: #444 } +time { color: #444 + ; font-size: 1.2em + } .gist { font-size: 0.8em } @@ -124,21 +126,93 @@ time { color: #444 } ; min-width: 400px ; max-width: 800px ; margin: 1em auto - ; padding: 0 3em + ; padding: 0 3em 1em ; font-size: 1.2em } #prev { float: left } #next { float: right } -#comments { display: none } +.sep { text-align: center + ; font-size: 2em + ; color: #666 + } + +/* show discussion */ +#sd-container { margin: 3em 0 } + +input[type=submit], +#sd { border: solid 1px #999 + ; border-right-color: #333 + ; border-bottom-color: #333 + ; padding: 0.4em 1em + ; color: #444 + ; background-color: #ececec + ; -moz-border-radius: 5px + ; -webkit-border-radius: 5px + ; border-radius: 5px + ; text-decoration: none + ; margin: 0 2px 2px 0 + } + +input[type=submit]:active, +#sd:active { margin: 2px 0 0 2px + ; color: #000 + ; background-color: #ffc + } + +#comment-stuff { display: none + ; color: #efefef + ; margin: 0 + ; padding: 2em 0 + } + +#comments { width: 70% + ; max-width: 400px + ; margin: 0 auto + } + +.comment { color: #555 + ; border-top: solid 2px #ccc + ; padding-bottom: 2em + ; margin-bottom: 2em + } + +.comment big { font-size: 2em + ; font-family: Georgia, serif + } + +#comment-form { width: 400px + ; margin: 2em auto 0 + } + +input[type=text], +textarea { font-size: 1.4em + ; color: #333 + ; width: 100% + ; padding: 0.2em + ; border: solid 1px #999 + ; -moz-border-radius: 5px + ; -webkit-border-radius: 5px + ; border-radius: 5px + ; font-family: verdana, sans-serif + } + +input:focus[type=text], +textarea:focus { border: solid 1px #333 } + +textarea { height: 100px } + +input[type=submit] { font-size: 1.1em + ; cursor: pointer + } footer { text-align: center ; font-size: 1.2em ; margin: 0 - ; padding: 1em - ; background-color: #a6bcdf - ; border-top: solid 1px #666 + ; padding: 1em 0 + ; background-color: #cdf + ; border-top: solid 1px #bbb ; clear: both } diff --git a/assets/blog.js b/assets/blog.js new file mode 100644 index 0000000..edadf14 --- /dev/null +++ b/assets/blog.js @@ -0,0 +1,78 @@ +;(function() { + var server = 'http://bohodev.net:8000/' + , getCommentsURL = function(post) { return server + 'comments/' + post } + , postCommentURL = function() { return server + 'comment' } + , countCommentsURL = function(post) { return server + 'count/' + post } + + function getComments() { + console.log('*** getComments()') + SJS.request({uri: getCommentsURL(SJS.filename)}, function(err, request, body) { + if (err) { + $('#comments').text('derp') + return + } + var data + , comments + , h = '' + try { + data = JSON.parse(body) + } catch (e) { + console && console.log && console.log('not json -> ' + body) + } + comments = data.comments + if (comments.length) { + h = data.comments.map(function(c) { + return tmpl('comment_tmpl', c) + }).join('') + } + $('#comments').html(h) + }) + } + jQuery(function($) { + $('#need-js').remove() + + SJS.request({uri: countCommentsURL(SJS.filename)}, function(err, request, body) { + if (err) return + var data + , n + try { + data = JSON.parse(body) + } catch (e) { + console && console.log && console.log('not json -> ' + body) + } + n = data.count + $('#sd').text(n > 0 ? 'show the discussion (' + n + ')' : 'start the discussion') + }) + + $('#sd').click(function() { + $('#sd-container').remove() + $('#comment-stuff').slideDown(1.5, function() { this.scrollIntoView(true) }) + getComments() + return false + }) + + var showdown = new Showdown.converter() + + $('#comment-form').submit(function() { + var comment = $(this).serializeObject() + var options = { method: 'POST' + , uri: postCommentURL() + , body: JSON.stringify(comment) + } + SJS.request(options, function(err, request, body) { + if (err) { + console.dir(err) + alert('derp') + return + } + + // FIXME check for error, how do we get the returned status code? + + comment.timestamp = +new Date() + comment.html = showdown.makeHtml(comment.body) + $('#comments').append(tmpl('comment_tmpl', comment)) + }) + return false + }) + }) +}()); diff --git a/assets/gitter.js b/assets/gitter.js index 6c86bb7..1866161 100644 --- a/assets/gitter.js +++ b/assets/gitter.js @@ -23,7 +23,7 @@ while ( div.innerHTML = '', all[0] - ); + ){/* braces here to satifsy closure-compiler */}; return v > 4 ? v : undef }()) @@ -594,7 +594,7 @@ , url = options.uri + '?callback=GITR.' + jsonpCallbackName GITR[jsonpCallbackName] = function(obj) { cb(null, null, obj) - setTimeout(function() { delete GITR[jsonpCallbackName] }, 0) + delete GITR[jsonpCallbackName] } load(url) } diff --git a/assets/jquery-serializeObject.js b/assets/jquery-serializeObject.js new file mode 100644 index 0000000..6956ba4 --- /dev/null +++ b/assets/jquery-serializeObject.js @@ -0,0 +1,31 @@ +/*! + * jQuery serializeObject - v0.2 - 1/20/2010 + * http://benalman.com/projects/jquery-misc-plugins/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ + +// Whereas .serializeArray() serializes a form into an array, .serializeObject() +// serializes a form into an (arguably more useful) object. + +;(function($,undefined){ + '$:nomunge'; // Used by YUI compressor. + + $.fn.serializeObject = function(){ + var obj = {}; + + $.each( this.serializeArray(), function(i,o){ + var n = o.name, + v = o.value; + + obj[n] = obj[n] === undefined ? v + : $.isArray( obj[n] ) ? obj[n].concat( v ) + : [ obj[n], v ]; + }); + + return obj; + }; + +})(jQuery); diff --git a/assets/request.js b/assets/request.js index 48a880e..eeae9d9 100644 --- a/assets/request.js +++ b/assets/request.js @@ -1,54 +1,31 @@ -(function() { +;(function() { if (typeof SJS === 'undefined') SJS = {} - var load, _jsonpCounter = 1 - SJS.request = function(options, cb) { // jsonp request, quacks like mikeal's request module - var jsonpCallbackName = '_jsonpCallback' + _jsonpCounter++ - , url = options.uri + '?callback=SJS.' + jsonpCallbackName - SJS[jsonpCallbackName] = function(obj) { - cb(null, obj) - setTimeout(function() { delete SJS[jsonpCallbackName] }, 0) + + // cors xhr request, quacks like mikeal's request module + SJS.request = function(options, cb) { + var url = options.uri + , method = options.method || 'GET' + , headers = options.headers || {} + , body = typeof options.body === 'undefined' ? null : String(options.body) + , xhr = new XMLHttpRequest() + + // withCredentials => cors + if ('withCredentials' in xhr) { + xhr.open(method, url, true) + } else if (typeof XDomainRequest === 'functon') { + xhr = new XDomainRequest() + xhr.open(method, url) + } else { + cb(new Error('cross domain requests not supported')) + return } - load(url) - } - - // bootstrap loader from LABjs - load = function(url) { - var oDOC = document - , handler - , head = oDOC.head || oDOC.getElementsByTagName("head") - - // loading code borrowed directly from LABjs itself - // (now removes script elem when done and nullifies its reference --sjs) - setTimeout(function () { - if ("item" in head) { // check if ref is still a live node list - if (!head[0]) { // append_to node not yet ready - setTimeout(arguments.callee, 25) - return - } - head = head[0]; // reassign from live node list ref to pure node ref -- avoids nasty IE bug where changes to DOM invalidate live node lists - } - var scriptElem = oDOC.createElement("script"), - scriptdone = false - scriptElem.onload = scriptElem.onreadystatechange = function () { - if ((scriptElem.readyState && scriptElem.readyState !== "complete" && scriptElem.readyState !== "loaded") || scriptdone) { - return false - } - scriptElem.onload = scriptElem.onreadystatechange = null - scriptElem.parentNode.removeChild(scriptElem) - scriptElem = null - scriptdone = true - }; - scriptElem.src = url - head.insertBefore(scriptElem, head.firstChild) - }, 0) - - // required: shim for FF <= 3.5 not having document.readyState - if (oDOC.readyState == null && oDOC.addEventListener) { - oDOC.readyState = "loading" - oDOC.addEventListener("DOMContentLoaded", function handler() { - oDOC.removeEventListener("DOMContentLoaded", handler, false) - oDOC.readyState = "complete" - }, false) + for (var k in headers) if (headers.hasOwnProperty(k)) { + xhr.setRequestHeader(k, headers[k]) } + xhr.onload = function() { + var response = xhr.responseText + cb(null, xhr, response) + } + xhr.send(body) } -}()) \ No newline at end of file +}()); diff --git a/assets/showdown.js b/assets/showdown.js new file mode 100644 index 0000000..226ae8b --- /dev/null +++ b/assets/showdown.js @@ -0,0 +1,1296 @@ +// +// 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,"~"); + + 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) + ""); + }); + + 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; +} + + +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 +/// MIT License + +var strftime = (function() { + var Weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday']; + + var WeekdaysShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + + var Months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December']; + + var MonthsShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', + 'Sep', 'Oct', 'Nov', 'Dec']; + + function pad(n, padding) { + padding = padding || '0'; + return n < 10 ? (padding + n) : n; + } + + function hours12(d) { + var hour = d.getHours(); + if (hour == 0) hour = 12; + else if (hour > 12) hour -= 12; + return hour; + } + + // Most of the specifiers supported by C's strftime + function strftime(fmt, d) { + d || (d = new Date()); + return fmt.replace(/%(.)/g, function(_, c) { + switch (c) { + case 'A': return Weekdays[d.getDay()]; + case 'a': return WeekdaysShort[d.getDay()]; + case 'B': return Months[d.getMonth()]; + case 'b': // fall through + case 'h': return MonthsShort[d.getMonth()]; + case 'D': return strftime('%m/%d/%y', d); + case 'd': return pad(d.getDate()); + case 'e': return d.getDate(); + case 'F': return strftime('%Y-%m-%d', d); + case 'H': return pad(d.getHours()); + case 'I': return pad(hours12(d)); + case 'k': return pad(d.getHours(), ' '); + case 'l': return pad(hours12(d), ' '); + case 'M': return pad(d.getMinutes()); + case 'm': return pad(d.getMonth() + 1); + case 'n': return '\n'; + case 'p': return d.getHours() < 12 ? 'AM' : 'PM'; + case 'R': return strftime('%H:%M', d); + case 'r': return strftime('%I:%M:%S %p', d); + case 'S': return pad(d.getSeconds()); + case 's': return d.getTime(); + case 'T': return strftime('%H:%M:%S', d); + case 't': return '\t'; + case 'u': + var day = d.getDay(); + return day == 0 ? 7 : day; // 1 - 7, Monday is first day of the week + case 'v': return strftime('%e-%b-%Y', d); + case 'w': return d.getDay(); // 0 - 6, Sunday is first day of the week + case 'Y': return d.getFullYear(); + case 'y': + var year = d.getYear(); + return year < 100 ? year : year - 100; + case 'Z': + var tz = d.toString().match(/\((\w+)\)/); + return tz && tz[1] || ''; + case 'z': + var off = d.getTimezoneOffset(); + return (off < 0 ? '-' : '+') + pad(off / 60) + pad(off % 60); + default: return c; + } + }); + } + return strftime; +}()); + +if (typeof exports !== 'undefined') exports.strftime = strftime; +else (function(global) { global.strftime = strftime }(this)); diff --git a/assets/tmpl.js b/assets/tmpl.js new file mode 100644 index 0000000..6d70fc2 --- /dev/null +++ b/assets/tmpl.js @@ -0,0 +1,35 @@ +// Simple JavaScript Templating +// John Resig - http://ejohn.org/ - MIT Licensed +;(function(){ + var cache = {}; + + this.tmpl = function tmpl(str, data){ + // Figure out if we're getting a template, or if we need to + // load the template - and be sure to cache the result. + var fn = !/\W/.test(str) ? + cache[str] = cache[str] || + tmpl(document.getElementById(str).innerHTML) : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');"); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + }; +})(); diff --git a/blog.rb b/blog.rb index 2024482..3edc8cf 100755 --- a/blog.rb +++ b/blog.rb @@ -46,6 +46,7 @@ posts.each_with_index do |post, i| :post => post, :previous => i < posts.length - 1 && posts[i + 1], :next => i > 0 && posts[i - 1], + :filename => post[:filename], :comments => post[:comments] }) end @@ -54,7 +55,9 @@ end index_template = File.read(File.join('templates', 'blog', 'index.html')) index_html = Mustache.render(index_template, { :posts => posts, :post => posts.first, - :previous => posts[1] + :previous => posts[1], + :filename => posts.first[:filename], + :comments => posts.first[:comments] }) # write landing page diff --git a/blog/37signals-chalk-dissected.html b/blog/37signals-chalk-dissected.html index 1e19ae4..3117f39 100644 --- a/blog/37signals-chalk-dissected.html +++ b/blog/37signals-chalk-dissected.html @@ -7,36 +7,20 @@ var _gaq = _gaq || []; _gaq.push( ['_setAccount', 'UA-214054-5'] , ['_trackPageview'] - ); + ) -(function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); -})(); -if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', ready, false) -} else if (window.attachEvent) { - window.attachEvent('onload', ready) -} -function ready() { - function html(id, h) { - document.getElementById(id).innerHTML = h - } - - var discussion = document.getElementById('discussion') - , discussionToggle = document.getElementById('discussion-toggle') - , hidden = true - discussionToggle.onclick = function() { - hidden = !hidden - discussion.style.display = hidden ? 'none' : 'block' - discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑' - SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) { - html('comments', body) - }) - } -} +;(function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js' + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s) +})() + + +

    sjs' blog

    @@ -292,25 +276,39 @@ addLineNumbersToAllGists(); +

    - -
    -
    -
    - -

    Name:

    -

    URL:

    -

    Thoughts:

    -

    -
    -
    +

    (discussion requires JavaScript)

    + +
    +
    + +

    +

    +

    +

    +

    +
    + diff --git a/blog/a-preview-of-mach-o-file-generation.html b/blog/a-preview-of-mach-o-file-generation.html index e2f0654..9403f26 100644 --- a/blog/a-preview-of-mach-o-file-generation.html +++ b/blog/a-preview-of-mach-o-file-generation.html @@ -7,36 +7,20 @@ var _gaq = _gaq || []; _gaq.push( ['_setAccount', 'UA-214054-5'] , ['_trackPageview'] - ); + ) -(function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); -})(); -if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', ready, false) -} else if (window.attachEvent) { - window.attachEvent('onload', ready) -} -function ready() { - function html(id, h) { - document.getElementById(id).innerHTML = h - } - - var discussion = document.getElementById('discussion') - , discussionToggle = document.getElementById('discussion-toggle') - , hidden = true - discussionToggle.onclick = function() { - hidden = !hidden - discussion.style.display = hidden ? 'none' : 'block' - discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑' - SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) { - html('comments', body) - }) - } -} +;(function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js' + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s) +})() + + +

    sjs' blog

    @@ -86,26 +70,40 @@ straightforward, an example is in asm/binary.rb, in the #output method.

    +

    - -
    -
    -
    - -

    Name:

    -

    URL:

    -

    Thoughts:

    -

    -
    -
    +

    (discussion requires JavaScript)

    + +
    +
    + +

    +

    +

    +

    +

    +
    + diff --git a/blog/basics-of-the-mach-o-file-format.html b/blog/basics-of-the-mach-o-file-format.html index 5afc886..de48c98 100644 --- a/blog/basics-of-the-mach-o-file-format.html +++ b/blog/basics-of-the-mach-o-file-format.html @@ -7,36 +7,20 @@ var _gaq = _gaq || []; _gaq.push( ['_setAccount', 'UA-214054-5'] , ['_trackPageview'] - ); + ) -(function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); -})(); -if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', ready, false) -} else if (window.attachEvent) { - window.attachEvent('onload', ready) -} -function ready() { - function html(id, h) { - document.getElementById(id).innerHTML = h - } - - var discussion = document.getElementById('discussion') - , discussionToggle = document.getElementById('discussion-toggle') - , hidden = true - discussionToggle.onclick = function() { - hidden = !hidden - discussion.style.display = hidden ? 'none' : 'block' - discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑' - SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) { - html('comments', body) - }) - } -} +;(function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js' + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s) +})() + + +

    sjs' blog

    @@ -309,26 +293,40 @@ would almost have a useful Mach object file.)

    +

    - -
    -
    -
    - -

    Name:

    -

    URL:

    -

    Thoughts:

    -

    -
    -
    +

    (discussion requires JavaScript)

    + +
    +
    + +

    +

    +

    +

    +

    +
    + diff --git a/blog/index.html b/blog/index.html index ae3c5ba..8eb7d32 100644 --- a/blog/index.html +++ b/blog/index.html @@ -3,34 +3,34 @@ blog :: samhuri.net + + +

    sjs' blog

    @@ -293,12 +293,41 @@ addLineNumbersToAllGists(); - -
    + + +

    +

    (discussion requires JavaScript)

    + +
    +
    + +
    +
    + +

    +

    +

    +

    +

    +
    +
    + diff --git a/blog/working-with-c-style-structs-in-ruby.html b/blog/working-with-c-style-structs-in-ruby.html index 86502a0..f12e1cc 100644 --- a/blog/working-with-c-style-structs-in-ruby.html +++ b/blog/working-with-c-style-structs-in-ruby.html @@ -7,36 +7,20 @@ var _gaq = _gaq || []; _gaq.push( ['_setAccount', 'UA-214054-5'] , ['_trackPageview'] - ); + ) -(function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); -})(); -if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', ready, false) -} else if (window.attachEvent) { - window.attachEvent('onload', ready) -} -function ready() { - function html(id, h) { - document.getElementById(id).innerHTML = h - } - - var discussion = document.getElementById('discussion') - , discussionToggle = document.getElementById('discussion-toggle') - , hidden = true - discussionToggle.onclick = function() { - hidden = !hidden - discussion.style.display = hidden ? 'none' : 'block' - discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑' - SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) { - html('comments', body) - }) - } -} +;(function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js' + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s) +})() + + +

    sjs' blog

    @@ -158,25 +142,39 @@ of the Mach-O file format

    +

    - -
    -
    -
    - -

    Name:

    -

    URL:

    -

    Thoughts:

    -

    -
    -
    +

    (discussion requires JavaScript)

    + +
    +
    + +

    +

    +

    +

    +

    +
    + diff --git a/combine.sh b/combine.sh new file mode 100755 index 0000000..2fd97f0 --- /dev/null +++ b/combine.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env zsh + +echo "request,showdown,strftime,tmpl,jquery-serializeObject,blog -> assets/blog-all.min.js" +cat assets/{request,showdown,strftime,tmpl,jquery-serializeObject,blog}.min.js >|assets/blog-all.min.js + +echo "gitter.storage-polyfill,store,proj -> assets/proj-all.min.js" +cat assets/{gitter,storage-polyfill,store,proj}.min.js >|assets/proj-all.min.js diff --git a/discussd/discussd.js b/discussd/discussd.js index f3857f3..06ee271 100755 --- a/discussd/discussd.js +++ b/discussd/discussd.js @@ -2,10 +2,14 @@ var fs = require('fs') , http = require('http') + , path = require('path') + , parseURL = require('url').parse , keys = require('keys') + , markdown = require('markdown') + , strftime = require('strftime').strftime , DefaultOptions = { host: 'localhost' , port: 2020 - , postsFile: 'posts.json' + , postsFile: path.join(__dirname, 'posts.json') } function main() { @@ -19,12 +23,15 @@ function main() { readJSON(options.postsFile, function(err, posts) { if (err) { console.error('failed to parse posts file, is it valid JSON?') - console.dir(e) + console.dir(err) process.exit(1) } + if (context.posts === null) { + var n = posts.published.length + , t = strftime('%Y-%m-%d %I:%M:%S %p') + console.log('(' + t + ') ' + 'loaded discussions for ' + n + ' posts...') + } context.posts = posts.published - var n = context.posts.length - console.log((context.posts === null ? '' : 're') + 'loaded ' + n + ' posts...') if (typeof cb == 'function') cb() }) } @@ -56,63 +63,232 @@ function readJSON(f, cb) { }) } -function requestHandler(context) { - function addComment(data) { - if (missingParams(data) || context.posts.indexOf(data.post) === -1) { - console.log('missing params or invalid post title in ' + JSON.stringify(data, null, 2)) - return false +// returns a request handler that returns a string +function createTextHandler(options) { + if (typeof options === 'string') { + options = { body: options } + } else { + options = options || {} + } + var body = options.body || '' + , code = options.cody || 200 + , type = options.type || 'text/plain' + , n = body.length + return function(req, res) { + var headers = res.headers || {} + headers['content-type'] = type + headers['content-length'] = n + +// console.log('code: ', code) +// console.log('headers: ', JSON.stringify(headers, null, 2)) +// console.log('body: ', body) + + res.writeHead(code, headers) + res.end(body) + } +} + +// Cross-Origin Resource Sharing +var createCorsHandler = (function() { + var AllowedOrigins = [ 'http://samhuri.net' + , 'http://localhost:8888' + ] + + return function(handler) { + handler = handler || createTextHandler('ok') + return function(req, res) { + var origin = req.headers.origin + console.log('origin: ', origin) + console.log('index: ', AllowedOrigins.indexOf(origin)) + if (AllowedOrigins.indexOf(origin) !== -1) { + res.headers = { 'Access-Control-Allow-Origin': origin + , 'Access-Control-Request-Method': 'POST, GET' + , 'Access-Control-Allow-Headers': 'content-type' + } + handler(req, res) + } else { + BadRequest(req, res) + } } - var comments = context.db.get(data.post) || [] - comments.push({ name: data.name - , email: data.email - , body: data.body + } +}()) + +var DefaultHandler = createTextHandler({ code: 404, body: 'not found' }) + , BadRequest = createTextHandler({ code: 400, body: 'bad request' }) + , ServerError = createTextHandler({ code: 500, body: 'server error' }) + , _routes = {} + +function route(method, pattern, handler) { + if (typeof pattern === 'function' && !handler) { + handler = pattern + pattern = '' + } + if (!pattern || typeof pattern.exec !== 'function') { + pattern = new RegExp('^/' + pattern) + } + var route = { pattern: pattern, handler: handler } + console.log('routing ' + method, pattern) + if (!(method in _routes)) _routes[method] = [] + _routes[method].push(route) +} + +function resolve(method, path) { + var rs = _routes[method] + , i = rs.length + , m + , r + while (i--) { + r = rs[i] + m = r.pattern.exec ? r.pattern.exec(path) : path.match(r.pattern) + if (m) return r.handler + } + console.warn('*** using default handler, this is probably not what you want') + return DefaultHandler +} + +function get(pattern, handler) { + route('GET', pattern, handler) +} + +function post(pattern, handler) { + route('POST', pattern, handler) +} + +function options(pattern, handler) { + route('OPTIONS', pattern, handler) +} + +function handleRequest(req, res) { + var handler = resolve(req.method, req.url) + try { + handler(req, res) + } catch (e) { + console.error('!!! error handling ' + req.method, req.url) + console.dir(e) + } +} + +function commentServer(context) { + function addComment(post, name, email, url, body) { + var comments = context.db.get(post) || [] + comments.push({ name: name + , email: email + , url: url + , body: body , timestamp: Date.now() }) - context.db.set(data.post, comments) - console.log('[' + new Date() + '] add comment ' + JSON.stringify(data, null, 2)) - return true + context.db.set(post, comments) + console.log('[' + new Date() + '] comment on ' + post) + console.log('name:', name) + console.log('email:', email) + console.log('url:', url) + console.log('body:', body) } - return function(req, res) { + function getComments(req, res) { + var post = parseURL(req.url).pathname.replace(/^\/comments\//, '') + , comments + if (context.posts.indexOf(post) === -1) { + console.warn('post not found: ' + post) + BadRequest(req, res) + return + } + comments = context.db.get(post) || [] + res.respond({comments: comments.map(function(c) { + delete c.email + c.html = markdown.parse(c.body) + // FIXME discount has a race condition, sometimes gives a string + // with trailing garbage. + while (c.html.charAt(c.html.length - 1) !== '>') { + console.log("!!! removing trailing garbage from discount's html") + c.html = c.html.slice(0, c.html.length - 1) + } + return c + })}) + } + + function postComment(req, res) { var body = '' - , m - if (req.method === 'POST' && req.url.match(/^\/comment\/?$/)) { - req.on('data', function(chunk) { body += chunk }) - req.on('end', function() { - var data - try { - data = JSON.parse(body) - } catch (x) { - badRequest(res) - return - } - if (!addComment(data)) { - badRequest(res) - return - } - res.writeHead(204) - res.end() - // TODO mail watchers about the comment - }) - } else if (req.method === 'GET' && (m = req.url.match(/^\/comments\/(.*)$/))) { - var post = m[1] - , comments - , s - if (context.posts.indexOf(post) === -1) { - badRequest(res) + req.on('data', function(chunk) { body += chunk }) + req.on('end', function() { + var data, post, name, email, url + try { + data = JSON.parse(body) + } catch (e) { + console.log('not json -> ' + body) + BadRequest(req, res) return } - comments = context.db.get(post) || [] - s = JSON.stringify({comments: comments}) - res.writeHead(200, { 'content-type': 'appliaction/json' - , 'content-length': s.length - }) - res.end(s) - } else { - console.log('unhandled request') - console.dir(req) - badRequest(res) + post = (data.post || '').trim() + name = (data.name || 'anonymous').trim() + email = (data.email || '').trim() + url = (data.url || '').trim() + if (!url.match(/^https?:\/\//)) url = 'http://' + url + body = data.body || '' + if (!post || !body || context.posts.indexOf(post) === -1) { + console.warn('mising post, body, or post not found: ' + post) + console.warn('body: ', body) + BadRequest(req, res) + return + } + addComment(post, name, email, url, body) + res.respond() + // TODO mail watchers about the comment + }) + } + + function countComments(req, res) { + var post = parseURL(req.url).pathname.replace(/^\/count\//, '') + , comments + if (context.posts.indexOf(post) === -1) { + console.warn('post not found: ' + post) + BadRequest(req, res) + return } + comments = context.db.get(post) || [] + res.respond({count: comments.length}) + } + + return { get: getComments + , count: countComments + , post: postComment + } +} + +function requestHandler(context) { + var comments = commentServer(context) + get(/comments\//, createCorsHandler(comments.get)) + get(/count\//, createCorsHandler(comments.count)) + post(/comment\/?/, createCorsHandler(comments.post)) + options(createCorsHandler()) + + return function(req, res) { + console.log(req.method + ' ' + req.url) + res.respond = function(obj) { + var s = '' + var headers = res.headers || {} + if (obj) { + try { + s = JSON.stringify(obj) + } catch (e) { + ServerError(req, res) + return + } + headers['content-type'] = 'application/json' + } + headers['content-length'] = s.length + + /* + console.log('code: ', s ? 200 : 204) + process.stdout.write('headers: ') + console.dir(headers) + console.log('body: ', s) + */ + + res.writeHead(s ? 200 : 204, headers) + res.end(s) + } + handleRequest(req, res) } } @@ -151,14 +327,6 @@ function parseArgs(defaults) { return options } -function badRequest(res) { - var s = 'bad request' - res.writeHead(400, { 'content-type': 'text/plain' - , 'content-length': s.length - }) - res.end(s) -} - var missingParams = (function() { var requiredParams = 'name email body'.split(' ') return function(d) { diff --git a/minify.sh b/minify.sh new file mode 100755 index 0000000..acbe575 --- /dev/null +++ b/minify.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env zsh + +setopt extendedglob + +for js (assets/*.js~*.min.js) { + target=${js%.js}.min.js + if [ ! -f $target ] || [ $js -nt $target ]; then + echo "$js -> $target" + closure-compiler < $js >| $target + fi +} diff --git a/templates/blog/index.html b/templates/blog/index.html index 2b3315a..2e0d3d1 100644 --- a/templates/blog/index.html +++ b/templates/blog/index.html @@ -3,34 +3,36 @@ blog :: samhuri.net + +{{#comments}} + + +{{/comments}}

    sjs' blog

    @@ -45,16 +47,17 @@ function ready() {
    {{! TODO extract a post partial used here and in post.html }} - {{#post}}
    + {{#post}}

    {{title}}

    {{{body}}} + {{/post}}
    - {{/post}}
    +

    {{#previous}} @@ -64,6 +67,36 @@ function ready() { {{/next}}
    +{{#comments}} +

    (discussion requires JavaScript)

    + +
    +
    + +
    +
    + +

    +

    +

    +

    +

    +
    +
    + +{{/comments}} diff --git a/templates/blog/post.html b/templates/blog/post.html index 0ded7b8..006b0d1 100644 --- a/templates/blog/post.html +++ b/templates/blog/post.html @@ -7,38 +7,22 @@ var _gaq = _gaq || []; _gaq.push( ['_setAccount', 'UA-214054-5'] , ['_trackPageview'] - ); + ) -(function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); -})(); -{{#comments}} -if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', ready, false) -} else if (window.attachEvent) { - window.attachEvent('onload', ready) -} -function ready() { - function html(id, h) { - document.getElementById(id).innerHTML = h - } - - var discussion = document.getElementById('discussion') - , discussionToggle = document.getElementById('discussion-toggle') - , hidden = true - discussionToggle.onclick = function() { - hidden = !hidden - discussion.style.display = hidden ? 'none' : 'block' - discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑' - SJS.request({uri: 'http://bohodev.net:8000/comments/{{filename}}'}, function(err, body) { - html('comments', body) - }) - } -} -{{/comments}} +;(function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js' + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s) +})() +{{#comments}} + + + +{{/comments}}

    sjs' blog

    @@ -55,6 +39,7 @@ function ready() { {{/post}}
    +

    {{#previous}} @@ -64,22 +49,35 @@ function ready() { {{/next}}
    - {{#comments}} -
    -
    -
    - -

    Name:

    -

    URL:

    -

    Thoughts:

    -

    -
    -
    +

    (discussion requires JavaScript)

    + +
    +
    + +

    +

    +

    +

    +

    +
    + {{/comments}}