Permalink
Browse files

Considerable refactor so we can now support arbitrary context fetchin…

…g and rendering pipelines.
  • Loading branch information...
1 parent 3483129 commit f2aea84ade068c2b4736ca4c825ee37754c2ef88 @debrouwere committed Jul 10, 2013
View
@@ -2,3 +2,4 @@
node_modules
lib
public
+build
View
@@ -84,7 +84,7 @@ You can pick any template language you like. Our recommendation would be [Jade](
title= meta.title
body
h1= meta.title
- != body
+ != content
### Static assets
@@ -112,11 +112,11 @@ Data sources are the files an individual route will use to render pages.
Context are the variables that get passed to a template to render it, and that also often determine the path and name of the rendered file. You might know them as _locals_ or _data_ or _template variables_.
-In most web frameworks, context mostly comes from a database. In Hector, context comes from the file path, a file's content (available as `body`) or its front matter (available as `meta`), any defaults or global variables you specify.
+In most web frameworks, context mostly comes from a database. In Hector, context comes from the file path, a file's content (available as `content`) or its front matter (available as `meta`), any defaults or global variables you specify.
Hector also uses context to fill in placeholders in a route or a layout.
-In addition to the context Hector passes on from the individual file that is being rendered, it will also include the context from all other files in the data source for a route. You'll find these under `data`. Hector also passes in a couple of template helpers. These are under `helpers`.
+In addition to the context Hector passes on from the individual file that is being rendered (available under `context`, but also expanded out into `content` and `meta`), it will also include the context from all other files in the data source for a route. You'll find these under `data`. Hector also passes in a couple of template helpers. These are under `helpers`.
context set
file path
@@ -138,7 +138,7 @@ The main use for context is filling out the placeholders in your template files,
If a route contains placeholders, like for example `{year}/{permalink}`, Hector will loop through all context sets, and render a file for each. Routes almost always have placeholders, so you can do things like render however many blogposts using the same route, rather than having to create a separate route for each individual file.
-If your route doesn't contain any placeholders and looks like, for example, `feed.xml`, Hector just render the template once. Any templates can access all the different context sets from a data source through the `data` variable. This is useful for generating feeds, archives and the like, where you put many different pieces of content on a single page, or it can be useful for generating prev/next links on individual blogposts.
+If your route doesn't contain any placeholders and looks like, for example, `feed.xml`, Hector just render the template once. Any templates can access all the different context sets from a data source through the `data` variable. If you specify a context in your route, which is optional for routes without placeholders, you can access that data through `context`. This is useful for generating feeds, archives and the like, where you put many different pieces of content on a single page, or it can be useful for generating prev/next links on individual blogposts.
Advanced users can use pipes to add, combine or remove context programmatically before it gets passed to the rendering engine. For example, if you have separate `{page}-side` and `{page}-main` files in a data source, you can modify the rendering pipeline. See "How to use pipes" below.)
@@ -1 +0,0 @@
-<!DOCTYPE html><html><head><title>The Standard Output of Stijn Debrouwere</title></head><body><h1>Hello world</h1></body></html>
@@ -1 +0,0 @@
-<!DOCTYPE html><html><head><title>The Standard Output of Stijn Debrouwere</title></head><body><h1>Hello, Wisconsin!</h1></body></html>
@@ -1 +0,0 @@
-<!DOCTYPE html><html><head><title>The Standard Output of Stijn Debrouwere</title></head><body><h1>Hi there, Waukesha</h1></body></html>
View
@@ -1 +0,0 @@
-<!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 +0,0 @@
-<?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
@@ -1,3 +1,5 @@
+_ = require 'underscore'
+
class exports.Format
constructor: (@raw, @defaults = {}) ->
# checks whether this is a fully-specified path
@@ -21,7 +23,7 @@ class exports.Format
return null unless matchObj
matches = matchObj[1..]
- context = @defaults
+ context = _.clone @defaults
for key in keys
context[key] = matches.shift()
@@ -36,5 +38,6 @@ class exports.Format
# fill the placeholders in our formatted string with
# the context variables
+ # TODO: throw an error if we can't fill every placeholder
fill: (context) ->
@toTemplate()(context)
View
@@ -11,8 +11,6 @@ routing = exports.routing = require './routing'
exports.build = (paths..., routes) ->
unless routes.length then routes = 'routes.yml'
- cwd = process.cwd()
-
switch paths.length
when 2
[source, destination] = paths
@@ -25,12 +23,14 @@ exports.build = (paths..., routes) ->
else
throw new Error 'Wrong arguments'
+ cwd = process.cwd()
source = fs.path.join cwd, source
destination = fs.path.join cwd, destination
- routes = fs.path.join source, routes
+ routesPath = fs.path.join source, routes
- settings = require routes
- router = new routing.Router settings.routes, settings.defaults, source
+ routes = require routesPath
+ routes.settings ?= {}
+ router = new routing.Router routes, source
router.load (err) ->
if err
@@ -0,0 +1,2 @@
+module.exports = (context, callback) ->
+ callback null, context
No changes.
@@ -0,0 +1,77 @@
+_ = require 'underscore'
+espy = require 'espy'
+async = require 'async'
+require 'colors'
+
+steps =
+ findCandidates: (callback) ->
+ recursive = yes
+ # espy.findFiles never throws an error but our
+ # callback requires it as its first argument
+ callback = _.partial callback, null
+ espy.findFilesFor @router.root.context, @root, recursive, callback
+
+ filterCandidates: (candidates, callback) ->
+ console.log "#{@name}".green
+ files = candidates.filter (file) =>
+ @context.match (file.replace @router.root.context, '')[1..]
+ callback null, files
+
+ getFileContext: espy.getContext
+
+ getSetMetadata: (meta={}) ->
+ filename = meta.origin.filename
+ root = @router.root.context + '/'
+ relativeFilename = filename.replace root, ''
+ filenameContext = @context.match relativeFilename
+ delete meta.origin
+ {filename, relativeFilename, context: filenameContext}
+
+ getPageMetadata: (meta={}) ->
+ layout = @layout.fill meta
+
+ # TODO: there's a better way to construct permalinks, see notes in routes.yml!
+ path = permalink = @route.fill 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'
+
+ {layout, @defaults, path, permalink, root: @router.root}
+
+ weaveContext: (sets, callback) ->
+ console.log "#{@name}".red
+
+ getSetMetadata = _.bind steps.getSetMetadata, this
+ getPageMetadata = _.bind steps.getPageMetadata, this
+
+ for name, set of sets
+ set.hector = getSetMetadata set.meta
+ _.extend set.meta, set.hector.context
+
+ if @route.isTemplate
+ pageMeta = getPageMetadata set.meta
+ _.extend set.hector, pageMeta
+
+ if @route.isTemplate
+ sets = _.values sets
+ else
+ meta = getPageMetadata {}
+ sets = [{context: sets, hector: meta, meta: {}}]
+
+ callback null, sets
+
+module.exports = (callback) ->
+ procedure = [
+ steps.findCandidates
+ steps.filterCandidates
+ steps.getFileContext
+ steps.weaveContext
+ ]
+
+ procedure = procedure.map (step) => _.bind step, this
+
+ async.waterfall procedure, (err, sets) =>
+ console.log "Found #{sets.length} context files for route #{@name}"
+ callback err, sets
No changes.
@@ -0,0 +1,23 @@
+fs = require 'fs'
+fs.path = require 'path'
+utils = require '../utils'
+_ = require 'underscore'
+
+findLayout = (templateName, root) ->
+ path = fs.path.join root, 'layouts', templateName
+ pattern = path + '.*'
+ templatePath = utils.template.find pattern
+
+ (context, callback) ->
+ utils.template.compile templatePath, context, callback
+
+module.exports = (locals, callback) ->
+ console.log 'being asked to render context for', locals
+
+ layout = findLayout locals.hector.layout, locals.hector.root.app
+ _.extend locals, {data: @router.data}
+
+ layout locals, (err, output) =>
+ console.log "#{locals.hector.path}".grey
+ @destination.add locals.hector.path, output
+ callback err
View
@@ -9,53 +9,27 @@ 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}
-
- 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
+requireLocal = (name) ->
+ name = name.replace '.js', ''
+ path = fs.path.join 'pipes', name
+ localPath = './' + path
+ require localPath
class Route
constructor: (@name, spec, @router) ->
+ load = (name) =>
+ pipe = requireLocal name
+ _.bind pipe, this
+
@specification = spec
@defaults = _.extend {}, @router.defaults, spec.defaults
+ @pipes = (spec.pipes or ['gather-context']).map load
+ @output = (spec.output or ['add-helpers', 'render-from-template']).map load
+
+ # REFACTOR: rename to `paths`, an array w/ fallbacks
+ # (and turn into array if we get a plain string from spec.route)
+ # (perhaps change naming to spec.path also)
@route = new Format spec.route, @defaults
@layout = new Format spec.layout, @defaults
@context = new Format spec.context, @defaults
@@ -70,50 +44,62 @@ class Route
format.split('/').slice(0, -1).join('/')
load: (callback) ->
- espy.findFilesFor @router.fileRoot, @root, (files) =>
- espy.getContext files, (err, context) =>
- @data = context
- callback err, @data
+ save = (err, @data) => callback err, @data
+ async.waterfall @pipes, save
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
-
+ process = (set, done) =>
+ console.log 'processing set', set.hector
+ seed = utils.functional.seed set
+ async.waterfall [seed, @output...], done
+
+ async.each @data, process, callback
+
+ # UNTESTED / DEPENDS ON OTHER REFACTORS / HERE AS INSPIRATION / A STUB
+ fill: (context) ->
+ for path in @path
+ try
+ return path.fill context
+ catch error
+ if error instanceof ReferenceError
+ continue
+ else
+ throw error
+ return null
class Router
- constructor: (@routes, @defaults, @fileRoot) ->
- (@routes[name] = new Route name, spec, @) for name, spec of @routes
+ constructor: (@options, source) ->
+ # we normally expect content, layouts and such to be in the same
+ # location as the routes.yml, but these defaults can be overridden
+ # (and in fact it's recommended to do so)
+ absolutize = (path) -> fs.path.resolve source, path
+ @root =
+ app: absolutize (@options.settings.root?.app or source)
+ context: absolutize (@options.settings.root?.context or source)
+
+ @routes = {}
+ (@routes[name] = new Route name, spec, @) for name, spec of @options.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
+ save = (err, @data) => callback err, @data
+ async.parallel (_.object tasks), save
generate: (callback) ->
buffer = new weaponize.Buffer()
- generateRoute = (route, done) -> route.generate buffer, done
+ 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
Oops, something went wrong.

0 comments on commit f2aea84

Please sign in to comment.