Permalink
Browse files

Update Hector to work with Weaponize rather than Railgun to optimize …

…and write output. Refactor Route class, add Page class. Add data from all routes to context. Handle both one2one and one2m routes. Improve error handling. Various bugfixes.
  • Loading branch information...
1 parent b8c2bb5 commit 3483129c63c8134550f32273d132948514e18852 @debrouwere committed Apr 15, 2013
View
4 NOTES.md
@@ -41,6 +41,10 @@ Or maybe --filter and --exclude make more sense
--filter posts recent --exclude pages => render every route except pages, and only rerender recent posts
+
+** double pony would be if Hector could check for a Git repo and take these actions based
+ on commit messages, even something as simple as looking for the words "full rebuild"
+ and when it finds them doing a force rebuild.
----------
DRAUGHTSMAN INTEGRATION
View
13 README.md
@@ -7,7 +7,7 @@ Static sites are fast, secure, don't crash under load, are easy to deploy and ch
* Support for many different templating and markup languages.
* Use any markup language as well as YAML, CSV and JSON content with whatever directory layout you like. See [Tilt.js] for a list of currently supported formats.
* Preprocess anything that compiles into CSS or JavaScript (like [LESS](http://lesscss.org) or [CoffeeScript](http://coffeescript.org)) and automatically optimize scripts, styles and HTML.
-* An easy switch from Jekyll.
+* An relatively straightforward switch from Jekyll.
* A full-featured plugin system. Hook into any part of Hector's data gathering and rendering process.
## Get started
@@ -168,6 +168,17 @@ If a variable is present in a context set, it'll be used instead of the default.
* template functions
* pipes: parsers/mungers/plugins/generators (a pipe that doesn't receive any context)/processors/whatever
+### Partial rendering
+
+* only rerender certain routes (specify an information source and
+ we'll find all applicable routes, or pass a route and we'll
+ rerender that route and all others with the same information source,
+ e.g. detail and list routes for your posts)
+* partial rendering: only render files that are newer than the last build
+* advanced partial rerendering through custom functions (we pass you
+ a list of what we want to render, you give us back a list of
+ files we actually should render)
+
## How to prototype your site
While you can rebuild your site every time you make a change to a template or a static asset, to see the results, you might consider using `Draughtsman` as a prototyping server.
View
1 build/2012/08/01/hello-world/index.html
@@ -0,0 +1 @@
+<!DOCTYPE html><html><head><title>The Standard Output of Stijn Debrouwere</title></head><body><h1>Hello world</h1></body></html>
View
1 build/2012/08/03/hello-wisconsin/index.html
@@ -0,0 +1 @@
+<!DOCTYPE html><html><head><title>The Standard Output of Stijn Debrouwere</title></head><body><h1>Hello, Wisconsin!</h1></body></html>
View
1 build/2012/08/05/hello-waukesha/index.html
@@ -0,0 +1 @@
+<!DOCTYPE html><html><head><title>The Standard Output of Stijn Debrouwere</title></head><body><h1>Hi there, Waukesha</h1></body></html>
View
1 build/about/index.html
@@ -0,0 +1 @@
+<!DOCTYPE html><html><head><title>The Standard Output of Stijn Debrouwere</title></head><body><h1>About me</h1><h2>Static page</h2></body></html>
View
1 build/feed.atom
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom"></feed><title>The feed for example.org</title><link href="http://example.org/feed.atom" rel="self"></link><link href="http://example.org/"></link><id>http://example.org/</id><author><name>Stijn Debrouwere</name><email>stijn@example.org</email></author><entry><title></title><link href="http://example.orgundefined"></link><updated></updated><content type="html"></content></entry><entry><title></title><link href="http://example.orgundefined"></link><updated></updated><content type="html"></content></entry><entry><title></title><link href="http://example.orgundefined"></link><updated></updated><content type="html"></content></entry>
View
7 examples/basic/layouts/base.jade
@@ -0,0 +1,7 @@
+!!! 5
+html
+ head
+ block title
+ title The Standard Output of Stijn Debrouwere
+ body
+ block content
View
10 examples/basic/layouts/blogpost.jade
@@ -1,6 +1,4 @@
-//extends base
-html
- head
- body
- h1= meta.title
- != body
+extends base
+block content
+ h1= meta.title
+ != content
View
15 examples/basic/layouts/feed.jade
@@ -0,0 +1,15 @@
+doctype xml
+feed(xmlns='http://www.w3.org/2005/Atom')
+title= meta.title
+link(href='http://example.org/feed.atom', rel='self')
+link(href='http://example.org/')
+id http://example.org/
+author
+ name Stijn Debrouwere
+ email stijn@example.org
+- each post in data
+ entry
+ title= post.title
+ link(href='http://example.org#{post.permalink}')
+ updated= post.date
+ content(type='html')= post.content
View
12 examples/basic/layouts/page.jade
@@ -1,7 +1,5 @@
-//extends base
-html
- head
- body
- h1= meta.title
- h2 Static page
- != body
+extends base
+block content
+ h1= meta.title
+ h2 Static page
+ != content
View
10 examples/basic/routes.yml
@@ -21,18 +21,20 @@ globals:
routes:
posts:
- route: '{year}/{month}/{day}/{permalink}'
+ route: '{year}/{month}/{day}/{permalink}/'
layout: '{layout}'
context: 'posts/{year}-{month}-{day}-{permalink}'
defaults:
layout: 'blogpost'
pages:
- route: '{permalink}'
+ route: '{permalink}/'
layout: '{layout}'
context: 'pages/{permalink}'
defaults:
layout: 'page'
feed:
- route: 'feed.xml'
+ route: 'feed.atom'
layout: 'feed'
- context: 'posts/{year}/{month}-{day}-{permalink}'
+ context: 'posts/{year}/{month}-{day}-{permalink}'
+ defaults:
+ title: 'The feed for example.org'
View
5 package.json
@@ -14,11 +14,12 @@
"espy": "0.x",
"glob": "3.x",
"js-yaml": "1.x",
- "railgun": "0.x",
+ "weaponize": "0.x",
"tilt": "0.x",
"underscore": "1.x",
"wrench": "1.x",
- "yaml": "0.x"
+ "yaml": "0.x",
+ "colors": "0.6.x"
},
"devDependencies": {},
"scripts": {
View
40 src/format.coffee
@@ -0,0 +1,40 @@
+class exports.Format
+ constructor: (@raw, @defaults = {}) ->
+ # checks whether this is a fully-specified path
+ # or rather a template with placeholders
+ if (@raw.indexOf '{') > -1
+ @isTemplate = yes
+ else
+ @isTemplate = no
+
+ # analog to matching a regular expression against a string
+ match: (str) ->
+ keys = @raw.match /\{([^\}]+)\}/g
+ keys = keys.map (match) ->
+ match.slice 1, -1
+ keys.push 'extension'
+
+ regex = @raw.replace /\{([^\}]+)\}/g, '(.+?)'
+ regex = new RegExp "#{regex}\.([^.]+)$"
+
+ matchObj = regex.exec(str)
+ return null unless matchObj
+
+ matches = matchObj[1..]
+ context = @defaults
+ for key in keys
+ context[key] = matches.shift()
+
+ context
+
+ toTemplate: ->
+ (context) =>
+ str = @raw
+ for key, value of context
+ str = str.replace "{#{key}}", value, 'g'
+ str
+
+ # fill the placeholders in our formatted string with
+ # the context variables
+ fill: (context) ->
+ @toTemplate()(context)
View
20 src/index.coffee
@@ -1,10 +1,11 @@
context = require 'espy'
tilt = require 'tilt'
-railgun = require 'railgun'
+weaponize = require 'weaponize'
_ = require 'underscore'
fs = require 'fs'
fs.path = require 'path'
yaml = require 'js-yaml'
+colors = require 'colors'
routing = exports.routing = require './routing'
exports.build = (paths..., routes) ->
@@ -30,7 +31,16 @@ exports.build = (paths..., routes) ->
settings = require routes
router = new routing.Router settings.routes, settings.defaults, source
- #console.log router.routes
- router.generate (err, bundle) ->
- console.log bundle
- #railgun.package bundle, destination
+
+ router.load (err) ->
+ if err
+ console.log "- could not load data".red
+ console.log err.message
+ return
+
+ router.generate (err, buffer) ->
+ if err
+ console.log "- could not process #{err.path}".red
+ console.log err.message
+ else
+ weaponize.package buffer, './build'
View
240 src/routing.coffee
@@ -1,151 +1,119 @@
-espy = require 'espy'
-tilt = require 'tilt'
-railgun = require 'railgun'
-_ = require 'underscore'
fs = require 'fs'
fs.path = require 'path'
-fs.glob = require 'glob'
async = require 'async'
+_ = require 'underscore'
+colors = require 'colors'
+espy = require 'espy'
+weaponize = require 'weaponize'
+utils = require './utils'
+{Format} = require './format'
+
+
+class Page
+ constructor: (@name, @data, @relatedData, @route) ->
+ @locals = @getLocals()
+ layoutName = new Format(@locals.meta.layout).fill(@locals.meta)
+ @layout = @locals.meta.layout = @findLayout layoutName
+ @path = @route.route.fill @locals.meta
+
+ # when a route specifies a directory (e.g. `pages/my-page/`)
+ # write to index.html inside of that directory
+ if @path[@path.length-1] is '/'
+ @path += 'index.html'
+
+ getLocals: ->
+ context =
+ # TODO
+ globals: _.extend {}, @route.defaults
+ filename: []
+ file: @data.meta
+
+ # routes that don't have any placeholders by definition
+ # can't contain any context
+ if @route.route.isTemplate
+ context.filename = @route.context.match @data.meta.origin.filename
+
+ meta = _.extend {}, (_.values context)...
+ data = @relatedData
+ content = @data.content
+ if not meta.layout then meta.layout = @route.specification.layout
+
+ {meta, data, content}
-class exports.Format
- constructor: (@raw, @defaults = {}) ->
- # is this a fully-specified path or a template with placeholders?
- if (@raw.indexOf '{') > -1
- @isTemplate = yes
- else
- @isTemplate = no
-
- # analog to matching a regular expression against a string
- match: (str) ->
- keys = @raw.match /\{([^\}]+)\}/g
- keys = keys.map (match) ->
- match.slice 1, -1
- keys.push 'extension'
-
- regex = @raw.replace /\{([^\}]+)\}/g, '(.+?)'
- regex = new RegExp "#{regex}\.([^.]+)$"
-
- matchObj = regex.exec(str)
- return null unless matchObj
-
- matches = matchObj[1..]
- context = @defaults
- for key in keys
- context[key] = matches.shift()
-
- context
-
- toTemplate: ->
- (context) =>
- str = @raw
- for key, value of context
- str = str.replace "{#{key}}", value, 'g'
- str
-
- # fill the placeholders in our formatted string with
- # the context variables
- fill: (context) ->
- @toTemplate()(context)
-
-class exports.Route
- constructor: (spec, @router) ->
+ findLayout: (templateName) ->
+ path = fs.path.join @route.router.fileRoot, 'layouts', templateName
+ pattern = path + '.*'
+ utils.findLayout pattern
+
+ render: (callback) ->
+ @layout @locals, (err, output) =>
+ console.log "#{@path}".grey
+ @route.destination.add @path, output
+ callback err
+
+
+class Route
+ constructor: (@name, spec, @router) ->
+ @specification = spec
@defaults = _.extend {}, @router.defaults, spec.defaults
- @route = new exports.Format spec.route, @defaults
- @layout = new exports.Format spec.layout, @defaults
- @context = new exports.Format spec.context, @defaults
+ @route = new Format spec.route, @defaults
+ @layout = new Format spec.layout, @defaults
+ @context = new Format spec.context, @defaults
@root = @getBaseDir @context.raw
- # this looks a bit strange, but what it does is, provided with a path to context like
- # `posts/{year}/{month}-{day}-{title}`, figure out that we want to start looking in
- # `posts`.
+ # this looks a bit strange, but what it does is, provided with a path
+ # like `posts/{year}/{month}-{day}-{title}`, figure out that we want
+ # to start looking for context in `posts`.
getBaseDir: (str) ->
- end = str.indexOf('{')
+ end = str.indexOf '{'
format = str.slice 0, end
format.split('/').slice(0, -1).join('/')
- getData: (callback) ->
- # TODO: should really be (errors, context) =>
- espy.findFilesFor @router.fileRoot, @root, (files) ->
- espy.getContext files, (errors, context) ->
- callback errors, context
-
- findLayout: (templateName) ->
- path = fs.path.join @router.fileRoot, 'layouts', templateName
- pattern = path + '.*'
- # we need to glob for this basepath, and among the options
- # (if any) we need to figure out if there are any with extensions
- # for template languages we can deal with
- matches = fs.glob.sync pattern
- for match in matches
- file = new tilt.File path: match
- handler = tilt.findHandler file
- # it's not enough to have a handler available,
- # the file we're dealing with should be a template language and
- # not e.g. a CSS preprocessor
- if handler? and handler.mime.output is 'text/html'
- return (context, callback) ->
- file.load ->
- handler.compiler file, context, callback
-
- generate: (bundle, callback) ->
- # TODO
- contextFromGlobals = _.extend {}, @defaults
-
- gen = (item, done) =>
- [name, set] = item
-
- relpath = set.meta.origin.filename.replace @router.fileRoot + '/', ''
- contextFromFilename = @context.match set.meta.origin.filename
- contextFromFile = set.meta
-
- locals =
- meta: _.extend {}, contextFromGlobals, contextFromFilename, contextFromFile
- # REGRESSION? -- used to be set.body; did I forget to update any references?
- content: set.content
-
- #data[name] = locals
-
- # locals.meta.layout can be variable, so we want to run
- # it through a formatter first
- layoutName = new exports.Format(locals.meta.layout).fill(locals.meta)
- layout = @findLayout layoutName
- path = @route.fill(locals.meta) + '/index.html'
-
- # TESTING
- # abspath here is sort of silly, since the bundle will cut it right
- # off again -- but we can worry about the fine details of the interop
- # later
- abspath = fs.path.join bundle.root, path
-
- layout locals, (err, output) ->
- bundle.push abspath, {content: output, compilerType: 'noop'}
- done err
-
- @getData (err, data) =>
- # render once for every context set
- async.forEach (_.pairs data), gen, (err) ->
- callback err, bundle
+ load: (callback) ->
+ espy.findFilesFor @router.fileRoot, @root, (files) =>
+ espy.getContext files, (err, context) =>
+ @data = context
+ callback err, @data
+
+ generate: (@destination, callback) ->
+ console.log "Generating #{@name}".bold, "\t#{@context.raw} -> #{@route.raw}"
+
+ # when dealing with a route like `posts/{permalink}`, render once
+ # for every context set, but when dealing with a route like `feed`,
+ # only generate once and pass all the context in one bundle
+ if @route.isTemplate
+ render = ([name, dataset], done) =>
+ page = new Page name, dataset, @router.data, this
+ page.render done
+
+ async.forEach (_.pairs @data), render, (err) ->
+ callback err
+ else
+ (new Page null, @data, @router.data, this).render callback
- ###
- # I wonder whether this is relevant?
- # Doesn't everything just work the same for individual files?
- if @route.isTemplate
- 'see above'
- else
- # render once
- 'todo'
- ###
-
-class exports.Router
+
+class Router
constructor: (@routes, @defaults, @fileRoot) ->
- (@routes[name] = new exports.Route spec, @) for name, spec of @routes
+ (@routes[name] = new Route name, spec, @) for name, spec of @routes
+
+ load: (callback) ->
+ # preload all data for all routes with `espy`
+ # so we can make it available under `data.<route>`
+ tasks = ([name, (_.bind route.load, route)] for name, route of @routes)
+ async.parallel (_.object tasks), (err, @data) =>
+ callback err
generate: (callback) ->
- bundle = new railgun.Bundle @fileRoot + 'index'
- console.log 'root', bundle.root
-
- # TEMPORARY -- I don't want to test all routes just yet,
- # so I'm picking them manually
- @routes.posts.generate bundle, =>
- @routes.pages.generate bundle, =>
- callback null, bundle
+ buffer = new weaponize.Buffer()
+
+ generateRoute = (route, done) -> route.generate buffer, done
+
+ routes = (_.values @routes)
+ async.each routes, generateRoute, (errors) ->
+ callback errors, buffer
+
+
+exports.Format = Format
+exports.Page = Page
+exports.Route = Route
+exports.Router = Router
View
70 src/sandbox.coffee
@@ -1,70 +0,0 @@
-context = require 'espy'
-tilt = require 'tilt'
-railgun = require 'railgun'
-_ = require 'underscore'
-routing = require './routing'
-
-
-# DUMMY DATA
-
-routes =
- posts:
- route: "{year}/{month}/{day}/{title}"
- template: "{layout}"
- context: "posts/{year}/{month}-{day}-{title}"
- defaults:
- language: "en"
-
-file =
- filename: "posts/2012/03-27-hello-beautiful-world.textile"
- context:
- layout: 'quote'
- content: 'This is a quote from somebody.'
-
-# turn a filename into context
-format = new router.Format(routes.posts.context, routes.posts.defaults)
-context = format.match file.filename
-console.log _.extend {}, file.context, context
-
-# generate a path
-format = new router.Format(routes.posts.route)
-template = format.toTemplate()
-path = template context
-
-console.log path
-console.log format.fill context
-
-bundle = new railgun.Bundle 'index.html', 'production'
-
-# this looks a bit strange, but what it does is, provided with a path to context like
-# `posts/{year}/{month}-{day}-{title}`, figure out that we want to start looking in
-# `posts`.
-getBaseDir = (format) ->
- end = format.indexOf('{')
- format = format.slice 0, end
- format.split('/').slice(0, -1).join('/')
-
-console.log getBaseDir "data/posts/{year}/{month}-{day}-{title}"
-
-# Go through each route and find appropriate context (TODO)
-for name, route of {} # routes
- # espy lacks some of the features we need, like giving
- # us the original filenames associated with context,
- # going through subdirectories (not necessarily for context sets -- those should be optional),
- # finding templates from a glob (`blogpost` whether it's `blogpost.jade` or `blogpost.haml` or whatever)
- # and lastly getting context from the filename itself (though I think it makes sense to have
- # that in `Format#match` in Hector)
- # -- anyhow, this is pseudocode
-
- espy.find getBaseDir route.context, (errors, context) ->
- for name, set of context
- templateName = new Format(route.template).fill(context)
- template = espy.findTemplate root, templateName
- path = new Format(route.route).fill(context)
- content = tilt.compile route.template
- compilerType: 'noop'
- bundle.push {path, content, compilerType}
-
-#railgun.optimize bundle, ->
-# railgun.package bundle, dest, ->
-# console.log 'done'
View
21 src/utils.coffee
@@ -0,0 +1,21 @@
+fs = require 'fs'
+fs.path = require 'path'
+fs.glob = require 'glob'
+tilt = require 'tilt'
+
+
+exports.findLayout = (pattern) ->
+ # figure out if there are any matches with extensions
+ # for template languages we can deal with
+ matches = fs.glob.sync pattern
+ for match in matches
+ file = new tilt.File path: match
+ handler = tilt.findHandler file
+ # it's not enough to have a handler available,
+ # the file we're dealing with should be a template language and
+ # not e.g. a CSS preprocessor
+ if handler? and handler.mime.output is 'text/html'
+ return (context, callback) ->
+ file.load ->
+ handler.compiler file, context, callback
+ return no
View
32 test/sandbox.coffee
@@ -0,0 +1,32 @@
+_ = require 'underscore'
+routing = require '../src/routing'
+
+
+# DUMMY DATA
+
+routes =
+ posts:
+ route: "{year}/{month}/{day}/{title}"
+ template: "{layout}"
+ context: "posts/{year}/{month}-{day}-{title}"
+ defaults:
+ language: "en"
+
+file =
+ filename: "posts/2012/03-27-hello-beautiful-world.textile"
+ context:
+ layout: 'quote'
+ content: 'This is a quote from somebody.'
+
+# turn a filename into context
+format = new router.Format(routes.posts.context, routes.posts.defaults)
+context = format.match file.filename
+console.log _.extend {}, file.context, context
+
+# generate a path
+format = new router.Format(routes.posts.route)
+template = format.toTemplate()
+path = template context
+
+console.log path
+console.log format.fill context

0 comments on commit 3483129

Please sign in to comment.