diff --git a/.gitignore b/.gitignore index 6937e90..cb1d476 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +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/TODO b/TODO index 2338058..0809069 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,8 @@ TODO ==== + * RSS + * use LABjs * last commit date diff --git a/assets/blog.css b/assets/blog.css new file mode 100644 index 0000000..947e49a --- /dev/null +++ b/assets/blog.css @@ -0,0 +1,246 @@ +body { background-color: #f7f7f7 + ; font-family: 'DejaVu Serif', 'xHoefler Text', Georgia, serif + ; margin: 0 + } + +body > header { display: block + ; background-color: #a6bcdf + ; border-bottom: solid 1em #eee + /* some browsers have trouble with 100% for the height so use em units :/ */ + ; -moz-border-bottom-left-radius: 500px 7em + ; -webkit-border-bottom-left-radius: 500px 7em + ; border-bottom-left-radius: 500px 7em + } + +h1 { text-align: right + ; font-size: 4em + ; font-weight: normal + ; margin: 0 + ; padding: 0.2em + ; color: #9ab + } + +h1 a { color: #f7f7f7 + ; border-bottom: none + ; text-decoration: none + ; text-shadow: #999 1px 1px 3px + } + +a#sjs:hover { color: #f7f7a7 } + +a { color: #22c } + +a.img { border: none } + +#breadcrumbs { font-size: 1.5em + ; color: #444 + ; margin: 0.3em + } + +#breadcrumbs a { text-shadow: none + ; color: #444 + ; border: none + ; text-decoration: underline + } + +.center { text-align: center + ; font-size: 1.2em + } + +#index { display: none + ; width: 80% + ; min-width: 200px + ; max-width: 800px + ; border: solid 1px #999 + ; -moz-border-radius: 10px + ; -webkit-border-radius: 10px + ; border-radius: 10px + ; background-color: #eee + ; margin: 1em auto + ; font-size: 1.2em + } + +#index ul { padding: 0 1em + ; list-style-type: none + } + +#index li { height: 1.4em } + +.date { float: right } + +#posts { border-left: solid 0.15em #999 + ; width: 80% + ; min-width: 400px + ; max-width: 680px + ; margin: 4em auto 2em + ; padding: 0 5% + ; font-size: 1.2em + ; line-height: 1.4em + } + +article { color: #222 + ; padding-bottom: 1em + } + +article:last-child { padding-bottom: 0 + ; margin-bottom: 1em + } + +article h1 { text-align: left + ; font-size: 2em + ; line-height: 1.1em + ; font-weight: normal + ; color: #222 + ; margin: 1em 0 + ; padding-left: 0 + } + +article h1 a { color: #222 + ; text-decoration: underline + ; border-bottom: none + ; text-shadow: #999 1px 1px 5px + ; -webkit-transition: text-shadow 0.4s ease-in + } + +article h1 a:hover { text-shadow: #990 1px 1px 8px + ; color: #222 + } + +article h2 { font-size: 1.8em + ; font-weight: normal + ; margin: 1em 0 + ; padding: 0 + ; color: #222 + } + +article h3 { font-size: 1.6em + ; font-weight: normal + } + +time { color: #444 + ; font-size: 1.2em + } + +.gist { font-size: 0.8em } + +#around { width: 80% + ; min-width: 400px + ; max-width: 800px + ; margin: 1em auto + ; padding: 0 3em 1em + ; font-size: 1.2em + } + +#prev { float: left } +#next { float: right } + +.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-spinner { text-align: center } + +#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 0 + ; background-color: #cdf + ; border-top: solid 1px #bbb + ; clear: both + } + +footer a { border-bottom: none + ; color: #25c + ; font-size: 1.3em + ; text-decoration: none + } + +/* iPhone */ + +@media only screen and (max-device-width:480px) { + h1 { font-size: 2em + ; margin-top: 1em + } + h2 { font-size: 1em } + + #breadcrumbs { font-size: 0.8em } + + article { width: 80% + ; min-width: 100px + ; max-width: 800px + ; padding-left: 1em + ; padding-bottom: 3em + } + + footer { font-size: 1em } +} diff --git a/assets/blog.js b/assets/blog.js new file mode 100644 index 0000000..40e1ecd --- /dev/null +++ b/assets/blog.js @@ -0,0 +1,96 @@ +;(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() + comment.name = (comment.name || '').trim() || 'anonymous' + comment.url = (comment.url || '').trim() + if (comment.url && !comment.url.match(/^https?:\/\//)) { + comment.url = 'http://' + comment.url + } + comment.body = comment.body || '' + if (!comment.body) { + alert("is that all you have to say?") + return false + } + + 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 false + } + + // FIXME check for error, how do we get the returned status code? + + $('#comment-form').get(0).reset() + + comment.timestamp = +new Date() + comment.html = showdown.makeHtml(comment.body) + comment.name = (comment.name || '').trim() || 'anonymous' + comment.url = (comment.url || '').trim() + if (comment.url && !comment.url.match(/^https?:\/\//)) { + comment.url = 'http://' + comment.url + } + $('#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 new file mode 100644 index 0000000..eeae9d9 --- /dev/null +++ b/assets/request.js @@ -0,0 +1,31 @@ +;(function() { + if (typeof SJS === 'undefined') SJS = {} + + // 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 + } + 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) + } +}()); 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 new file mode 100755 index 0000000..3edc8cf --- /dev/null +++ b/blog.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'json' +require 'rdiscount' +require 'mustache' + +srcdir = ARGV.shift.to_s +destdir = ARGV.shift.to_s + +unless File.directory?(srcdir) && File.directory?(destdir) + puts 'usage: blog.rb ' + exit 1 +end + +template = File.read(File.join('templates', 'blog', 'post.html')) + +# read posts +posts_file = File.join(srcdir, 'posts.json') +Posts = JSON.parse(File.read(posts_file)) +posts = Posts['published'].map do |filename| + lines = File.readlines(File.join(srcdir, filename)) + post = { :filename => filename } + loop do + line = lines.shift.strip + m = line.match(/(\w+):/) + if m && param = m[1].downcase + post[param.to_sym] = line.sub(Regexp.new('^' + param + ':\s*', 'i'), '').strip + elsif line.match(/^----\s*$/) + lines.shift while lines.first.strip.empty? + break + else + puts "ignoring unknown header: #{line}" + end + end + post[:content] = lines.join + post[:body] = RDiscount.new(post[:content]).to_html + # comments on by default + post[:comments] = true if post[:comments].nil? + post +end + +# generate posts +posts.each_with_index do |post, i| + post[:html] = Mustache.render(template, { :title => post[:title], + :post => post, + :previous => i < posts.length - 1 && posts[i + 1], + :next => i > 0 && posts[i - 1], + :filename => post[:filename], + :comments => post[:comments] + }) +end + +# generate landing page +index_template = File.read(File.join('templates', 'blog', 'index.html')) +index_html = Mustache.render(index_template, { :posts => posts, + :post => posts.first, + :previous => posts[1], + :filename => posts.first[:filename], + :comments => posts.first[:comments] + }) + +# write landing page +File.open(File.join(destdir, 'index.html'), 'w') {|f| f.puts(index_html) } + +# write posts +posts.each do |post| + File.open(File.join(destdir, post[:filename]), 'w') {|f| f.puts(post[:html]) } +end diff --git a/blog.sh b/blog.sh new file mode 100755 index 0000000..8874f01 --- /dev/null +++ b/blog.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if [[ ! -d _blog ]]; then + git clone git://github.com/samsonjs/blog.git _blog +else + cd _blog + git pull + cd .. +fi + +./blog.rb _blog diff --git a/blog/37signals-chalk-dissected.html b/blog/37signals-chalk-dissected.html new file mode 100644 index 0000000..79e0961 --- /dev/null +++ b/blog/37signals-chalk-dissected.html @@ -0,0 +1,314 @@ + + + +37signals' Chalk Dissected :: samhuri.net + + + + + +
    +

    sjs' blog

    +
    + +
    + +
    +
    +

    37signals' Chalk Dissected

    + +
    +

    Update 2010-11-05: I dove into the JavaScript a little and explained most of it. Sam Stephenson tweeted that Chalk is written in CoffeeScript and compiled on the fly when served using Brochure. That's hot! (for those unaware Sam Stephenson works at 37signals, and is also the man behind Prototype.)

    + + + + +

    37signals recently released a blackboard web app for iPad called Chalk.

    + + + + +

    It includes Thomas Fuchs new mobile JS framework Zepto, a few images, iOS SpringBoard icon, and of course HTML, CSS, and JavaScript. It weighs in at about 244k including 216k of images. HTML, CSS, and JavaScript are not minified (except Zepto), but they are gzipped. Because the image-to-text ratio is high gzip can only shave off 12k. There is absolutely nothing there that isn't required though. The code and resources are very tight, readable, and beautiful.

    + + + + +

    The manifest is a nice summary of the contents, and allows browsers to cache the app for offline use. Combine this with mobile Safari's "Add to Home Screen" button and you have yourself a free chalkboard app that works offline.

    + + + + +
    CACHE MANIFEST
    +
    +/
    +/zepto.min.js
    +/chalk.js
    +/images/background.jpg
    +/images/chalk.png
    +/images/chalk-sprites.png
    +/images/chalk-tile-erase.jpg
    +/images/chalk-tile-red.png
    +/images/chalk-tile-white.png
    +/stylesheets/chalk.css
    +
    + + + + +

    Not much there, just 10 requests to fetch the whole thing. 11 including the manifest. In we go.

    + + + + +

     

    + + +

    HTML

    + + + + +

    2k, 61 lines. 10 of which are Google Analytics JavaScript. Let's glance at some of it.

    + + + +

    Standard html5 doctype, and a manifest for application caching.

    + +

    The rest of the HTML is mainly structural. There is not a single text node in the entire tree (excluding whitespace). The chalkboard is a canvas element and an image element used to render the canvas contents as an image for sharing. The other elements are just sprites and buttons. There are div elements for the light switch and shade (a dimmer on each side), share button, instructions on sharing, close button, ledge, chalk, eraser and corresponding indicators. Phew, that was a mouthful. (oblig: "that's what she said!")

    + +

    The interesting thing about the HTML is that without any JavaScript or CSS the document would be a completely blank white page (except for a strange looking share button w/ no title). Talk about progressive enhancement. Here's a look at the HTML:

    + + + +

    Onward.

    + +

     

    +

    Zepto

    + +

    Zepto is a tiny, modern JS framework for mobile WebKit browsers such as those found on iPhone and Android handsets. I'm not going to cover it here but I'll mention that it's similar in feel to jQuery. In fact it tries to mimic jQuery very closely to make migrations from Zepto to jQuery easy, and vice versa. The reason it weighs in at just under 6k (2k gzipped) is that it doesn't overreach or have to support legacy crap like IE6. It was started by Thomas Fuchs so you know it's good.

    + +

     

    +

    Display (CSS & Images)

    + +

    6.6k, 385 lines. This is basically half of the text portion, excluding Zepto. There are 6 images including one called chalk-sprites.png. Interesting. Let's look at the background first though.

    + +

     

    +

    Background

    + +

     

    +
    +
    +background.jpg 1024x946px
    + +

    The background is the blackboard itself, and is almost square at 1024x946. The cork border and light switch are there too. This is set as the background-image of the html element and is positioned at a negative x or y in order to centre it properly. CSS media queries are used to detect the screen's orientation. This way the same image is used for both orientations, clever.

    + + + +

     

    +

    Chalkboard

    + +

    Just a canvas element positioned over the chalkboard using media queries. There's also an image element called "output" used to render an image for sharing.

    + + + +

     

    +

    Sprites

    + +

     

    +
    +
    +chalk-sprites.png
    + +

    Sprites are used for all the other elements: ledge, chalk, eraser, tool indicator, share button, instructions, and close button (to leave the sharing mode). Positioned using CSS, standard stuff. There is white text alongside those green arrows. If you want to see it we'll have to change the background to black.

    + +

     

    +

    Light Switch & Shade

    + +

    When you touch the light switch on the left side of the chalkboard - only visible in landscape orientation - the cork border dims and the ledge and share button disappear, leaving the chalkboard under the spotlight all classy like. The shade consists of two "dimmer" div elements inside a shade div, which is hidden by default.

    + +

    The dimmers background color is black at 67% opacity. The shade element fades in using -webkit-transition: on its visibility property while the dimmers use CSS3 transitions on their background. The dimmers are positioned using media queries as well, one on each side of the board. Interestingly their parent shade has a height and width of 0. Rather than each having a unique id they just have the class "dim" and the :nth-child pseudo-class selector is used to position them independently.

    + + + +

    If you took a look at the HTML before you'll have noticed there's no shade class defined on the body element. Looks like they're using JavaScript to add the shade class to body, triggering the transitions to the visible shades and setting the dimmers backgrounds to black at the same time, causing the fading effect. The shade fades in while the ledge and share button fade out.

    + +

    The light switch itself is displayed only in landscape orientation, again using a media query.

    + +

     

    +

    Tools

    + +

    There are 2 layers to the tools on the ledge. There are the images of the tools and their indicators, but also an anchor element for each tool that acts as targets to select them. When tools are select the indicators fade in and out using CSS3 transitions on opacity by adding and removing the class "active" on the tool.

    + + + +

    There are pattern images for each colour of chalk, and one for the the eraser. The eraser "pattern" is the entire blackboard so erasing it doesn't look ugly. I love that kind of attention to detail.

    + +

     

    +

    Sharing

    + +

    The shade effect that happens when you hit the share button is similar to the shade effect used for the light switch. It's a bit more complex as the sharing instructions are positioned differently in portrait and landscape orientations, but there's nothing really new in there (that I can see).

    + +

    The rest of the CSS is largely presentational stuff like removing margins and padding, and positioning using lots of media queries. You can see it all at chalk.37signals.com/stylesheets/chalk.css.

    + +

     

    +

    JavaScript (and CoffeeScript)

    + +

    5.5k in about 170 lines. That's just half the size of the CSS.

    + +

    Sam Stephenson shared the original CoffeeScript source with us. It's about 150 lines, and is a bit easier to read as CS is far cleaner than JS.

    + +

    The bulk of the magic is done w/ hardware accelerated CSS3 rather than slow JS animation using setInterval and setTimeout to change properties. That sort of thing isn't novel anymore anyway. The fact that JS is really only used for drawing and toggling CSS classes is pretty awesome!

    + +

    The entire contents of the JS reside inside the DOMContentLoaded event handler attached to window.

    + +

     

    +

    Initialization

    + +

     

    + + +

    First we get a handle on all the elements and the canvas' 2d drawing context. I almost want to say views and controls as it really feels just like hooking up a controller and view in a desktop GUI app. Sometimes the line between dynamic web page and web app are blurred, not so here. Chalk is 100% app.

    + +

    The canvas' dimensions and pen are initialized in lines 13 - 19, and then the chalkboard background is drawn onto the canvas using the drawImage() method.

    + +

    The canvas offsets are cached for calculations, and are updated when the window fires the "orientationChange" event. Next up tools (a.k.a. pens) are created and initialized.

    + +

     

    +

    Tools

    + +

     

    + + +

    createPattern(name, callback) loads one of the pattern images, chalk-tile-*, and then creates a pattern in the drawing context and passes it to the given callback.

    + +

    setStroke(pattern, width) effectively sets the pen used for drawing, described as a pattern & stroke width. The patterns are initialized and the white pen is passed to setStroke since it's the default tool.

    + +

    The last part defines the 3 tools, note that the active tool "white_chalk" is at the end. Also note that the tool names are the ids of the target elements in the ledge. activateTool(tool) accepts a tool name. The tool to activate is moved to the end of the tools array on lines 31-32, activeTool is set to the given tool as well on line 32. The reason for moving the active tool to the end of the array is revealed in the for loop on line 34, the order of the tools array determines their z-index ordering (highest number is in front). Then the 'active' CSS class is added to the active tool to show the indicator, and then the pen is set by assigning a pen to the context's strokeStyle property.

    + +

    Finally the white_chalk tool is activated and the click event for the tool targets is setup.

    + +

     

    +

    Drawing

    + +

     

    + + +

    Drawing is done by listening for touch events on the canvas element. An array of points to draw is initialized to a 1-element array containing null. Null values make the draw function break up the line being drawn by skipping the next point in the array. x and y coords are initialized in touchstart, points are appended to the points array in touchmove, and the touchend handler appends two points and null to the points array to end the line. I'm not sure why [x, y] is used as the points in the touchend handler rather than coords from the event. Please leave a comment if you know why!

    + +

    The draw function is called for each point in the points array at 30ms intervals. A line is started by calling context.beginPath(), each point is drawn, and then the line is ended with context.stroke(). The 2nd condition of the while loop ensures that we don't draw for too long, as bad things would happen if the function were executed a 2nd time while it was already running.

    + +

    Sam Stephenson was kind enough to clarify these points. See his comment below the post for clarification on using [x, y] in the touchend handler and the 10ms limit when drawing points.

    + +

     

    +

    Light Switch & Shade

    + +

     

    + + +

    When the light switch is touched (or clicked) the shade class on the body element is toggled. Nothing to it.

    + +

     

    +

    Sharing

    + +

     

    + + +

    The share window is opened after a 10ms delay, just enough time for any drawing to be completed before rendering the image. The image is created by assigning the result of canvas' toDataURL() method to the output image element's src attribute.

    + +

    When the share window is closed the output image element gets its src set to the sprites image. I'm not sure why that was done. As Sam mentions in his comment below, this is done to reclaim the memory used by the rendered image.

    + +

    The rest of the code there just sets up event handlers and toggles CSS classes.

    + +

     

    +

    That's it!

    + +

    That about covers it. Don't have an iPad? Play around with it anyway, but be warned that you can't draw anything. You can select chalk and the eraser and hit the light switch. I instinctively tried touching my MacBook's display but alas it doesn't magically respond to touches, lame.

    + +

    Have fun drawing. Thanks to 37signals for a beautiful (and useful) example of a few modern web technologies.

    + + + + + +
    +
    +

    + +

    (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 new file mode 100644 index 0000000..03b5dff --- /dev/null +++ b/blog/a-preview-of-mach-o-file-generation.html @@ -0,0 +1,109 @@ + + + +A preview of Mach-O file generation :: samhuri.net + + + + + +
    +

    sjs' blog

    +
    + +
    + +
    +
    +

    A preview of Mach-O file generation

    + +
    +

    This month I got back into an x86 compiler I started last May. It lives on github.

    + + + + +

    The code is a bit of a mess but it mostly works. It generates Mach object +files that are linked with gcc to produce executable binaries.

    + + + + +

    The Big Refactoring of January 2010 has come to an end and the tests pass +again, even if printing is broken it prints something, and more +importantly compiles test/test_huge.code into something that works.

    + + + + +

    After print is fixed I can clean up the code before implementing anything +new. I wasn't sure if I'd get back into this or not and am pretty excited +about it. I'm learning a lot from this project.

    + + + + +

    If you are following the Mach-O posts you might want to look at +asm/machofile.rb, a library for creating Mach-O files. Using it is quite +straightforward, an example is in asm/binary.rb, in the #output method.

    + + + + +

    Definitely time for bed now!

    + + +
    +
    +

    + +

    (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 new file mode 100644 index 0000000..94ab5f9 --- /dev/null +++ b/blog/basics-of-the-mach-o-file-format.html @@ -0,0 +1,332 @@ + + + +Basics of the Mach-O file format :: samhuri.net + + + + + +
    +

    sjs' blog

    +
    + +
    + +
    +
    +

    Basics of the Mach-O file format

    + +
    +

    This post is part of a series on generating basic x86 Mach-O files +with Ruby. The + +first post introduced CStruct, a Ruby class used to serialize +simple struct-like objects.

    + + + + +

    Please note that the best way to learn about Mach-O properly is to +read Apple's + +documentation on Mach-O, which is pretty good combined with the +comments in /usr/include/mach-o/*.h. These posts will only cover +the basics necessary to generate a simple object file for linking with +ld or gcc, and are not meant to be comprehensive.

    + + + + +

    Mach-O File Format Overview

    + + + + +

    A Mach-O file consists of 2 main pieces: the header and +the data. The header is basically a map of the file describing +what it contains and the position of everything contained in it. The +data comes directly after the header and consists of a number of +binary blobs of data, one after the other.

    + + + + +

    The header contains 3 types of records: the Mach header, +segments, and sections. Each binary blob is described +by a named section in the header. Sections are grouped into one or +more named segments. The Mach header is just one part of the header +and should not be confused with the entire header. It contains +information about the file as a whole, and specifies the number of +segments as well.

    + + + + +

    Take a quick look at Figure 1 in + +Apple's Mach-O overview, which illustrates this quite nicely.

    + + + + +

    A very basic Mach object file consists of a header followed by single +blob of machine code. That blob could be described by a single +section named __text, inside a single nameless segment. Here's a +diagram showing the layout of such a file:

    + + + + +
    +
    +            ,---------------------------,
    +  Header    |  Mach header              |
    +            |    Segment 1              |
    +            |      Section 1 (__text)   | --,
    +            |---------------------------|   | 
    +  Data      |           blob            | <-'
    +            '---------------------------'      
    +
    + + + + +

    The Mach Header

    + + + + +

    The Mach header contains the architecture (cpu type), the type of +file (object in our case), and the number of segments. There is more +to it but that's about all we care about. To see exactly what's in a +Mach header fire up a shell and type otool -h /bin/zsh (on a +Mac).

    + + + + +

    Using + +CStruct we define the Mach header like so:

    + + + + + + + + + +

    Segments

    + + + + +

    Segments, or segment commands, specify where in memory the +segment should be loaded by the OS, and the number of bytes to +allocate for that segment. They also specify which bytes inside the +file are part of that segment, and how many sections it contains.

    + + + + +

    One benefit to generating an object file rather than an executable is +that we let the linker worry about some details. One of those details +is where in memory segments will ultimately end up.

    + + + + +

    Names are optional and can be arbitrary, but the convention is to +name segments with uppercase letters preceded by two underscores, +e.g. __DATA or __TEXT

    + + + + +

    The code exposes some more details about segment commands, but should +be easy enough to follow.

    + + + + + + + + + +

    Sections

    + + + + +

    All sections within a segment are described one after the other +directly after each segment command. Sections define their name, +address in memory, size, offset of section data within the file, and +segment name. The segment name might seem redundant but in the next +post we'll see why this is useful information to have in the section +header.

    + + + + +

    Sections can optionally specify a map to addresses within their +binary blob, called a relocation table. This is used by the +linker. Since we're letting the linker work out where to place +everything in memory the addresses inside our machine code will need +to be updated.

    + + + + +

    By convention segments are named with lowercase letters preceded by +two underscores, e.g. __bss or __text

    + + + + +

    Finally, the Ruby code describing section structs:

    + + + + + + + + + +

    macho.rb

    + + + + +

    As much of the Mach-O format as we need is defined in + +asm/macho.rb. The Mach header, Segment commands, sections, +relocation tables, and symbol table structs are all there, with a few +constants as well.

    + + + + +

    I'll cover symbol tables and relocation tables in my next post.

    + + + + +

    Looking at real Mach-O files

    + + + + +

    To see the segments and sections of an object file, run +otool -l /usr/lib/crt1.o. -l is for load commands. +If you want to see why we stick to generating object files instead of +executables run otool -l /bin/zsh. They are complicated +beasts.

    + + + + +

    If you want to see the actual data for a section otool provides a +couple of ways to do this. The first is to use +otool -d <segment> <section> for an arbitrary +section. To see the contents of a well-known section, such as __text +in the __TEXT segment, use otool -t /usr/bin/true. You can +also disassemble the __text section with +otool -tv /usr/bin/true.

    + + + + +

    You'll get to know otool quite well if you work with Mach-O.

    + + + + +

    Take a break

    + + + + +

    That was probably a lot to digest, and to make real sense of it you +might need to read some of the + +official documentation.

    + + + + +

    We're close to being able to describe a minimal Mach object file +that can be linked, and the resulting binary executed. By the end of +the next post we'll be there.

    + + + + +

    (You can almost do that with what we know now. If you +create a Mach file with a Mach header (ncmds=1), a single unnamed +segment (nsects=1), and then a section named __text with a segment +name of __TEXT, and some x86 machine code as the section data, you +would almost have a useful Mach object file.)

    + + + + +

    Till next time, happy hacking!

    + + +
    +
    +

    + +

    (discussion requires JavaScript)

    + +
    +
    +
    +
    +
    + +

    +

    +

    +

    +

    +
    +
    + + diff --git a/blog/index.html b/blog/index.html new file mode 100644 index 0000000..8a83e9e --- /dev/null +++ b/blog/index.html @@ -0,0 +1,334 @@ + + + +blog :: samhuri.net + + + + + +
    +

    sjs' blog

    +
    + + + +
    + +
    +
    +

    37signals' Chalk Dissected

    + +
    +

    Update 2010-11-05: I dove into the JavaScript a little and explained most of it. Sam Stephenson tweeted that Chalk is written in CoffeeScript and compiled on the fly when served using Brochure. That's hot! (for those unaware Sam Stephenson works at 37signals, and is also the man behind Prototype.)

    + + + + +

    37signals recently released a blackboard web app for iPad called Chalk.

    + + + + +

    It includes Thomas Fuchs new mobile JS framework Zepto, a few images, iOS SpringBoard icon, and of course HTML, CSS, and JavaScript. It weighs in at about 244k including 216k of images. HTML, CSS, and JavaScript are not minified (except Zepto), but they are gzipped. Because the image-to-text ratio is high gzip can only shave off 12k. There is absolutely nothing there that isn't required though. The code and resources are very tight, readable, and beautiful.

    + + + + +

    The manifest is a nice summary of the contents, and allows browsers to cache the app for offline use. Combine this with mobile Safari's "Add to Home Screen" button and you have yourself a free chalkboard app that works offline.

    + + + + +
    CACHE MANIFEST
    +
    +/
    +/zepto.min.js
    +/chalk.js
    +/images/background.jpg
    +/images/chalk.png
    +/images/chalk-sprites.png
    +/images/chalk-tile-erase.jpg
    +/images/chalk-tile-red.png
    +/images/chalk-tile-white.png
    +/stylesheets/chalk.css
    +
    + + + + +

    Not much there, just 10 requests to fetch the whole thing. 11 including the manifest. In we go.

    + + + + +

     

    + + +

    HTML

    + + + + +

    2k, 61 lines. 10 of which are Google Analytics JavaScript. Let's glance at some of it.

    + + + +

    Standard html5 doctype, and a manifest for application caching.

    + +

    The rest of the HTML is mainly structural. There is not a single text node in the entire tree (excluding whitespace). The chalkboard is a canvas element and an image element used to render the canvas contents as an image for sharing. The other elements are just sprites and buttons. There are div elements for the light switch and shade (a dimmer on each side), share button, instructions on sharing, close button, ledge, chalk, eraser and corresponding indicators. Phew, that was a mouthful. (oblig: "that's what she said!")

    + +

    The interesting thing about the HTML is that without any JavaScript or CSS the document would be a completely blank white page (except for a strange looking share button w/ no title). Talk about progressive enhancement. Here's a look at the HTML:

    + + + +

    Onward.

    + +

     

    +

    Zepto

    + +

    Zepto is a tiny, modern JS framework for mobile WebKit browsers such as those found on iPhone and Android handsets. I'm not going to cover it here but I'll mention that it's similar in feel to jQuery. In fact it tries to mimic jQuery very closely to make migrations from Zepto to jQuery easy, and vice versa. The reason it weighs in at just under 6k (2k gzipped) is that it doesn't overreach or have to support legacy crap like IE6. It was started by Thomas Fuchs so you know it's good.

    + +

     

    +

    Display (CSS & Images)

    + +

    6.6k, 385 lines. This is basically half of the text portion, excluding Zepto. There are 6 images including one called chalk-sprites.png. Interesting. Let's look at the background first though.

    + +

     

    +

    Background

    + +

     

    +
    +
    +background.jpg 1024x946px
    + +

    The background is the blackboard itself, and is almost square at 1024x946. The cork border and light switch are there too. This is set as the background-image of the html element and is positioned at a negative x or y in order to centre it properly. CSS media queries are used to detect the screen's orientation. This way the same image is used for both orientations, clever.

    + + + +

     

    +

    Chalkboard

    + +

    Just a canvas element positioned over the chalkboard using media queries. There's also an image element called "output" used to render an image for sharing.

    + + + +

     

    +

    Sprites

    + +

     

    +
    +
    +chalk-sprites.png
    + +

    Sprites are used for all the other elements: ledge, chalk, eraser, tool indicator, share button, instructions, and close button (to leave the sharing mode). Positioned using CSS, standard stuff. There is white text alongside those green arrows. If you want to see it we'll have to change the background to black.

    + +

     

    +

    Light Switch & Shade

    + +

    When you touch the light switch on the left side of the chalkboard - only visible in landscape orientation - the cork border dims and the ledge and share button disappear, leaving the chalkboard under the spotlight all classy like. The shade consists of two "dimmer" div elements inside a shade div, which is hidden by default.

    + +

    The dimmers background color is black at 67% opacity. The shade element fades in using -webkit-transition: on its visibility property while the dimmers use CSS3 transitions on their background. The dimmers are positioned using media queries as well, one on each side of the board. Interestingly their parent shade has a height and width of 0. Rather than each having a unique id they just have the class "dim" and the :nth-child pseudo-class selector is used to position them independently.

    + + + +

    If you took a look at the HTML before you'll have noticed there's no shade class defined on the body element. Looks like they're using JavaScript to add the shade class to body, triggering the transitions to the visible shades and setting the dimmers backgrounds to black at the same time, causing the fading effect. The shade fades in while the ledge and share button fade out.

    + +

    The light switch itself is displayed only in landscape orientation, again using a media query.

    + +

     

    +

    Tools

    + +

    There are 2 layers to the tools on the ledge. There are the images of the tools and their indicators, but also an anchor element for each tool that acts as targets to select them. When tools are select the indicators fade in and out using CSS3 transitions on opacity by adding and removing the class "active" on the tool.

    + + + +

    There are pattern images for each colour of chalk, and one for the the eraser. The eraser "pattern" is the entire blackboard so erasing it doesn't look ugly. I love that kind of attention to detail.

    + +

     

    +

    Sharing

    + +

    The shade effect that happens when you hit the share button is similar to the shade effect used for the light switch. It's a bit more complex as the sharing instructions are positioned differently in portrait and landscape orientations, but there's nothing really new in there (that I can see).

    + +

    The rest of the CSS is largely presentational stuff like removing margins and padding, and positioning using lots of media queries. You can see it all at chalk.37signals.com/stylesheets/chalk.css.

    + +

     

    +

    JavaScript (and CoffeeScript)

    + +

    5.5k in about 170 lines. That's just half the size of the CSS.

    + +

    Sam Stephenson shared the original CoffeeScript source with us. It's about 150 lines, and is a bit easier to read as CS is far cleaner than JS.

    + +

    The bulk of the magic is done w/ hardware accelerated CSS3 rather than slow JS animation using setInterval and setTimeout to change properties. That sort of thing isn't novel anymore anyway. The fact that JS is really only used for drawing and toggling CSS classes is pretty awesome!

    + +

    The entire contents of the JS reside inside the DOMContentLoaded event handler attached to window.

    + +

     

    +

    Initialization

    + +

     

    + + +

    First we get a handle on all the elements and the canvas' 2d drawing context. I almost want to say views and controls as it really feels just like hooking up a controller and view in a desktop GUI app. Sometimes the line between dynamic web page and web app are blurred, not so here. Chalk is 100% app.

    + +

    The canvas' dimensions and pen are initialized in lines 13 - 19, and then the chalkboard background is drawn onto the canvas using the drawImage() method.

    + +

    The canvas offsets are cached for calculations, and are updated when the window fires the "orientationChange" event. Next up tools (a.k.a. pens) are created and initialized.

    + +

     

    +

    Tools

    + +

     

    + + +

    createPattern(name, callback) loads one of the pattern images, chalk-tile-*, and then creates a pattern in the drawing context and passes it to the given callback.

    + +

    setStroke(pattern, width) effectively sets the pen used for drawing, described as a pattern & stroke width. The patterns are initialized and the white pen is passed to setStroke since it's the default tool.

    + +

    The last part defines the 3 tools, note that the active tool "white_chalk" is at the end. Also note that the tool names are the ids of the target elements in the ledge. activateTool(tool) accepts a tool name. The tool to activate is moved to the end of the tools array on lines 31-32, activeTool is set to the given tool as well on line 32. The reason for moving the active tool to the end of the array is revealed in the for loop on line 34, the order of the tools array determines their z-index ordering (highest number is in front). Then the 'active' CSS class is added to the active tool to show the indicator, and then the pen is set by assigning a pen to the context's strokeStyle property.

    + +

    Finally the white_chalk tool is activated and the click event for the tool targets is setup.

    + +

     

    +

    Drawing

    + +

     

    + + +

    Drawing is done by listening for touch events on the canvas element. An array of points to draw is initialized to a 1-element array containing null. Null values make the draw function break up the line being drawn by skipping the next point in the array. x and y coords are initialized in touchstart, points are appended to the points array in touchmove, and the touchend handler appends two points and null to the points array to end the line. I'm not sure why [x, y] is used as the points in the touchend handler rather than coords from the event. Please leave a comment if you know why!

    + +

    The draw function is called for each point in the points array at 30ms intervals. A line is started by calling context.beginPath(), each point is drawn, and then the line is ended with context.stroke(). The 2nd condition of the while loop ensures that we don't draw for too long, as bad things would happen if the function were executed a 2nd time while it was already running.

    + +

    Sam Stephenson was kind enough to clarify these points. See his comment below the post for clarification on using [x, y] in the touchend handler and the 10ms limit when drawing points.

    + +

     

    +

    Light Switch & Shade

    + +

     

    + + +

    When the light switch is touched (or clicked) the shade class on the body element is toggled. Nothing to it.

    + +

     

    +

    Sharing

    + +

     

    + + +

    The share window is opened after a 10ms delay, just enough time for any drawing to be completed before rendering the image. The image is created by assigning the result of canvas' toDataURL() method to the output image element's src attribute.

    + +

    When the share window is closed the output image element gets its src set to the sprites image. I'm not sure why that was done. As Sam mentions in his comment below, this is done to reclaim the memory used by the rendered image.

    + +

    The rest of the code there just sets up event handlers and toggles CSS classes.

    + +

     

    +

    That's it!

    + +

    That about covers it. Don't have an iPad? Play around with it anyway, but be warned that you can't draw anything. You can select chalk and the eraser and hit the light switch. I instinctively tried touching my MacBook's display but alas it doesn't magically respond to touches, lame.

    + +

    Have fun drawing. Thanks to 37signals for a beautiful (and useful) example of a few modern web technologies.

    + + + + + +
    +
    +

    + +

    (discussion requires JavaScript)

    + +
    +
    +
    +
    +
    + +

    +

    +

    +

    +

    +
    +
    + + diff --git a/blog/using-emacs-to-develop-mojo-apps-for-webos.html b/blog/using-emacs-to-develop-mojo-apps-for-webos.html new file mode 100644 index 0000000..719ded1 --- /dev/null +++ b/blog/using-emacs-to-develop-mojo-apps-for-webos.html @@ -0,0 +1,188 @@ + + + +Using Emacs to Develop Mojo Apps for WebOS :: samhuri.net + + + + + +
    +

    sjs' blog

    +
    + +
    + +
    +
    +

    Using Emacs to Develop Mojo Apps for WebOS

    + +
    +

    + The latest technology I've been learning is Palm's SDK for webOS, + Mojo. My first impression is that it's a great platform and + Palm could do a great job of 2.0 if they cut down on some of the + verbosity of gluing together the UI. I have learned to like + JavaScript over the years as I learned that despite its + warts there + are good parts too. If you squint just right you can + see that it's scheme with Algol syntax. HTML and CSS are what + they are, but with WebKit running the show and only a single engine + to target it's not that bad. I've gone from Eclipse to Emacs + for the coding itself and highly recommend Emacs for Mojo + development. There is nothing that I miss from the Eclipse or + Komodo Edit thanks to the fact that Mojo uses open languages and + standards. +

    + + + + +

    + As far as actual development goes the Mojo documentation steers you + towards a combination of Eclipse, Palm's Mojo plugin for Eclipse, + and the Aptana Studio plugin. My editor of choice is Emacs but + I decided to give it a spin just to get started quickly, how bad + could it be? I'm not going to get into details but I will say that I + don't think I'll ever use Eclipse for anything; it's far too + sluggish and provides no compelling features for the languages + that I use. I tried Komodo Edit and it was significantly + better but still not for me. Emacs is great for editing HTML, + JavaScript, and CSS so all I really missed from the IDEs were the + shortcuts to package, install, and launch apps in the + emulator. I headed over to + the Emacs Wiki and + downloaded Jonathan + Arkell's Mojo + support for Emacs which provided a great base to get + started with. There are wrappers around (all?) of the Palm SDK + commands but it needed a bit of work to make it just do what I + wanted with as little input and thought as possible. +

    + + + + +

    + A couple of of Lisp hacking sessions later and I'm happy enough with + mojo.el to bump the version to v0.9. I've checked off what I + feel are the most important checkpoints on + the webOS + Internals comparison of editors and the framework is in + place to make implementing most of the remaining commands very + trivial. I might take a bit of time today to flesh things out + just to check more points off so people feel more confident that + it's a fully featured environment, because it certainly is. +

    + + + + +

    + It now requires json.el in order to parse appinfo.json. json.el + might be included with Emacs if you have a very recent version, + otherwise you can google for it or get it from + my config + file repo on github where you can also find my latest + version of mojo.el. You still just (require 'mojo) in your + .emacs file. +

    + + + + +

    + The wrappers around Palm SDK commands now search upwards for the + Mojo project root directory (from the default-directory for + current-buffer) and parse appinfo.json to give you sane defaults for + mojo-package, mojo-install, mojo-launch, mojo-delete, and + mojo-inspect. You can list installed apps and when entering app + ids there is completion and history, as you have come to expect in + Emacs. The most useful command for development is + mojo-package-install-and-inspect which does exactly what it says: + packages, installs, and launches the application for + inspection. No interaction is required as long as you are + editing a buffer inside your Mojo project. +

    + + + + +

    + If you read the install instructions in mojo.el and decide to setup + some keybindings then you will have single-task commands for + packaging, installing, launching, or all three steps at once. +

    + + + + +

    + Please give me some feedback if you try this out. I've + developed it on Mac OS X and Jonathan on Windows so please try it on + Linux and send me a patch or even better a pull request on github if + it needs some work. There is room for improvement. The next feature + on my radar before I would consider it worthy of a v1.0 tag is + intelligent switching to corresponding buffers, + e.g. mojo-switch-to-view, mojo-switch-to-assistant, things like + that. Basically things I miss from the Rails package for Emacs. +

    + + + + +

    Happy hacking!

    + + +
    +
    +

    + +

    (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 new file mode 100644 index 0000000..b27c472 --- /dev/null +++ b/blog/working-with-c-style-structs-in-ruby.html @@ -0,0 +1,181 @@ + + + +Working with C-style structs in Ruby :: samhuri.net + + + + + +
    +

    sjs' blog

    +
    + +
    + +
    +
    +

    Working with C-style structs in Ruby

    + +
    +

    This is the beginning of a series on generating Mach-O object files in +Ruby. We start small by introducing some Ruby tools that are useful when +working with binary data. Subsequent articles will cover a subset of the +Mach-O file format, then generating Mach object files suitable for linking +with ld or gcc to produce working executables. A basic knowledge of Ruby and C +are assumed. You can likely wing it on the Ruby side of things if you know any +similar languages.

    + + + + +

    First we need to read and write structured binary files with Ruby. Array#pack and +String#unpack +get the job done at a low level, but every time I use them I have to look up +the documentation. It would also be nice to encapsulate serializing and +deserializing into classes describing the various binary data structures. The +built-in Struct +class sounds promising but did not meet my needs, nor was it easily +extended to meet them.

    + + + + +

    Meet CStruct, +a class that you can use to describe a binary structure, somewhat similar to +how you would do it in C. Subclassing CStruct results in a class whose +instances can be serialized, and unserialized, with little effort. You can +subclass descendants of CStruct to extend them with additional members. +CStruct does not implement much more than is necessary for the compiler. For +example there is no support for floating point. If you want to use this for +more general purpose tasks be warned that it may require some work. Anything +supported by Array#pack is fairly easy to add though.

    + + + + +

    First a quick example and then we'll get into the CStruct class itself. In +C you may write the following to have one struct "inherit" from another:

    + + + + +

    + + + + +

    With CStruct in Ruby that translates to:

    + + + + +

    + + + + +

    CStructs act like Ruby's built-in Struct to a certain extent. They are +instantiated the same way, by passing values to #new in the same order they +are defined in the class. You can find out the size (in bytes) of a CStruct +instance using the #bytesize method, or of any member using #sizeof(name).

    + + + + +

    The most important method (for us) is #serialize, which returns a binary +string representing the contents of the CStruct.

    + + + + +

    (I know that CStruct.new_from_bin should be called CStruct.unserialize, you +can see where my focus was when I wrote it.)

    + + + + +

    CStruct#serialize automatically creates a "pack pattern", which is an array +of strings used to pack each member in turn. The pack pattern is mapped to the +result of calling Array#pack on each corresponding member, and then the +resulting strings are joined together. Serializing strings complicates matters +so we cannot build up a pack pattern string and then serialize it in one go, +but conceptually it's quite similar.

    + + + + +

    Unserializing is the same process in reverse, and was mainly added for +completeness and testing purposes.

    + + + + +

    That's about all you need to know to use CStruct. The code needs some work +but I decided to just go with what I have already so I can get on with the +more interesting and fun tasks.

    + + + + +

    Next in this series: Basics +of the Mach-O file format

    + + +

    +
    +

    + +

    (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 new file mode 100755 index 0000000..4fffe8b --- /dev/null +++ b/discussd/discussd.js @@ -0,0 +1,342 @@ +#!/usr/bin/env node + +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: path.join(__dirname, 'posts.json') + } + +function main() { + var options = parseArgs(DefaultOptions) + , db = new keys.Dirty('./discuss.dirty') + , context = { db: db + , posts: null + } + , server = http.createServer(requestHandler(context)) + , loadPosts = function(cb) { + readJSON(options.postsFile, function(err, posts) { + if (err) { + console.error('failed to parse posts file, is it valid JSON?') + 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 + if (typeof cb == 'function') cb() + }) + } + , listen = function() { + console.log(process.argv[0] + ' listening on ' + options.host + ':' + options.port) + server.listen(options.port, options.host) + } + loadPosts(function() { + fs.watchFile(options.postsFile, loadPosts) + if (db._loaded) { + listen() + } else { + db.db.on('load', listen) + } + }) +} + +function readJSON(f, cb) { + fs.readFile(f, function(err, buf) { + var data + if (!err) { + try { + data = JSON.parse(buf.toString()) + } catch (e) { + err = e + } + } + cb(err, data) + }) +} + +// 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 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(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) + } + + 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 = '' + 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 + } + post = (data.post || '').trim() + name = (data.name || 'anonymous').trim() + email = (data.email || '').trim() + url = (data.url || '').trim() + if (url && !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) + } +} + +function parseArgs(defaults) { + var expectingArg + , options = Object.keys(defaults).reduce(function(os, k) { + os[k] = defaults[k] + return os + }, {}) + process.argv.slice(2).forEach(function(arg) { + if (expectingArg) { + options[expectingArg] = arg + expectingArg = null + } else { + // remove leading dashes + while (arg.charAt(0) === '-') { + arg = arg.slice(1) + } + switch (arg) { + case 'h': + case 'host': + expectingArg = 'host' + break + + case 'p': + case 'port': + expectingArg = 'port' + break + + default: + console.warn('unknown option: ' + arg + ' (setting anyway)') + expectingArg = arg + } + } + }) + return options +} + +var missingParams = (function() { + var requiredParams = 'name email body'.split(' ') + return function(d) { + var anyMissing = false + requiredParams.forEach(function(p) { + var v = (d[p] || '').trim() + if (!v) anyMissing = true + }) + return anyMissing + } +}()) + +if (module == require.main) main() diff --git a/index.html b/index.html index 8bebb21..c55e112 100644 --- a/index.html +++ b/index.html @@ -30,6 +30,8 @@
    • projects
    • +
    • blog
    • +
    • json-diff
    • riak-js docs
    • 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 new file mode 100644 index 0000000..e027779 --- /dev/null +++ b/templates/blog/index.html @@ -0,0 +1,102 @@ + + + +blog :: samhuri.net + + + +{{#comments}} + + +{{/comments}} +
      +

      sjs' blog

      +
      + + + +
      + {{! TODO extract a post partial used here and in post.html }} +
      + {{#post}} +
      +

      {{title}}

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

      +
      + {{#previous}} + + {{/previous}} + {{#next}} + + {{/next}} +
      +
      +{{#comments}} +

      (discussion requires JavaScript)

      + +
      +
      +
      +
      +
      + +

      +

      +

      +

      +

      +
      +
      + +{{/comments}} + diff --git a/templates/blog/post.html b/templates/blog/post.html new file mode 100644 index 0000000..0f0e9de --- /dev/null +++ b/templates/blog/post.html @@ -0,0 +1,84 @@ + + + +{{title}} :: samhuri.net + + +{{#comments}} + + + +{{/comments}} +
      +

      sjs' blog

      +
      + +
      + {{! TODO extract a post partial used here and in index.html }} +
      + {{#post}} +
      +

      {{title}}

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

      +
      + {{#previous}} + + {{/previous}} + {{#next}} + + {{/next}} +
      +
      +{{#comments}} +

      (discussion requires JavaScript)

      + +
      +
      +
      +
      +
      + +

      +

      +

      +

      +

      +
      +
      + +{{/comments}} +