quick and dirty comments

This commit is contained in:
Sami Samhuri 2010-12-08 22:25:34 -08:00
parent 050f2df426
commit 4b8bf805cd
9 changed files with 431 additions and 2 deletions

View file

@ -131,6 +131,8 @@ time { color: #444 }
#prev { float: left }
#next { float: right }
#comments { display: none }
footer { text-align: center
; font-size: 1.2em
; margin: 0

54
assets/request.js Normal file
View file

@ -0,0 +1,54 @@
(function() {
if (typeof SJS === 'undefined') SJS = {}
var load, _jsonpCounter = 1
SJS.request = function(options, cb) { // jsonp request, quacks like mikeal's request module
var jsonpCallbackName = '_jsonpCallback' + _jsonpCounter++
, url = options.uri + '?callback=SJS.' + jsonpCallbackName
SJS[jsonpCallbackName] = function(obj) {
cb(null, obj)
setTimeout(function() { delete SJS[jsonpCallbackName] }, 0)
}
load(url)
}
// bootstrap loader from LABjs
load = function(url) {
var oDOC = document
, handler
, head = oDOC.head || oDOC.getElementsByTagName("head")
// loading code borrowed directly from LABjs itself
// (now removes script elem when done and nullifies its reference --sjs)
setTimeout(function () {
if ("item" in head) { // check if ref is still a live node list
if (!head[0]) { // append_to node not yet ready
setTimeout(arguments.callee, 25)
return
}
head = head[0]; // reassign from live node list ref to pure node ref -- avoids nasty IE bug where changes to DOM invalidate live node lists
}
var scriptElem = oDOC.createElement("script"),
scriptdone = false
scriptElem.onload = scriptElem.onreadystatechange = function () {
if ((scriptElem.readyState && scriptElem.readyState !== "complete" && scriptElem.readyState !== "loaded") || scriptdone) {
return false
}
scriptElem.onload = scriptElem.onreadystatechange = null
scriptElem.parentNode.removeChild(scriptElem)
scriptElem = null
scriptdone = true
};
scriptElem.src = url
head.insertBefore(scriptElem, head.firstChild)
}, 0)
// required: shim for FF <= 3.5 not having document.readyState
if (oDOC.readyState == null && oDOC.addEventListener) {
oDOC.readyState = "loading"
oDOC.addEventListener("DOMContentLoaded", function handler() {
oDOC.removeEventListener("DOMContentLoaded", handler, false)
oDOC.readyState = "complete"
}, false)
}
}
}())

14
blog.rb
View file

@ -15,7 +15,9 @@ end
template = File.read(File.join('templates', 'blog', 'post.html'))
Posts = JSON.parse(File.read(File.join(srcdir, 'posts.json')))
# 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 }
@ -33,24 +35,32 @@ posts = Posts['published'].map do |filename|
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]
:next => i > 0 && posts[i - 1],
: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]
})
# 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

View file

@ -14,6 +14,28 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', ready, false)
} else if (window.attachEvent) {
window.attachEvent('onload', ready)
}
function ready() {
function html(id, h) {
document.getElementById(id).innerHTML = h
}
var discussion = document.getElementById('discussion')
, discussionToggle = document.getElementById('discussion-toggle')
, hidden = true
discussionToggle.onclick = function() {
hidden = !hidden
discussion.style.display = hidden ? 'none' : 'block'
discussionToggle.innerHTML = hidden ? '&darr; show discussion &darr;' : '&uarr; hide discussion &uarr;'
SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) {
html('comments', body)
})
}
}
</script>
<header>
<h1><a href=index.html>sjs' blog</a></h1>
@ -274,6 +296,21 @@ addLineNumbersToAllGists();
<a id=prev href=a-preview-of-mach-o-file-generation.html>&larr; A preview of Mach-O file generation</a>
<br style=clear:both>
</div>
<div class=center><a id=discussion-toggle href=#>&darr; show discussion &darr;</a></div>
<div id=discussion>
<div id=comment-form>
<form method=post action=http://bohodev.net:8000/comment>
<input name=from type=hidden value=37signals-chalk-dissected.html>
<p>Name: <input name=name size=30></p>
<p>URL: <input name=url size=30></p>
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
<p><input type=submit value=Add to discussion></p>
</form>
</div>
<div id=comments>
<img id=discussion-spinner src=../assets/spinner.gif>
</div>
</div>
<footer>
<a href=https://twitter.com/_sjs>@_sjs</a>
</footer>

View file

@ -14,6 +14,28 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', ready, false)
} else if (window.attachEvent) {
window.attachEvent('onload', ready)
}
function ready() {
function html(id, h) {
document.getElementById(id).innerHTML = h
}
var discussion = document.getElementById('discussion')
, discussionToggle = document.getElementById('discussion-toggle')
, hidden = true
discussionToggle.onclick = function() {
hidden = !hidden
discussion.style.display = hidden ? 'none' : 'block'
discussionToggle.innerHTML = hidden ? '&darr; show discussion &darr;' : '&uarr; hide discussion &uarr;'
SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) {
html('comments', body)
})
}
}
</script>
<header>
<h1><a href=index.html>sjs' blog</a></h1>
@ -69,6 +91,21 @@ straightforward, an example is in asm/binary.rb, in the #output method.</p>
<a id=next href=37signals-chalk-dissected.html>37signals' Chalk Dissected &rarr;</a>
<br style=clear:both>
</div>
<div class=center><a id=discussion-toggle href=#>&darr; show discussion &darr;</a></div>
<div id=discussion>
<div id=comment-form>
<form method=post action=http://bohodev.net:8000/comment>
<input name=from type=hidden value=a-preview-of-mach-o-file-generation.html>
<p>Name: <input name=name size=30></p>
<p>URL: <input name=url size=30></p>
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
<p><input type=submit value=Add to discussion></p>
</form>
</div>
<div id=comments>
<img id=discussion-spinner src=../assets/spinner.gif>
</div>
</div>
<footer>
<a href=https://twitter.com/_sjs>@_sjs</a>
</footer>

View file

@ -14,6 +14,28 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', ready, false)
} else if (window.attachEvent) {
window.attachEvent('onload', ready)
}
function ready() {
function html(id, h) {
document.getElementById(id).innerHTML = h
}
var discussion = document.getElementById('discussion')
, discussionToggle = document.getElementById('discussion-toggle')
, hidden = true
discussionToggle.onclick = function() {
hidden = !hidden
discussion.style.display = hidden ? 'none' : 'block'
discussionToggle.innerHTML = hidden ? '&darr; show discussion &darr;' : '&uarr; hide discussion &uarr;'
SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) {
html('comments', body)
})
}
}
</script>
<header>
<h1><a href=index.html>sjs' blog</a></h1>
@ -292,6 +314,21 @@ would almost have a useful Mach object file.)</i></p>
<a id=next href=a-preview-of-mach-o-file-generation.html>A preview of Mach-O file generation &rarr;</a>
<br style=clear:both>
</div>
<div class=center><a id=discussion-toggle href=#>&darr; show discussion &darr;</a></div>
<div id=discussion>
<div id=comment-form>
<form method=post action=http://bohodev.net:8000/comment>
<input name=from type=hidden value=basics-of-the-mach-o-file-format.html>
<p>Name: <input name=name size=30></p>
<p>URL: <input name=url size=30></p>
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
<p><input type=submit value=Add to discussion></p>
</form>
</div>
<div id=comments>
<img id=discussion-spinner src=../assets/spinner.gif>
</div>
</div>
<footer>
<a href=https://twitter.com/_sjs>@_sjs</a>
</footer>

View file

@ -14,6 +14,28 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', ready, false)
} else if (window.attachEvent) {
window.attachEvent('onload', ready)
}
function ready() {
function html(id, h) {
document.getElementById(id).innerHTML = h
}
var discussion = document.getElementById('discussion')
, discussionToggle = document.getElementById('discussion-toggle')
, hidden = true
discussionToggle.onclick = function() {
hidden = !hidden
discussion.style.display = hidden ? 'none' : 'block'
discussionToggle.innerHTML = hidden ? '&darr; show discussion &darr;' : '&uarr; hide discussion &uarr;'
SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) {
html('comments', body)
})
}
}
</script>
<header>
<h1><a href=index.html>sjs' blog</a></h1>
@ -140,6 +162,21 @@ of the Mach-O file format</a></i><p>
<a id=next href=basics-of-the-mach-o-file-format.html>Basics of the Mach-O file format &rarr;</a>
<br style=clear:both>
</div>
<div class=center><a id=discussion-toggle href=#>&darr; show discussion &darr;</a></div>
<div id=discussion>
<div id=comment-form>
<form method=post action=http://bohodev.net:8000/comment>
<input name=from type=hidden value=working-with-c-style-structs-in-ruby.html>
<p>Name: <input name=name size=30></p>
<p>URL: <input name=url size=30></p>
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
<p><input type=submit value=Add to discussion></p>
</form>
</div>
<div id=comments>
<img id=discussion-spinner src=../assets/spinner.gif>
</div>
</div>
<footer>
<a href=https://twitter.com/_sjs>@_sjs</a>
</footer>

174
discussd/discussd.js Executable file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env node
var fs = require('fs')
, http = require('http')
, keys = require('keys')
, DefaultOptions = { host: 'localhost'
, port: 2020
, postsFile: '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(e)
process.exit(1)
}
context.posts = posts.published
var n = context.posts.length
console.log((context.posts === null ? '' : 're') + 'loaded ' + n + ' posts...')
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)
})
}
function requestHandler(context) {
function addComment(data) {
if (missingParams(data) || context.posts.indexOf(data.post) === -1) {
console.log('missing params or invalid post title in ' + JSON.stringify(data, null, 2))
return false
}
var comments = context.db.get(data.post) || []
comments.push({ name: data.name
, email: data.email
, body: data.body
, timestamp: Date.now()
})
context.db.set(data.post, comments)
console.log('[' + new Date() + '] add comment ' + JSON.stringify(data, null, 2))
return true
}
return function(req, res) {
var body = ''
, m
if (req.method === 'POST' && req.url.match(/^\/comment\/?$/)) {
req.on('data', function(chunk) { body += chunk })
req.on('end', function() {
var data
try {
data = JSON.parse(body)
} catch (x) {
badRequest(res)
return
}
if (!addComment(data)) {
badRequest(res)
return
}
res.writeHead(204)
res.end()
// TODO mail watchers about the comment
})
} else if (req.method === 'GET' && (m = req.url.match(/^\/comments\/(.*)$/))) {
var post = m[1]
, comments
, s
if (context.posts.indexOf(post) === -1) {
badRequest(res)
return
}
comments = context.db.get(post) || []
s = JSON.stringify({comments: comments})
res.writeHead(200, { 'content-type': 'appliaction/json'
, 'content-length': s.length
})
res.end(s)
} else {
console.log('unhandled request')
console.dir(req)
badRequest(res)
}
}
}
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
}
function badRequest(res) {
var s = 'bad request'
res.writeHead(400, { 'content-type': 'text/plain'
, 'content-length': s.length
})
res.end(s)
}
var missingParams = (function() {
var requiredParams = 'name email body'.split(' ')
return function(d) {
var anyMissing = false
requiredParams.forEach(function(p) {
var v = (d[p] || '').trim()
if (!v) anyMissing = true
})
return anyMissing
}
}())
if (module == require.main) main()

View file

@ -14,6 +14,30 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
{{#comments}}
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', ready, false)
} else if (window.attachEvent) {
window.attachEvent('onload', ready)
}
function ready() {
function html(id, h) {
document.getElementById(id).innerHTML = h
}
var discussion = document.getElementById('discussion')
, discussionToggle = document.getElementById('discussion-toggle')
, hidden = true
discussionToggle.onclick = function() {
hidden = !hidden
discussion.style.display = hidden ? 'none' : 'block'
discussionToggle.innerHTML = hidden ? '&darr; show discussion &darr;' : '&uarr; hide discussion &uarr;'
SJS.request({uri: 'http://bohodev.net:8000/comments/{{filename}}'}, function(err, body) {
html('comments', body)
})
}
}
{{/comments}}
</script>
<header>
<h1><a href=index.html>sjs' blog</a></h1>
@ -40,6 +64,23 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
{{/next}}
<br style=clear:both>
</div>
<div class=center><a id=discussion-toggle href=#>&darr; show discussion &darr;</a></div>
{{#comments}}
<div id=discussion>
<div id=comment-form>
<form method=post action=http://bohodev.net:8000/comment>
<input name=from type=hidden value={{#post}}{{filename}}{{/post}}>
<p>Name: <input name=name size=30></p>
<p>URL: <input name=url size=30></p>
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
<p><input type=submit value=Add to discussion></p>
</form>
</div>
<div id=comments>
<img id=discussion-spinner src=../assets/spinner.gif>
</div>
</div>
{{/comments}}
<footer>
<a href=https://twitter.com/_sjs>@_sjs</a>
</footer>