[WIP] use harp to generate the site
61
Makefile
|
|
@ -1,56 +1,21 @@
|
|||
JAVASCRIPTS=$(shell echo assets/js/*.js)
|
||||
STYLESHEETS=$(shell echo assets/css/*.css)
|
||||
POSTS=$(shell echo _blog/published/*.html) $(shell echo _blog/published/*.md)
|
||||
JSON=harp.json $(shell echo public/*.json) $(shell echo public/*/*.json)
|
||||
EJS=$(shell echo public/*.ejs) $(shell echo public/*/*.ejs)
|
||||
JAVASCRIPTS=$(shell echo public/js/*.js)
|
||||
STYLESHEETS=$(shell echo public/css/*.css)
|
||||
POSTS=$(shell echo public/posts/*.html) $(shell echo public/posts/*.md)
|
||||
|
||||
all: proj blog combine
|
||||
all: compile
|
||||
|
||||
proj: projects.json templates/proj/index.html templates/proj/project.html
|
||||
compile: posts
|
||||
@echo
|
||||
./bin/projects.js projects.json public/proj
|
||||
./bin/compile.sh
|
||||
|
||||
blog: _blog/blog.json templates/blog/index.html templates/blog/post.html $(POSTS)
|
||||
posts: $(POSTS)
|
||||
@echo
|
||||
cd _blog && git pull
|
||||
./bin/blog.rb _blog public
|
||||
./bin/posts.rb public public
|
||||
|
||||
minify: $(JAVASCRIPTS) $(STYLESHEETS)
|
||||
publish: compile
|
||||
@echo
|
||||
./bin/minify.sh
|
||||
./bin/publish.sh --delete public/
|
||||
|
||||
combine: minify $(JAVASCRIPTS) $(STYLESHEETS)
|
||||
@echo
|
||||
./bin/combine.sh
|
||||
|
||||
publish_assets: combine
|
||||
@echo
|
||||
./bin/publish.sh --delete public/css public/images public/js
|
||||
./bin/publish.sh public/f
|
||||
|
||||
publish_blog: blog publish_assets
|
||||
@echo
|
||||
./bin/publish.sh --delete public/blog
|
||||
scp public/blog/posts.json bohodev.net:discussd/posts.json
|
||||
scp discussd/discussd.js bohodev.net:discussd/discussd.js
|
||||
scp public/s42/.htaccess samhuri.net:s42.ca/.htaccess
|
||||
ssh bohodev.net bin/restart-discussd.sh
|
||||
|
||||
publish_proj: proj publish_assets
|
||||
@echo
|
||||
./bin/publish.sh --delete public/proj
|
||||
|
||||
publish_index: public/index.html
|
||||
@echo
|
||||
./bin/publish.sh public/index.html
|
||||
|
||||
publish: publish_index publish_blog publish_proj
|
||||
@echo
|
||||
./bin/publish.sh public/.htaccess
|
||||
./bin/publish.sh public/favicon.ico
|
||||
|
||||
clean:
|
||||
rm -rf public/proj/*
|
||||
rm -rf public/blog/*
|
||||
rm public/css/*.css
|
||||
rm public/js/*.js
|
||||
|
||||
.PHONY: proj blog
|
||||
.PHONY: publish
|
||||
|
|
|
|||
6
TODO
|
|
@ -1,6 +0,0 @@
|
|||
TODO
|
||||
====
|
||||
|
||||
* remove comments
|
||||
|
||||
* use harp
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
body { margin: 0
|
||||
; padding: 0
|
||||
}
|
||||
|
||||
h1 { margin: 0
|
||||
; padding: 0.2em
|
||||
; color: #9ab
|
||||
}
|
||||
|
||||
.center { text-align: center
|
||||
; font-size: 1.2em
|
||||
}
|
||||
|
||||
.hidden { display: none }
|
||||
|
||||
#index { width: 80%
|
||||
; min-width: 300px
|
||||
; max-width: 800px
|
||||
; border: solid 1px #999
|
||||
; -moz-border-radius: 10px
|
||||
; -webkit-border-radius: 10px
|
||||
; border-radius: 10px
|
||||
; background-color: #eee
|
||||
; margin: 1em auto
|
||||
; padding: 1em
|
||||
; font-size: 1.2em
|
||||
; line-height: 1.5em
|
||||
; list-style-type: none
|
||||
}
|
||||
|
||||
.date { float: right }
|
||||
|
||||
#article,
|
||||
article { width: 80%
|
||||
; min-width: 400px
|
||||
; max-width: 800px
|
||||
; margin: 0.6em auto
|
||||
; font-size: 1.2em
|
||||
; line-height: 1.4em
|
||||
; color: #222
|
||||
}
|
||||
|
||||
#article h1,
|
||||
article h1 { text-align: left
|
||||
; font-size: 2em
|
||||
; line-height: 1.2em
|
||||
; font-weight: normal
|
||||
; color: #222
|
||||
; margin: 0
|
||||
; padding-left: 0
|
||||
}
|
||||
|
||||
#article h1 a,
|
||||
article h1 a { color: #222
|
||||
; text-decoration: underline
|
||||
; border-bottom: none
|
||||
; text-shadow: #ccc 1px 1px 5px
|
||||
; -webkit-transition: text-shadow 0.4s ease-in
|
||||
}
|
||||
|
||||
#article h1 a:hover,
|
||||
article h1 a:hover { text-shadow: 1px 1px 6px #ffc
|
||||
; color: #000
|
||||
}
|
||||
|
||||
#article h2,
|
||||
article h2 { font-size: 1.8em
|
||||
; font-weight: normal
|
||||
; text-align: left
|
||||
; margin: 1em 0
|
||||
; padding: 0
|
||||
; color: #222
|
||||
}
|
||||
|
||||
#article h3,
|
||||
article h3 { font-size: 1.6em
|
||||
; font-weight: normal
|
||||
}
|
||||
|
||||
.time,
|
||||
time { color: #444
|
||||
; font-size: 1.2em
|
||||
}
|
||||
|
||||
.permalink { font-size: 1em }
|
||||
|
||||
.gist { font-size: 0.8em }
|
||||
|
||||
/* 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: 600px
|
||||
; margin: 0 auto
|
||||
}
|
||||
|
||||
.comment { color: #555
|
||||
; border-top: solid 2px #ccc
|
||||
; padding-bottom: 2em
|
||||
; margin-bottom: 2em
|
||||
}
|
||||
|
||||
.comment big { font-size: 2em
|
||||
; font-family: Verdana, sans-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
|
||||
}
|
||||
|
||||
pre { background-color: #eeeef3
|
||||
; margin: 0.5em 1em 1em
|
||||
; padding: 0.5em
|
||||
; border: dashed 1px #ccc
|
||||
}
|
||||
|
||||
footer { margin: 0 auto
|
||||
; padding: 0.2em 0
|
||||
; border-top: solid 1px #ddd
|
||||
; clear: both
|
||||
; width: 80%
|
||||
}
|
||||
|
||||
footer p { margin: 0.5em }
|
||||
|
||||
footer a { border-bottom: none
|
||||
; color: #25c
|
||||
; font-size: 1.2em
|
||||
; text-decoration: none
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
ul { behavior: none
|
||||
; padding-bottom: 25px
|
||||
}
|
||||
|
||||
img { behavior: url(../js/iepngfix.htc)
|
||||
; behavior: url(../../js/iepngfix.htc)
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
ul#projects li { list-style-type: none }
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
/* phones and iPad */
|
||||
|
||||
@media only screen and (orientation: portrait) and (min-device-width: 768px) and (max-device-width: 1024px),
|
||||
only screen and (min-device-width: 320px) and (max-device-width: 480px),
|
||||
only screen and (max-device-width: 800px)
|
||||
{
|
||||
ul.nav { padding: 0.5em
|
||||
; width: 60%
|
||||
; max-width: 600px
|
||||
}
|
||||
|
||||
ul.nav li { display: block
|
||||
; font-size: 1.5em
|
||||
; line-height: 1.8em
|
||||
}
|
||||
|
||||
ul.nav li:after { content: '' }
|
||||
}
|
||||
|
||||
/* phones */
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 480px),
|
||||
handheld and (max-device-width: 800px)
|
||||
{
|
||||
/* common */
|
||||
|
||||
h1 { font-size: 2em
|
||||
; margin-top: 0.5em
|
||||
}
|
||||
h2 { font-size: 1.3em; line-height: 1.2em }
|
||||
|
||||
.navbar { font-size: 0.9em }
|
||||
.navbar { width: 32% }
|
||||
#breadcrumbs { margin-left: 5px }
|
||||
|
||||
#show-posts { margin-top: 1em
|
||||
; font-size: 0.8em
|
||||
}
|
||||
|
||||
#forkme { display: none }
|
||||
|
||||
ul.nav { width: 80% }
|
||||
|
||||
ul.nav li { font-size: 1.4em
|
||||
; line-height: 1.6em
|
||||
}
|
||||
|
||||
td { font-size: 1em
|
||||
; line-height: 1.1em
|
||||
}
|
||||
|
||||
#blog img { max-width: 100% }
|
||||
|
||||
#index { width: 90%
|
||||
; min-width: 200px
|
||||
; margin: 0.3em auto 1em
|
||||
; padding: 0.5em
|
||||
; font-size: 1em
|
||||
}
|
||||
|
||||
#index li > span.date { display: block
|
||||
; float: none
|
||||
; color: #666
|
||||
; font-size: 0.8em
|
||||
}
|
||||
|
||||
#blog #article h1,
|
||||
#blog article h1 { font-size: 1.6em
|
||||
; line-height: 1.2em
|
||||
; margin-top: 0
|
||||
}
|
||||
|
||||
#blog article h2 { font-size: 1.4em }
|
||||
|
||||
#article,
|
||||
article { min-width: 310px
|
||||
; margin: 0
|
||||
; padding: 0.6em 0.4em
|
||||
; font-size: 0.8em
|
||||
}
|
||||
|
||||
.time,
|
||||
time { font-size: 1.0em }
|
||||
|
||||
pre, .gist { font-size: 0.8em }
|
||||
|
||||
#comment-stuff { padding: 0
|
||||
; margin-top: 2em
|
||||
}
|
||||
|
||||
#comments { width: 100% }
|
||||
|
||||
#comment-form { width: 90%
|
||||
; margin: 0 auto
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
textarea { font-size: 1.2em
|
||||
; width: 95%
|
||||
}
|
||||
|
||||
input[type=submit] { font-size: 1em }
|
||||
|
||||
/* proj */
|
||||
#info { width: 70%
|
||||
; padding: 0 1em
|
||||
}
|
||||
|
||||
#info > div { clear: left
|
||||
; width: 100%
|
||||
; max-width: 100%
|
||||
; padding: 0.5em 0.2em 1em
|
||||
; border-left: none
|
||||
; font-size: 1em
|
||||
}
|
||||
|
||||
#stats { font-size: 1em; margin-bottom: 0.5em }
|
||||
|
||||
footer { margin: 0
|
||||
; padding: 0.5em 0
|
||||
; font-size: 1em
|
||||
; width: 100%
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* landscape */
|
||||
|
||||
@media only screen and (orientation: landscape) and (min-device-width: 768px) and (max-device-width: 1024px),
|
||||
only screen and (orientation: landscape) and (min-device-width: 320px) and (max-device-width: 480px),
|
||||
handheld and (orientation: landscape) and (max-device-width: 800px)
|
||||
{
|
||||
body { font-size: 0.8em }
|
||||
}
|
||||
|
||||
|
||||
/* iPad portrait */
|
||||
@media only screen and (orientation: portrait) and (min-device-width: 768px) and (max-device-width: 1024px)
|
||||
{
|
||||
article > header > h1 { font-size: 1.8em }
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
td { font-size: 1.5em
|
||||
; line-height: 1.6em
|
||||
}
|
||||
|
||||
td:nth-child(2) { padding: 0 10px }
|
||||
|
||||
.highlight { font-size: 1.2em }
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
#stats a { text-decoration: none }
|
||||
|
||||
#info { text-align: center
|
||||
; margin: 1em auto
|
||||
; padding: 1em
|
||||
; border: solid 1px #ccc
|
||||
; width: 90%
|
||||
; max-width: 950px
|
||||
; background-color: #fff
|
||||
; -moz-border-radius: 20px
|
||||
; -webkit-border-radius: 20px
|
||||
; border-radius: 20px
|
||||
; behavior: url(../js/border-radius.htc)
|
||||
; behavior: url(../../js/border-radius.htc)
|
||||
}
|
||||
|
||||
h4 { margin: 0.5em 0 0.7em }
|
||||
|
||||
#info > div { text-align: center
|
||||
; font-size: 1.3em
|
||||
; width: 31%
|
||||
; max-width: 400px
|
||||
; float: left
|
||||
; display: inline
|
||||
; padding: 0.5em 0.2em
|
||||
; border-left: dashed 1px #aaa
|
||||
}
|
||||
|
||||
#info > div:first-child { border-left: none }
|
||||
|
||||
#info ul { list-style-type: none
|
||||
; text-align: center
|
||||
; padding: 0
|
||||
; margin: 0
|
||||
}
|
||||
|
||||
#info li { padding: 0.2em 0
|
||||
; margin: 0
|
||||
}
|
||||
|
||||
#info > br.clear { clear: both }
|
||||
|
||||
#contributors-box a { line-height: 1.8em }
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
body { background-color: #f7f7f7
|
||||
; color: #222
|
||||
; font-family: 'Helvetica Neue', Verdana, sans-serif
|
||||
}
|
||||
|
||||
h1 { text-align: center
|
||||
; font-size: 2em
|
||||
; font-weight: normal
|
||||
; margin: 0.8em 0 0.4em
|
||||
; padding: 0
|
||||
}
|
||||
|
||||
h2 { text-align: center
|
||||
; font-size: 1.7em
|
||||
; line-height: 1.1em
|
||||
; font-weight: normal
|
||||
; margin: 0.2em 0 1em
|
||||
; padding: 0
|
||||
}
|
||||
|
||||
a { color: #0E539C }
|
||||
|
||||
a.img { border: none }
|
||||
|
||||
.navbar { display: inline-block
|
||||
; width: 33%
|
||||
; font-size: 1.5em
|
||||
; line-height: 1.8em
|
||||
; margin: 0
|
||||
; padding: 0
|
||||
}
|
||||
|
||||
.navbar a { text-shadow: none }
|
||||
|
||||
#breadcrumbs a { color: #222 }
|
||||
#title { text-align: center }
|
||||
#archive { text-align: right }
|
||||
|
||||
#forkme { position: absolute
|
||||
; top: 0
|
||||
; right: 0
|
||||
; border: none
|
||||
}
|
||||
|
||||
ul.nav { text-align: center
|
||||
; max-width: 400px
|
||||
; margin: 0 auto
|
||||
; padding: 1em
|
||||
; border: solid 1px #ccc
|
||||
; background-color: #fff
|
||||
; -moz-border-radius: 20px
|
||||
; -webkit-border-radius: 20px
|
||||
; border-radius: 20px
|
||||
; behavior: url(js/border-radius.htc)
|
||||
; behavior: url(../js/border-radius.htc)
|
||||
}
|
||||
|
||||
ul.nav li { display: block
|
||||
; font-size: 1.6em
|
||||
; line-height: 1.8em
|
||||
; margin: 0
|
||||
; padding: 0
|
||||
}
|
||||
|
||||
ul.nav li a { padding: 5px
|
||||
; text-decoration: none
|
||||
; border-bottom: solid 1px #fff
|
||||
; text-shadow: #ccc 2px 2px 3px
|
||||
}
|
||||
ul.nav li a:visited { color: #227 }
|
||||
|
||||
ul.nav li a:hover,
|
||||
ul.nav li a:active { text-shadow: #cca 2px 2px 3px
|
||||
; border-bottom: solid 1px #aaa
|
||||
}
|
||||
|
||||
ul.nav li a:active { text-shadow: none }
|
||||
|
||||
footer { text-align: center
|
||||
; font-size: 1.2em
|
||||
; margin: 1em
|
||||
}
|
||||
|
||||
footer a { border-bottom: none }
|
||||
|
||||
#promote-js { margin-top: 3em
|
||||
; text-align: center
|
||||
}
|
||||
|
||||
#promote-js img { border: none }
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
;(function() {
|
||||
if (typeof console === 'undefined')
|
||||
window.console = {}
|
||||
if (typeof console.log !== 'function')
|
||||
window.console.log = function(){}
|
||||
if (typeof console.dir !== 'function')
|
||||
window.console.dir = 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(cb) {
|
||||
SJS.request({uri: getCommentsURL(SJS.filename)}, function(err, request, body) {
|
||||
if (err) {
|
||||
if (typeof cb === 'function') cb(err)
|
||||
return
|
||||
}
|
||||
var data
|
||||
, comments
|
||||
, h = ''
|
||||
try {
|
||||
data = JSON.parse(body)
|
||||
}
|
||||
catch (e) {
|
||||
console.log('not json -> ' + body)
|
||||
return
|
||||
}
|
||||
comments = data.comments
|
||||
if (comments && comments.length) {
|
||||
h = data.comments.map(function(c) {
|
||||
return tmpl('comment_tmpl', c)
|
||||
}).join('')
|
||||
$('#comments').html(h)
|
||||
}
|
||||
if (typeof cb === 'function') cb()
|
||||
})
|
||||
}
|
||||
|
||||
function showComments(cb) {
|
||||
$('#sd-container').remove()
|
||||
getComments(function(err) {
|
||||
$('#comments-spinner').hide()
|
||||
if (err) {
|
||||
$('#comments').text('derp')
|
||||
if (typeof cb === 'function') cb(err)
|
||||
}
|
||||
else {
|
||||
$('#comment-stuff').slideDown(1.5, function() {
|
||||
if (typeof cb === 'function') cb()
|
||||
else this.scrollIntoView(true)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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.log('not json -> ' + body)
|
||||
return
|
||||
}
|
||||
n = data.count
|
||||
$('#sd').text(n > 0 ? 'show the discussion (' + n + ')' : 'start the discussion')
|
||||
})
|
||||
|
||||
// jump to comment if linked directly
|
||||
var hash = window.location.hash || ''
|
||||
if (/^#comment-\d+/.test(hash)) {
|
||||
showComments(function (err) {
|
||||
if (!err) {
|
||||
window.location.hash = ''
|
||||
window.location.hash = hash
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$('#sd').click(showComments)
|
||||
|
||||
var showdown = new Showdown.converter()
|
||||
, tzOffset = -new Date().getTimezoneOffset() * 60 * 1000
|
||||
|
||||
$('#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?")
|
||||
document.getElementById('thoughts').focus()
|
||||
return false
|
||||
}
|
||||
comment.timestamp = +new Date() + tzOffset
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
$('#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
|
||||
})
|
||||
})
|
||||
}());
|
||||
|
|
@ -1,766 +0,0 @@
|
|||
/// gitter
|
||||
/// http://github.com/samsonjs/gitter
|
||||
/// @_sjs
|
||||
///
|
||||
/// Copyright 2010 - 2012 Sami Samhuri <sami@samhuri.net>
|
||||
/// MIT License
|
||||
|
||||
(function() {
|
||||
"use strict"
|
||||
|
||||
var global = (function() { return this || (1, eval)('this') }())
|
||||
, isBrowser = 'document' in global
|
||||
, ie
|
||||
|
||||
if (isBrowser) {
|
||||
ie = (function() {
|
||||
var undef
|
||||
, v = 3
|
||||
, div = document.createElement('div')
|
||||
, all = div.getElementsByTagName('i')
|
||||
|
||||
while (
|
||||
div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
|
||||
all[0]
|
||||
);
|
||||
|
||||
return v > 4 ? v : undef
|
||||
}())
|
||||
}
|
||||
|
||||
var inherits
|
||||
if ('create' in Object) {
|
||||
// util.inherits from node
|
||||
inherits = function(ctor, superCtor) {
|
||||
ctor.super_ = superCtor
|
||||
ctor.prototype = Object.create(superCtor.prototype, {
|
||||
constructor: {
|
||||
value: ctor,
|
||||
enumerable: false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
else if ([].__proto__) {
|
||||
inherits = function(ctor, superCtor) {
|
||||
ctor.super_ = superCtor
|
||||
ctor.prototype.__proto__ = superCtor.prototype
|
||||
ctor.prototype.constructor = ctor
|
||||
}
|
||||
}
|
||||
else { // ie8
|
||||
var __hasProp = Object.prototype.hasOwnProperty
|
||||
inherits = function(child, parent) {
|
||||
for (var key in parent) {
|
||||
if (__hasProp.call(parent, key)) child[key] = parent[key]
|
||||
}
|
||||
function ctor() { this.constructor = child }
|
||||
ctor.prototype = parent.prototype
|
||||
child.prototype = new ctor
|
||||
child.__super__ = parent.prototype
|
||||
return child
|
||||
}
|
||||
}
|
||||
|
||||
var api = {
|
||||
// Blob
|
||||
blob: function(user, repo, sha, cb) {
|
||||
return new Blob(user, repo, sha, cb)
|
||||
}
|
||||
|
||||
// Branch
|
||||
, branch: function(user, repo, name, cb) {
|
||||
return new Branch(user, repo, name, cb)
|
||||
}
|
||||
|
||||
// Commit
|
||||
, commit: function(user, repo, sha, cb) {
|
||||
return new Commit(user, repo, sha, cb)
|
||||
}
|
||||
|
||||
// Download
|
||||
, download: function(user, repo, id, cb) {
|
||||
return new Download(user, repo, id, cb)
|
||||
}
|
||||
|
||||
// Issue
|
||||
, issue: function(user, repo, id, cb) {
|
||||
return new Issue(user, repo, id, cb)
|
||||
}
|
||||
|
||||
// Organization
|
||||
, org: function(name, cb) {
|
||||
return new Org(name, cb)
|
||||
}
|
||||
, members: function(name, cb) {
|
||||
return new Org(name).fetchMembers(cb)
|
||||
}
|
||||
|
||||
// Repo
|
||||
, repo: function(user, repo, cb) {
|
||||
return new Repo(user, repo, cb)
|
||||
}
|
||||
, branches: function(user, repo, cb) {
|
||||
return new Repo(user, repo).fetchBranches(cb)
|
||||
}
|
||||
, collaborators: function(user, repo, cb) {
|
||||
return new Repo(user, repo).fetchCollaborators(cb)
|
||||
}
|
||||
, contributors: function(user, repo, cb) {
|
||||
return new Repo(user, repo).fetchContributors(cb)
|
||||
}
|
||||
, downloads: function(user, repo, cb) {
|
||||
return new Repo(user, repo).fetchDownloads(cb)
|
||||
}
|
||||
, forks: function(user, repo, cb) {
|
||||
return new Repo(user, repo).fetchForks(cb)
|
||||
}
|
||||
, issues: function(user, repo, cb) {
|
||||
return new Repo(user, repo).fetchIssues(cb)
|
||||
}
|
||||
, languages: function(user, repo, cb) {
|
||||
return new Repo(user, repo).fetchLanguages(cb)
|
||||
}
|
||||
, tags: function(user, repo, cb) {
|
||||
return new Repo(user, repo).fetchTags(cb)
|
||||
}
|
||||
, watchers: function(user, repo, cb) {
|
||||
return new Repo(user, repo).fetchWatchers(cb)
|
||||
}
|
||||
|
||||
, ref: function(user, repo, name, cb) {
|
||||
return new Ref(user, repo, name, cb)
|
||||
}
|
||||
|
||||
// Tag
|
||||
, tag: function(user, repo, name, cb) {
|
||||
return new Tag(user, repo, name, cb)
|
||||
}
|
||||
|
||||
// Tree
|
||||
, tree: function(user, repo, sha, cb) {
|
||||
return new Tree(user, repo, sha, cb)
|
||||
}
|
||||
|
||||
// User
|
||||
, user: function(login, cb) {
|
||||
return new User(login, cb)
|
||||
}
|
||||
, followers: function(login, cb) {
|
||||
return new User(login).fetchFollowers(cb)
|
||||
}
|
||||
, following: function(login, cb) {
|
||||
return new User(login).fetchFollowing(cb)
|
||||
}
|
||||
, repos: function(login, cb) {
|
||||
return new User(login).fetchRepos(cb)
|
||||
}
|
||||
, watched: function(login, cb) {
|
||||
return new User(login).fetchWatched(cb)
|
||||
}
|
||||
|
||||
// Why not, expose the resources directly as well.
|
||||
, Blob: Blob
|
||||
, Branch: Branch
|
||||
, Commit: Commit
|
||||
, Download: Download
|
||||
, Issue: Issue
|
||||
, Org: Org
|
||||
, Ref: Ref
|
||||
, Repo: Repo
|
||||
, Tree: Tree
|
||||
, User: User
|
||||
}
|
||||
|
||||
// when running in the browser request is set later, in shim()
|
||||
var request
|
||||
|
||||
if (isBrowser) {
|
||||
shim()
|
||||
global.GITR = api
|
||||
}
|
||||
else {
|
||||
var https = require('https')
|
||||
request = function(options, cb) {
|
||||
var req = https.request(options, function(response) {
|
||||
var bodyParts = []
|
||||
response.on('data', function(b) { bodyParts.push(b) })
|
||||
response.on('end', function() {
|
||||
var body = bodyParts.join('')
|
||||
if (response.statusCode === 200) {
|
||||
cb(null, body, response)
|
||||
}
|
||||
else {
|
||||
console.dir(options, response, body)
|
||||
var err = new Error('http error')
|
||||
err.statusCode = response.statusCode
|
||||
err.body = body
|
||||
cb(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
req.end()
|
||||
req.on('error', function(err) { cb(err) })
|
||||
}
|
||||
module.exports = api
|
||||
}
|
||||
|
||||
|
||||
// Generic Resource //
|
||||
//
|
||||
// Used as the prototype by createResource. Provides
|
||||
// methods for fetching the resource and related
|
||||
// sub-resources.
|
||||
function Resource() {}
|
||||
|
||||
// Fetch data for this resource and pass it to the
|
||||
// callback after mixing the data into the object.
|
||||
// Data is also available via the `data` property.
|
||||
Resource.prototype.fetch = function(cb) {
|
||||
if (this.data) {
|
||||
cb(null, this.data)
|
||||
}
|
||||
else {
|
||||
var self = this
|
||||
fetch(this.path, function(err, data) {
|
||||
// console.log('FETCH', self.path, err, data)
|
||||
if (err) {
|
||||
// console.log(err)
|
||||
}
|
||||
else {
|
||||
self.data = data
|
||||
mixin(self, data)
|
||||
}
|
||||
if (typeof cb === 'function') {
|
||||
cb.call(self, err, data)
|
||||
}
|
||||
})
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
Resource.prototype.fetchSubResource = function(thing, cb) {
|
||||
if (this['_' + thing]) {
|
||||
cb(null, this['_' + thing])
|
||||
}
|
||||
else {
|
||||
var self = this
|
||||
fetch(this.path + '/' + thing, function(err, data) {
|
||||
// console.log('FETCH SUBRESOURCE', self.path, thing, err, data)
|
||||
if (err) {
|
||||
// console.log(self.path, err)
|
||||
}
|
||||
else {
|
||||
self['_' + thing] = data
|
||||
}
|
||||
if (typeof cb === 'function') {
|
||||
cb.call(self, err, data)
|
||||
}
|
||||
})
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
var __slice = Array.prototype.slice
|
||||
|
||||
// Create a resource w/ Resource as the prototype.
|
||||
//
|
||||
// spec: an object with the following properties:
|
||||
//
|
||||
// - constructor: a constructor function
|
||||
// - has: a list of resources belonging to this resource
|
||||
//
|
||||
// Typically the constructor accepts one or more arguments specifying
|
||||
// the name or pieces of info identifying the specific resource and
|
||||
// used to build the URL to fetch it. It also accepts an optional
|
||||
// callback as the last parameter.
|
||||
//
|
||||
// The constructor must set the `path` property which is used to
|
||||
// fetch the resource.
|
||||
//
|
||||
// If a callback is provided then the resource is immediately
|
||||
// fetched and the callback is threaded through to the `fetch`
|
||||
// method. The callback function has the signature
|
||||
// `function(err, data)`.
|
||||
//
|
||||
// The `has` list specifies sub-resources, e.g. a user has repos,
|
||||
// followers, etc. An organization has members.
|
||||
//
|
||||
// Each related sub-resource gets a method named appropriately,
|
||||
// e.g. the User resource has followers so User objects have a
|
||||
// `fetchFollowers` method.
|
||||
function createResource(spec) {
|
||||
var subResources = spec.has ? __slice.call(spec.has) : null
|
||||
, resource = function(/* ..., cb */) {
|
||||
var args = __slice.call(arguments)
|
||||
, lastArgIsCallback = typeof args[args.length - 1] === 'function'
|
||||
, cb = lastArgIsCallback ? args.pop() : null
|
||||
, result = spec.constructor.apply(this, args)
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
this.fetch(cb)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
inherits(resource, Resource)
|
||||
|
||||
if (subResources) {
|
||||
subResources.forEach(function(thing) {
|
||||
var fnName = 'fetch' + toTitleCase(thing)
|
||||
resource.prototype[fnName] = function(cb) {
|
||||
return this.fetchSubResource(thing, cb)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return resource
|
||||
}
|
||||
|
||||
|
||||
// Define Resources //
|
||||
|
||||
var Blob = createResource({
|
||||
constructor: function(user, repo, sha) {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.sha = sha
|
||||
this.path = '/repos/' + [user, repo].map(encodeURIComponent).join('/') + '/git/blobs/' + sha
|
||||
}
|
||||
})
|
||||
|
||||
var Branch = createResource({
|
||||
constructor: function (user, repo, name) {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.name = name
|
||||
this.path = '/repos/' + [user, repo].map(encodeURIComponent).join('/') + '/git/refs/heads/' + name
|
||||
}
|
||||
})
|
||||
|
||||
var Commit = createResource({
|
||||
constructor: function Commit(user, repo, sha) {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.sha = sha
|
||||
this.path = '/repos/' + [user, repo].map(encodeURIComponent).join('/') + '/git/commits/' + sha
|
||||
}
|
||||
})
|
||||
|
||||
var Download = createResource({
|
||||
constructor: function(user, repo, id) {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.id = id
|
||||
this.path = '/repos/' + [user, repo].map(encodeURIComponent).join('/') + '/downloads/' + id
|
||||
}
|
||||
})
|
||||
|
||||
var Issue = createResource({
|
||||
constructor: function(user, repo, id) {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.id = id
|
||||
this.path = '/repos/' + [user, repo].map(encodeURIComponent).join('/') + '/issues/' + id
|
||||
}
|
||||
})
|
||||
|
||||
var Org = createResource({
|
||||
constructor: function(name) {
|
||||
this.name = name
|
||||
this.path = '/orgs/' + encodeURIComponent(nam)
|
||||
}
|
||||
|
||||
, has: 'members repos'.split(' ')
|
||||
})
|
||||
|
||||
var Ref = createResource({
|
||||
constructor: function (user, repo, name) {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.name = name
|
||||
this.path = '/repos/' + [user, repo].map(encodeURIComponent).join('/') + '/git/refs/' + name
|
||||
}
|
||||
})
|
||||
|
||||
var Repo = createResource({
|
||||
constructor: function(user, repo) {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.path = '/repos/' + [user, repo].map(encodeURIComponent).join('/')
|
||||
}
|
||||
|
||||
, has: ('branches collaborators contributors downloads' +
|
||||
' forks languages tags teams watchers').split(' ')
|
||||
})
|
||||
|
||||
var Tag = createResource({
|
||||
constructor: function (user, repo, name) {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.name = name
|
||||
this.path = '/repos/' + [user, repo].map(encodeURIComponent).join('/') + '/git/refs/tags/' + name
|
||||
}
|
||||
})
|
||||
|
||||
var Tree = createResource({
|
||||
constructor: function(user, repo, sha) {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.sha = sha
|
||||
this.path = '/repos/' + [user, repo].map(encodeURIComponent).join('/') + '/git/trees/' + sha
|
||||
}
|
||||
})
|
||||
|
||||
var User = createResource({
|
||||
constructor: function(login) {
|
||||
// Allow creating a user from an object returned by the API
|
||||
if (login.login) {
|
||||
login = login.login
|
||||
}
|
||||
this.login = login
|
||||
this.path = '/users/' + encodeURIComponent(login)
|
||||
}
|
||||
|
||||
, has: 'followers following repos watched'.split(' ')
|
||||
})
|
||||
|
||||
|
||||
// Fetch data from github. JSON is parsed and keys are camelized.
|
||||
//
|
||||
// path: the path to the resource
|
||||
// cb: callback(err, data)
|
||||
function fetch(path, cb) {
|
||||
request({ host: 'api.github.com', path: path }, function(err, body, response) {
|
||||
// JSONP requests in the browser return the object directly
|
||||
var data = body
|
||||
|
||||
// Requests in Node return json text, try to parse it
|
||||
if (response && /json/i.exec(response.headers['content-type'])) {
|
||||
try {
|
||||
data = JSON.parse(body)
|
||||
}
|
||||
catch (e) {
|
||||
err = e
|
||||
data = null
|
||||
}
|
||||
}
|
||||
|
||||
cb(err, camelize(data))
|
||||
})
|
||||
}
|
||||
|
||||
// created_at => createdAt
|
||||
function camel(s) {
|
||||
return s.replace(/_(.)/g, function(_, l) { return l.toUpperCase() })
|
||||
}
|
||||
|
||||
// camelize all keys of an object, or all objects in an array
|
||||
function camelize(obj) {
|
||||
if (!obj || typeof obj === 'string') return obj
|
||||
if (Array.isArray(obj)) return obj.map(camelize)
|
||||
if (typeof obj === 'object') {
|
||||
return Object.keys(obj).reduce(function(camelizedObj, k) {
|
||||
camelizedObj[camel(k)] = camelize(obj[k])
|
||||
return camelizedObj
|
||||
}, {})
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
function toTitleCase(s) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1)
|
||||
}
|
||||
|
||||
function mixin(a, b) {
|
||||
for (var k in b) {
|
||||
if (b.hasOwnProperty(k)) a[k] = b[k]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Browser Utilities //
|
||||
|
||||
function shim() {
|
||||
shimBind()
|
||||
shimES5()
|
||||
shimRequest()
|
||||
}
|
||||
|
||||
function shimBind() {
|
||||
// bind from Prototype
|
||||
if (!Function.prototype.bind) {
|
||||
(function(){
|
||||
function update(array, args) {
|
||||
var arrayLength = array.length, length = args.length
|
||||
while (length--) array[arrayLength + length] = args[length]
|
||||
return array
|
||||
}
|
||||
function merge(array, args) {
|
||||
array = __slice.call(array, 0)
|
||||
return update(array, args)
|
||||
}
|
||||
Function.prototype.bind = function(context) {
|
||||
if (arguments.length < 2 && typeof arguments[0] === 'undefined') return this
|
||||
var __method = this, args = __slice.call(arguments, 1)
|
||||
return function() {
|
||||
var a = merge(args, arguments)
|
||||
return __method.apply(context, a)
|
||||
}
|
||||
}
|
||||
}())
|
||||
}
|
||||
}
|
||||
|
||||
// a few functions from Kris Kowal's es5-shim
|
||||
// https://github.com/kriskowal/es5-shim
|
||||
function shimES5() {
|
||||
var has = Object.prototype.hasOwnProperty
|
||||
|
||||
// ES5 15.2.3.6
|
||||
if (!Object.defineProperty || ie === 8) { // ie8
|
||||
Object.defineProperty = function(object, property, descriptor) {
|
||||
if (typeof descriptor == "object" && object.__defineGetter__) {
|
||||
if (has.call(descriptor, "value")) {
|
||||
if (!object.__lookupGetter__(property) && !object.__lookupSetter__(property)) {
|
||||
// data property defined and no pre-existing accessors
|
||||
object[property] = descriptor.value
|
||||
}
|
||||
if (has.call(descriptor, "get") || has.call(descriptor, "set")) {
|
||||
// descriptor has a value property but accessor already exists
|
||||
throw new TypeError("Object doesn't support this action")
|
||||
}
|
||||
}
|
||||
// fail silently if "writable", "enumerable", or "configurable"
|
||||
// are requested but not supported
|
||||
else if (typeof descriptor.get == "function") {
|
||||
object.__defineGetter__(property, descriptor.get)
|
||||
}
|
||||
if (typeof descriptor.set == "function") {
|
||||
object.__defineSetter__(property, descriptor.set)
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
}
|
||||
|
||||
// ES5 15.2.3.14
|
||||
// http://whattheheadsaid.com/2010/10/a-safer-object-keys-compatibility-implementation
|
||||
if (!Object.keys) { // ie8
|
||||
(function() {
|
||||
var hasDontEnumBug = true,
|
||||
dontEnums = [
|
||||
'toString',
|
||||
'toLocaleString',
|
||||
'valueOf',
|
||||
'hasOwnProperty',
|
||||
'isPrototypeOf',
|
||||
'propertyIsEnumerable',
|
||||
'constructor'
|
||||
],
|
||||
dontEnumsLength = dontEnums.length
|
||||
|
||||
for (var key in {"toString": null}) {
|
||||
hasDontEnumBug = false
|
||||
}
|
||||
|
||||
Object.keys = function (object) {
|
||||
|
||||
if (
|
||||
typeof object !== "object" && typeof object !== "function"
|
||||
|| object === null
|
||||
)
|
||||
throw new TypeError("Object.keys called on a non-object")
|
||||
|
||||
var keys = []
|
||||
for (var name in object) {
|
||||
if (has.call(object, name)) {
|
||||
keys.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDontEnumBug) {
|
||||
for (var i = 0, ii = dontEnumsLength; i < ii; i++) {
|
||||
var dontEnum = dontEnums[i]
|
||||
if (has.call(object, dontEnum)) {
|
||||
keys.push(dontEnum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
}())
|
||||
} // Object.keys
|
||||
|
||||
//
|
||||
// Array
|
||||
// =====
|
||||
//
|
||||
|
||||
// ES5 15.4.3.2
|
||||
if (!Array.isArray) {
|
||||
Array.isArray = function(obj) {
|
||||
return Object.prototype.toString.call(obj) == "[object Array]"
|
||||
}
|
||||
}
|
||||
|
||||
// ES5 15.4.4.18
|
||||
if (!Array.prototype.forEach) { // ie8
|
||||
Array.prototype.forEach = function(block, thisObject) {
|
||||
var len = this.length >>> 0
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (i in this) {
|
||||
block.call(thisObject, this[i], i, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ES5 15.4.4.19
|
||||
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map
|
||||
if (!Array.prototype.map) { // ie8
|
||||
Array.prototype.map = function(fun /*, thisp*/) {
|
||||
var len = this.length >>> 0
|
||||
if (typeof fun != "function") {
|
||||
throw new TypeError()
|
||||
}
|
||||
|
||||
var res = new Array(len)
|
||||
var thisp = arguments[1]
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (i in this) {
|
||||
res[i] = fun.call(thisp, this[i], i, this)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
// ES5 15.4.4.20
|
||||
if (!Array.prototype.filter) { // ie8
|
||||
Array.prototype.filter = function (block /*, thisp */) {
|
||||
var values = []
|
||||
, thisp = arguments[1]
|
||||
for (var i = 0; i < this.length; i++) {
|
||||
if (block.call(thisp, this[i])) {
|
||||
values.push(this[i])
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
// ES5 15.4.4.21
|
||||
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduce
|
||||
if (!Array.prototype.reduce) { // ie8
|
||||
Array.prototype.reduce = function(fun /*, initial*/) {
|
||||
var len = this.length >>> 0
|
||||
if (typeof fun != "function") {
|
||||
throw new TypeError()
|
||||
}
|
||||
|
||||
// no value to return if no initial value and an empty array
|
||||
if (len == 0 && arguments.length == 1) {
|
||||
throw new TypeError()
|
||||
}
|
||||
|
||||
var i = 0
|
||||
if (arguments.length >= 2) {
|
||||
var rv = arguments[1]
|
||||
} else {
|
||||
do {
|
||||
if (i in this) {
|
||||
rv = this[i++]
|
||||
break
|
||||
}
|
||||
|
||||
// if array contains no values, no initial value to return
|
||||
if (++i >= len) {
|
||||
throw new TypeError()
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
|
||||
for (; i < len; i++) {
|
||||
if (i in this) {
|
||||
rv = fun.call(null, rv, this[i], i, this)
|
||||
}
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
} // Array.prototype.reduce
|
||||
} // function shimES5()
|
||||
|
||||
// jsonp request, quacks like node's http.request
|
||||
function shimRequest() {
|
||||
var load, _jsonpCounter = 1
|
||||
|
||||
// request is declared earlier
|
||||
request = function(options, cb) {
|
||||
var jsonpCallbackName = '_jsonpCallback' + _jsonpCounter++
|
||||
, url = 'https://' + options.host + options.path + '?callback=GITR.' + jsonpCallbackName
|
||||
GITR[jsonpCallbackName] = function(response) {
|
||||
if (response.meta.status >= 200 && response.meta.status < 300) {
|
||||
cb(null, response.data)
|
||||
}
|
||||
else {
|
||||
var err = new Error('http error')
|
||||
err.statusCode = response.meta.status
|
||||
err.response = response
|
||||
cb(err)
|
||||
}
|
||||
setTimeout(function() { delete GITR[jsonpCallbackName] }, 0)
|
||||
}
|
||||
load(url)
|
||||
}
|
||||
|
||||
// bootstrap loader from LABjs (load is declared earlier)
|
||||
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) // setTimeout
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
} // function load(url)
|
||||
|
||||
} // function shimRequest()
|
||||
|
||||
}())
|
||||
31
assets/js/jquery-serializeObject.js
vendored
|
|
@ -1,31 +0,0 @@
|
|||
/*!
|
||||
* 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);
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
;(function() {
|
||||
if (typeof console === 'undefined') {
|
||||
console = {log:function(){}}
|
||||
}
|
||||
|
||||
var global = this
|
||||
global.SJS = {
|
||||
proj: function(name) {
|
||||
SJS.projName = name
|
||||
var data = createObjectStore(name)
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('DOMContentLoaded', ready, false)
|
||||
} else if (window.attachEvent) {
|
||||
window.attachEvent('onload', ready)
|
||||
}
|
||||
function ready() {
|
||||
function addClass(el, name) {
|
||||
var c = el.className || name
|
||||
if (!c.match(new RegExp('\b' + name + '\b', 'i'))) c += ' ' + name
|
||||
}
|
||||
function html(id, h) {
|
||||
document.getElementById(id).innerHTML = h
|
||||
}
|
||||
|
||||
var body = document.getElementsByTagName('body')[0]
|
||||
, text
|
||||
if ('innerText' in body) {
|
||||
text = function(id, text) {
|
||||
document.getElementById(id).innerText = text
|
||||
}
|
||||
} else {
|
||||
text = function(id, text) {
|
||||
document.getElementById(id).textContent = text
|
||||
}
|
||||
}
|
||||
|
||||
function highlight(id) {
|
||||
document.getElementById(id).style.className = ' highlight'
|
||||
}
|
||||
function textHighlight(id, t) {
|
||||
text(id, t)
|
||||
document.getElementById(id).className = ' highlight'
|
||||
}
|
||||
function hide(id) {
|
||||
document.getElementById(id).style.display = 'none'
|
||||
}
|
||||
|
||||
function langsByUsage(langs) {
|
||||
return Object.keys(langs).sort(function(a, b) {
|
||||
return langs[a] < langs[b] ? -1 : 1
|
||||
})
|
||||
}
|
||||
|
||||
function listify(things) {
|
||||
return '<ul><li>' + things.join('</li><li>') + '</li></ul>'
|
||||
}
|
||||
|
||||
function updateBranches(name, branches) {
|
||||
function branchLink(b) {
|
||||
return '<a href=https://github.com/samsonjs/' + name + '/tree/' + b.name + '>' + b.name + '</a>'
|
||||
}
|
||||
html('branches', listify(branches.map(branchLink)))
|
||||
}
|
||||
|
||||
function updateContributors(contributors) {
|
||||
function userLink(u) {
|
||||
return '<a href=https://github.com/' + u.login + '>' + (u.name || u.login) + '</a>'
|
||||
}
|
||||
html('contributors', listify(contributors.map(userLink)))
|
||||
}
|
||||
|
||||
function updateLangs(langs) {
|
||||
html('langs', listify(langsByUsage(langs)))
|
||||
}
|
||||
|
||||
function updateN(name, things) {
|
||||
textHighlight('n' + name, things.length)
|
||||
if (things.length === 1) hide(name.charAt(0) + 'plural')
|
||||
}
|
||||
|
||||
var t = data.get('t-' + name)
|
||||
if (!t || +new Date() - t > 3600 * 1000) {
|
||||
console.log('stale ' + String(t))
|
||||
data.set('t-' + name, +new Date())
|
||||
GITR.repo('samsonjs', name)
|
||||
.fetchBranches(function(err, branches) {
|
||||
if (err) {
|
||||
text('branches', '(oops)')
|
||||
} else {
|
||||
data.set('branches', branches)
|
||||
updateBranches(name, branches)
|
||||
}
|
||||
})
|
||||
.fetchLanguages(function(err, langs) {
|
||||
if (err) {
|
||||
text('langs', '(oops)')
|
||||
return
|
||||
}
|
||||
data.set('langs', langs)
|
||||
updateLangs(langs)
|
||||
})
|
||||
.fetchContributors(function(err, users) {
|
||||
if (err) {
|
||||
text('contributors', '(oops)')
|
||||
} else {
|
||||
data.set('contributors', users)
|
||||
updateContributors(users)
|
||||
}
|
||||
})
|
||||
.fetchWatchers(function(err, users) {
|
||||
if (err) {
|
||||
text('nwatchers', '?')
|
||||
} else {
|
||||
data.set('watchers', users)
|
||||
updateN('watchers', users)
|
||||
}
|
||||
})
|
||||
.fetchForks(function(err, repos) {
|
||||
if (err) {
|
||||
text('nforks', '?')
|
||||
} else {
|
||||
data.set('forks', repos)
|
||||
updateN('forks', repos)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log('hit ' + t + ' (' + (+new Date() - t) + ')')
|
||||
updateBranches(name, data.get('branches'))
|
||||
updateLangs(data.get('langs'))
|
||||
updateContributors(data.get('contributors'))
|
||||
updateN('watchers', data.get('watchers'))
|
||||
updateN('forks', data.get('forks'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
;(function() {
|
||||
if (typeof window.SJS === 'undefined') window.SJS = {}
|
||||
|
||||
// cors xhr request, quacks like mikeal's request module
|
||||
window.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 === 'function') {
|
||||
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() {
|
||||
if (xhr.status === 200 || xhr.status === 204) {
|
||||
cb(null, xhr, xhr.responseText)
|
||||
}
|
||||
else {
|
||||
console.log('xhr error ' + xhr.status + ': ' + xhr.responseText)
|
||||
cb(new Error('error: ' + xhr.status))
|
||||
}
|
||||
}
|
||||
xhr.send(body)
|
||||
}
|
||||
}());
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
if (!window.localStorage || !window.sessionStorage) (function () {
|
||||
|
||||
var Storage = function (type) {
|
||||
function createCookie(name, value, days) {
|
||||
var date, expires;
|
||||
|
||||
if (days) {
|
||||
date = new Date();
|
||||
date.setTime(date.getTime()+(days*24*60*60*1000));
|
||||
expires = "; expires="+date.toGMTString();
|
||||
} else {
|
||||
expires = "";
|
||||
}
|
||||
document.cookie = name+"="+value+expires+"; path=/";
|
||||
}
|
||||
|
||||
function readCookie(name) {
|
||||
var nameEQ = name + "=",
|
||||
ca = document.cookie.split(';'),
|
||||
i, c;
|
||||
|
||||
for (i=0; i < ca.length; i++) {
|
||||
c = ca[i];
|
||||
while (c.charAt(0)==' ') {
|
||||
c = c.substring(1,c.length);
|
||||
}
|
||||
|
||||
if (c.indexOf(nameEQ) == 0) {
|
||||
return c.substring(nameEQ.length,c.length);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setData(data) {
|
||||
data = JSON.stringify(data);
|
||||
if (type == 'session') {
|
||||
window.top.name = data;
|
||||
} else {
|
||||
createCookie('localStorage', data, 365);
|
||||
}
|
||||
}
|
||||
|
||||
function clearData() {
|
||||
if (type == 'session') {
|
||||
window.top.name = '';
|
||||
} else {
|
||||
createCookie('localStorage', '', 365);
|
||||
}
|
||||
}
|
||||
|
||||
function getData() {
|
||||
var data = type == 'session' ? window.top.name : readCookie('localStorage');
|
||||
return data ? JSON.parse(data) : {};
|
||||
}
|
||||
|
||||
|
||||
// initialise if there's already data
|
||||
var data = getData();
|
||||
|
||||
return {
|
||||
clear: function () {
|
||||
data = {};
|
||||
clearData();
|
||||
},
|
||||
getItem: function (key) {
|
||||
return data[key] || null;
|
||||
},
|
||||
key: function (i) {
|
||||
// not perfect, but works
|
||||
var ctr = 0;
|
||||
for (var k in data) {
|
||||
if (ctr == i) return k;
|
||||
else ctr++;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
removeItem: function (key) {
|
||||
delete data[key];
|
||||
setData(data);
|
||||
},
|
||||
setItem: function (key, value) {
|
||||
data[key] = value+''; // forces the value to a string
|
||||
setData(data);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (!window.localStorage) window.localStorage = new Storage('local');
|
||||
if (!window.sessionStorage) window.sessionStorage = new Storage('session');
|
||||
|
||||
}());
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
;(function() {
|
||||
var global = this
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
global.createObjectStore = function(namespace) {
|
||||
function makeKey(k) {
|
||||
return '--' + namespace + '-' + (k || '')
|
||||
}
|
||||
return {
|
||||
clear: function() {
|
||||
var i = localStorage.length
|
||||
, k
|
||||
, prefix = new RegExp('^' + makeKey())
|
||||
while (--i) {
|
||||
k = localStorage.key(i)
|
||||
if (k.match(prefix)) {
|
||||
localStorage.remove(k)
|
||||
}
|
||||
}
|
||||
},
|
||||
get: function(key) {
|
||||
var val = localStorage[makeKey(key)]
|
||||
try {
|
||||
while (typeof val === 'string') val = JSON.parse(val)
|
||||
} catch (e) {
|
||||
//console.log('string?')
|
||||
}
|
||||
return val
|
||||
},
|
||||
set: function(key, val) {
|
||||
localStorage[makeKey(key)] = typeof val === 'string' ? val : JSON.stringify(val)
|
||||
},
|
||||
remove: function(key) {
|
||||
delete localStorage[makeKey(key)]
|
||||
}
|
||||
}
|
||||
}
|
||||
global.ObjectStore = createObjectStore('default')
|
||||
} else {
|
||||
// Create an in-memory store, should probably fall back to cookies
|
||||
global.createObjectStore = function() {
|
||||
var store = {}
|
||||
return {
|
||||
clear: function() { store = {} },
|
||||
get: function(key) { return store[key] },
|
||||
set: function(key, val) { store[key] = val },
|
||||
remove: function(key) { delete store[key] }
|
||||
}
|
||||
}
|
||||
global.ObjectStore = createObjectStore()
|
||||
}
|
||||
}());
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/// strftime
|
||||
/// http://github.com/samsonjs/strftime
|
||||
/// @_sjs
|
||||
///
|
||||
/// Copyright 2010 Sami Samhuri <sami@samhuri.net>
|
||||
/// 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));
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
// 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;
|
||||
};
|
||||
}());
|
||||
262
bin/blog.rb
|
|
@ -1,262 +0,0 @@
|
|||
#!/usr/bin/env ruby
|
||||
# encoding: utf-8
|
||||
|
||||
require 'time'
|
||||
require 'rubygems'
|
||||
require 'bundler/setup'
|
||||
require 'builder'
|
||||
require 'json'
|
||||
require 'mustache'
|
||||
require 'rdiscount'
|
||||
|
||||
DefaultKeywords = ['sjs', 'sami samhuri', 'sami', 'samhuri', 'samhuri.net', 'blog']
|
||||
|
||||
ShortURLCodeSet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
ShortURLBase = ShortURLCodeSet.length.to_f
|
||||
|
||||
def main
|
||||
srcdir = ARGV.shift.to_s
|
||||
destdir = ARGV.shift.to_s
|
||||
Dir.mkdir(destdir) unless File.exists?(destdir)
|
||||
unless File.directory?(srcdir)
|
||||
puts 'usage: blog.rb <source dir> <dest dir>'
|
||||
exit 1
|
||||
end
|
||||
b = Blag.new srcdir, destdir
|
||||
puts 'title: ' + b.title
|
||||
puts 'subtitle: ' + b.subtitle
|
||||
puts 'url: ' + b.url
|
||||
puts "#{b.posts.size} posts"
|
||||
b.generate!
|
||||
puts 'done blog'
|
||||
end
|
||||
|
||||
class Blag
|
||||
attr_accessor :title, :subtitle, :url
|
||||
|
||||
def self.go! src, dest
|
||||
self.new(src, dest).generate!
|
||||
end
|
||||
|
||||
def initialize src, dest
|
||||
@src = src
|
||||
@dest = dest
|
||||
@blog_dest = File.join(dest, 'blog')
|
||||
@css_dest = File.join(dest, 'css')
|
||||
read_blog
|
||||
end
|
||||
|
||||
def generate!
|
||||
generate_posts
|
||||
generate_index
|
||||
generate_rss
|
||||
generate_posts_json
|
||||
generate_archive
|
||||
generate_short_urls
|
||||
copy_assets
|
||||
end
|
||||
|
||||
def generate_index
|
||||
# generate landing page
|
||||
index_template = File.read(File.join('templates', 'blog', 'index.html'))
|
||||
post = posts.first
|
||||
values = { :post => post,
|
||||
:styles => post[:styles],
|
||||
:article => html(post),
|
||||
:previous => posts[1],
|
||||
:filename => post[:filename],
|
||||
:url => post[:relative_url],
|
||||
:comments => post[:comments]
|
||||
}
|
||||
index_html = Mustache.render(index_template, values)
|
||||
File.open(File.join(@blog_dest, 'index.html'), 'w') {|f| f.puts(index_html) }
|
||||
end
|
||||
|
||||
def generate_posts
|
||||
page_template = File.read(File.join('templates', 'blog', 'post.html'))
|
||||
posts.each_with_index do |post, i|
|
||||
values = { :title => post[:title],
|
||||
:link => post[:link],
|
||||
:styles => post[:styles],
|
||||
:article => html(post),
|
||||
:previous => i < posts.length - 1 && posts[i + 1],
|
||||
:next => i > 0 && posts[i - 1],
|
||||
:filename => post[:filename],
|
||||
:url => post[:relative_url],
|
||||
:comments => post[:comments],
|
||||
:keywords => (DefaultKeywords + post[:tags]).join(',')
|
||||
}
|
||||
post[:html] = Mustache.render(page_template, values)
|
||||
File.open(File.join(@blog_dest, post[:filename]), 'w') {|f| f.puts(post[:html]) }
|
||||
end
|
||||
end
|
||||
|
||||
def generate_posts_json
|
||||
json = JSON.generate({ :published => posts.map {|p| p[:filename]} })
|
||||
File.open(File.join(@blog_dest, 'posts.json'), 'w') { |f| f.puts(json) }
|
||||
end
|
||||
|
||||
def generate_archive
|
||||
archive_template = File.read(File.join('templates', 'blog', 'archive.html'))
|
||||
html = Mustache.render(archive_template, :posts => posts)
|
||||
File.open(File.join(@blog_dest, 'archive'), 'w') { |f| f.puts(html) }
|
||||
end
|
||||
|
||||
def generate_rss
|
||||
# posts rss
|
||||
File.open(rss_file, 'w') { |f| f.puts(rss_for_posts.target!) }
|
||||
end
|
||||
|
||||
def generate_short_urls
|
||||
htaccess = ['RewriteEngine on', 'RewriteRule ^$ http://samhuri.net [R=301,L]']
|
||||
posts.reverse.each_with_index do |post, i|
|
||||
code = shorten(i + 1)
|
||||
htaccess << "RewriteRule ^#{code}$ #{post[:url]} [R=301,L]"
|
||||
end
|
||||
File.open(File.join(@dest, 's42', '.htaccess'), 'w') do |f|
|
||||
f.puts(htaccess)
|
||||
end
|
||||
end
|
||||
|
||||
def shorten(n)
|
||||
short = ''
|
||||
while n > 0
|
||||
short = ShortURLCodeSet[n % ShortURLBase, 1] + short
|
||||
n = (n / ShortURLBase).floor
|
||||
end
|
||||
short
|
||||
end
|
||||
|
||||
def copy_assets
|
||||
Dir[File.join(@src, 'css', '*.css')].each do |stylesheet|
|
||||
minified = File.join(@css_dest, File.basename(stylesheet).sub('.css', '.min.css'))
|
||||
`yui-compressor #{stylesheet} #{minified}`
|
||||
end
|
||||
Dir[File.join(@src, 'files', '*')].each do |file|
|
||||
FileUtils.copy(file, File.join(@dest, 'f', File.basename(file)))
|
||||
end
|
||||
Dir[File.join(@src, 'images', '*')].each do |file|
|
||||
FileUtils.copy(file, File.join(@dest, 'images', 'blog', File.basename(file)))
|
||||
end
|
||||
end
|
||||
|
||||
def posts
|
||||
prefix = File.join(@src, 'published') + '/'
|
||||
@posts ||= Dir[File.join(prefix, '*')].sort.reverse.map do |filename|
|
||||
lines = File.readlines(filename)
|
||||
post = { :filename => filename.sub(prefix, '').sub(/\.(html|m(ark)?d(own)?)$/i, '') }
|
||||
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[:type] = post[:link] ? :link : :post
|
||||
post[:title] += " →" if post[:type] == :link
|
||||
post[:styles] = (post[:styles] || '').split(/\s*,\s*/)
|
||||
post[:tags] = (post[:tags] || '').split(/\s*,\s*/)
|
||||
post[:relative_url] = post[:filename].sub(/\.html$/, '')
|
||||
post[:url] = @url + '/' + post[:relative_url]
|
||||
post[:timestamp] = post[:timestamp].to_i
|
||||
post[:content] = lines.join
|
||||
post[:body] = RDiscount.new(post[:content], :smart).to_html
|
||||
post[:rfc822] = Time.at(post[:timestamp]).rfc822
|
||||
# comments on by default
|
||||
post[:comments] = (post[:comments] == 'on' || post[:comments].nil?)
|
||||
post
|
||||
end.sort { |a, b| b[:timestamp] <=> a[:timestamp] }
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def blog_file
|
||||
File.join(@src, 'blog.json')
|
||||
end
|
||||
|
||||
def read_blog
|
||||
blog = JSON.parse(File.read(blog_file))
|
||||
@title = blog['title']
|
||||
@subtitle = blog['subtitle']
|
||||
@url = blog['url']
|
||||
end
|
||||
|
||||
def html(post)
|
||||
Mustache.render(template(post[:type]), post)
|
||||
end
|
||||
|
||||
def template(type)
|
||||
if type == :post
|
||||
@post_template ||= File.read(File.join('templates', 'blog', 'post.mustache'))
|
||||
elsif type == :link
|
||||
@link_template ||= File.read(File.join('templates', 'blog', 'link.mustache'))
|
||||
else
|
||||
raise 'unknown post type: ' + type
|
||||
end
|
||||
end
|
||||
|
||||
def rss_template(type)
|
||||
if type == :post
|
||||
@post_rss_template ||= File.read(File.join('templates', 'blog', 'post.rss.html'))
|
||||
elsif type == :link
|
||||
@link_rss_template ||= File.read(File.join('templates', 'blog', 'link.rss.html'))
|
||||
else
|
||||
raise 'unknown post type: ' + type
|
||||
end
|
||||
end
|
||||
|
||||
def rss_file
|
||||
File.join(@blog_dest, 'sjs.rss')
|
||||
end
|
||||
|
||||
def rss_html(post)
|
||||
Mustache.render(rss_template(post[:type]), { :post => post })
|
||||
end
|
||||
|
||||
def rss_for_posts(options = {})
|
||||
title = options[:title] || @title
|
||||
subtitle = options[:subtitle] || @subtitle
|
||||
url = options[:url] || @url
|
||||
rss_posts ||= options[:posts] || posts[0, 10]
|
||||
|
||||
xml = Builder::XmlMarkup.new
|
||||
xml.instruct! :xml, :version => '1.0'
|
||||
xml.instruct! 'xml-stylesheet', :href => 'http://samhuri.net/css/blog-all.min.css', :type => 'text/css'
|
||||
|
||||
rss_posts.each do |post|
|
||||
post[:styles].each do |style|
|
||||
xml.instruct! 'xml-stylesheet', :href => "http://samhuri.net/css/#{style}.min.css", :type => 'text/css'
|
||||
end
|
||||
end
|
||||
|
||||
xml.rss :version => '2.0' do
|
||||
xml.channel do
|
||||
xml.title title
|
||||
xml.description subtitle
|
||||
xml.link url
|
||||
xml.pubDate posts.first[:rfc822]
|
||||
|
||||
rss_posts.each do |post|
|
||||
xml.item do
|
||||
xml.title post[:title]
|
||||
xml.description rss_html(post)
|
||||
xml.pubDate post[:rfc822]
|
||||
xml.author post[:author]
|
||||
xml.link post[:link] || post[:url]
|
||||
xml.guid post[:url]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
xml
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
main if $0 == __FILE__
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#!/usr/bin/env zsh
|
||||
|
||||
### javascript ###
|
||||
|
||||
# blog
|
||||
echo "request,showdown,strftime,tmpl,jquery-serializeObject,blog -> blog-all.min.js"
|
||||
cat public/js/{request,showdown,strftime,tmpl,jquery-serializeObject,blog}.min.js >|public/js/blog-all.min.js
|
||||
|
||||
# project index
|
||||
echo "gitter,store -> proj-index-all.min.js"
|
||||
cat public/js/{gitter,store}.min.js >|public/js/proj-index-all.min.js
|
||||
|
||||
# projects
|
||||
echo "gitter,store,proj -> proj-all.min.js"
|
||||
cat public/js/{gitter,store,proj}.min.js >|public/js/proj-all.min.js
|
||||
|
||||
|
||||
### css ###
|
||||
|
||||
# blog
|
||||
echo "style,blog -> blog-all.min.css"
|
||||
cat public/css/{style,blog}.min.css >|public/css/blog-all.min.css
|
||||
|
||||
# project index
|
||||
echo "style,proj-common -> proj-index-all.min.css"
|
||||
cat public/css/{style,proj-common}.min.css >|public/css/proj-index-all.min.css
|
||||
|
||||
# projects
|
||||
echo "style,proj-common,proj -> proj-all.min.css"
|
||||
cat public/css/{style,proj-common,proj}.min.css >|public/css/proj-all.min.css
|
||||
11
bin/compile.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/bash
|
||||
|
||||
harp compile public public
|
||||
|
||||
for FILENAME in public/posts/*.html public/projects/*.html; do
|
||||
[[ "$FILENAME" = "index.html" ]] && continue
|
||||
|
||||
DIRNAME="${FILENAME%.html}"
|
||||
mkdir -p "$DIRNAME"
|
||||
mv "$FILENAME" "$DIRNAME/index.html"
|
||||
done
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
#!/usr/bin/env zsh
|
||||
|
||||
setopt extendedglob
|
||||
|
||||
[[ ! -d public/js ]] && mkdir public/js
|
||||
for js (assets/js/*.js) {
|
||||
target=public/js/${${js:t}%.js}.min.js
|
||||
if [ ! -f $target ] || [ $js -nt $target ]; then
|
||||
echo "$js -> $target"
|
||||
closure < $js >| $target
|
||||
fi
|
||||
}
|
||||
|
||||
[[ ! -d public/css ]] && mkdir public/css
|
||||
for css (assets/css/*.css) {
|
||||
target=public/css/${${css:t}%.css}.min.css
|
||||
if [ ! -f $target ] || [ $css -nt $target ]; then
|
||||
echo "$css -> $target"
|
||||
yui-compressor $css $target
|
||||
fi
|
||||
}
|
||||
188
bin/posts.rb
Executable file
|
|
@ -0,0 +1,188 @@
|
|||
#!/usr/bin/env ruby
|
||||
# encoding: utf-8
|
||||
|
||||
require 'time'
|
||||
require 'rubygems'
|
||||
require 'bundler/setup'
|
||||
require 'builder'
|
||||
require 'json'
|
||||
require 'rdiscount'
|
||||
require 'mustache'
|
||||
|
||||
DEFAULT_KEYWORDS = %w[samsonjs sjs sami samhuri sami samhuri samhuri.net]
|
||||
|
||||
def main
|
||||
srcdir = ARGV.shift.to_s
|
||||
destdir = ARGV.shift.to_s
|
||||
Dir.mkdir(destdir) unless File.exists?(destdir)
|
||||
unless File.directory? srcdir
|
||||
puts 'usage: blog.rb <source dir> <dest dir>'
|
||||
exit 1
|
||||
end
|
||||
b = Blag.new srcdir, destdir
|
||||
puts 'title: ' + b.title
|
||||
puts 'subtitle: ' + b.subtitle
|
||||
puts 'url: ' + b.url
|
||||
puts "#{b.posts.size} posts"
|
||||
b.generate!
|
||||
puts 'done blog'
|
||||
end
|
||||
|
||||
class Blag
|
||||
attr_accessor :title, :subtitle, :url
|
||||
|
||||
def self.go! src, dest
|
||||
self.new(src, dest).generate!
|
||||
end
|
||||
|
||||
def initialize src, dest
|
||||
@src = src
|
||||
@dest = dest
|
||||
read_blog
|
||||
end
|
||||
|
||||
def generate!
|
||||
generate_posts_json
|
||||
generate_rss
|
||||
end
|
||||
|
||||
def generate_posts_json
|
||||
posts_data = posts.reverse.inject({}) do |all, p|
|
||||
all[p[:slug]] = {
|
||||
title: p[:title],
|
||||
date: p[:date],
|
||||
timestamp: p[:timestamp],
|
||||
tags: p[:tags],
|
||||
author: p[:author],
|
||||
url: p[:relative_url],
|
||||
link: p[:link],
|
||||
styles: p[:styles]
|
||||
}.delete_if { |k, v| v.nil? }
|
||||
|
||||
all
|
||||
end
|
||||
json = JSON.pretty_generate posts_data
|
||||
filename = File.join @dest, 'posts', '_data.json'
|
||||
File.open(filename, 'w') { |f| f.puts json }
|
||||
|
||||
filename = File.join @dest, '_data.json'
|
||||
data = JSON.parse File.read(filename)
|
||||
post = latest_post
|
||||
data['latest'] = posts_data[post[:slug]].merge('body' => post[:body])
|
||||
json = JSON.pretty_generate data
|
||||
File.open(filename, 'w') { |f| f.puts json }
|
||||
end
|
||||
|
||||
def generate_rss
|
||||
# posts rss
|
||||
File.open(rss_file, 'w') { |f| f.puts rss_for_posts.target! }
|
||||
end
|
||||
|
||||
def latest_post
|
||||
posts.first
|
||||
end
|
||||
|
||||
def posts
|
||||
prefix = @src + '/posts/'
|
||||
@posts ||= Dir[File.join(prefix, '*')].sort.reverse.map do |filename|
|
||||
next if File.directory?(filename) || filename =~ /_data\.json/
|
||||
|
||||
lines = File.readlines filename
|
||||
post = {
|
||||
slug: filename.sub(prefix, '').sub(/\.(html|md)$/i, '')
|
||||
}
|
||||
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
|
||||
if post[:styles]
|
||||
post[:styles] = post[:styles].split(/\s*,\s*/)
|
||||
end
|
||||
post[:type] = post[:link] ? :link : :post
|
||||
post[:title] += " →" if post[:type] == :link
|
||||
post[:tags] = (post[:tags] || '').split(/\s*,\s*/)
|
||||
post[:relative_url] = '/posts/' + post[:slug]
|
||||
post[:url] = @url + post[:relative_url]
|
||||
post[:timestamp] = post[:timestamp].to_i
|
||||
post[:content] = lines.join
|
||||
post[:body] = RDiscount.new(post[:content], :smart).to_html
|
||||
post[:rfc822] = Time.at(post[:timestamp]).rfc822
|
||||
post
|
||||
end.compact.sort { |a, b| b[:timestamp] <=> a[:timestamp] }
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def blog_file
|
||||
File.join(@src, '_data.json')
|
||||
end
|
||||
|
||||
def read_blog
|
||||
blog = JSON.parse(File.read(blog_file))
|
||||
@title = blog['title']
|
||||
@subtitle = blog['subtitle']
|
||||
@url = blog['url']
|
||||
end
|
||||
|
||||
def rss_template(type)
|
||||
if type == :post
|
||||
@post_rss_template ||= File.read(File.join('templates', 'post.rss.html'))
|
||||
elsif type == :link
|
||||
@link_rss_template ||= File.read(File.join('templates', 'link.rss.html'))
|
||||
else
|
||||
raise 'unknown post type: ' + type
|
||||
end
|
||||
end
|
||||
|
||||
def rss_file
|
||||
File.join @dest, 'sjs.rss'
|
||||
end
|
||||
|
||||
def rss_html post
|
||||
Mustache.render rss_template(post[:type]), post: post
|
||||
end
|
||||
|
||||
def rss_for_posts options = {}
|
||||
title = options[:title] || @title
|
||||
subtitle = options[:subtitle] || @subtitle
|
||||
url = options[:url] || @url
|
||||
rss_posts ||= options[:posts] || posts[0, 10]
|
||||
|
||||
xml = Builder::XmlMarkup.new
|
||||
xml.instruct! :xml, :version => '1.0'
|
||||
xml.instruct! 'xml-stylesheet', :href => 'http://samhuri.net/css/style.css', :type => 'text/css'
|
||||
|
||||
xml.rss :version => '2.0' do
|
||||
xml.channel do
|
||||
xml.title title
|
||||
xml.description subtitle
|
||||
xml.link url
|
||||
xml.pubDate posts.first[:rfc822]
|
||||
|
||||
rss_posts.each do |post|
|
||||
xml.item do
|
||||
xml.title post[:title]
|
||||
xml.description rss_html(post)
|
||||
xml.pubDate post[:rfc822]
|
||||
xml.author post[:author]
|
||||
xml.link post[:link] || post[:url]
|
||||
xml.guid post[:url]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
xml
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
main if $0 == __FILE__
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
var fs = require('fs')
|
||||
, path = require('path')
|
||||
, mustache = require('mustache')
|
||||
|
||||
, rootDir = path.join(__dirname, '..')
|
||||
, projectFile = path.join(rootDir, process.argv[2])
|
||||
, templateDir = path.join(rootDir, 'templates', 'proj')
|
||||
, targetDir = path.join(rootDir, process.argv[3])
|
||||
|
||||
try {
|
||||
fs.mkdirSync(targetDir, 0775)
|
||||
}
|
||||
catch (e) {
|
||||
if (e.code != 'EEXIST') throw e
|
||||
}
|
||||
|
||||
function main() {
|
||||
var ctx = {}
|
||||
fs.readFile(path.join(templateDir, 'project.html'), function(err, html) {
|
||||
if (err) throw err
|
||||
ctx.template = html.toString()
|
||||
fs.readFile(projectFile, function(err, json) {
|
||||
if (err) throw err
|
||||
var projects = JSON.parse(json).projects
|
||||
, index = path.join(targetDir, 'index.html')
|
||||
|
||||
// write project index
|
||||
fs.readFile(path.join(templateDir, 'index.html'), function(err, tpl) {
|
||||
if (err) throw err
|
||||
fs.mkdir(targetDir, 0775, function(err) {
|
||||
if (err && err.code !== 'EEXIST') throw err
|
||||
fs.unlink(index, function(err) {
|
||||
if (err && err.code !== 'ENOENT') throw err
|
||||
var vals = { projects: projects }
|
||||
, html = mustache.to_html(tpl.toString(), vals)
|
||||
fs.writeFile(index, html, function(err) {
|
||||
if (err) throw err
|
||||
console.log('* (project index)')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// write project pages
|
||||
ctx.n = 0
|
||||
projects.forEach(function(project) {
|
||||
ctx.n += 1
|
||||
buildProject(project.name, project, ctx)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function buildProject(name, project, ctx) {
|
||||
var dir = path.join(targetDir, name)
|
||||
, index = path.join(dir, 'index.html')
|
||||
|
||||
try {
|
||||
fs.mkdirSync(dir, 0775)
|
||||
}
|
||||
catch (e) {
|
||||
if (e.code != 'EEXIST') throw e
|
||||
}
|
||||
|
||||
fs.unlink(index, function(err) {
|
||||
if (err && err.code !== 'ENOENT') throw err
|
||||
project.name = name
|
||||
fs.writeFile(index, mustache.to_html(ctx.template, project), function(err) {
|
||||
if (err) console.error('error: ', err.message)
|
||||
ctx.n -= 1
|
||||
console.log('* ' + name + (err ? ' (failed)' : ''))
|
||||
if (ctx.n === 0) console.log('done projects')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (module == require.main) main()
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
# exit on errors
|
||||
set -e
|
||||
|
||||
bail() {
|
||||
echo fail: $*
|
||||
exit 1
|
||||
}
|
||||
|
||||
# exit on errors
|
||||
set -e
|
||||
|
||||
publish_host=samhuri.net
|
||||
publish_dir=samhuri.net/public/
|
||||
|
||||
|
|
|
|||
|
|
@ -1,343 +0,0 @@
|
|||
#!/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' ]
|
||||
|
||||
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) {
|
||||
return { get: getComments
|
||||
, count: countComments
|
||||
, post: postComment
|
||||
}
|
||||
|
||||
function addComment(post, name, email, url, body, timestamp) {
|
||||
var comments = context.db.get(post) || []
|
||||
comments.push({ id: comments.length + 1
|
||||
, name: name
|
||||
, email: email
|
||||
, url: url
|
||||
, body: body
|
||||
, timestamp: timestamp || Date.now()
|
||||
})
|
||||
context.db.set(post, comments)
|
||||
console.log('[' + timestamp + '] 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) || []
|
||||
comments.forEach(function(c, i) {
|
||||
c.id = c.id || (i + 1)
|
||||
})
|
||||
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, timestamp
|
||||
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
|
||||
}
|
||||
timestamp = +data.timestamp || Date.now()
|
||||
addComment(post, name, email, url, body, timestamp)
|
||||
res.respond()
|
||||
})
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
console.log('headers:', 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()
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
{ "name" : "discussd"
|
||||
, "description" : "comment server"
|
||||
, "version" : "1.0.0"
|
||||
, "homepage" : "http://samhuri.net/proj/samhuri.net"
|
||||
, "author" : "Sami Samhuri <sami@samhuri.net>"
|
||||
, "repository" :
|
||||
{ "type" : "git"
|
||||
, "url" : "https://github.com/samsonjs/samhuri.net.git"
|
||||
}
|
||||
, "bugs" :
|
||||
{ "mail" : "sami@samhuri.net"
|
||||
, "url" : "https://github.com/samsonjs/samhuri.net/issues"
|
||||
}
|
||||
, "dependencies" :
|
||||
{ "dirty" : "0.9.x"
|
||||
, "keys" : "0.1.x"
|
||||
, "markdown" : "0.5.x"
|
||||
, "strftime" : "0.6.x"
|
||||
}
|
||||
, "bin" : { "discussd" : "./discussd/discussd.js" }
|
||||
, "engines" : { "node" : ">=0.6.0" }
|
||||
, "licenses" :
|
||||
[ { "type" : "MIT"
|
||||
, "url" : "http://github.com/samsonjs/samhuri.net/raw/master/LICENSE"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
<geekbench version="Geekbench 2.1.6" checksum="5ce6d993f5d4dee345bb111e9e706c59">
|
||||
<score>5713</score>
|
||||
<elapsed>21.8</elapsed>
|
||||
<metrics>
|
||||
<metric id="1" name="Platform" value="Mac OS X x86 (32-bit)" ivalue="0" />
|
||||
<metric id="2" name="Compiler" value="GCC 4.0.1 (Apple Inc. build 5493)" ivalue="0" />
|
||||
<metric id="3" name="Operating System" value="Mac OS X 10.6.4 (Build 10F569)" ivalue="0" />
|
||||
<metric id="4" name="Model" value="MacBook Pro (15-inch Early 2010)" ivalue="0" />
|
||||
<metric id="5" name="Motherboard" value="Apple Inc. Mac-F22586C8 MacBookPro6,2" ivalue="0" />
|
||||
<metric id="6" name="Processor" value="Intel(R) Core(TM) i7 CPU M 620 @ 2.67GHz" ivalue="0" />
|
||||
<metric id="7" name="Processor ID" value="GenuineIntel Family 6 Model 37 Stepping 5" ivalue="0" />
|
||||
<metric id="8" name="Logical Processors" value="4" ivalue="4" />
|
||||
<metric id="9" name="Physical Processors" value="1" ivalue="1" />
|
||||
<metric id="10" name="Processor Frequency" value="2.66 GHz" ivalue="2660000000" />
|
||||
<metric id="11" name="L1 Instruction Cache" value="32.0 KB" ivalue="32768" />
|
||||
<metric id="12" name="L1 Data Cache" value="32.0 KB" ivalue="32768" />
|
||||
<metric id="13" name="L2 Cache" value="256 KB" ivalue="262144" />
|
||||
<metric id="14" name="L3 Cache" value="4.00 MB" ivalue="4194304" />
|
||||
<metric id="15" name="Bus Frequency" value="4.80 GHz" ivalue="4800000000" />
|
||||
<metric id="16" name="Memory" value="4.00 GB" ivalue="4294967296" />
|
||||
<metric id="17" name="Memory Type" value="1067 MHz DDR3" ivalue="0" />
|
||||
<metric id="18" name="SIMD" value="1" ivalue="1" />
|
||||
<metric id="19" name="BIOS" value="Apple Inc. MBP61.88Z.0057.B09.1004161215" ivalue="0" />
|
||||
<metric id="20" name="Processor Model" value="Intel(R) Core(TM) i7 CPU M 620 @ 2.67GHz" ivalue="0" />
|
||||
<metric id="21" name="Processor Cores" value="2" ivalue="2" />
|
||||
</metrics>
|
||||
<sections>
|
||||
<section name="Integer" id="1" percent="44">
|
||||
<score>4470</score>
|
||||
<benchmarks>
|
||||
<benchmark name="Blowfish" id="101" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="92609949.7" comment="88.3 MB/sec" score="2010" percent="20" />
|
||||
<result threads="4" simd="0" result="269210809.1" comment="256.7 MB/sec" score="6265" percent="62" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Text Compress" id="102" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="8568700.9" comment="8.17 MB/sec" score="2555" percent="25" />
|
||||
<result threads="4" simd="0" result="21004227.1" comment="20.0 MB/sec" score="6106" percent="61" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Text Decompress" id="103" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="11352456.9" comment="10.8 MB/sec" score="2634" percent="26" />
|
||||
<result threads="4" simd="0" result="27168160.0" comment="25.9 MB/sec" score="6503" percent="65" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Image Compress" id="104" units="9">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="18851687.1" comment="18.9 Mpixels/sec" score="2282" percent="22" />
|
||||
<result threads="4" simd="0" result="48609944.5" comment="48.6 Mpixels/sec" score="5777" percent="57" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Image Decompress" id="105" units="9">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="34061955.1" comment="34.1 Mpixels/sec" score="2029" percent="20" />
|
||||
<result threads="4" simd="0" result="69241502.3" comment="69.2 Mpixels/sec" score="4244" percent="42" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Lua" id="107" units="10">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="1632442.3" comment="1.63 Mnodes/sec" score="4240" percent="42" />
|
||||
<result threads="4" simd="0" result="3460218.6" comment="3.46 Mnodes/sec" score="8995" percent="89" />
|
||||
</results>
|
||||
</benchmark>
|
||||
</benchmarks>
|
||||
</section>
|
||||
<section name="Floating Point" id="2" percent="85">
|
||||
<score>8587</score>
|
||||
<benchmarks>
|
||||
<benchmark name="Mandelbrot" id="201" units="1">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="1831908421.9" comment="1.83 Gflops" score="2753" percent="27" />
|
||||
<result threads="4" simd="0" result="6017003497.0" comment="6.02 Gflops" score="9195" percent="91" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Dot Product" id="202" units="1">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="2166144987.1" comment="2.17 Gflops" score="4483" percent="44" />
|
||||
<result threads="4" simd="0" result="4136458217.1" comment="4.14 Gflops" score="9077" percent="90" />
|
||||
<result threads="1" simd="1" result="6445899500.2" comment="6.45 Gflops" score="5380" percent="53" />
|
||||
<result threads="4" simd="1" result="11719913214.7" comment="11.7 Gflops" score="11269" percent="100" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="LU Decomposition" id="203" units="1">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="862951460.1" comment="863.0 Mflops" score="969" percent="9" />
|
||||
<result threads="4" simd="0" result="1710120960.0" comment="1.71 Gflops" score="1950" percent="19" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Primality Test" id="204" units="1">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="873167792.2" comment="873.2 Mflops" score="5846" percent="58" />
|
||||
<result threads="4" simd="0" result="1873531827.9" comment="1.87 Gflops" score="10095" percent="100" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Sharpen Image" id="205" units="9">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="15552665.5" comment="15.6 Mpixels/sec" score="6666" percent="66" />
|
||||
<result threads="4" simd="0" result="43314232.1" comment="43.3 Mpixels/sec" score="18796" percent="100" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Blur Image" id="206" units="9">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="6729715.9" comment="6.73 Mpixels/sec" score="8504" percent="85" />
|
||||
<result threads="4" simd="0" result="19848362.5" comment="19.8 Mpixels/sec" score="25243" percent="100" />
|
||||
</results>
|
||||
</benchmark>
|
||||
</benchmarks>
|
||||
</section>
|
||||
<section name="Memory" id="3" percent="41">
|
||||
<score>4145</score>
|
||||
<benchmarks>
|
||||
<benchmark name="Read Sequential" id="302" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="5774319466.4" comment="5.38 GB/sec" score="4392" percent="43" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Write Sequential" id="304" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="5169705892.2" comment="4.81 GB/sec" score="7039" percent="70" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Stdlib Allocate" id="306" units="4">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="14493313.9" comment="14.5 Mallocs/sec" score="3884" percent="38" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Stdlib Write" id="307" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="5425082428.3" comment="5.05 GB/sec" score="2441" percent="24" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Stdlib Copy" id="308" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="3291104187.2" comment="3.07 GB/sec" score="2973" percent="29" />
|
||||
</results>
|
||||
</benchmark>
|
||||
</benchmarks>
|
||||
</section>
|
||||
<section name="Stream" id="4" percent="31">
|
||||
<score>3150</score>
|
||||
<benchmarks>
|
||||
<benchmark name="Stream Copy" id="401" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="4744386743.4" comment="4.42 GB/sec" score="3231" percent="32" />
|
||||
<result threads="1" simd="1" result="5096230024.5" comment="4.75 GB/sec" score="3660" percent="36" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Stream Scale" id="402" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="4801360820.0" comment="4.47 GB/sec" score="3446" percent="34" />
|
||||
<result threads="1" simd="1" result="5158998840.0" comment="4.80 GB/sec" score="3560" percent="35" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Stream Add" id="403" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="3756338657.2" comment="3.50 GB/sec" score="2317" percent="23" />
|
||||
<result threads="1" simd="1" result="5535615495.4" comment="5.16 GB/sec" score="3706" percent="37" />
|
||||
</results>
|
||||
</benchmark>
|
||||
<benchmark name="Stream Triad" id="404" units="2">
|
||||
<results>
|
||||
<result threads="1" simd="0" result="3718230106.6" comment="3.46 GB/sec" score="2506" percent="25" />
|
||||
<result threads="1" simd="1" result="5587777462.5" comment="5.20 GB/sec" score="2780" percent="27" />
|
||||
</results>
|
||||
</benchmark>
|
||||
</benchmarks>
|
||||
</section>
|
||||
</sections>
|
||||
</geekbench>
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
|
||||
/* Fluid widths */
|
||||
|
||||
body
|
||||
{ width: 100%
|
||||
; min-width: 0
|
||||
; font-size: 80%
|
||||
}
|
||||
|
||||
#masthead
|
||||
{ width: 100% }
|
||||
|
||||
#masthead .grid_24
|
||||
{ text-align: center }
|
||||
|
||||
#header .container_24,
|
||||
#footer .container_24
|
||||
#masthead .container_24, /* doesn't seem to work */
|
||||
#content .container_24, /* doesn't seem to work */
|
||||
#content .container_24 .grid_15, /* doesn't seem to work */
|
||||
.sidebar, /* doesn't seem to work */
|
||||
{ width: 97% }
|
||||
|
||||
#masthead .grid_24 { width: 97% }
|
||||
#masthead .grid_24 .grid_7 { width: 100%; margin-bottom: 1em }
|
||||
#masthead .grid_24 .grid_11 { width: 95% }
|
||||
#masthead .hosts { width: 100%; padding-right: 10px }
|
||||
|
||||
#masthead .hosts .host
|
||||
{ width: 44%
|
||||
; display: inline-block
|
||||
; float: none
|
||||
; clear: left
|
||||
}
|
||||
|
||||
#episode { min-height: 0 }
|
||||
#episode h2 { font-size: 1.4em }
|
||||
h5, #episode h5 { font-size: 0.8em; line-height: 1.2em }
|
||||
|
||||
#episode p,
|
||||
#episode #sponsors
|
||||
{ font-size: 0.7em; line-height: 1.3em }
|
||||
|
||||
#episode #episode_links { font-size: 0.7em; line-height: 1.2em }
|
||||
|
||||
.player { width: 100% }
|
||||
.player .transport { width: 65% }
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
if (!window.__fiveShiftInjected__) {
|
||||
window.__fiveShiftInjected__ = true
|
||||
|
||||
$(function() {
|
||||
|
||||
// load custom css
|
||||
var head = document.getElementsByTagName('head')[0]
|
||||
, css = document.createElement('link')
|
||||
css.rel = 'stylesheet'
|
||||
css.type = 'text/css'
|
||||
css.href = 'http://samhuri.net/f/fiveshift.css?t=' + +new Date()
|
||||
head.appendChild(css)
|
||||
|
||||
// These don't center properly via CSS for some reason
|
||||
;[ '#masthead .container_24'
|
||||
, '#content .container_24'
|
||||
, '#content .container_24 .grid_15'
|
||||
, '.sidebar'
|
||||
].forEach(function(selector) {
|
||||
$(selector).css('width', '97%')
|
||||
})
|
||||
|
||||
// Fix up the viewport
|
||||
$('meta[name="viewport"]').attr('content','width=device-width,initial-scale=1.0')
|
||||
})
|
||||
}
|
||||
1
f/hi.js
|
|
@ -1 +0,0 @@
|
|||
alert('hi')
|
||||
7
harp.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"globals": {
|
||||
"site": "samhuri.net",
|
||||
"author": "Sami Samhuri",
|
||||
"url": "http://samhuri.net"
|
||||
}
|
||||
}
|
||||
22
package.json
|
|
@ -1,22 +0,0 @@
|
|||
{ "name" : "samhuri.net"
|
||||
, "description" : "samhuri.net"
|
||||
, "version" : "1.0.0"
|
||||
, "homepage" : "http://samhuri.net/proj/samhuri.net"
|
||||
, "author" : "Sami Samhuri <sami@samhuri.net>"
|
||||
, "repository" :
|
||||
{ "type" : "git"
|
||||
, "url" : "https://github.com/samsonjs/samhuri.net.git"
|
||||
}
|
||||
, "bugs" :
|
||||
{ "mail" : "sami@samhuri.net"
|
||||
, "url" : "https://github.com/samsonjs/samhuri.net/issues"
|
||||
}
|
||||
, "dependencies" : { "mustache" : "0.3.x" }
|
||||
, "bin" : { "discussd" : "./discussd/discussd.js" }
|
||||
, "engines" : { "node" : ">=0.2.0" }
|
||||
, "licenses" :
|
||||
[ { "type" : "MIT"
|
||||
, "url" : "http://github.com/samsonjs/samhuri.net/raw/master/LICENSE"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
{ "projects" :
|
||||
[ { "name" : "strftime"
|
||||
, "description" : "strftime for JavaScript"
|
||||
}
|
||||
, { "name" : "bin"
|
||||
, "description" : "~/bin"
|
||||
}
|
||||
, { "name" : "compiler"
|
||||
, "description" : "an x86 compiler written in ruby"
|
||||
}
|
||||
, { "name" : "config"
|
||||
, "description" : "important dot files (zsh, emacs, vim, screen)"
|
||||
}
|
||||
, { "name" : "format"
|
||||
, "description" : "printf for JavaScript"
|
||||
}
|
||||
, { "name" : "gitter"
|
||||
, "description" : "a GitHub client for Node (v3 API)"
|
||||
}
|
||||
, { "name" : "samhuri.net"
|
||||
, "description" : "this site"
|
||||
}
|
||||
, { "name" : "ThePusher"
|
||||
, "description" : "Github post receive hook router"
|
||||
}
|
||||
, { "name" : "lake"
|
||||
, "description" : "A simple implementation of Scheme in C"
|
||||
}
|
||||
, { "name" : "mojo.el"
|
||||
, "description" : "turn emacs into a sweet mojo editor"
|
||||
}
|
||||
, { "name" : "NorthWatcher"
|
||||
, "description" : "cron for filesystem changes"
|
||||
}
|
||||
, { "name" : "batteries"
|
||||
, "description" : "a general purpose node library"
|
||||
}
|
||||
, { "name" : "repl-edit"
|
||||
, "description" : "edit Node repl commands with your text editor"
|
||||
}
|
||||
, { "name" : "cheat.el"
|
||||
, "description" : "cheat from emacs"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -7,6 +7,10 @@ SetOutputFilter DEFLATE
|
|||
ExpiresActive On
|
||||
ExpiresDefault A259200
|
||||
|
||||
<Directory ~ "/(projects|posts)/">
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
</Directory>
|
||||
|
||||
# Set up caching on media files for 1 month
|
||||
<FilesMatch "\.(ico|gif|jpg|jpeg|png|pdf|mov|mp3|m4r|m4a)$">
|
||||
ExpiresDefault A2419200
|
||||
|
|
@ -19,3 +23,7 @@ ExpiresDefault A259200
|
|||
ExpiresDefault A1209600
|
||||
Header append Cache-Control "private, must-revalidate"
|
||||
</FilesMatch>
|
||||
|
||||
Redirect 301 /proj/ /projects/
|
||||
Redirect 301 /blog/sjs.rss /sjs.rss
|
||||
Redirect 301 /blog/ /posts/
|
||||
|
|
|
|||
1
public/404.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
<p align="center">four oh four</p>
|
||||
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
16
public/_data.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"title": "samhuri.net",
|
||||
"subtitle": "words mean things",
|
||||
"url": "http://samhuri.net",
|
||||
"latest": {
|
||||
"title": "Structure of an Ember app",
|
||||
"date": "February 3, 2014",
|
||||
"timestamp": 1391479549,
|
||||
"tags": [
|
||||
"ember.js"
|
||||
],
|
||||
"author": "sjs",
|
||||
"url": "/posts/2014.02.03-ember-structure",
|
||||
"body": "<p>I made a diagram of an Ember app. There’s <a href=\"http://discuss.emberjs.com/t/diagram-of-an-ember-apps-structure/4060\">a discussion about it</a> on the\n<a href=\"http://discuss.emberjs.com/\">Ember Discussion Forum</a>. Here is the source file, created with OmniGraffle: <a href=\"https://www.dropbox.com/s/onnmn1oq096hv5f/Ember%20structure.graffle\">Ember structure.graffle</a></p>\n\n<p><img src=\"/f/ember-structure.png\" alt=\"Structure of an Ember app\" /></p>\n"
|
||||
}
|
||||
}
|
||||
53
public/_layout.ejs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<% if (typeof title != 'undefined') { %>
|
||||
<title><%= title %> - <%= site %></title>
|
||||
<% } else { %>
|
||||
<title><%= site %></title>
|
||||
<% } %>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<link rel="icon" type="image/gif" href="/images/s.gif">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="alternate" type="application/rss+xml" href="http://samhuri.net/sjs.rss" title="samhuri.net">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="/">samhuri.net</a></h1>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
<a href="/archive">posts</a>
|
||||
<a href="/projects">projects</a>
|
||||
</nav>
|
||||
|
||||
<%- yield %>
|
||||
|
||||
<footer>
|
||||
<p>You can <a href="https://twitter.com/_sjs">find me on twitter</a>.</p>
|
||||
<p><a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
var _gaq = _gaq || []
|
||||
_gaq.push( ['_setAccount', 'UA-214054-5']
|
||||
, ['_trackPageview']
|
||||
)
|
||||
|
||||
;(function() {
|
||||
var ga = document.createElement('script')
|
||||
ga.type = 'text/javascript'
|
||||
ga.async = true
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'
|
||||
var s = document.getElementsByTagName('script')[0]
|
||||
s.parentNode.insertBefore(ga, s)
|
||||
}())
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
9
public/archive.ejs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<ul id="index">
|
||||
<% for (var slug in public.posts._data) { %>
|
||||
<% var post = public.posts._data[slug]; %>
|
||||
<li>
|
||||
<span class="date"><%= post.date %></span>
|
||||
<a href="<%= post.url %>"><%= post.title %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>discussions - sjs's blog</title>
|
||||
<link rel="stylesheet" href="/css/blog-all.min.css">
|
||||
<style>
|
||||
#comment-stuff { display: block }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="comment-stuff">
|
||||
<form id="comment-form">
|
||||
<p><input name="post" type="text" placeholder="filename"></p>
|
||||
<p><input name="name" type="text" placeholder="name"></p>
|
||||
<p><input name="email" type="text" placeholder="email"></p>
|
||||
<p><input name="url" type="text" placeholder="url"></p>
|
||||
<p><input name="timestamp" type="text" placeholder="timestamp"></p>
|
||||
<p><textarea id="thoughts" name="body" placeholder="thoughts"></textarea></p>
|
||||
<p align="center"><input type="submit" value="so there"></p>
|
||||
</form>
|
||||
</div>
|
||||
<script type="text/html" id="comment_tmpl">
|
||||
<div class="comment">
|
||||
<p>
|
||||
<% if (url) { %>
|
||||
<a href="<%= url %>"><%= name %></a>
|
||||
<% } else { %>
|
||||
<%= name %>
|
||||
<% } %>
|
||||
@ <%= strftime('%F %I:%M %p', new Date(timestamp)) %>
|
||||
</p>
|
||||
<blockquote><%= html %></blockquote>
|
||||
</div>
|
||||
</script>
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
|
||||
<script src="../js/blog-all.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
public/images/ch1-Z-G-4.gif
Normal file
|
After Width: | Height: | Size: 360 B |
BIN
public/images/download.png
Normal file
|
After Width: | Height: | Size: 139 B |
BIN
public/images/keyboard.jpg
Normal file
|
After Width: | Height: | Size: 468 KiB |
BIN
public/images/menu.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
9
public/index.ejs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<% var post = public._data.latest %>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h1><a href="<%= post.url %>"><%= post.title %></a></h1>
|
||||
<time><%= post.date %></time>
|
||||
</header>
|
||||
<%- post.body %>
|
||||
</article>
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>samhuri.net</title>
|
||||
<link rel="icon" type="image/gif" href="images/s.gif">
|
||||
<link rel="stylesheet" href="css/style.min.css">
|
||||
<link rel="stylesheet" media="screen" href="css/mobile.min.css">
|
||||
<!--[if lt IE 7]>
|
||||
<link rel="stylesheet" href="css/ie6.min.css">
|
||||
<![endif]-->
|
||||
<!--[if lt IE 8]>
|
||||
<link rel="stylesheet" href="css/ie7.min.css">
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>samhuri.net</h1>
|
||||
</header>
|
||||
|
||||
<a href="https://github.com/samsonjs/samhuri.net"><img id="forkme" src="images/forkme@2x.png" width="140" height="140" alt="Fork me on GitHub"></a>
|
||||
|
||||
<nav>
|
||||
<ul class="nav" id="main">
|
||||
<li><a href="proj">projects</a></li>
|
||||
<li><a href="blog">blog</a></li>
|
||||
<li><a href="json-diff">json-diff</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<footer>
|
||||
<p>You can find me on Twitter as <a href="https://twitter.com/_sjs">@_sjs</a>.</p>
|
||||
<p><a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
var _gaq = _gaq || []
|
||||
_gaq.push( ['_setAccount', 'UA-214054-5']
|
||||
, ['_trackPageview']
|
||||
, ['_trackPageLoadTime']
|
||||
)
|
||||
|
||||
;(function() {
|
||||
var ga = document.createElement('script')
|
||||
ga.type = 'text/javascript'
|
||||
ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'
|
||||
var s = document.getElementsByTagName('script')[0]
|
||||
s.parentNode.insertBefore(ga, s)
|
||||
}())
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
|
|
@ -1,258 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>JSON Diff</title>
|
||||
<meta charset=utf8>
|
||||
<link rel="stylesheet" href="json-diff.css" type="text/css" media="screen" charset="utf-8">
|
||||
<script>
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push( ['_setAccount', 'UA-214054-5']
|
||||
, ['_trackPageview']
|
||||
);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
</script>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
var jsonBoxA, jsonBoxB, n;
|
||||
|
||||
function init() {
|
||||
document.addEventListener("click", clickHandler, false);
|
||||
|
||||
jsonBoxA = document.getElementById("jsonA");
|
||||
jsonBoxB = document.getElementById("jsonB");
|
||||
|
||||
startCompare();
|
||||
}
|
||||
|
||||
function swapBoxes() {
|
||||
console.log('>>> swapBoxes()');
|
||||
var tmp = jsonBoxA.value;
|
||||
jsonBoxA.value = jsonBoxB.value;
|
||||
jsonBoxB.value = tmp;
|
||||
}
|
||||
|
||||
function clearBoxes() {
|
||||
console.log('>>> clearBoxes()');
|
||||
jsonBoxA.value = "";
|
||||
jsonBoxB.value = "";
|
||||
}
|
||||
|
||||
function startCompare() {
|
||||
console.log('startCompare()');
|
||||
var objA, objB;
|
||||
n = 0;
|
||||
|
||||
jsonBoxA.style.backgroundColor = "";
|
||||
jsonBoxB.style.backgroundColor = "";
|
||||
|
||||
try {
|
||||
objA = eval("(" + jsonBoxA.value + ")");
|
||||
} catch(e) {
|
||||
jsonBoxA.style.backgroundColor = "rgba(255,0,0,0.5)";
|
||||
}
|
||||
try {
|
||||
objB = eval("(" + jsonBoxB.value + ")");
|
||||
} catch(e) {
|
||||
jsonBoxB.style.backgroundColor = "rgba(255,0,0,0.5)";
|
||||
}
|
||||
|
||||
results = document.getElementById("results");
|
||||
while (results.firstChild)
|
||||
results.removeChild(results.firstChild);
|
||||
|
||||
compareTree(objA, objB, "root", results);
|
||||
}
|
||||
|
||||
function markChanged(node) {
|
||||
document.getElementById('first').style.display = 'block';
|
||||
node.setAttribute('id', 'change-' + n);
|
||||
n += 1;
|
||||
var nextNode = document.createElement('a');
|
||||
nextNode.setAttribute('href', '#change-' + n);
|
||||
nextNode.appendChild(document.createTextNode('↓ next change ↓'))
|
||||
node.appendChild(nextNode);
|
||||
}
|
||||
|
||||
function compareTree(a, b, name, results) {
|
||||
var typeA = typeofReal(a);
|
||||
var typeB = typeofReal(b);
|
||||
|
||||
console.log('compareTree(a=(' + typeA + ')' + a +
|
||||
', b=(' + typeB + ')' + b +
|
||||
', name=(' + typeof name + ')' + name +
|
||||
', results=(' + typeof results + ')' + results + ')');
|
||||
|
||||
var typeSpanA = document.createElement("span");
|
||||
typeSpanA.appendChild(document.createTextNode("(" + typeA + ")"))
|
||||
typeSpanA.setAttribute("class", "typeName");
|
||||
|
||||
var typeSpanB = document.createElement("span");
|
||||
typeSpanB.appendChild(document.createTextNode("(" + typeB + ")"))
|
||||
typeSpanB.setAttribute("class", "typeName");
|
||||
|
||||
var aString = (typeA === "object" || typeA === "array") ? "": String(a) + " ";
|
||||
var bString = (typeB === "object" || typeB === "array") ? "": String(b) + " ";
|
||||
|
||||
var leafNode = document.createElement("span");
|
||||
leafNode.appendChild(document.createTextNode(name));
|
||||
if (a === undefined)
|
||||
{
|
||||
leafNode.setAttribute("class", "added");
|
||||
leafNode.appendChild(document.createTextNode(": " + bString));
|
||||
leafNode.appendChild(typeSpanB);
|
||||
markChanged(leafNode);
|
||||
}
|
||||
else if (b === undefined)
|
||||
{
|
||||
leafNode.setAttribute("class", "removed");
|
||||
leafNode.appendChild(document.createTextNode(": " + aString));
|
||||
leafNode.appendChild(typeSpanA);
|
||||
markChanged(leafNode);
|
||||
}
|
||||
else if (typeA !== typeB || (typeA !== "object" && typeA !== "array" && a !== b))
|
||||
{
|
||||
leafNode.setAttribute("class", "changed");
|
||||
leafNode.appendChild(document.createTextNode(": " + aString));
|
||||
leafNode.appendChild(typeSpanA);
|
||||
leafNode.appendChild(document.createTextNode(" => " + bString));
|
||||
leafNode.appendChild(typeSpanB);
|
||||
|
||||
if (name === 'key') leafNode.setAttribute('class', 'changed key');
|
||||
else markChanged(leafNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
leafNode.appendChild(document.createTextNode(": " + aString));
|
||||
leafNode.appendChild(typeSpanA);
|
||||
}
|
||||
|
||||
if (typeA === "object" || typeA === "array" || typeB === "object" || typeB === "array")
|
||||
{
|
||||
var keys = [];
|
||||
for (var i in a) keys.push(i);
|
||||
for (var i in b) keys.push(i);
|
||||
keys.sort();
|
||||
|
||||
var listNode = document.createElement("ul");
|
||||
listNode.appendChild(leafNode);
|
||||
|
||||
for (var i = 0; i < keys.length; i++)
|
||||
{
|
||||
if (keys[i] === keys[i - 1])
|
||||
continue;
|
||||
|
||||
var li = document.createElement("li");
|
||||
listNode.appendChild(li);
|
||||
|
||||
compareTree(a && a[keys[i]], b && b[keys[i]], keys[i], li);
|
||||
}
|
||||
|
||||
results.appendChild(listNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
results.appendChild(leafNode);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function isArray(value) {
|
||||
return value && typeof value === "object" && value.constructor === Array;
|
||||
}
|
||||
|
||||
function typeofReal(value) {
|
||||
return isArray(value) ? "array": value === null ? 'null' : typeof value;
|
||||
}
|
||||
|
||||
function clickHandler(e) {
|
||||
e = e || window.event;
|
||||
if (e.target.nodeName.toUpperCase() === "UL")
|
||||
{
|
||||
if (e.target.getAttribute("closed") === "yes")
|
||||
e.target.setAttribute("closed", "no");
|
||||
else
|
||||
e.target.setAttribute("closed", "yes");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="init();">
|
||||
<div style="position:absolute;top:0.3em;left:0.3em;font-size:1.5em;color:#222"><a href=../ style='color:#222'>samhuri.net</a></div>
|
||||
<p align="center" style="margin: 2em;">Courtesy of <a href="http://tlrobinson.net/projects/javascript-fun/jsondiff/">tlrobinson</a>.
|
||||
<br>
|
||||
This version differs in that it supports <code>null</code> values,<br>
|
||||
downplays the significance of changed properties with the key <code>key</code>,<br>
|
||||
and provides links to jump from one change to the next.</p>
|
||||
|
||||
<h2>JSON Diff</h2>
|
||||
<div class="contentbox" id="instructions">
|
||||
<ul>
|
||||
<li>Paste some JSON in each of the text fields. Click "Compare" to see the diff.</li>
|
||||
<li>Changed portions are displayed in <span class="changed">yellow</span>. Additions are displayed in <span class="added">green</span>. Deletions are displayed in <span class="removed">red</span>.</li>
|
||||
<li>It also works as a JSON viewer. Click the disclosure triangles to display/hide portions of the JSON.</li>
|
||||
<li>Invalid JSON is indicated by the text fields turning red.</li>
|
||||
<li>Swap the contents of the text areas by clicking "Swap". Clear them by clicking "Clear".</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="contentbox" id="inputs">
|
||||
<textarea id="jsonA">{
|
||||
"__class":"SLUser",
|
||||
"displayName":"Sami Samhuri",
|
||||
"accountSuspended": false,
|
||||
"newAttribute":null,
|
||||
"addressStreet1":null,
|
||||
"url":null,
|
||||
"addressZip":null,
|
||||
"email":"foo@bar.com",
|
||||
"addressRegion":null,
|
||||
"businessName":null,
|
||||
"addressStreet2":null,
|
||||
"addressCountry":null,
|
||||
"hashedPassword":"dc7754ea14e2d4f07bb3ec6a099480f318529dce",
|
||||
"uuid":"dc80bc3053d135e52411ce67bd758211"
|
||||
}
|
||||
</textarea>
|
||||
<textarea id="jsonB">
|
||||
{
|
||||
"__class":"SLUser",
|
||||
"displayName":"Foo Bar",
|
||||
"accountSuspended": false,
|
||||
"addressStreet1":"123 Fake St",
|
||||
"url":"bar.com",
|
||||
"addressZip":"V9C 0E6",
|
||||
"email":"foo@bar.com",
|
||||
"addressRegion":"BC",
|
||||
"businessName":"stuff",
|
||||
"addressStreet2":null,
|
||||
"addressCountry":"Canada",
|
||||
"hashedPassword":"dc7754ea14e2d4f07bb3ec6a099480f318529dce",
|
||||
"uuid":"d0e11b7c73d483335ac7697b042e36c5"
|
||||
}</textarea>
|
||||
<input type="button" value="Compare" id="compare" onclick="startCompare();" />
|
||||
<input type="button" value="Swap" id="swap" onclick="swapBoxes();"/>
|
||||
<input type="button" value="Clear" id="clear" onclick="clearBoxes();"/>
|
||||
</div>
|
||||
<p align=center id=first style=display:none><a href=#change-0>first change</a></p>
|
||||
<div class="contentbox" id="results">
|
||||
</div>
|
||||
<div class="contentbox" id="issues">
|
||||
<h3>About</h3>
|
||||
<p>JSON Diff is a simple way to visualize and compare <a href="http://json.org">JSON</a>.</p>
|
||||
<h3>Known Issues</h3>
|
||||
<ul>
|
||||
<li>Diff algorithm not very intelligent when dealing with arrays</li>
|
||||
<li>Probably doesn't work in IE</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<p>
|
||||
© 2006-2010 Thomas Robinson. <a rel="license" href="http://creativecommons.org/licenses/by-nc/3.0/us/">Some rights reserved</a>. </p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
body {
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
#results li > span, #results ul > span {
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
#results li {
|
||||
margin-top: 1px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
#results ul {
|
||||
padding-left: 15px;
|
||||
margin-left: -15px;
|
||||
padding-top: 0px;
|
||||
margin-top: 0px;
|
||||
background: url(open.png) no-repeat 2px 5px;
|
||||
list-style-type: none;
|
||||
}
|
||||
#results ul[closed="yes"] {
|
||||
background: url(closed.png) no-repeat 2px 5px;
|
||||
}
|
||||
#results ul[closed="yes"] > * {
|
||||
display: none;
|
||||
}
|
||||
#results ul[closed="yes"] > *:first-child {
|
||||
display: block;
|
||||
}
|
||||
.typeName {
|
||||
color: gray;
|
||||
}
|
||||
.changed {
|
||||
background-color: #fcff7f;
|
||||
}
|
||||
.changed.key {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.added {
|
||||
background-color: #8bff7f;
|
||||
}
|
||||
.removed {
|
||||
background-color: #fd7f7f;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 49%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.contentbox {
|
||||
border: 1px dashed black;
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin: 0px;;
|
||||
}
|
||||
|
||||
#results {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
#inputs {
|
||||
text-align: center;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
10
public/posts/2006.02.08-first-post.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Title: First Post!
|
||||
Date: February 8, 2006
|
||||
Timestamp: 1139368860
|
||||
Author: sjs
|
||||
Tags: life
|
||||
----
|
||||
|
||||
so it's 2am and i should be asleep, but instead i'm setting up a blog. i got a new desk last night and so today i finally got my apartment re-arranged and it's much better now. that's it for now... time to sleep.
|
||||
|
||||
(speaking of sleep, this new <a href="http://www.musuchouse.com/">sleeping bag</a> design makes so much sense. awesome.)
|
||||
14
public/posts/2006.02.08-touch-screen-on-steroids.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
Title: Touch Screen on Steroids
|
||||
Date: February 8, 2006
|
||||
Timestamp: 1139407560
|
||||
Author: sjs
|
||||
Tags: technology, touch
|
||||
----
|
||||
|
||||
If you thought the PowerBook's two-finger scrolling was cool check out this touch screen:
|
||||
|
||||
<a href="http://mrl.nyu.edu/~jhan/ftirtouch/">Multi-Touch Interaction Research</a>
|
||||
|
||||
> "While touch sensing is commonplace for single points of contact, multi-touch sensing enables a user to interact with a system with more than one finger at a time, as in chording and bi-manual operations. Such sensing devices are inherently also able to accommodate multiple users simultaneously, which is especially useful for larger interaction scenarios such as interactive walls and tabletops."
|
||||
|
||||
This is really amazing. Forget traditional tablet PCs... <i>this</i> is revolutionary and useful in so many applications. I hope this kind of technology is mainstream by 2015.
|
||||
10
public/posts/2006.02.15-urban-extreme-gymnastics.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Title: Urban Extreme Gymnastics?
|
||||
Date: February 15, 2006
|
||||
Timestamp: 1140028860
|
||||
Author: sjs
|
||||
Tags: amusement
|
||||
----
|
||||
|
||||
This crazy russian goes all over the place scaling buildings, doing all sorts of flips, bouncing off the walls literally. He'd be impossible to catch.
|
||||
|
||||
<a href="http://www.videobomb.com/posts/show/46">Russian parkour (urban extreme gymnastics)</a>
|
||||
12
public/posts/2006.02.18-girlfriend-x.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
Title: Girlfriend X
|
||||
Date: February 18, 2006
|
||||
Timestamp: 1140292200
|
||||
Author: sjs
|
||||
Tags: crazy, funny
|
||||
----
|
||||
|
||||
This is hilarious! Someone wrote software that manages a "parallel" dating style.
|
||||
|
||||
> In addition to storing each woman's contact information and picture, the Girlfriend profiles include a Score Card where you track her sexual preferences, her menstrual cycles and how she styles her pubic hair.
|
||||
|
||||
It's called [Girlfriend X](http://www.wired.com/news/columns/0,70231-0.html), but that's a link to an article about it. I didn't go to the actual website. I just thing it's amusing someone went through the trouble to do this. Maybe there's a demand for it. *\*shrug\**
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
Title: Jump to view/controller in TextMate
|
||||
Date: February 18, 2006
|
||||
Timestamp: 1140303060
|
||||
Author: sjs
|
||||
Tags: hacking, rails, textmate, rails, textmate
|
||||
----
|
||||
|
||||
<a href="http://blog.inquirylabs.com/2006/02/17/controller-to-view-and-back-again-in-textmate/trackback/">Duane</a> came up with a way to jump to the controller method for the view you're editing, or vice versa in TextMate while coding using Rails. This is a huge time-saver, thanks!
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
Title: Some TextMate snippets for Rails Migrations
|
||||
Date: February 18, 2006
|
||||
Timestamp: 1140331680
|
||||
Author: sjs
|
||||
Tags: textmate, rails, hacking, rails, snippets, textmate
|
||||
----
|
||||
|
||||
My arsenal of snippets and macros in TextMate is building as I read through the rails canon, <a href="http://www.pragmaticprogrammer.com/titles/rails/" title="Agile Web Development With Rails">Agile Web Development...</a> I'm only 150 pages in so I haven't had to add much so far because I started with the bundle found on the <a href="http://wiki.rubyonrails.org/rails/pages/TextMate">rails wiki</a>. The main ones so far are for migrations.
|
||||
|
||||
Initially I wrote a snippet for adding a table and one for dropping a table, but I don't want to write it twice every time! If I'm adding a table in **up** then I probably want to drop it in **down**.
|
||||
|
||||
What I did was create one snippet that writes both lines, then it's just a matter of cut & paste to get it in **down**. The drop_table line should be inserted in the correct method, but that doesn't seem possible. I hope I'm wrong!
|
||||
|
||||
Scope should be *source.ruby.rails* and the triggers I use are above the snippets.
|
||||
|
||||
mcdt: **M**igration **C**reate and **D**rop **T**able
|
||||
|
||||
create_table "${1:table}" do |t|
|
||||
$0
|
||||
end
|
||||
${2:drop_table "$1"}
|
||||
|
||||
mcc: **M**igration **C**reate **C**olumn
|
||||
|
||||
t.column "${1:title}", :${2:string}
|
||||
|
||||
marc: **M**igration **A**dd and **R**emove **C**olumn
|
||||
|
||||
add_column "${1:table}", "${2:column}", :${3:string}
|
||||
${4:remove_column "$1", "$2"}
|
||||
|
||||
I realize this might not be for everyone, so here are my original 4 snippets that do the work of *marc* and *mcdt*.
|
||||
|
||||
mct: **M**igration **C**reate **T**able
|
||||
|
||||
create_table "${1:table}" do |t|
|
||||
$0
|
||||
end
|
||||
|
||||
mdt: **M**igration **D**rop **T**able
|
||||
|
||||
drop_table "${1:table}"
|
||||
|
||||
mac: **M**igration **A**dd **C**olumn
|
||||
|
||||
add_column "${1:table}", "${2:column}", :${3:string}
|
||||
|
||||
mrc: **M**igration **R**remove **C**olumn
|
||||
|
||||
remove_column "${1:table}", "${2:column}"
|
||||
|
||||
I'll be adding more snippets and macros. There should be a central place where the rails bundle can be improved and extended. Maybe there is...
|
||||
215
public/posts/2006.02.20-obligatory-post-about-ruby-on-rails.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
Title: Obligatory Post about Ruby on Rails
|
||||
Date: February 20, 2006
|
||||
Timestamp: 1140424260
|
||||
Author: sjs
|
||||
Tags: rails, coding, hacking, migration, rails, testing
|
||||
Styles: typocode
|
||||
----
|
||||
|
||||
<p><em>I'm a Rails newbie and eager to learn. I welcome any suggestions or criticism you have. You can direct them to <a href="mailto:sjs@uvic.ca">my inbox</a> or leave me a comment below.</em></p>
|
||||
|
||||
<p>I finally set myself up with a blog. I mailed my dad the address and mentioned that it was running <a href="http://www.typosphere.org/">Typo</a>, which is written in <a href="http://www.rubyonrails.com/">Ruby on Rails</a>. The fact that it is written in Rails was a big factor in my decision. I am currently reading <a href="http://www.pragmaticprogrammer.com/titles/rails/">Agile Web Development With Rails</a> and it will be great to use Typo as a learning tool, since I will be modifying my blog anyways regardless of what language it's written in.</p>
|
||||
|
||||
<p>Clearly Rails made an impression on me somehow or I wouldn't be investing this time on it. But my dad asked me a very good question:</p>
|
||||
|
||||
> Rails? What is so special about it? I looked at your page and it looks pretty normal to me. I miss the point of this new Rails technique for web development.
|
||||
|
||||
<p>It's unlikely that he was surprised at my lengthy response, but I was. I have been known to write him long messages on topics that interest me. However, I've only been learning Rails for two weeks or so. Could I possibly have so much to say about it already? Apparently I do.</p><h2>Ruby on Rails background</h2>
|
||||
|
||||
|
||||
<p>I assume a pretty basic knowledge of what Rails is, so if you're not familiar with it now's a good time to read something on the official <a href="http://www.rubyonrails.com/">Rails website</a> and watch the infamous <a href="http://www.rubyonrails.com/screencasts">15-minute screencast</a>, where Rails creator, <a href="http://www.loudthinking.com/">David Heinemeier Hansson</a>, creates a simple blog application.</p>
|
||||
|
||||
|
||||
<p>The screencasts are what sparked my curiosity, but they hardly scratch the surface of Rails. After that I spent hours reading whatever I could find about Rails before deciding to take the time to learn it well. As a result, a lot of what you read here will sound familiar if you've read other blogs and articles about Rails. This post wasn't planned so there's no list of references yet. I hope to add some links though so please contact me if any ideas or paraphrasing here is from your site, or if you know who I should give credit to.</p>
|
||||
|
||||
|
||||
<h2>Rails through my eyes</h2>
|
||||
|
||||
|
||||
<p>Rails is like my Black & Decker toolkit. I have a hammer, power screwdriver, tape measure, needle-nose pliers, wire cutters, a level, etc. This is exactly what I need—no more, no less. It helps me get things done quickly and easily that would otherwise be painful and somewhat difficult. I can pick up the tools and use them without much training. Therefore I am instantly productive with them.</p>
|
||||
|
||||
|
||||
<p>The kit is suitable for many people who need these things at home, such as myself. Companies build skyscrapers and huge malls and apartments, and they clearly need more powerful tools than I. There are others that just need to drive in a nail to hang a picture, in which case the kit I have is overkill. They're better off just buying and using a single hammer. I happen to fall in the big grey middle <a href="http://web.archive.org/web/20070316171839/http://poignantguide.net/ruby/chapter-3.html#section2">chunk</a>, not the other two.</p>
|
||||
|
||||
|
||||
<p>I'm a university student. I code because it's satisfying and fun to create software. I do plan on coding for a living when I graduate. I don't work with ancient databases, or create monster sites like Amazon, Google, or Ebay. The last time I started coding a website from scratch I was using <a href="http://www.php.net/">PHP</a>, that was around the turn of the millennium. [It was a fan site for a <a href="http://www.nofx.org/">favourite band</a> of mine.]</p>
|
||||
|
||||
|
||||
<p>After a year or so I realized I didn't have the time to do it properly (ie. securely and cleanly) if I wanted it to be done relatively soon. A slightly customized <a href="http://www.mediawiki.org/wiki/MediaWiki">MediaWiki</a> promptly took it's place. It did all that I needed quite well, just in a less specific way.</p>
|
||||
|
||||
|
||||
<p>The wiki is serving my site extremely well, but there's still that itch to create my <strong>own</strong> site. I feel if Rails was around back then I may have been able to complete the project in a timely manner. I was also frustrated with PHP. Part of that is likely due to a lack of experience and of formal programming education at that time, but it was still not fun for me. It wasn't until I started learning Rails that I thought "<em>hey, I could create that site pretty quickly using this!</em>"</p>
|
||||
|
||||
|
||||
<p>Rails fits my needs like a glove, and this is where it shines. Many professionals are making money creating sites in Rails, so I'm not trying to say it's for amateurs only or something equally silly.</p>
|
||||
|
||||
|
||||
<h2>Web Frameworks and iPods?</h2>
|
||||
|
||||
|
||||
<p>Some might say I have merely been swept up in hype and am following the herd. You may be right, and that's okay. I'm going to tell you a story. There was a guy who didn't get one of the oh-so-shiny iPods for a long time, though they looked neat. His discman plays mp3 CDs, and that was good enough for him. The latest iPod, which plays video, was sufficiently cool enough for him to forget that <strong>everyone</strong> at his school has an iPod and he would be trendy just like them now.</p>
|
||||
|
||||
|
||||
<p>Shocker ending: he is I, and I am him. Now I know why everyone has one of those shiny devices. iPods and web frameworks have little in common except that many believe both the iPod and Rails are all hype and flash. I've realized that something creating this kind of buzz may actually just be a good product. I feel that this is the only other thing the iPod and Rails have in common: they are both <strong>damn good</strong>. Enough about the iPod, everyone hates hearing about it. My goal is to write about the other thing everyone is tired of hearing about.</p>
|
||||
|
||||
|
||||
<h2>Why is Rails special?</h2>
|
||||
|
||||
|
||||
<p><strong>Rails is not magic.</strong> There are no exclusive JavaScript libraries or HTML tags. We all have to produce pages that render in the same web browsers. My dad was correct, there <em>is</em> nothing special about my website either. It's more or less a stock Typo website.</p>
|
||||
|
||||
|
||||
<p>So what makes developing with Rails different? For me there are four big things that set Rails apart from the alternatives:</p>
|
||||
|
||||
|
||||
<ol>
|
||||
<li>Separating data, function, and design</li>
|
||||
<li>Readability (which is underrated) </li>
|
||||
<li>Database migrations</li>
|
||||
<li>Testing is so easy it hurts</li>
|
||||
</ol>
|
||||
|
||||
|
||||
<h3>MVC 101 <em>(or, Separating data, function, and design)</em></h3>
|
||||
|
||||
|
||||
<p>Now I'm sure you've heard about separating content from design. Rails takes that one step further from just using CSS to style your website. It uses what's known as the MVC paradigm: <strong>Model-View-Controller</strong>. This is a tried and tested development method. I'd used MVC before in Cocoa programming on Mac OS X, so I was already sold on this point.</p>
|
||||
|
||||
|
||||
<ul>
|
||||
<li>The model deals with your data. If you're creating an online store you have a product model, a shopping cart model, a customer model, etc. The model takes care of storing this data in the database (persistence), and presenting it to you as an object you can manipulate at runtime.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<ul>
|
||||
<li>The view deals <em>only</em> with presentation. That's it, honestly. An interface to your app.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<ul>
|
||||
<li>The controller binds the model to the view, so that when the user clicks on the <strong>Add to cart</strong> link the controller is wired to call the <code>add_product</code> method of the cart model and tell it which product to add. Then the controller takes the appropriate action such as redirecting the user to the shopping cart view.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>Of course this is not exclusive to Rails, but it's an integral part of it's design.</p>
|
||||
|
||||
|
||||
<h3>Readability</h3>
|
||||
|
||||
|
||||
<p>Rails, and <a href="http://www.ruby-lang.org/">Ruby</a>, both read amazingly like spoken English. This code is more or less straight out of Typo. You define relationships between objects like this:</p>
|
||||
|
||||
|
||||
<div class="typocode"><pre><code class="typocode_ruby "><span class="keyword">class </span><span class="class">Article</span> <span class="punct"><</span> <span class="constant">Content</span>
|
||||
<span class="ident">has_many</span> <span class="symbol">:comments</span><span class="punct">,</span> <span class="symbol">:dependent</span> <span class="punct">=></span> <span class="constant">true</span><span class="punct">,</span> <span class="symbol">:order</span> <span class="punct">=></span> <span class="punct">"</span><span class="string">created_at ASC</span><span class="punct">"</span>
|
||||
<span class="ident">has_many</span> <span class="symbol">:trackbacks</span><span class="punct">,</span> <span class="symbol">:dependent</span> <span class="punct">=></span> <span class="constant">true</span><span class="punct">,</span> <span class="symbol">:order</span> <span class="punct">=></span> <span class="punct">"</span><span class="string">created_at ASC</span><span class="punct">"</span>
|
||||
<span class="ident">has_and_belongs_to_many</span> <span class="symbol">:categories</span><span class="punct">,</span> <span class="symbol">:foreign_key</span> <span class="punct">=></span> <span class="punct">'</span><span class="string">article_id</span><span class="punct">'</span>
|
||||
<span class="ident">has_and_belongs_to_many</span> <span class="symbol">:tags</span><span class="punct">,</span> <span class="symbol">:foreign_key</span> <span class="punct">=></span> <span class="punct">'</span><span class="string">article_id</span><span class="punct">'</span>
|
||||
<span class="ident">belongs_to</span> <span class="symbol">:user</span>
|
||||
<span class="punct">...</span></code></pre></div>
|
||||
|
||||
<p><code>dependent => true</code> means <em>if an article is deleted, it's comments go with it</em>. Don't worry if you don't understand it all, this is just for you to see some actual Rails code.</p>
|
||||
|
||||
|
||||
<p>In the Comment model you have:</p>
|
||||
|
||||
|
||||
<div class="typocode"><pre><code class="typocode_ruby "><span class="keyword">class </span><span class="class">Comment</span> <span class="punct"><</span> <span class="constant">Content</span>
|
||||
<span class="ident">belongs_to</span> <span class="symbol">:article</span>
|
||||
<span class="ident">belongs_to</span> <span class="symbol">:user</span>
|
||||
|
||||
<span class="ident">validates_presence_of</span> <span class="symbol">:author</span><span class="punct">,</span> <span class="symbol">:body</span>
|
||||
<span class="ident">validates_against_spamdb</span> <span class="symbol">:body</span><span class="punct">,</span> <span class="symbol">:url</span><span class="punct">,</span> <span class="symbol">:ip</span>
|
||||
<span class="ident">validates_age_of</span> <span class="symbol">:article_id</span>
|
||||
<span class="punct">...</span></code></pre></div>
|
||||
|
||||
<p>(I snuck in some validations as well)</p>
|
||||
|
||||
|
||||
<p>But look how it reads! Read it out loud. I'd bet that my mom would more or less follow this, and she's anything but a programmer. That's not to say programming should be easy for grandma, <strong>but code should be easily understood by humans</strong>. Let the computer understand things that are natural for me to type, since we're making it understand a common language anyways.</p>
|
||||
|
||||
|
||||
<p>Ruby and Ruby on Rails allow and encourage you to write beautiful code. That is so much more important than you may realize, because it leads to many other virtues. Readability is obvious, and hence maintainability. You must read code to understand and modify it. Oh, and happy programmers will be more productive than frustrated programmers.</p>
|
||||
|
||||
|
||||
<h3 id="migrations">Database Migrations</h3>
|
||||
|
||||
|
||||
<p>Here's one more life-saver: migrations. Migrations are a way to version your database schema from within Rails. So you have a table, call it <code>albums</code>, and you want to add the date the album was released. You could modify the database directly, but that's not fun. Even if you only have one server, all your configuration will be in one central place, the app. And Rails doesn't care if you have PostgreSQL, MySQL, or SQLite behind it. You can develop and test on SQLite and deploy on MySQL and the migrations will just work in both environments.</p>
|
||||
|
||||
|
||||
<div class="typocode"><pre><code class="typocode_ruby "><span class="keyword">class </span><span class="class">AddDateReleased</span> <span class="punct"><</span> <span class="constant">ActiveRecord</span><span class="punct">::</span><span class="constant">Migration</span>
|
||||
<span class="keyword">def </span><span class="method">self.up</span>
|
||||
<span class="ident">add_column</span> <span class="punct">"</span><span class="string">albums</span><span class="punct">",</span> <span class="punct">"</span><span class="string">date_released</span><span class="punct">",</span> <span class="symbol">:datetime</span>
|
||||
<span class="constant">Albums</span><span class="punct">.</span><span class="ident">update_all</span> <span class="punct">"</span><span class="string">date_released = now()</span><span class="punct">"</span>
|
||||
<span class="keyword">end</span>
|
||||
|
||||
<span class="keyword">def </span><span class="method">self.down</span>
|
||||
<span class="ident">remove_column</span> <span class="punct">"</span><span class="string">albums</span><span class="punct">",</span> <span class="punct">"</span><span class="string">date_released</span><span class="punct">"</span>
|
||||
<span class="keyword">end</span>
|
||||
<span class="keyword">end</span></code></pre></div>
|
||||
|
||||
<p>Then you run the migration (<code>rake migrate</code> does that) and boom, your up to date. If you're wondering, the <code>self.down</code> method indeed implies that you can take this the other direction as well. Think <code>rake migrate VERSION=X</code>.</p>
|
||||
|
||||
|
||||
<p><em>Along with the other screencasts is one on <a href="http://www.rubyonrails.org/screencasts">migrations</a> featuring none other than David Hansson. You should take a look, it's the third video.</em></p>
|
||||
|
||||
|
||||
<h3>Testing so easy it hurts</h3>
|
||||
|
||||
|
||||
<p>To start a rails project you type <code>rails project_name</code> and it creates a directory structure with a fresh project in it. This includes a directory appropriately called <em>test</em> which houses unit tests for the project. When you generate models and controllers it creates test stubs for you in that directory. Basically, it makes it so easy to test that you're a fool not to do it. As someone wrote on their site: <em>It means never having to say "<strong>I introduced a new bug while fixing another.</strong>"</em></p>
|
||||
|
||||
|
||||
<p>Rails builds on the unit testing that comes with Ruby. On a larger scale, that means that Rails is unlikely to flop on you because it is regularly tested using the same method. Ruby is unlikely to flop for the same reason. That makes me look good as a programmer. If you code for a living then it's of even more value to you.</p>
|
||||
|
||||
|
||||
<p><em>I don't know why it hurts. Maybe it hurts developers working with other frameworks or languages to see us have it so nice and easy.</em></p>
|
||||
|
||||
|
||||
<h2>Wrapping up</h2>
|
||||
|
||||
|
||||
<p>Rails means I have fun doing web development instead of being frustrated (CSS hacks aside). David Hansson may be right when he said you have to have been soured by Java or PHP to fully appreciate Rails, but that doesn't mean you won't enjoy it if you <em>do</em> like Java or PHP.</p>
|
||||
|
||||
|
||||
<p><a href="http://www.relevancellc.com/blogs/wp-trackback.php?p=31">Justin Gehtland</a> rewrote a Java app using Rails and the number of lines of code of the Rails version was very close to that of the XML configuration for the Java version. Java has strengths, libraries available <strong>now</strong> seems to be a big one, but it's too big for my needs. If you're like me then maybe you'll enjoy Rails as much as I do.</p>
|
||||
|
||||
|
||||
<h2>You're not done, you lied to me!</h2>
|
||||
|
||||
|
||||
<p>Sort of... there are a few things that it seems standard to include when someone writes about how Rails saved their life and gave them hope again. For completeness sake, I feel compelled to mention some principles common amongst those who develop Rails, and those who develop on Rails. It's entirely likely that there's nothing new for you here unless you're new to Rails or to programming, in which case I encourage you to read on.</p>
|
||||
|
||||
|
||||
<h3>DRY</h3>
|
||||
|
||||
|
||||
<p>Rails follows the DRY principle religiously. That is, <strong>Don't Repeat Yourself</strong>. Like MVC, I was already sold on this. I had previously encountered it in <a href="http://www.pragmaticprogrammer.com/ppbook/index.shtml">The Pragmatic Programmer</a>. Apart from telling <em>some_model</em> it <code>belongs_to :other_model</code> and <em>other_model</em> that it <code>has_many :some_models</code> nothing has jumped out at me which violates this principle. However, I feel that reading a model's code and seeing it's relationships to other models right there is a Good Thing™.</p>
|
||||
|
||||
|
||||
<h3>Convention over configuration <em>(or, Perceived intelligence)</em></h3>
|
||||
|
||||
|
||||
<p>Rails' developers also have the mantra "<em>convention over configuration</em>", which you can see from the video there. (you did watch it, didn't you? ;) Basically that just means Rails has sane defaults, but is still flexible if you don't like the defaults. You don't have to write even one line of SQL with Rails, but if you need greater control then you <em>can</em> write your own SQL. A standard cliché: <em>it makes the simple things easy and the hard possible</em>.</p>
|
||||
|
||||
|
||||
<p>Rails seems to have a level of intelligence which contributes to the wow-factor. After <a href="#migrations">these relationships</a> are defined I can now filter certain negative comments like so:</p>
|
||||
|
||||
|
||||
<div class="typocode"><pre><code class="typocode_ruby "><span class="ident">article</span> <span class="punct">=</span> <span class="constant">Article</span><span class="punct">.</span><span class="ident">find</span> <span class="symbol">:first</span>
|
||||
<span class="keyword">for</span> <span class="ident">comment</span> <span class="keyword">in</span> <span class="ident">article</span><span class="punct">.</span><span class="ident">comments</span> <span class="keyword">do</span>
|
||||
<span class="ident">print</span> <span class="ident">comment</span> <span class="keyword">unless</span> <span class="ident">comment</span><span class="punct">.</span><span class="ident">downcase</span> <span class="punct">==</span> <span class="punct">'</span><span class="string">you suck!</span><span class="punct">'</span>
|
||||
<span class="keyword">end</span></code></pre></div>
|
||||
|
||||
<p>Rails knows to look for the field <strong>article_id</strong> in the <strong>comments</strong> table of the database. This is just a convention. You can call it something else but then you have to tell Rails what you like to call it.</p>
|
||||
|
||||
|
||||
<p>Rails understands pluralization, which is a detail but it makes everything feel more natural. If you have a <strong>Person</strong> model then it will know to look for the table named <strong>people</strong>.</p>
|
||||
|
||||
|
||||
<h3>Code as you learn</h3>
|
||||
|
||||
|
||||
<p>I love how I've only been coding in Rails for a week or two and I can do so much already. It's natural, concise and takes care of the inane details. I love how I <em>know</em> that I don't even have to explain that migration example. It's plainly clear what it does to the database. It doesn't take long to get the basics down and once you do it goes <strong>fast</strong>.</p>
|
||||
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
Title: TextMate Snippets for Rails Assertions
|
||||
Date: February 20, 2006
|
||||
Timestamp: 1140508320
|
||||
Author: sjs
|
||||
Tags: textmate, rails, coding, rails, snippets, testing, textmate
|
||||
----
|
||||
|
||||
This time I've got a few snippets for assertions. Using these to type up your tests quickly, and then hitting **⌘R** to run the tests without leaving TextMate, makes testing your Rails app that much more convenient. Just when you thought it was already too easy! (Don't forget that you can use **⌥⌘↓** to move between your code and the corresponding test case.)
|
||||
|
||||
This time I'm posting the .plist files to make it easier for you to add them to TextMate. All you need to do is copy these to **~/Library/Application Support/TextMate/Bundles/Rails.tmbundle/Snippets**.
|
||||
|
||||
<p style="text-align: center;"><a href="/f/assert_snippets.zip">Assertion Snippets for Rails</a></p>
|
||||
|
||||
If anyone would rather I list them all here I can do that as well. Just leave a comment.
|
||||
|
||||
*(I wanted to include a droplet in the zip file that will copy the snippets to the right place, but my 3-hour attempt at writing the AppleScript to do so left me feeling quite bitter. Maybe I was just mistaken in thinking it would be easy to pick up AppleScript.)*
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
Title: TextMate: Insert text into self.down
|
||||
Date: February 21, 2006
|
||||
Timestamp: 1140562500
|
||||
Author: sjs
|
||||
Tags: textmate, rails, hacking, commands, macro, rails, snippets, textmate
|
||||
Styles: typocode
|
||||
----
|
||||
|
||||
<p><em><strong>UPDATE:</strong> I got everything working and it's all packaged up <a href="2006.02.22-intelligent-migration-snippets-0.1-for-textmate">here</a>. There's an installation script this time as well.</em></p>
|
||||
|
||||
<p>Thanks to <a href="http://thread.gmane.org/gmane.editors.textmate.general/8520">a helpful thread</a> on the TextMate mailing list I have the beginning of a solution to insert text at 2 (or more) locations in a file.</p>
|
||||
|
||||
|
||||
<p>I implemented this for a new snippet I was working on for migrations, <code>rename_column</code>. Since the command is the same in self.up and self.down simply doing a reverse search for <code>rename_column</code> in my <a href="2006.02.21-textmate-move-selection-to-self-down">hackish macro</a> didn't return the cursor the desired location.</p><p>That's enough introduction, here's the program to do the insertion:</p>
|
||||
|
||||
|
||||
<div class="typocode"><pre><code class="typocode_ruby "><span class="comment">#!/usr/bin/env ruby</span>
|
||||
<span class="keyword">def </span><span class="method">indent</span><span class="punct">(</span><span class="ident">s</span><span class="punct">)</span>
|
||||
<span class="ident">s</span> <span class="punct">=~</span> <span class="punct">/</span><span class="regex">^(<span class="escape">\s</span>*)</span><span class="punct">/</span>
|
||||
<span class="punct">'</span><span class="string"> </span><span class="punct">'</span> <span class="punct">*</span> <span class="global">$1</span><span class="punct">.</span><span class="ident">length</span>
|
||||
<span class="keyword">end</span>
|
||||
|
||||
<span class="ident">up_line</span> <span class="punct">=</span> <span class="punct">'</span><span class="string">rename_column "${1:table}", "${2:column}", "${3:new_name}"$0</span><span class="punct">'</span>
|
||||
<span class="ident">down_line</span> <span class="punct">=</span> <span class="punct">"</span><span class="string">rename_column <span class="escape">\"</span>$$1<span class="escape">\"</span>, <span class="escape">\"</span>$$3<span class="escape">\"</span>, <span class="escape">\"</span>$$2<span class="escape">\"\n</span></span><span class="punct">"</span>
|
||||
|
||||
<span class="comment"># find the end of self.down and insert 2nd line</span>
|
||||
<span class="ident">lines</span> <span class="punct">=</span> <span class="constant">STDIN</span><span class="punct">.</span><span class="ident">read</span><span class="punct">.</span><span class="ident">to_a</span><span class="punct">.</span><span class="ident">reverse</span>
|
||||
<span class="ident">ends_seen</span> <span class="punct">=</span> <span class="number">0</span>
|
||||
<span class="ident">lines</span><span class="punct">.</span><span class="ident">each_with_index</span> <span class="keyword">do</span> <span class="punct">|</span><span class="ident">line</span><span class="punct">,</span> <span class="ident">i</span><span class="punct">|</span>
|
||||
<span class="ident">ends_seen</span> <span class="punct">+=</span> <span class="number">1</span> <span class="keyword">if</span> <span class="ident">line</span> <span class="punct">=~</span> <span class="punct">/</span><span class="regex">^<span class="escape">\s</span>*end<span class="escape">\b</span></span><span class="punct">/</span>
|
||||
<span class="keyword">if</span> <span class="ident">ends_seen</span> <span class="punct">==</span> <span class="number">2</span>
|
||||
<span class="ident">lines</span><span class="punct">[</span><span class="ident">i</span><span class="punct">..</span><span class="ident">i</span><span class="punct">]</span> <span class="punct">=</span> <span class="punct">[</span><span class="ident">lines</span><span class="punct">[</span><span class="ident">i</span><span class="punct">],</span> <span class="ident">indent</span><span class="punct">(</span><span class="ident">lines</span><span class="punct">[</span><span class="ident">i</span><span class="punct">])</span> <span class="punct">*</span> <span class="number">2</span> <span class="punct">+</span> <span class="ident">down_line</span><span class="punct">]</span>
|
||||
<span class="keyword">break</span>
|
||||
<span class="keyword">end</span>
|
||||
<span class="keyword">end</span>
|
||||
|
||||
<span class="comment"># return the new text, escaping special chars</span>
|
||||
<span class="ident">print</span> <span class="ident">up_line</span> <span class="punct">+</span> <span class="ident">lines</span><span class="punct">.</span><span class="ident">reverse</span><span class="punct">.</span><span class="ident">to_s</span><span class="punct">.</span><span class="ident">gsub</span><span class="punct">('</span><span class="string">[$`<span class="escape">\\</span>]</span><span class="punct">',</span> <span class="punct">'</span><span class="string"><span class="escape">\\\\</span>\1</span><span class="punct">').</span><span class="ident">gsub</span><span class="punct">('</span><span class="string"><span class="escape">\\</span>$<span class="escape">\\</span>$</span><span class="punct">',</span> <span class="punct">'</span><span class="string">$</span><span class="punct">')</span></code></pre></div>
|
||||
|
||||
<p>Save this as a command in your Rails, or <a href="http://blog.inquirylabs.com/">syncPeople on Rails</a>, bundle. The command options should be as follows:</p>
|
||||
|
||||
|
||||
<ul>
|
||||
<li><strong>Save:</strong> Nothing</li>
|
||||
<li><strong>Input:</strong> Selected Text or Nothing</li>
|
||||
<li><strong>Output:</strong> Insert as Snippet</li>
|
||||
<li><strong>Activation:</strong> Whatever you want, I'm going to use a macro described below and leave this empty</li>
|
||||
<li><strong>Scope Selector:</strong> source.ruby.rails</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>The first modification it needs is to get the lines to insert as command line arguments so we can use it for other snippets. Secondly, regardless of the <strong>Re-indent pasted text</strong> setting the text returned is indented incorrectly.</p>
|
||||
|
||||
|
||||
The macro I'm thinking of to invoke this is tab-triggered and will simply:
|
||||
<ul>
|
||||
<li>Select word (<tt><strong>⌃W</strong></tt>)</li>
|
||||
<li>Delete (<tt><strong>⌫</strong></tt>)</li>
|
||||
<li>Select to end of file (<tt><strong>⇧⌘↓</strong></tt>)</li>
|
||||
<li>Run command "Put in self.down"</li>
|
||||
</ul>
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
Title: TextMate: Move selection to self.down
|
||||
Date: February 21, 2006
|
||||
Timestamp: 1140510360
|
||||
Author: sjs
|
||||
Tags: textmate, rails, hacking, hack, macro, rails, textmate
|
||||
----
|
||||
|
||||
<p><strong>UPDATE:</strong> <em>This is obsolete, see <a href="2006.02.21-textmate-insert-text-into-self-down">this post</a> for a better solution.</em></p>
|
||||
|
||||
<p><a href="2006.02.18-some-textmate-snippets-for-rails-migrations.html#comment-3">Duane's comment</a> prompted me to think about how to get the <code>drop_table</code> and <code>remove_column</code> lines inserted in the right place. I don't think TextMate's snippets are built to do this sort of text manipulation. It would be nicer, but a quick hack will suffice for now.</p><p>Use <acronym title="Migration Create and Drop Table">MCDT</acronym> to insert:</p>
|
||||
|
||||
<div class="typocode"><pre><code class="typocode_ruby "><span class="ident">create_table</span> <span class="punct">"</span><span class="string">table</span><span class="punct">"</span> <span class="keyword">do</span> <span class="punct">|</span><span class="ident">t</span><span class="punct">|</span>
|
||||
|
||||
<span class="keyword">end</span>
|
||||
<span class="ident">drop_table</span> <span class="punct">"</span><span class="string">table</span><span class="punct">"</span></code></pre></div>
|
||||
|
||||
<p>Then press tab once more after typing the table name to select the code <code>drop_table "table"</code>. I created a macro that cuts the selected text, finds <code>def self.down</code> and pastes the line there. Then it searches for the previous occurence of <code>create_table</code> and moves the cursor to the next line, ready for you to add some columns.</p>
|
||||
|
||||
|
||||
<p>I have this bound to <strong>⌃⌥⌘M</strong> because it wasn't in use. If your Control key is to the left the A key it's quite comfortable to hit this combo. Copy the following file into <strong>~/Library/Application Support/TextMate/Bundles/Rails.tmbundle/Macros</strong>.</p>
|
||||
|
||||
|
||||
<p style="text-align: center;"><a href="http://sami.samhuri.net/files/move-to-self.down.plist">Move selection to self.down</a></p>
|
||||
|
||||
|
||||
<p>This works for the <acronym title="Migration Add and Remove Column">MARC</acronym> snippet as well. I didn't tell you the whole truth, the macro actually finds the previous occurence of <code>(create_table|add_column)</code>.</p>
|
||||
|
||||
|
||||
<p>The caveat here is that if there is a <code>create_table</code> or <code>add_column</code> between <code>self.down</code> and the table you just added, it will jump back to the wrong spot. It's still faster than doing it all manually, but should be improved. If you use these exclusively, the order they occur in <code>self.down</code> will be opposite of that in <code>self.up</code>. That means either leaving things backwards or doing the re-ordering manually. =/</p>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
Title: Intelligent Migration Snippets 0.1 for TextMate
|
||||
Date: February 22, 2006
|
||||
Timestamp: 1140607680
|
||||
Author: sjs
|
||||
Tags: mac os x, textmate, rails, hacking, migrations, snippets
|
||||
----
|
||||
|
||||
*This should be working now. I've tested it under a new user account here.*
|
||||
|
||||
*This does requires the syncPeople bundle to be installed to work. That's ok, because you should get the [syncPeople on Rails bundle][syncPeople] anyways.*
|
||||
|
||||
When writing database migrations in Ruby on Rails it is common to create a table in the `self.up` method and then drop it in `self.down`. The same goes for adding, removing and renaming columns.
|
||||
|
||||
I wrote a Ruby program to insert code into both methods with a single snippet. All the TextMate commands and macros that you need are included.
|
||||
|
||||
### See it in action ###
|
||||
|
||||
I think this looks cool in action. Plus I like to show off what what TextMate can do to people who may not use it, or don't have a Mac. It's just over 30 seconds long and weighs in at around 700kb.
|
||||
|
||||
<p style="text-align: center">
|
||||
<img src="/images/download.png" title="Download" alt="Download">
|
||||
<a href="/f/ims-demo.mov">Download Demo Video</a>
|
||||
</p>
|
||||
|
||||
### Features ###
|
||||
|
||||
There are 3 snippets which are activated by the following tab triggers:
|
||||
|
||||
* __mcdt__: Migration Create and Drop Table
|
||||
* __marc__: Migration Add and Remove Column
|
||||
* __mnc__: Migration Rename Column
|
||||
|
||||
### Installation ###
|
||||
|
||||
Run **Quick Install.app** to install these commands to your <a [syncPeople on Rails bundle](syncPeople) if it exists, and to the default Rails bundle otherwise. (I highly recommend you get the syncPeople bundle if you haven't already.)
|
||||
|
||||
<p style="text-align: center">
|
||||
<img src="/images/download.png" title="Download" alt="Download">
|
||||
<a href="/f/IntelligentMigrationSnippets-0.1.dmg">Download Intelligent Migration Snippets</a>
|
||||
</p>
|
||||
|
||||
This is specific to Rails migrations, but there are probably other uses for something like this. You are free to use and distribute this code.
|
||||
|
||||
[syncPeople]: http://blog.inquirylabs.com/
|
||||
31
public/posts/2006.02.23-sjs-rails-bundle-0.2-for-textmate.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
Title: SJ's Rails Bundle 0.2 for TextMate
|
||||
Date: February 23, 2006
|
||||
Timestamp: 1140743880
|
||||
Author: sjs
|
||||
Tags: textmate, rails, coding, bundle, macros, rails, snippets, textmate
|
||||
Styles: typocode
|
||||
----
|
||||
|
||||
Everything that you've seen posted on my blog is now available in one bundle. Snippets for Rails database migrations and assertions are all included in this bundle.
|
||||
|
||||
There are 2 macros for class-end and def-end blocks, bound to <strong>⌃C</strong> and <strong>⌃D</strong> respectively. Type the class or method definition, except for <code>class</code> or <code>def</code>, and then type the keyboard shortcut and the rest is filled in for you.
|
||||
|
||||
I use an underscore to denote the position of the cursor in the following example:
|
||||
|
||||
|
||||
<div class="typocode"><pre><code class="typocode_ruby "><span class="ident">method</span><span class="punct">(</span><span class="ident">arg1</span><span class="punct">,</span> <span class="ident">arg2_</span><span class="punct">)</span></code></pre></div>
|
||||
|
||||
Typing <strong>⌃D</strong> at this point results in this code:
|
||||
|
||||
|
||||
<div class="typocode"><pre><code class="typocode_ruby "><span class="keyword">def </span><span class="method">method</span><span class="punct">(</span><span class="ident">arg1</span><span class="punct">,</span> <span class="ident">arg2</span><span class="punct">)</span>
|
||||
<span class="ident">_</span>
|
||||
<span class="keyword">end</span></code></pre></div>
|
||||
|
||||
There is a list of the snippets in Features.rtf, which is included in the disk image. Of course you can also browse them in the Snippets Editor built into TextMate.
|
||||
|
||||
Without further ado, here is the bundle:
|
||||
|
||||
<p style="text-align: center;"><img src="/images/download.png" title="Download" alt="Download"> <a href="/f/SJRailsBundle-0.2.dmg">Download SJ's Rails Bundle 0.2</a></p>
|
||||
|
||||
This is a work in progress, so any feedback you have is very helpful in making the next release better.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
Title: Generate self.down in your Rails migrations
|
||||
Date: March 3, 2006
|
||||
Timestamp: 1141450680
|
||||
Author: sjs
|
||||
Tags: rails, textmate, migrations, rails, textmate
|
||||
----
|
||||
|
||||
<a href="http://lunchboxsoftware.com/">Scott</a> wrote a really <a href="http://lunchroom.lunchboxsoftware.com/articles/2005/11/29/auto-fill-your-reverse-migrations">cool program</a> that will scan `self.up` and then consult db/schema.rb to automatically fill in `self.down` for you. Brilliant!
|
||||
22
public/posts/2006.03.03-i-dont-mind-fairplay-either.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
Title: I don't mind FairPlay either
|
||||
Date: March 3, 2006
|
||||
Timestamp: 1141451760
|
||||
Author: sjs
|
||||
Tags: apple, mac os x, life, drm, fairplay, ipod, itunes
|
||||
----
|
||||
|
||||
I think that <a href="http://jim.roepcke.com/2006/03/02#item7471">Jim is right</a> about Apple's DRM not being all that evil.
|
||||
|
||||
I buy music from the iTunes Music Store *because* I bought an iPod. The fact I can't play them on another device doesn't matter to me. With my purchased songs I can:
|
||||
|
||||
* listen to the songs I buy all I want
|
||||
* burn them to CD
|
||||
* stream them to my amplifier via AirPort Express
|
||||
|
||||
I don't buy a ton of music from the iTMS, but I can't tell the difference between any of those songs and the songs I ripped from CDs and they're all mixed in one collection. That's good enough for me.
|
||||
|
||||
I dislike DRM as much as the next guy, but like CSS encryption on DVDs, FairPlay is something I can live with.
|
||||
|
||||
It reminds me of how here in North America I have to live with the crappy cell phone companies that lock their phones to their networks. If it's something I need or want, sometimes I'll live with restrictions because there are no alternatives yet.
|
||||
|
||||
*__Update:__ It's almost settled. The pope <a href="http://www.catholicnews.com/data/stories/cns/0601282.htm">got an iPod</a> so all that's left is to see if he buys any music off of iTunes. If he does, then it can't be evil. heh...*
|
||||
10
public/posts/2006.03.03-spore.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Title: Spore
|
||||
Date: March 3, 2006
|
||||
Timestamp: 1141450980
|
||||
Author: sjs
|
||||
Tags: amusement, technology, cool, fun, games
|
||||
----
|
||||
|
||||
<a href="http://video.google.com/videoplay?docid=8372603330420559198&q=spore">This game</a> that <a href="http://jim.roepcke.com/">Jim</a> <a href="http://jim.roepcke.com/2006/03/01#item7470">blogged about</a> is probably the coolest game I've seen.
|
||||
|
||||
You really just have to watch the video, I won't bother explaining it here. I don't really play games much, but this I would play.
|
||||
10
public/posts/2006.04.04-zsh-terminal-goodness-on-os-x.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Title: zsh terminal goodness on OS X
|
||||
Date: April 4, 2006
|
||||
Timestamp: 1144187820
|
||||
Author: sjs
|
||||
Tags: mac os x, apple, osx, terminal, zsh
|
||||
----
|
||||
|
||||
<a href="http://www.apple.com/">Apple</a> released the <a href="http://docs.info.apple.com/article.html?artnum=303411">OS X 10.4.6 update</a> which fixed a <strong>really</strong> annoying bug for me. Terminal (and <a href="http://iterm.sourceforge.net/">iTerm</a>) would fail to open a new window/tab when your shell is <a href="http://zsh.sourceforge.net/">zsh</a>. iTerm would just open then immediately close the window, while Terminal would display the message: <code>[Command completed]</code> in a now-useless window.
|
||||
|
||||
Rebooting twice to get the fix was reminiscent of <a href="http://www.microsoft.com/windows/default.mspx">Windows</a>, but well worth it.
|
||||
8
public/posts/2006.05.07-os-x-and-fitts-law.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Title: OS X and Fitt's law
|
||||
Date: May 7, 2006
|
||||
Timestamp: 1147059780
|
||||
Author: sjs
|
||||
Tags: mac os x, apple, mac, os, usability, x
|
||||
----
|
||||
|
||||
I've realized that OS X really does obey Fitt's law in all 4 corners now. Apple menu in the top left, Spotlight top right, and the bottom 2 are always accessible for drag n drop, unless the dock is hidden. I rarely ever use it because I usually have pretty good chunks of the desktop showing, but it is useful.
|
||||
10
public/posts/2006.05.07-wikipediafs-on-linux-in-python.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Title: WikipediaFS on Linux, in Python
|
||||
Date: May 7, 2006
|
||||
Timestamp: 1147060140
|
||||
Author: sjs
|
||||
Tags: hacking, python, linux, fuse, linux, mediawiki, python, wikipediafs
|
||||
----
|
||||
|
||||
Till now I've been using my own version of <a href="http://meta.wikimedia.org/wiki/Pywikipedia">pywikipedia</a> for scripting MediaWiki, and it works well. But I read about <a href="http://wikipediafs.sourceforge.net/">WikipediaFS</a> and had to check it out. It's a user space filesystem for Linux that's built using the <a href="http://fuse.sourceforge.net/wiki/index.php/LanguageBindings">Python bindings</a> for <a href="http://fuse.sourceforge.net/">FUSE</a>. What it does is mounts a filesystem that represents your wiki, with articles as text files. You can use them just like any other files with mv, cp, ls, vim, and so on.
|
||||
|
||||
There hasen't been any action on that project for 13 months though, and it doesn't work on my wiki (MediaWiki 1.4.15) so I'm going to try and make it work after I upgrade to MediaWiki 1.6.3 tonight. This will be pretty cool when it works. I haven't looked at the code yet but it's only 650 lines.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
Title: Ich bin Ausländer und spreche nicht gut Deutsch
|
||||
Date: June 5, 2006
|
||||
Timestamp: 1149527460
|
||||
Author: sjs
|
||||
Tags: life, munich, seekport, work
|
||||
----
|
||||
|
||||
How's this for an update: I'm working in Munich for the summer at a European search engine called <a href="http://www.seekport.co.uk/">Seekport</a>. The search engine isn't all they do, as right now I'm programming a desktop widget that shows live scores & news from World Cup matches (in English and Arabic). I'm building it on top of the <a href="http://widgets.yahoo.com/">Yahoo! Widget Engine</a> because it needs to run on Windows. Even though I quite like the Y! Engine, I would still prefer to be coding in straight HTML, CSS & JavaScript like Dashboard programmers get to use. The Y! Engine uses XML (it is somewhat HTML-like) and JavaScript.
|
||||
|
||||
The place I'm living in is like a dormitory for younger people. I share a bathroom & kitchen with a German guy named Sebastian who is 21 and an artist; a stonecutter actually. I only met him briefly yesterday, but he seems nice. I'm going to teach him English, and he'll teach me German, though his English is much better than my German. It's a pretty quiet place, and we get breakfast included, dinner can be bought for €2,50, and Internet access is included as well. I brought my Mac Mini with me, and as soon as I find an AC adapter I'll be ready to go with the 'net at home. I probably won't blog again till then, since I'm at work right now.
|
||||
|
||||
Germany is great so far, and as soon as I get learning some German I'll be a much happier person. I consider it rude of me to expect everyone to converse with me in English, like I have to do right now.
|
||||
|
||||
(Oh, and they sell beer by the litre in Germany! They call it a maß.)
|
||||
14
public/posts/2006.06.09-never-buy-a-german-keyboard.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
Title: Never buy a German keyboard!
|
||||
Date: June 9, 2006
|
||||
Timestamp: 1149841020
|
||||
Author: sjs
|
||||
Tags: apple, apple, german, keyboard
|
||||
----
|
||||
|
||||
Nothing personal, but the backtick/tilde is located where the rest of the left shift key should be, and the return key is double-height, forcing the backslash/bar to the right of the dash/underscore (that'd be the apostrophe/double quote for pretty much everyone else who types qwerty). Note that I'm talking about using a German keyboard with an English layout. The German layout is flat out impossible for coding.
|
||||
|
||||
<a href="/images/keyboard.jpg"><img src="/images/keyboard.jpg" title="German Apple Keyboard" alt="German Apple Keyboard"></a>
|
||||
|
||||
For some reason it gets even worse with a German Apple keyboard. Square brackets, where for art though? Through trial and error I found them using Alt/Opt+5/6... non-Apple German keyboards I've seen use Alt Gr+8/9, which is just as bad but at least they were <strong>labeled</strong>. I know why coders here don't use the German layout! I feel uneasy just talking about it.
|
||||
|
||||
Here's a <a href="/f/german_keys.txt">text file</a> with each character of the 4 rows in it, normal and then shifted, in qwerty, qwertz, and dvorak. I personally think that some ways the German keys change must be some sick joke (double quote moved up to shift-2, single quote almost staying put, angle brackets being shoved aside only to put the semi-colon and colon on different keys as well). If you ask me lots of that could be avoided by getting rid of the key that replaced the backtick/tilde, and putting the 3 vowels with the umlaut (ü, ö, and ä) on Alt Gr/Opt+[aou]. But hey, I don't type in German so what do I know.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
Title: There's nothing regular about regular expressions
|
||||
Date: June 10, 2006
|
||||
Timestamp: 1149928080
|
||||
Author: sjs
|
||||
Tags: technology, book, regex
|
||||
----
|
||||
|
||||
I'm almost half way reading Jeffrey Friedl's book <a href="http://www.oreilly.com/catalog/regex2/">Mastering Regular Expressions</a> and I have to say that for a book on something that could potentially bore you to tears, he really does an excellent job of keeping it interesting. Even though a lot of the examples are contrived (I'm sure out of necessity), he also uses real examples of regexes that he's actually used at <a href="http://www.yahoo.com/">Yahoo!</a>.
|
||||
|
||||
As someone who has to know how everything works it's also an excellent lesson in patience, as he frequently says "here, take this knowledge and just accept it for now until I can explain why in the next chapter (or in 3 chapters!)". But it's all with good reason and when he does explain he does it well.
|
||||
|
||||
Reading about the different NFA and DFA engines and which tools use which made me go "ahhh, /that's/ why I can't do that in grep!" It's not just that I like to know how things work either, he's 100% correct about having to know information like that to wield the power of regexes in all situations. This book made me realize that regex implementations can be wildly different and that you really need to consider the job before jumping into using a specific regex flavour, as he calls them. I'm fascinated by learning why DFA regex implementations would successfully allow `^\w+=.(\\\n.)*` to match certain lines, allowing for trailing backslashes to mean continuation but why NFA engines would fail to do the same without tweaking it a bit.
|
||||
|
||||
It requires more thinking than the last 2 computer books I read, *Programming Ruby* (the "pixaxe" book) and *Agile Web Development With Rails* so it's noticeably slower reading. It's also the kind of book I will read more than once, for sure. There's just no way I can glean everything from it in one reading. If you use regular expressions at all then you need this book. This is starting to sound like an advertisement so I'll say no more.
|
||||
|
||||
QOTD, p. 329, about matching nested pairs of parens:
|
||||
|
||||
\(([^()]|\(([^()]|\(([^()]|\(([^()])*\))*\))*\))*\)
|
||||
Wow, that's ugly.
|
||||
|
||||
(Don't worry, there's a much better solution on the next 2 pages after that quote.)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
Title: Apple pays attention to detail
|
||||
Date: June 11, 2006
|
||||
Timestamp: 1150014600
|
||||
Author: sjs
|
||||
Tags: technology, mac os x, apple
|
||||
----
|
||||
|
||||
I think this has to be one of the big reasons why people who love their Mac, love their Mac (or other Apple product). I usually just have cheap PC speakers plugged into my Mac mini, but I didn't bring any with me to Munich and the internal Mac mini speaker isn't very loud, so I'm using headphones to watch movies. My Mac remembers the volume setting when the headphones ore plugged in, and when they're not, so I don't accidentally blow my ears. It's like my iPod pausing when the headphones are unplugged. It's excruciating attention to the smallest, (seemingly) most unimportant detail. I love it, and I'm hooked.
|
||||
41
public/posts/2006.07.06-working-with-the-zend-framework.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
Title: Working with the Zend Framework
|
||||
Date: July 6, 2006
|
||||
Timestamp: 1152196560
|
||||
Author: sjs
|
||||
Tags: coding, technology, php, framework, php, seekport, zend
|
||||
----
|
||||
|
||||
At <a href="http://www.seekport.co.uk/">Seekport</a> I'm currently working on an app to handle
|
||||
the config of their business-to-business search engine. It's web-based and I'm using PHP, since
|
||||
that's what they're re-doing the front-end in. Right now it's a big mess of Perl, the main
|
||||
developer (for the front-end) is gone, and they're having trouble managing it. I have read
|
||||
through it, and it's pretty dismal. They have config mixed with logic and duplicated code all
|
||||
over the place. There's an 1100 line sub in one of the perl modules. Agh!
|
||||
|
||||
Anyway, I've been looking at basically every damn PHP framework there is and most of them
|
||||
aren't that great (sorry to the devs, but they're not). It's not really necessary for my little
|
||||
project, but it helps in both writing and maintaining it. Many of them are unusable because
|
||||
they're still beta and have bugs, and I need to develop the app not debug a framework. Some of
|
||||
them are nice, but not really what I'm looking for, such as <a
|
||||
href="http://www.qcodo.com/">Qcodo</a>, which otherwise look really cool.
|
||||
|
||||
<a href="http://cakephp.org/">CakePHP</a> and <a
|
||||
href="http://www.symfony-project.com/">Symfony</a> seem to want to be <a
|
||||
href="http://www.rubyonrails.org/">Rails</a> so badly, but fall short in many ways, code beauty
|
||||
being the most obvious one.
|
||||
|
||||
I could go on about them all, I looked at over a dozen and took at least 5 of them for a
|
||||
test-drive. The only one I really think has a chance to be <em>the</em> PHP framework is the <a
|
||||
href="http://framework.zend.com/">Zend Framework</a>. I really don't find it that amazing, but
|
||||
it feels right, whereas the others feel very thrown-together. In other words, it does a good
|
||||
job of not making it feel like PHP. ;-)
|
||||
|
||||
Nothing they're doing is relovutionary, and I question the inclusion of things like PDF
|
||||
handling when they don't even seem to have relationships figured out, but it provides a nice
|
||||
level of convenience above PHP without forcing you into their pattern of thinking. A lot of the
|
||||
other frameworks I tried seemed like one, big, unbreakable unit. With Zend I can really tell
|
||||
that nothing is coupled.
|
||||
|
||||
So I'll probably be writing some notes here about my experience with this framework. I also
|
||||
hope to throw Adobe's <a href="http://labs.adobe.com/technologies/spry/">Spry</a> into the mix.
|
||||
That little JS library is a lot of fun.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
Title: Ubuntu: Linux for Linux users please
|
||||
Date: July 13, 2006
|
||||
Timestamp: 1152804840
|
||||
Author: sjs
|
||||
Tags: linux, linux, ubuntu
|
||||
----
|
||||
|
||||
<a href="http://www.ubuntu.com/">Ubuntu</a> is a fine Linux distro, which is why it's popular. I still use <a href="http://www.gentoo.org/">Gentoo</a> on my servers but Ubuntu is fast to set up for a desktop. Linux for humans it certainly is, but dammit sometimes I want Linux like I'm used to.
|
||||
|
||||
It should ship with build-essentials (gcc & others) installed. It *shouldn't* ask me if I'm sure I want to restart at the GDM login screen. I have no session open and already clicked twice to choose Restart.
|
||||
|
||||
Other things aren't quite ready for humans yet. Network config needs some work. It's very slow to apply changes. Connecting to Windows printers should be easier (ie. let us browse to find them, or just search and present a list). Fonts aren't amazing yet, though Mac OS X has spoiled me as far as fonts are concerned.
|
||||
|
||||
Other than these things I think Ubuntu Dapper is a fine release. It installed on my work laptop without a problem and detected the volume keys and wireless network card flawlessly.
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
Title: Ruby and Rails have spoiled me rotten
|
||||
Date: July 17, 2006
|
||||
Timestamp: 1153140000
|
||||
Author: sjs
|
||||
Tags: rails, ruby, php, coding, framework, php, rails, ruby, zend
|
||||
----
|
||||
|
||||
It's true. I'm sitting here coding in PHP using the <a href="http://framework.zend.com/">Zend Framework</a> and all I can think about is how much nicer Rails is, or how much easier it is to do [x] in Ruby. It's not that the Zend Framework is bad or anything, it's quite nice, but you just can't match Ruby's expressiveness in a language like PHP. Add the amazing convenience Rails builds on top of Ruby and that's a really hard combo to compete with.
|
||||
|
||||
I'd love to be using mixins instead of mucking around with abstract classes and interfaces, neither of which will just let you share a method between different classes. Writing proxy methods in these tiny in-between classes is annoying. (ie. inherit from Zend_class, then my real classes inherit from the middle-man class) I *could* add things to Zend's classes, but then upgrades are a bitch. I miss Ruby. I could use something like <a href="http://www.advogato.org/article/470.html">whytheluckystiff's PHP mixins</a>, which is a clever hack, but still a hack.
|
||||
|
||||
I keep looking at Rails code to see how things are done there, and I already coded a nearly complete prototype in Rails as a reference. I could have finished the thing in Rails by now, seriously. I'm still playing catch-up writing validations and model classes for all my objects, stuff I could've had for free using Rails, with an extra 10 mins to add validations and make sure they're all working nicely.
|
||||
|
||||
It's no wonder <a href="http://www.loudthinking.com/">David H. Hansson</a> wasn't able to write a framework he was happy with in PHP. After using Rails everything seems like a chore. I'm just coding solved problems over again in an inferior language.
|
||||
|
||||
But hey, I'm learning things and I still got to use Ruby even if the code won't be used later. I guess this experience will just make me appreciate the richness of Ruby and Rails even more.
|
||||
47
public/posts/2006.07.19-late-static-binding.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
Title: Late static binding
|
||||
Date: July 19, 2006
|
||||
Timestamp: 1153329780
|
||||
Author: sjs
|
||||
Tags: php, coding, coding, php
|
||||
----
|
||||
|
||||
*Update: This has <a href="http://www.php.net/~derick/meeting-notes.html#late-static-binding-using-this-without-or-perhaps-with-a-different-name">been discussed</a> and will be uh, sort of fixed, in PHP6. You'll be able to use static::my_method() to get the real reference to self in class methods. Not optimal, but still a solution I guess.*
|
||||
|
||||
As colder on ##php (freenode) told me today, class methods in PHP don't have what they call late static binding. What's that? It means that this code:
|
||||
|
||||
<pre>
|
||||
<code>
|
||||
class Foo
|
||||
{
|
||||
public static function my_method()
|
||||
{
|
||||
echo "I'm a " . get_class() . "!\n";
|
||||
}
|
||||
}
|
||||
|
||||
class Bar extends Foo
|
||||
{}
|
||||
|
||||
Bar::my_method();
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
outputs "I'm a Foo!", instead of "I'm a Bar!". That's not fun.
|
||||
|
||||
Using <code>__CLASS__</code> in place of <code>get_class()</code> makes zero difference. You end up with proxy methods in each subclass of Foo that pass in the real name of the calling class, which sucks.
|
||||
|
||||
<pre>
|
||||
<code>
|
||||
class Bar extends Foo
|
||||
{
|
||||
public static function my_method()
|
||||
{
|
||||
return parent::my_method( get_class() );
|
||||
}
|
||||
}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
I was told that they had a discussion about this on the internal PHP list, so at least they're thinking about this stuff. Too bad PHP5 doesn't have it. I guess I should just be glad I won't be maintaining this code.
|
||||
|
||||
The resident PHP coder said "just make your code simpler", which is what I was trying to do by removing duplication. Too bad that plan sort of backfired. I guess odd things like this are where PHP starts to show that OO was tacked on as an after-thought.
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
Title: Class method? Instance method? It doesn't matter to PHP
|
||||
Date: July 21, 2006
|
||||
Timestamp: 1153493760
|
||||
Author: sjs
|
||||
Tags: php, coding
|
||||
----
|
||||
|
||||
*Update: This has <a href="http://www.php.net/~derick/meeting-notes.html#method-calls">been discussed</a> for PHP6. A little late, but I guess better than never.*
|
||||
|
||||
I made a mistake while I was coding, for shame! Anyway this particular mistake was that I invoked a class method on the wrong class. The funny part was that this method was an instance method in the class which I typed by mistake. In the error log I saw something like "Invalid use of $this in class function."
|
||||
|
||||
I knew for a fact I hadn't used $this in a class method, so it was kind of a confusing error. I went to the file in question and found out that it was calling an instance method as a class method. Now that is some crazy shit.
|
||||
|
||||
I would fully expect the PHP parser to give me an error like "No class method [foo] in class [blah]", rather than try and execute it as a class method. The syntax is completely different; you use :: to call a class method and -> to call an instance method. And you use the name of a <em>class</em> when you call a class method.
|
||||
|
||||
This code:
|
||||
|
||||
<pre><code>
|
||||
class Foo {
|
||||
public static function static_fun()
|
||||
{
|
||||
return "This is a class method!\n";
|
||||
}
|
||||
|
||||
public function not_static()
|
||||
{
|
||||
return "This is an instance method!\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo '<pre>';
|
||||
echo "From Foo:\n";
|
||||
echo Foo::static_fun();
|
||||
echo Foo::not_static();
|
||||
echo "\n";
|
||||
|
||||
echo "From \$foo = new Foo():\n";
|
||||
$foo = new Foo();
|
||||
echo $foo->static_fun();
|
||||
echo $foo->not_static();
|
||||
echo '</pre>';
|
||||
</code></pre>
|
||||
|
||||
Produces:
|
||||
|
||||
<pre><code>
|
||||
From Foo:
|
||||
This is a class method!
|
||||
This is an instance method!
|
||||
|
||||
From $foo = new Foo():
|
||||
This is a class method!
|
||||
This is an instance method!
|
||||
</code></pre>
|
||||
|
||||
What the fuck?! <a href="http://www.php.net/manual/en/language.oop5.static.php">http://www.php.net/manual/en/language.oop5.static.php</a> is lying to everyone.
|
||||
8
public/posts/2006.08.22-where-are-my-headphones.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Title: Where are my headphones?
|
||||
Date: August 22, 2006
|
||||
Timestamp: 1156257060
|
||||
Author: sjs
|
||||
Tags: life, seekport
|
||||
----
|
||||
|
||||
Some people left Seekport this month and 2 of the remaining employees moved into the office I’m working in. That’s fine, and I’m leaving at the end of the week, but man I’m going crazy. This guy’s pounding on his keyboard like it’s a fucking whack-a-mole game! I don’t know what kind of keyboard he learned to type on but it must’ve been horrible. It sounds like he must go through at least 10 of those things in a year. I don’t know if I’ll make it till Friday without yelling "AGH! STOP THE MADNESS YOU CRAZY BASTARD YOU JUST HAVE TO TOUCH THE KEYS!"
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Title: Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo
|
||||
Date: September 16, 2006
|
||||
Timestamp: 1158469860
|
||||
Author: sjs
|
||||
Tags: amusement, buffalo
|
||||
----
|
||||
|
||||
Wouldn't the sentence 'I want to put a hyphen between the words Fish and And and And and Chips in my Fish-And-Chips sign' have been clearer if quotation marks had been placed before Fish, and between Fish and and, and and and And, and And and and, and and and And, and And and and, and and and Chips, as well as after Chips?
|
||||
|
||||
→ <a href="http://en.wikipedia.org/wiki/Buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo">Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo</a>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
Title: Some features you might have missed in iTunes 7
|
||||
Date: September 22, 2006
|
||||
Timestamp: 1158969540
|
||||
Author: sjs
|
||||
Tags: apple, apple, itunes
|
||||
----
|
||||
|
||||
<img src="/images/menu.png" style="float: right; margin: 10px;" title="New menu" alt="New menu">
|
||||
|
||||
Besides the <a href="http://www.tuaw.com/2006/09/12/walkthrough-itunes-7s-big-new-features/">big changes</a> in <a href="http://www.apple.com/itunes">iTunes 7</a> there have been some minor changes that are still pretty useful.
|
||||
|
||||
Here's a quick recap of a few major features:
|
||||
|
||||
* <a href="http://sami.samhuri.net/files/coverflow.png">Coverflow</a> built in, new views to flip through album covers and video thumbnails
|
||||
* iTunes Music Store is now just the iTunes Store
|
||||
* New look, no more brushed metal
|
||||
* The menu on the left is more structured and easier to navigate (for me)
|
||||
* Games support
|
||||
|
||||
And now some of the smaller gems are listed below.
|
||||
|
||||
<h3 style="clear: right;">Video controls</h3>
|
||||
|
||||
<a href="/images/itunes-controls.png"><img src="/images/itunes-controls-thumb.png" style="float: left; margin: 10px;" title="iTunes video controls" alt="iTunes video controls"></a>
|
||||
|
||||
Similar to the <a href="/images/quicktime-controls.png">Quicktime full screen controls</a>, iTunes now sports video controls as well. It was really annoying to have to exit fullscreen to control the video, and now we don't have to. The controls are available when you have a floating video window open as well as when you're <a href="/images/itunes-controls-fullscreen.png">full screen</a>.
|
||||
|
||||
<h3 style="clear: left;">Smart playlists</h3>
|
||||
|
||||
It's always bothered me that I couldn't remove tracks from smart playlists. After all they are supposed to be smart, so they should remember customizations to them since even the basic ones do that. They're getting smarter. You still can't add arbitrary tracks to one, but I've never wanted to do that except just now to see if you could.
|
||||
|
||||
### Gapless playback (and more) ###
|
||||
|
||||
<a href="/images/gapless.png"><img src="/images/gapless-thumb.png" style="float: right; padding: 10px;" title="Gapless playback" alt="Gapless playback"></a>
|
||||
|
||||
You can set tracks to be part of a gapless album, and your iPod makes use of that information too. Sweet. Another new flag is "Skip when shuffling".
|
||||
|
||||
There's also a new field for audio named "Album Artist", but I have no idea what that means.
|
||||
|
||||
<h3 style="clear: right;">More video metadata</h3>
|
||||
|
||||
<a href="http://sami.samhuri.net/files/metadata.png"><img src="./Some features you might have missed in iTunes 7 - samhuri.net_files/metadata-thumb.png" style="float: right:margin:10px;" title="iTunes video controls" alt="iTunes video controls"></a>
|
||||
|
||||
For videos you can now set the Show, Season Number, Episode Number, and Episode ID for each video. Why they let you do this for Movies and Music Videos I'm not sure, but there it is.
|
||||
|
||||
<h3 style="clear: right;">What's still missing?</h3>
|
||||
|
||||
I want to be able to change more than one movie's type to Movie, Music Video, or TV Show at once. Manually doing it for more than one season of a show gets old very fast, and I'm reluctant to write a program to let you do just that but I may if I have time.
|
||||
|
||||
I'm sure I have other gripes, but honestly iTunes is a full-featured app and while there's room for improvement they do get a lot right with it as well.
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
Title: Coping with Windows XP activiation on a Mac
|
||||
Date: December 17, 2006
|
||||
Timestamp: 1166427000
|
||||
Author: sjs
|
||||
Tags: parallels, windows, apple, mac os x, bootcamp
|
||||
----
|
||||
|
||||
**Update:** This needs to be run at system startup, before you log in. I have XP Home and haven't been able to get it to run that way yet.
|
||||
|
||||
I can't test my method until I get XP Pro, if I get XP Pro at all. However chack left a <a href="2006.12.17-coping-with-windows-xp-activiation-on-a-mac.html#comment-1">comment</a> saying that he got it to work on XP Pro, so it seems we've got a solution here.
|
||||
|
||||
<hr>
|
||||
|
||||
### What is this about? ###
|
||||
|
||||
I recently deleted my <a href="http://www.microsoft.com/windowsxp/default.mspx">Windows XP</a> disk image for <a href="http://www.parallels.com/en/products/workstation/mac/">Parallels Desktop</a> and created a <a href="http://www.apple.com/macosx/bootcamp/">Boot Camp</a> partition for a new Windows XP installation. I created a new VM in Parallels and it used my Boot Camp partition without a problem. The only problem is that Windows XP Home wants to re-activate every time you change from Parallels to Boot Camp or vice versa. It's very annoying, so what can we do about it?
|
||||
|
||||
I was reading the Parallels forums and found out that you can <a href="http://forums.parallels.com/post30939-4.html">backup your activation</a> and <a href="http://forums.parallels.com/post32573-13.html">restore it later</a>. After reading that I developed a <a href="http://forums.parallels.com/post33487-22.html">solution</a> for automatically swapping your activation on each boot so you don't have to worry about it.
|
||||
|
||||
I try and stick to Linux and OS X especially for any shell work, and on Windows I would use zsh on cygwin if I use any shell at all, but I think I have managed to hack together a crude batch file to solve this activation nuisance. It's a hack but it sure as hell beats re-activating twice or more every day. It also reinforced my love of zsh and utter dislike of the Windows "shell".
|
||||
|
||||
If anyone actually knows how to write batch files I'd like to hear any suggestions you might have.
|
||||
|
||||
<hr>
|
||||
|
||||
### Make sure things will work ###
|
||||
|
||||
You will probably just want to test my method of testing for Parallels and Boot Camp first. The easiest way is to just open a command window and run this command:
|
||||
|
||||
ipconfig /all | find "Parallels"
|
||||
|
||||
If you see a line of output like **"Description . . . . : Parallels Network Adapter"** and you are in Parallels then the test works. If you see no output and you are in Boot Camp then the test works.
|
||||
|
||||
*If you see no output in Parallels or something is printed and you're in Boot Camp, then please double check that you copied the command line correctly, and that you really are running Windows where you think you are. ;-)*
|
||||
|
||||
If you're lazy then you can download <a href="http://sami.samhuri.net/files/parallels/test.bat">this test script</a> and run it in a command window. Run it in both Parallels and Boot Camp to make sure it gets them both right. The output will either be "Boot Camp" or "Parallels", and a line above that which you can just ignore.
|
||||
|
||||
<hr>
|
||||
|
||||
**NOTE:** If you're running Windows in Boot Camp right now then do Step #2 before Step #1.
|
||||
|
||||
<hr>
|
||||
|
||||
## Step #1 ##
|
||||
|
||||
Run Windows in Parallels, activate it, then open a command window and run:
|
||||
|
||||
mkdir C:\Windows\System32\Parallels
|
||||
copy C:\Windows\System32\wpa.* C:\Windows\System32\Parallels
|
||||
|
||||
Download <a href="http://sami.samhuri.net/files/parallels/backup-parallels-wpa.bat">backup-parallels-wpa.bat</a>
|
||||
|
||||
<hr>
|
||||
|
||||
## Step #2 ##
|
||||
|
||||
Run Windows using Boot Camp, activate it, then run:
|
||||
|
||||
mkdir C:\Windows\System32\BootCamp
|
||||
copy C:\Windows\System32\wpa.* C:\Windows\System32\BootCamp
|
||||
|
||||
Download <a href="http://sami.samhuri.net/files/parallels/backup-bootcamp-wpa.bat">backup-bootcamp-wpa.bat</a>
|
||||
|
||||
<hr>
|
||||
|
||||
## Step #3: Running the script at startup ##
|
||||
|
||||
Now that you have your activations backed up you need to have the correct ones copied into place every time your system boots. Save this file anywhere you want.
|
||||
|
||||
If you have XP Pro then you can get it to run using the Group Policy editor. Save the activate.bat script provided here anywhere and then have it run as a system service. Go Start -> Run... -> gpedit.msc [enter] Computer Configuration -> Windows Settings -> Scripts (Startup/Shutdown) -> Startup -> Add.
|
||||
|
||||
<p>If you have XP Home then the best you can do is run this script from your Startup folder (Start -> All Programs -> Startup), but that is not really going to work because eventually Windows will not even let you log in until you activate it. What a P.O.S.</p>
|
||||
|
||||
@echo off
|
||||
|
||||
ipconfig /all | find "Parallels" > network.tmp
|
||||
for /F "tokens=14" %%x in (network.tmp) do set parallels=%x
|
||||
del network.tmp
|
||||
|
||||
if defined parallels (
|
||||
echo Parallels
|
||||
copy C:\Windows\System32\Parallels\wpa.* C:\Windows\System32
|
||||
) else (
|
||||
echo Boot Camp
|
||||
copy C:\Windows\System32\BootCamp\wpa.* C:\Windows\System32
|
||||
)
|
||||
|
||||
Download <a href="http://sami.samhuri.net/files/parallels/activate.bat">activate.bat</a>
|
||||
|
||||
<hr>
|
||||
|
||||
### You're done! ###
|
||||
|
||||
That's all you have to do. You should now be able to run Windows in Boot Camp and Parallels as much as you want without re-activating the stupid thing again!
|
||||
|
||||
If MS doesn't get their act together with this activation bullshit then maybe the Parallels folks might have to include something hack-ish like this by default.
|
||||
|
||||
This method worked for me and hopefully it will work for you as well. I'm interested to know if it does or doesn't so please leave a comment or e-mail me.
|
||||
|
||||
<hr>
|
||||
|
||||
#### Off-topic rant ####
|
||||
|
||||
I finally bought Windows XP this week and I'm starting to regret it because of all the hoops they make you jump through to use it. I only use it to fix sites in IE because it can't render a web page properl and I didn't want to buy it just for that. I thought that it would be good to finally get a legit copy since I was using a pirated version and was sick of working around validation bullshit for updates. Now I have to work around MS's activation bullshit and it's just as bad! Screw Microsoft for putting their customers through this sort of thing. Things like this and the annoying balloons near the system tray just fuel my contempt for Windows and reinforce my love of Linux and Mac OS X.
|
||||
|
||||
I don't make money off any of my sites, which is why I didn't want to have to buy stupid Windows. I hate MS so much for making shitty IE the standard browser.
|
||||