├── test ├── fixtures │ ├── pages │ │ ├── about.hbs │ │ ├── home.hbs │ │ └── blog.hbs │ └── layouts │ │ ├── default.hbs │ │ ├── base.hbs │ │ └── post.hbs └── main.js ├── .gitattributes ├── .jshintrc ├── .verbrc.md ├── .gitignore ├── docs └── layouts.md ├── LICENSE-MIT ├── package.json ├── README.md └── index.js /test/fixtures/pages/about.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | title: About Page 3 | --- 4 | {{> info }} -------------------------------------------------------------------------------- /test/fixtures/pages/home.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home Page 3 | --- 4 | {{> info }} -------------------------------------------------------------------------------- /test/fixtures/layouts/default.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | {{> header }} 5 | {{ body }} 6 | {{> footer }} -------------------------------------------------------------------------------- /test/fixtures/layouts/base.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{site.title}} 6 | 7 | 8 | {{body}} 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/pages/blog.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | show-full: false 4 | title: Blog Page 5 | --- 6 | {{> info }} 7 |
8 | {{#each posts}} 9 | {{> post this}} 10 | {{/each}} 11 |
-------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | *.* text eol=lf 3 | *.css text eol=lf 4 | *.html text eol=lf 5 | *.js text eol=lf 6 | *.json text eol=lf 7 | *.less text eol=lf 8 | *.md text eol=lf 9 | *.yml text eol=lf 10 | 11 | *.jpg binary 12 | *.gif binary 13 | *.png binary 14 | *.jpeg binary -------------------------------------------------------------------------------- /test/fixtures/layouts/post.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
5 | {{body}} 6 |
7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "boss": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "eqnull": true, 7 | "immed": true, 8 | "latedef": true, 9 | "newcap": true, 10 | "noarg": true, 11 | "node": true, 12 | "sub": true, 13 | "undef": true, 14 | "unused": true, 15 | "globals": { 16 | "define": true, 17 | "before": true, 18 | "after": true, 19 | "describe": true, 20 | "it": true 21 | } 22 | } -------------------------------------------------------------------------------- /.verbrc.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: ['verb-tag-jscomments'] 3 | --- 4 | # {%= name %} {%= badge('fury') %} 5 | 6 | > {%= description %} 7 | 8 | ## Install 9 | {%= include("install") %} 10 | 11 | ## API 12 | {%= jscomments("index.js") %} 13 | 14 | ## Authors 15 | {%= include("authors", { 16 | authors: [ 17 | { 18 | name: 'Brian Woodward', 19 | username: 'doowb' 20 | }, 21 | { 22 | name: 'Jon Schlinkert', 23 | username: 'jonschlinkert' 24 | } 25 | ] 26 | }) %} 27 | 28 | ## License 29 | {%= copyright() %} 30 | {%= license() %} 31 | 32 | *** 33 | 34 | {%= include("footer") %} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.csv 3 | *.dat 4 | *.diff 5 | *.err 6 | *.gz 7 | *.log 8 | *.orig 9 | *.out 10 | *.pid 11 | *.rej 12 | *.seed 13 | *.swo 14 | *.swp 15 | *.vi 16 | *.yo-rc.json 17 | *.zip 18 | *~ 19 | .ruby-version 20 | lib-cov 21 | 22 | # OS or Editor folders 23 | *.esproj 24 | *.sublime-project 25 | *.sublime-workspace 26 | ._* 27 | .cache 28 | .DS_Store 29 | .idea 30 | .project 31 | .settings 32 | .tmproj 33 | nbproject 34 | Thumbs.db 35 | 36 | # Komodo 37 | *.komodoproject 38 | .komodotools 39 | 40 | # grunt-html-validation 41 | validation-status.json 42 | validation-report.json 43 | 44 | # Vendor packages 45 | node_modules 46 | bower_components 47 | vendor 48 | 49 | # General folders and files to ignore 50 | _gh_pages 51 | tmp 52 | temp 53 | TODO.md -------------------------------------------------------------------------------- /docs/layouts.md: -------------------------------------------------------------------------------- 1 | Default settings for body regex/delimiters: 2 | 3 | ```js 4 | var options = { 5 | delims: ['{{', '}}'], // start and end delimiters for body tag 6 | expression: '{{ body }}', // default body tag for empty layouts 7 | matter: '\\s*body\\s*', // inner contents of body tag regex 8 | }; 9 | ``` 10 | 11 | Assuming `parsedLayouts` have been read from the file system and parsed, we can now add them to the `layouts` cache: 12 | 13 | ```js 14 | var parsedLayouts = glob.sync('layouts/*.hbs'); 15 | parsedLayouts.forEach(function (layout) { 16 | // `layout` must have at `data` and `content` properties 17 | layouts.set(layout.name, layout); 18 | }); 19 | ``` 20 | 21 | ## Render the stack 22 | 23 | Render the entire layout stack for a specific page object: 24 | 25 | ```js 26 | var page = {data: {a: 'b', layout: 'default'}, content: 'Howdy {{name}}!'}; 27 | var template = layouts.render(page); 28 | ``` 29 | 30 | ### page object 31 | 32 | The `page` object must have `data` and `content` properties! -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Brian Woodward, contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assemble-layouts", 3 | "description": "Assemble plugin for rendering nested template layouts.", 4 | "version": "0.1.8", 5 | "homepage": "https://github.com/assemble/assemble-layouts", 6 | "author": { 7 | "name": "Brian Woodward", 8 | "url": "https://github.com/doowb" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/assemble/assemble-layouts.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/assemble/assemble-layouts/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/assemble/assemble-layouts/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "keywords": [ 24 | "layout", 25 | "nested", 26 | "layouts", 27 | "template", 28 | "templates", 29 | "assemble", 30 | "assembleplugin" 31 | ], 32 | "main": "index.js", 33 | "engines": { 34 | "node": ">=0.8" 35 | }, 36 | "scripts": { 37 | "test": "mocha -R spec" 38 | }, 39 | "devDependencies": { 40 | "verb": "~0.2.0", 41 | "verb-tag-jscomments": "^0.1.2", 42 | "chai": "~1.9.1", 43 | "mocha": "~1.18.2", 44 | "fs-utils": "^0.4.3", 45 | "gray-matter": "^0.4.2" 46 | }, 47 | "dependencies": { 48 | "delims": "^0.1.4", 49 | "falsey": "^0.1.0", 50 | "vinyl": "^0.2.3", 51 | "xtend": "^3.0.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Layouts = require('../'); 4 | var file = require('fs-utils'); 5 | var matter = require('gray-matter'); 6 | var File = require('vinyl'); 7 | 8 | function loadLayouts (layouts) { 9 | file.find('test/fixtures/layouts/*.hbs').forEach(function (filepath) { 10 | var obj = matter.read(filepath); 11 | var layout = new File({contents: new Buffer(obj.content)}); 12 | layout.data = obj.data; 13 | layout.orig = new Buffer(obj.original); 14 | layouts.set(file.basename(filepath), layout); 15 | }); 16 | } 17 | 18 | function loadPages () { 19 | return file.find('test/fixtures/pages/*.hbs').map(function (filepath) { 20 | var obj = matter(file.readFileSync(filepath)); 21 | var page = new File({contents: new Buffer(obj.content)}); 22 | page.data = obj.data; 23 | page.orig = new Buffer(obj.original); 24 | return page; 25 | }); 26 | } 27 | 28 | describe('Layouts', function () { 29 | it('should create a layout stack', function () { 30 | var layouts = new Layouts({layout: 'default'}); 31 | loadLayouts(layouts); 32 | console.log(layouts); 33 | console.log(); 34 | 35 | var stack = layouts.createStack({layout: 'default'}); 36 | console.log('stack', stack); 37 | console.log(); 38 | 39 | var pages = loadPages(); 40 | console.log('pages', pages); 41 | console.log(); 42 | 43 | pages.forEach(function (page) { 44 | var template = layouts.flatten(page); 45 | console.log('template', template.contents.toString()); 46 | }); 47 | 48 | }); 49 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # assemble-layouts [![NPM version](https://badge.fury.io/js/assemble-layouts.png)](http://badge.fury.io/js/assemble-layouts) 2 | 3 | > Assemble plugin for rendering nested template layouts. 4 | 5 | ## Install 6 | Install with [npm](npmjs.org): 7 | 8 | ```bash 9 | npm i assemble-layouts --save-dev 10 | ``` 11 | 12 | ## API 13 | ### Layouts 14 | 15 | Create a new instance of `Layouts` to generate flattened layout stacks. 16 | 17 | **Example:** 18 | 19 | ```js 20 | var layouts = new Layouts(options); 21 | ``` 22 | 23 | Default settings for body regex/delimiters: 24 | 25 | ```js 26 | var options = { 27 | delims: ['{{', '}}'], // start and end delimiters for body tag 28 | expression: '{{ body }}', // default body tag for empty layouts 29 | matter: '\\s*body\\s*', // inner contents of body tag regex 30 | }; 31 | ``` 32 | 33 | Assuming `parsedLayouts` have been read from the file system and parsed, we can now add them to the `layouts` cache: 34 | 35 | ```js 36 | var parsedLayouts = glob.sync('layouts/*.hbs'); 37 | parsedLayouts.forEach(function (layout) { 38 | // `layout` must have at `data` and `content` properties 39 | layouts.set(layout.name, layout); 40 | }); 41 | ``` 42 | 43 | ### Render the stack 44 | 45 | Render the entire layout stack for a specific page object: 46 | 47 | ```js 48 | var page = {data: {a: 'b', layout: 'default'}, content: 'Howdy {{name}}!'}; 49 | var template = layouts.render(page); 50 | ``` 51 | 52 | #### page object 53 | 54 | The `page` object must have `data` and `content` properties! 55 | 56 | * `options` {Object}: global options for how to determine layouts. 57 | 58 | 59 | ### .flatten 60 | 61 | Flatten the entire layout stack based on the `file` and `options` 62 | and how the layout stack is defined. 63 | 64 | * `file` {Object}: object containing `data` and `contents` properties. 65 | * `options` {Object}: additional options to override `global` and/or `file` options 66 | 67 | 68 | ### .set 69 | 70 | Store a layout. 71 | 72 | * `name` {String}: name of the layout to store. 73 | * `layout` {Object}: object containing `data` and `content` properties. 74 | 75 | 76 | ### .get 77 | 78 | Return a stored layout. 79 | 80 | * `name` {String}: name of the layout 81 | 82 | 83 | ### .createStack 84 | 85 | Create a layout stack based on options and layout data. Returned stack is 86 | an array with the layouts to use going from the top level parent to the 87 | lowest level child. 88 | 89 | * `options` {Object}: used to determine the layout to use. 90 | 91 | 92 | ### .useLayout 93 | 94 | Return a valid layout name if one should be used, otherwise, returns `null` 95 | to indicate a layout should not be used. 96 | 97 | * `layout` {String}: layout to use, or a negative value to not use a layout 98 | 99 | ## Authors 100 | 101 | **Brian Woodward** 102 | 103 | + [github/doowb](https://github.com/doowb) 104 | + [twitter/doowb](http://twitter.com/doowb) 105 | 106 | **Jon Schlinkert** 107 | 108 | + [github/jonschlinkert](https://github.com/jonschlinkert) 109 | + [twitter/jonschlinkert](http://twitter.com/jonschlinkert) 110 | 111 | 112 | ## License 113 | Copyright (c) 2014 Brian Woodward, contributors. 114 | Released under the MIT license 115 | 116 | *** 117 | 118 | _This file was generated by [verb-cli](https://github.com/assemble/verb-cli) on July 24, 2014._ -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var delims = require('delims'); 4 | var extend = require('xtend'); 5 | var isFalsey = require('falsey'); 6 | var File = require('vinyl'); 7 | 8 | 9 | /** 10 | * ## Layouts 11 | * 12 | * Create a new instance of `Layouts` to generate flattened layout stacks. 13 | * 14 | * **Example:** 15 | * 16 | * ```js 17 | * var layouts = new Layouts(options); 18 | * ``` 19 | * 20 | * {%= docs("layouts") %} 21 | * 22 | * @class `Layouts` 23 | * @param {Object} `options` global options for how to determine layouts. 24 | * @returns {Layouts} new `Layouts` instance 25 | * @constructor 26 | */ 27 | 28 | function Layouts(options) { 29 | if (!this instanceof Layouts) { 30 | return new Layouts(options); 31 | } 32 | 33 | var defaults = { 34 | delims: ['{{', '}}'], 35 | tag: '{{ body }}', 36 | re: '\\s*body\\s*', 37 | flags: 'gi', 38 | beginning: '', 39 | end: '', 40 | body: '' 41 | }; 42 | 43 | this.options = extend(defaults, options); 44 | this.options.matter = this.options.re; 45 | this.templates = {}; 46 | this.tag = this.options.tag; 47 | this.regex = delims(this.options.delims, this.options).evaluate; 48 | } 49 | 50 | 51 | /** 52 | * ## .flatten 53 | * 54 | * Flatten the entire layout stack based on the `file` and `options` 55 | * and how the layout stack is defined. 56 | * 57 | * @param {Object} `file` object containing `data` and `contents` properties. 58 | * @param {Object} `options` additional options to override `global` and/or `file` options 59 | * @returns {String} flattened template 60 | */ 61 | 62 | Layouts.prototype.flatten = function (file, options) { 63 | var opts = extend({}, this.options, file.data, options); 64 | var stack = this.createStack(opts.layout); 65 | 66 | // Setup the object to be returned, and store file.contents on `orig` 67 | var results = new File({contents: new Buffer(this.tag)}); 68 | results.data = file.data; 69 | results.orig = file.contents; 70 | 71 | // loop over the layout stack building the context and content 72 | results = stack.reduce(function (acc, name) { 73 | var layout = this.get(name); 74 | acc.data = extend(acc.data, layout.data); 75 | acc.contents = this._inject(acc.contents, layout.contents); 76 | return acc; 77 | }.bind(this), results); 78 | 79 | // Pass the accumlated, final results 80 | results.data = extend(results.data, file.data); 81 | results.contents = this._inject(results.contents, file.contents); 82 | return results; 83 | }; 84 | 85 | 86 | /** 87 | * ## ._inject 88 | * 89 | * Injects the inner content into the outer content based on the body regex 90 | * 91 | * @param {String} `outer` content that wraps the inner content 92 | * @param {String} `inner` content to be injected into the outer content 93 | * @returns {String} resulting content 94 | * @api private 95 | */ 96 | 97 | Layouts.prototype._inject = function (outer, inner) { 98 | return new Buffer(outer.toString('utf8').replace(this.regex, inner.toString('utf8'))); 99 | }; 100 | 101 | 102 | /** 103 | * ## .set 104 | * 105 | * Store a layout. 106 | * 107 | * @param {String} `name` name of the layout to store. 108 | * @param {Object} `layout` object containing `data` and `content` properties. 109 | */ 110 | 111 | Layouts.prototype.set = function (name, layout) { 112 | this.templates[name] = layout; 113 | return this; 114 | }; 115 | 116 | 117 | /** 118 | * ## .get 119 | * 120 | * Return a stored layout. 121 | * 122 | * @param {String} `name` name of the layout 123 | * @returns {Object} object containing `data` and `content` properties. 124 | */ 125 | 126 | Layouts.prototype.get = function (name) { 127 | return this.templates[name]; 128 | }; 129 | 130 | 131 | /** 132 | * ## .createStack 133 | * 134 | * Create a layout stack based on options and layout data. Returned stack is 135 | * an array with the layouts to use going from the top level parent to the 136 | * lowest level child. 137 | * 138 | * @param {Object} `options` used to determine the layout to use. 139 | * @returns {Array} `stack` parent => child layouts. 140 | */ 141 | 142 | Layouts.prototype.createStack = function (name) { 143 | name = this.useLayout(name); 144 | var template = null; 145 | var stack = []; 146 | while (name && (template = this.get(name))) { 147 | stack.unshift(name); 148 | name = this.useLayout(template.data && template.data.layout); 149 | } 150 | return stack; 151 | }; 152 | 153 | 154 | /** 155 | * ## .useLayout 156 | * 157 | * Return a valid layout name if one should be used, otherwise, returns `null` 158 | * to indicate a layout should not be used. 159 | * 160 | * @param {String} `layout` layout to use, or a negative value to not use a layout 161 | * @returns {*} either a valid layout name or null 162 | */ 163 | 164 | Layouts.prototype.useLayout = function (layout) { 165 | if (!layout || isFalsey(layout)) { 166 | return null; 167 | } 168 | return layout; 169 | }; 170 | 171 | module.exports = Layouts; --------------------------------------------------------------------------------