├── fixtures └── templates │ ├── includes │ ├── next.txt │ └── prev.txt │ ├── layouts │ ├── default.txt │ └── post.txt │ └── indices │ ├── posts.txt │ └── archive-index.txt ├── .travis.yml ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .editorconfig ├── gulpfile.js ├── LICENSE ├── .verb.md ├── package.json ├── index.js ├── README.md ├── examples ├── pagination.js └── archives.js └── test.js /fixtures/templates/includes/next.txt: -------------------------------------------------------------------------------- 1 | ">Next 2 | -------------------------------------------------------------------------------- /fixtures/templates/includes/prev.txt: -------------------------------------------------------------------------------- 1 | ">Prev 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | - "0.12" 6 | - "4.0" 7 | - "iojs" 8 | matrix: 9 | fast_finish: true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text eol=lf 3 | 4 | # binaries 5 | *.ai binary 6 | *.psd binary 7 | *.jpg binary 8 | *.gif binary 9 | *.png binary 10 | *.jpeg binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.sublime-* 3 | _gh_pages 4 | bower_components 5 | node_modules 6 | npm-debug.log 7 | actual 8 | test/actual 9 | temp 10 | tmp 11 | TODO.md 12 | vendor 13 | .idea 14 | benchmark 15 | coverage 16 | -------------------------------------------------------------------------------- /fixtures/templates/layouts/default.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | 8 | {% body %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /fixtures/templates/layouts/post.txt: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |

<%= title %>

5 |

<%= year %>-<%= month %>

6 |
{% body %}
7 |
[<%= include("prev") %>]  |  [<%= include("next") %>] 8 | -------------------------------------------------------------------------------- /fixtures/templates/indices/posts.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | <% pagination.items.forEach(function (post) { %> 4 |
5 |

<%= post.data.title %>

6 |
 7 |   
8 | <% }); %> 9 |
10 | 11 |
[<%= include("prev") %>]  |  [<%= include("next") %>] 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "boss": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "eqnull": true, 7 | "esnext": true, 8 | "immed": true, 9 | "latedef": false, 10 | "laxcomma": false, 11 | "mocha": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "node": true, 15 | "sub": true, 16 | "undef": true, 17 | "unused": true 18 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [test/fixtures/*] 16 | insert_final_newline = false 17 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /fixtures/templates/indices/archive-index.txt: -------------------------------------------------------------------------------- 1 | 2 |

<% if (typeof year !== "undefined") {%><%= year %><% } %><% if (typeof month !== "undefined") { %>-<%= month %><% } %>

3 | 4 |
5 | <% pagination.items.forEach(function (post) { %> 6 |
7 |

<%= post.data.title %>

8 |
 9 |   
10 | <% }); %> 11 |
12 | 13 |
[<%= include("prev") %>]  |  [<%= include("next") %>] 14 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var mocha = require('gulp-mocha'); 3 | var istanbul = require('gulp-istanbul'); 4 | var jshint = require('gulp-jshint'); 5 | var del = require('rimraf'); 6 | require('jshint-stylish'); 7 | 8 | gulp.task('coverage', function () { 9 | return gulp.src(['index.js']) 10 | .pipe(istanbul()) 11 | .pipe(istanbul.hookRequire()); 12 | }); 13 | 14 | gulp.task('coverage:clean', function (cb) { 15 | del('coverage', cb); 16 | }); 17 | 18 | gulp.task('mocha', ['coverage'], function () { 19 | return gulp.src('test.js') 20 | .pipe(mocha({reporter: 'spec'})) 21 | .pipe(istanbul.writeReports()); 22 | }); 23 | 24 | gulp.task('jshint', function () { 25 | return gulp.src(['index.js']) 26 | .pipe(jshint()) 27 | .pipe(jshint.reporter('jshint-stylish')) 28 | .pipe(jshint.reporter('fail')); 29 | }); 30 | 31 | gulp.task('default', ['mocha', 'jshint']); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015, Brian Woodward. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 | # {%= name %} {%= badge("fury") %} {%= badge("travis") %} 2 | 3 | > {%= description %} 4 | 5 | {%= include("install-npm", {save: true}) %} 6 | 7 | ## Usage 8 | 9 | ```js 10 | var indexer = require('{%= name %}'); 11 | ``` 12 | 13 | ## API 14 | {%= apidocs("index.js") %} 15 | 16 | ### .addIndices 17 | 18 | `addIndices` method decorated onto the given `collection` Iterators over a list of `pages` (built with `list.paginate`) and adds each page to the collection as a new index view 19 | 20 | **Params** 21 | 22 | * `pages` **{Array}**: Array of pages return from `list.paginate` 23 | * `opts` **{Object}**: Method options to override plugin options. Will also be added to locals for each index view. 24 | * `opts.index` **{Object}**: Optional instance of `View` to use as the basis for the index views being added. Required if `createView` is not passed on plugin or method options. 25 | * `opts.createView` **{Function}**: Function to create a view instance for the index view being added. Required if `index` is not passed on plugin or method options. 26 | * `returns` **{Object}**: Returns `collection` to enable chaining 27 | 28 | **Example** 29 | 30 | ```js 31 | collection.addIndices(pages, locals); 32 | ``` 33 | 34 | ## Related projects 35 | {%= related(verb.related.list, {remove: name}) %} 36 | 37 | ## Running tests 38 | {%= include("tests") %} 39 | 40 | ## Contributing 41 | {%= include("contributing") %} 42 | 43 | ## Author 44 | {%= include("author") %} 45 | 46 | ## License 47 | {%= copyright() %} 48 | {%= license() %} 49 | 50 | *** 51 | 52 | {%= include("footer") %} 53 | 54 | {%= reflinks(['templates']) %} 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assemble-indexer", 3 | "description": "Assemble plugin to add index views to template collections.", 4 | "version": "0.1.2", 5 | "homepage": "https://github.com/assemble/assemble-indexer", 6 | "author": "Brian Woodward (https://github.com/doowb)", 7 | "repository": "assemble/assemble-indexer", 8 | "bugs": { 9 | "url": "https://github.com/assemble/assemble-indexer/issues" 10 | }, 11 | "license": "MIT", 12 | "files": [ 13 | "index.js" 14 | ], 15 | "main": "index.js", 16 | "engines": { 17 | "node": ">=0.10.0" 18 | }, 19 | "scripts": { 20 | "test": "mocha" 21 | }, 22 | "dependencies": { 23 | "mixin-deep": "^1.1.3", 24 | "write": "^0.2.1" 25 | }, 26 | "devDependencies": { 27 | "ansi-cyan": "^0.1.1", 28 | "ansi-green": "^0.1.1", 29 | "ansi-grey": "^0.1.1", 30 | "ansi-red": "^0.1.1", 31 | "assemble-loader": "^0.1.1", 32 | "assemble-permalinks": "^0.1.0", 33 | "async": "^1.4.2", 34 | "engine-base": "^0.1.2", 35 | "error-symbol": "^0.1.0", 36 | "gulp": "^3.9.0", 37 | "gulp-istanbul": "^0.10.0", 38 | "gulp-jshint": "^1.11.2", 39 | "gulp-mocha": "^2.1.3", 40 | "jshint-stylish": "^2.0.1", 41 | "mocha": "*", 42 | "parser-front-matter": "^1.2.5", 43 | "relative": "^3.0.1", 44 | "rimraf": "^2.4.3", 45 | "should": "^7.1.0", 46 | "success-symbol": "^0.1.0", 47 | "templates": "^0.1.22" 48 | }, 49 | "verb": { 50 | "related": { 51 | "list": [ 52 | "template", 53 | "templates", 54 | "assemble", 55 | "paginationator" 56 | ] 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * assemble-indexer 3 | * 4 | * Copyright (c) 2015, Brian Woodward. 5 | * Licensed under the MIT License. 6 | */ 7 | 8 | 'use strict'; 9 | var merge = require('mixin-deep'); 10 | 11 | /** 12 | * Add `addIndices` to a [templates][] collection that will 13 | * add index views to the collection when given an array of pages. 14 | * 15 | * ``` 16 | * var archives = app.create('archives') 17 | * .use(indexer()) 18 | * .addIndices(pages); 19 | * ``` 20 | * 21 | * @param {Object} `options` 22 | * @param {Object} `options.index` Optional instance of `View` to use as the basis for the index views being added. Required if `createView` is not passed on plugin or method options. 23 | * @param {Function} `options.createView` Function to create a view instance for the index view being added. Required if `index` is not passed on plugin or method options. 24 | * @return {Function} Function to use as a plugin for [templates][] 25 | * @api public 26 | * @name indexer 27 | */ 28 | 29 | module.exports = function indexer (options) { 30 | options = options || {}; 31 | 32 | /** 33 | * Plugin passed to [templates][] `.use` method. 34 | * 35 | * @param {Object} `collection` collection instance the plugin is added to. 36 | */ 37 | 38 | return function plugin (collection) { 39 | collection.define('addIndices', function (pages, opts) { 40 | opts = merge({}, options, opts); 41 | 42 | var createView = opts.createView; 43 | if (typeof createView !== 'function') { 44 | var index = opts.index; 45 | createView = createViewFn(index); 46 | delete opts.index; 47 | } 48 | 49 | pages.forEach(function (pagination) { 50 | var locals = merge({}, opts); 51 | locals.pages = pages; 52 | locals.pagination = pagination; 53 | var view = createView(locals); 54 | view.key = view.url || view.path; 55 | collection.addView(view); 56 | }); 57 | return collection; 58 | }); 59 | }; 60 | }; 61 | 62 | /** 63 | * Default method for creating a new index view. 64 | * 65 | * ```js 66 | * var view = createViewFn(locals); 67 | * ``` 68 | * 69 | * @param {Object} `locals` Combined locals for this index view. 70 | * @return {Object} New View instance to be used as the index view 71 | */ 72 | 73 | function createViewFn (index) { 74 | if (typeof index !== 'object' || !index.isView) { 75 | throw new Error('expected index to be an instance of View'); 76 | } 77 | 78 | return function (locals) { 79 | var view = index.clone({deep: true}); 80 | view.locals = merge({}, view.locals, locals); 81 | 82 | if (typeof view.permalink === 'function') { 83 | view.permalink(view.data.permalink, locals); 84 | return view; 85 | } 86 | return view; 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # assemble-indexer [![NPM version](https://badge.fury.io/js/assemble-indexer.svg)](http://badge.fury.io/js/assemble-indexer) [![Build Status](https://travis-ci.org/assemble/assemble-indexer.svg)](https://travis-ci.org/assemble/assemble-indexer) 2 | 3 | > Assemble plugin to add index views to template collections. 4 | 5 | Install with [npm](https://www.npmjs.com/) 6 | 7 | ```sh 8 | $ npm i assemble-indexer --save 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | var indexer = require('assemble-indexer'); 15 | ``` 16 | 17 | ## API 18 | 19 | ### [indexer](index.js#L29) 20 | 21 | Add `addIndices` to a [templates](https://github.com/jonschlinkert/templates) collection that will add index views to the collection when given an array of pages. 22 | 23 | **Params** 24 | 25 | * `options` **{Object}** 26 | * `options.index` **{Object}**: Optional instance of `View` to use as the basis for the index views being added. Required if `createView` is not passed on plugin or method options. 27 | * `options.createView` **{Function}**: Function to create a view instance for the index view being added. Required if `index` is not passed on plugin or method options. 28 | * `returns` **{Function}**: Function to use as a plugin for [templates](https://github.com/jonschlinkert/templates) 29 | 30 | **Example** 31 | 32 | ``` 33 | var archives = app.create('archives') 34 | .use(indexer()) 35 | .addIndices(pages); 36 | ``` 37 | 38 | ### .addIndices 39 | 40 | `addIndices` method decorated onto the given `collection` Iterators over a list of `pages` (built with `list.paginate`) and adds each page to the collection as a new index view 41 | 42 | **Params** 43 | 44 | * `pages` **{Array}**: Array of pages return from `list.paginate` 45 | * `opts` **{Object}**: Method options to override plugin options. Will also be added to locals for each index view. 46 | * `opts.index` **{Object}**: Optional instance of `View` to use as the basis for the index views being added. Required if `createView` is not passed on plugin or method options. 47 | * `opts.createView` **{Function}**: Function to create a view instance for the index view being added. Required if `index` is not passed on plugin or method options. 48 | * `returns` **{Object}**: Returns `collection` to enable chaining 49 | 50 | **Example** 51 | 52 | ```js 53 | collection.addIndices(pages, locals); 54 | ``` 55 | 56 | ## Related projects 57 | 58 | * [assemble](https://www.npmjs.com/package/assemble): Static site generator for Grunt.js, Yeoman and Node.js. Used by Zurb Foundation, Zurb Ink, H5BP/Effeckt,… [more](https://www.npmjs.com/package/assemble) | [homepage](http://assemble.io) 59 | * [paginationator](https://www.npmjs.com/package/paginationator): Paginate an array into pages of items. | [homepage](https://github.com/doowb/paginationator) 60 | * [template](https://www.npmjs.com/package/template): Render templates using any engine. Supports, layouts, pages, partials and custom template types. Use template… [more](https://www.npmjs.com/package/template) | [homepage](https://github.com/jonschlinkert/template) 61 | * [templates](https://www.npmjs.com/package/templates): System for creating and managing template collections, and rendering templates with any node.js template engine.… [more](https://www.npmjs.com/package/templates) | [homepage](https://github.com/jonschlinkert/templates) 62 | 63 | ## Running tests 64 | 65 | Install dev dependencies: 66 | 67 | ```sh 68 | $ npm i -d && npm test 69 | ``` 70 | 71 | ## Contributing 72 | 73 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](https://github.com/assemble/assemble-indexer/issues/new). 74 | 75 | ## Author 76 | 77 | **Brian Woodward** 78 | 79 | + [github/doowb](https://github.com/doowb) 80 | + [twitter/doowb](http://twitter.com/doowb) 81 | 82 | ## License 83 | 84 | Copyright © 2015 Brian Woodward 85 | Released under the MIT license. 86 | 87 | *** 88 | 89 | _This file was generated by [verb-cli](https://github.com/assemble/verb-cli) on September 20, 2015._ -------------------------------------------------------------------------------- /examples/pagination.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var red = require('ansi-red'); 3 | var cyan = require('ansi-cyan'); 4 | var grey = require('ansi-grey'); 5 | var green = require('ansi-green'); 6 | var error = require('error-symbol'); 7 | var templates = require('templates'); 8 | var success = require('success-symbol'); 9 | var matter = require('parser-front-matter'); 10 | var permalink = require('assemble-permalinks'); 11 | var writeFile = require('write'); 12 | var async = require('async'); 13 | var List = templates.List; 14 | var app = templates(); 15 | 16 | app.engine('txt', require('engine-base')); 17 | 18 | app.onLoad(/\.txt$/, function (view, next) { 19 | matter.parse(view, next); 20 | }); 21 | 22 | /** 23 | * Create a collection 24 | */ 25 | 26 | app.create('layouts', {viewType: ['layout']}); 27 | app.create('posts') 28 | .use(permalink(':base/:name.html')); 29 | 30 | app.create('includes', {viewType: ['partial'], engine: 'txt'}); 31 | 32 | app.helper('relative', require('relative')); 33 | 34 | app.layout('default', { 35 | content: [ 36 | '', 37 | '', 38 | ' ', 39 | ' ', 40 | ' <%= title %>', 41 | ' ', 42 | ' ', 43 | ' {% body %}', 44 | ' ', 45 | '', 46 | ].join('\n') 47 | }); 48 | 49 | app.include('prev', { content: '">Prev' }); 50 | app.include('next', { content: '">Next' }); 51 | 52 | /** 53 | * Add some posts to the `posts` collection 54 | */ 55 | function content (title, body) { 56 | return [ 57 | '---', 58 | 'title: ' + title, 59 | '---', 60 | '

<%= title %>

', 61 | '
' + body + '
', 62 | '
[<%= include("prev") %>]  |  [<%= include("next") %>]' 63 | ].join('\n'); 64 | } 65 | 66 | app.posts({ 67 | 'a/b/c/a.txt': { 68 | locals: {base: 'pagination/blog'}, 69 | content: content('A', 'aaa') 70 | }, 71 | 'a/b/c/b.txt': { 72 | locals: {base: 'pagination/blog'}, 73 | content: content('B', 'bbb') 74 | }, 75 | 'a/b/c/c.txt': { 76 | locals: {base: 'pagination/blog'}, 77 | content: content('C', 'ccc') 78 | }, 79 | 'a/b/c/d.txt': { 80 | locals: {base: 'pagination/blog'}, 81 | content: content('D', 'ddd') 82 | }, 83 | 'a/b/c/e.txt': { 84 | locals: {base: 'pagination/blog'}, 85 | content: content('E', 'eee') 86 | }, 87 | 'a/b/c/f.txt': { 88 | locals: {base: 'pagination/blog'}, 89 | content: content('F', 'fff') 90 | }, 91 | 'a/b/c/g.txt': { 92 | locals: {base: 'pagination/blog'}, 93 | content: content('G', 'ggg') 94 | }, 95 | 'a/b/c/h.txt': { 96 | locals: {base: 'pagination/blog'}, 97 | content: content('H', 'hhh') 98 | }, 99 | 'a/b/c/i.txt': { 100 | locals: {base: 'pagination/blog'}, 101 | content: content('I', 'iii') 102 | }, 103 | 'a/b/c/j.txt': { 104 | locals: {base: 'pagination/blog'}, 105 | content: content('J', 'jjj') 106 | }, 107 | }); 108 | 109 | var list = new List(app.posts) 110 | var pagination = list.paginate({limit: 3}) 111 | 112 | async.eachSeries(list.items, function (post, next) { 113 | post.permalink(post.data.permalink, post.locals); 114 | process.stdout.write(cyan('Rendering ') + grey(post.url) + cyan(' => ')); 115 | post.render({layout: 'default'}, function (err, res) { 116 | if (err) return next(err); 117 | var dest = path.join(__dirname, '../actual', post.url); 118 | process.stdout.write(grey(path.relative(process.cwd(), dest)) + '... '); 119 | writeFile(dest, post.content, function (err) { 120 | if (err) { 121 | process.stdout.write(red(error) + '\n'); 122 | return next(err); 123 | } 124 | process.stdout.write(green(success) + '\n'); 125 | next(); 126 | }); 127 | }); 128 | }, function (err) { 129 | if (err) return console.error(err); 130 | console.log('done'); 131 | }); 132 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | require('should'); 5 | var permalink = require('assemble-permalinks'); 6 | var templates = require('templates'); 7 | var assert = require('assert'); 8 | var fs = require('fs'); 9 | 10 | var indexer = require('./'); 11 | 12 | var List = templates.List; 13 | var app, index; 14 | 15 | describe('indexer', function () { 16 | beforeEach(function () { 17 | app = templates(); 18 | app.initialize(); 19 | index = app.view({path: 'index.hbs', content: ''}) 20 | .use(permalink(':index(pagination.idx):name.html', { 21 | index: function (i) { 22 | return i ? ((i + 1) + '/') : ''; 23 | } 24 | })); 25 | }); 26 | 27 | it('should throw an error when an index instance is not passed', function () { 28 | var list = new List(); 29 | list.addList([ 30 | {path: 'a.hbs', content: 'aaa'}, 31 | {path: 'b.hbs', content: 'bbb'}, 32 | {path: 'c.hbs', content: 'ccc'}, 33 | {path: 'd.hbs', content: 'ddd'}, 34 | {path: 'e.hbs', content: 'eee'}, 35 | ]); 36 | var pages = list.paginate({limit: 2}); 37 | 38 | (function () { 39 | app.create('archives') 40 | .use(indexer()) 41 | .addIndices(pages); 42 | }).should.throw('expected index to be an instance of View'); 43 | }); 44 | 45 | it('should add `addIndices` to a templates collection', function () { 46 | app.create('archives') 47 | .use(indexer({index: index})); 48 | app.archives.should.have.property('addIndices'); 49 | }); 50 | 51 | it('should create index views with default options', function () { 52 | var list = new List(); 53 | list.addList([ 54 | {path: 'a.hbs', content: 'aaa'}, 55 | {path: 'b.hbs', content: 'bbb'}, 56 | {path: 'c.hbs', content: 'ccc'}, 57 | {path: 'd.hbs', content: 'ddd'}, 58 | {path: 'e.hbs', content: 'eee'}, 59 | ]); 60 | var pages = list.paginate({limit: 2}); 61 | 62 | app.create('archives') 63 | .use(indexer({index: index})) 64 | .addIndices(pages); 65 | 66 | var keys = Object.keys(app.views.archives); 67 | keys.length.should.equal(pages.length); 68 | pages.forEach(function (page) { 69 | var key = (page.isFirst ? '' : page.current + '/') + 'index.html'; 70 | assert.equal(keys.indexOf(key) === -1, false); 71 | assert.deepEqual(app.views.archives[key].locals.pagination, page); 72 | }); 73 | }); 74 | 75 | it('should create index views with default options when permalink is not installed', function () { 76 | var i = 0; 77 | var index = { 78 | isView: true, 79 | path: 'index.hbs', 80 | content: 'index', 81 | clone: function () { 82 | var obj = { 83 | path: (i === 0 ? '' : (i + 1) + '/') + 'index.html', 84 | content: 'index' 85 | }; 86 | i++; 87 | return obj; 88 | } 89 | }; 90 | 91 | var list = new List(); 92 | list.addList([ 93 | {path: 'a.hbs', content: 'aaa'}, 94 | {path: 'b.hbs', content: 'bbb'}, 95 | {path: 'c.hbs', content: 'ccc'}, 96 | {path: 'd.hbs', content: 'ddd'}, 97 | {path: 'e.hbs', content: 'eee'}, 98 | ]); 99 | var pages = list.paginate({limit: 2}); 100 | 101 | app.create('archives') 102 | .use(indexer({index: index})) 103 | .addIndices(pages); 104 | 105 | var keys = Object.keys(app.views.archives); 106 | keys.length.should.equal(pages.length); 107 | pages.forEach(function (page) { 108 | var key = (page.isFirst ? '' : page.current + '/') + 'index.html'; 109 | assert.equal(keys.indexOf(key) === -1, false); 110 | assert.deepEqual(app.views.archives[key].locals.pagination, page); 111 | }); 112 | }); 113 | 114 | it('should create index views with custom createView function on plugin options', function () { 115 | var contents = fs.readFileSync('fixtures/templates/indices/archive-index.txt'); 116 | var archiveIndexView = app.view({path: 'archive-index.hbs', contents: contents}) 117 | .use(permalink(':index(pagination.idx):name.html', { 118 | index: function (i) { 119 | return i ? ((i + 1) + '/') : ''; 120 | } 121 | })); 122 | 123 | var list = new List(); 124 | list.addList([ 125 | {path: 'a.hbs', content: 'aaa'}, 126 | {path: 'b.hbs', content: 'bbb'}, 127 | {path: 'c.hbs', content: 'ccc'}, 128 | {path: 'd.hbs', content: 'ddd'}, 129 | {path: 'e.hbs', content: 'eee'}, 130 | ]); 131 | var pages = list.paginate({limit: 2}); 132 | 133 | app.create('archives') 134 | .use(indexer({ 135 | createView: function (locals) { 136 | var view = archiveIndexView.clone(); 137 | view.locals = locals; 138 | view.permalink(view.data.permalink, locals); 139 | return view; 140 | } 141 | })) 142 | .addIndices(pages); 143 | 144 | var keys = Object.keys(app.views.archives); 145 | keys.length.should.equal(pages.length); 146 | pages.forEach(function (page) { 147 | var key = (page.isFirst ? '' : page.current + '/') + 'archive-index.html'; 148 | assert.equal(keys.indexOf(key) === -1, false); 149 | assert.deepEqual(app.views.archives[key].locals.pagination, page); 150 | assert.deepEqual(app.views.archives[key].contents, contents); 151 | }); 152 | }); 153 | 154 | it('should create index views with custom createView function on method options', function () { 155 | var contents = fs.readFileSync('fixtures/templates/indices/archive-index.txt'); 156 | var archiveIndexView = app.view({path: 'archive-index.hbs', contents: contents}) 157 | .use(permalink(':index(pagination.idx):name.html', { 158 | index: function (i) { 159 | return i ? ((i + 1) + '/') : ''; 160 | } 161 | })); 162 | 163 | var list = new List(); 164 | list.addList([ 165 | {path: 'a.hbs', content: 'aaa'}, 166 | {path: 'b.hbs', content: 'bbb'}, 167 | {path: 'c.hbs', content: 'ccc'}, 168 | {path: 'd.hbs', content: 'ddd'}, 169 | {path: 'e.hbs', content: 'eee'}, 170 | ]); 171 | var pages = list.paginate({limit: 2}); 172 | 173 | app.create('archives') 174 | .use(indexer()) 175 | .addIndices(pages, { 176 | createView: function (locals) { 177 | var view = archiveIndexView.clone(); 178 | view.locals = locals; 179 | view.permalink(view.data.permalink, locals); 180 | return view; 181 | } 182 | }); 183 | 184 | var keys = Object.keys(app.views.archives); 185 | keys.length.should.equal(pages.length); 186 | pages.forEach(function (page) { 187 | var key = (page.isFirst ? '' : page.current + '/') + 'archive-index.html'; 188 | assert.equal(keys.indexOf(key) === -1, false); 189 | assert.deepEqual(app.views.archives[key].locals.pagination, page); 190 | assert.deepEqual(app.views.archives[key].contents, contents); 191 | }); 192 | }); 193 | 194 | it('should create index views with additional locals', function () { 195 | var contents = fs.readFileSync('fixtures/templates/indices/archive-index.txt'); 196 | var archiveIndexView = app.view({path: 'archive-index.hbs', contents: contents}) 197 | .use(permalink(':index(pagination.idx):name.html', { 198 | index: function (i) { 199 | return i ? ((i + 1) + '/') : ''; 200 | } 201 | })); 202 | 203 | var list = new List(); 204 | list.addList([ 205 | {path: 'a.hbs', content: 'aaa'}, 206 | {path: 'b.hbs', content: 'bbb'}, 207 | {path: 'c.hbs', content: 'ccc'}, 208 | {path: 'd.hbs', content: 'ddd'}, 209 | {path: 'e.hbs', content: 'eee'}, 210 | ]); 211 | var pages = list.paginate({limit: 2}); 212 | 213 | app.create('archives') 214 | .use(indexer({index: archiveIndexView})) 215 | .addIndices(pages, {title: 'Archives'}); 216 | 217 | var keys = Object.keys(app.views.archives); 218 | keys.length.should.equal(pages.length); 219 | pages.forEach(function (page) { 220 | var key = (page.isFirst ? '' : page.current + '/') + 'archive-index.html'; 221 | assert.equal(keys.indexOf(key) === -1, false); 222 | assert.deepEqual(app.views.archives[key].locals.title, 'Archives'); 223 | assert.deepEqual(app.views.archives[key].locals.pagination, page); 224 | assert.deepEqual(app.views.archives[key].contents, contents); 225 | }); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /examples/archives.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var async = require('async'); 3 | var red = require('ansi-red'); 4 | var cyan = require('ansi-cyan'); 5 | var grey = require('ansi-grey'); 6 | var writeFile = require('write'); 7 | var green = require('ansi-green'); 8 | var error = require('error-symbol'); 9 | var templates = require('templates'); 10 | var success = require('success-symbol'); 11 | var loader = require('assemble-loader'); 12 | var matter = require('parser-front-matter'); 13 | var permalink = require('assemble-permalinks'); 14 | var indexer = require('../'); 15 | 16 | var List = templates.List; 17 | var app = templates() 18 | .use(loader()); 19 | 20 | app.engine('txt', require('engine-base')); 21 | 22 | app.option('view engine', 'txt'); 23 | 24 | app.option('renameKey', function (fp) { 25 | return path.basename(fp, path.extname(fp)); 26 | }); 27 | app.helper('relative', require('relative')); 28 | 29 | app.data({base: 'archives/blog'}); 30 | app.data({archiveBase: 'archives/blog/archives'}); 31 | 32 | app.onLoad(/\.(txt|md)$/, function (view, next) { 33 | matter.parse(view, next); 34 | }); 35 | 36 | app.create('layouts', {viewType: ['layout']}) 37 | .load(path.join(__dirname, '../fixtures/templates/layouts/*.txt')); 38 | 39 | app.create('includes', {viewType: ['partial'], engine: 'txt'}) 40 | .load(path.join(__dirname, '../fixtures/templates/includes/*.txt')); 41 | 42 | /** 43 | * Create a collection 44 | */ 45 | 46 | app.create('post') 47 | .use(permalink(':base/:title.html')); 48 | 49 | /** 50 | * Archive posts into a simple list of posts 51 | */ 52 | 53 | app.create('archives') 54 | .option('renameKey', renameKey) 55 | .use(indexer()); 56 | 57 | /** 58 | * Archive posts into lists based on years 59 | */ 60 | 61 | app.create('yearArchives') 62 | .option('renameKey', renameKey) 63 | .use(indexer()); 64 | 65 | /** 66 | * Archive posts into lists based on year-months 67 | */ 68 | 69 | app.create('monthArchives') 70 | .option('renameKey', renameKey) 71 | .use(indexer()); 72 | 73 | 74 | /** 75 | * Load a collection of list index templates 76 | */ 77 | 78 | var indices = app.load(path.join(__dirname, '../fixtures/templates/indices/*.txt')); 79 | 80 | /** 81 | * Get the post list index template (view) to use 82 | */ 83 | 84 | var posts = indices.getView('posts') 85 | .use(permalink(':base(0)/:base(1)/:index(pagination.idx)/index.html', { 86 | base: function (idx) { 87 | var res = app.cache.data.base; 88 | return res.split('/')[idx]; 89 | }, 90 | index: function (i) { 91 | return i ? ((i + 1)) : ''; 92 | } 93 | })); 94 | 95 | /** 96 | * Get the archive list index template (view) to use 97 | */ 98 | 99 | var archives = indices.getView('archive-index') 100 | .use(permalink(':base(0)/:base(1)/:base(2)/:slug/:index(pagination.idx)/index.html', { 101 | base: function (idx) { 102 | var res = app.cache.data.archiveBase; 103 | return res.split('/')[idx]; 104 | }, 105 | index: function (i) { 106 | return i ? ((i + 1)) : ''; 107 | } 108 | })); 109 | 110 | /** 111 | * Add some posts to the `posts` collection 112 | */ 113 | 114 | app.posts({ 115 | 'a/b/c/a.txt': { 116 | locals: {base: 'archives/blog', year: '2012', month: '01'}, 117 | content: '---\ntitle: A\n---\naaa' 118 | }, 119 | 'a/b/c/b.txt': { 120 | locals: {base: 'archives/blog', year: '2011', month: '11'}, 121 | content: '---\ntitle: B\n---\nbbb' 122 | }, 123 | 'a/b/c/c.txt': { 124 | locals: {base: 'archives/blog', year: '2010', month: '04'}, 125 | content: '---\ntitle: C\n---\nccc' 126 | }, 127 | 'a/b/c/d.txt': { 128 | locals: {base: 'archives/blog', year: '2010', month: '06'}, 129 | content: '---\ntitle: D\n---\nddd' 130 | }, 131 | 'a/b/c/e.txt': { 132 | locals: {base: 'archives/blog', year: '2011', month: '08'}, 133 | content: '---\ntitle: E\n---\neee' 134 | }, 135 | 'a/b/c/f.txt': { 136 | locals: {base: 'archives/blog', year: '2013', month: '04'}, 137 | content: '---\ntitle: F\n---\nfff' 138 | }, 139 | 'a/b/c/g.txt': { 140 | locals: {base: 'archives/blog', year: '2015', month: '12'}, 141 | content: '---\ntitle: G\n---\nggg' 142 | }, 143 | 'a/b/c/h.txt': { 144 | locals: {base: 'archives/blog', year: '2010', month: '04'}, 145 | content: '---\ntitle: H\n---\nhhh' 146 | }, 147 | 'a/b/c/i.txt': { 148 | locals: {base: 'archives/blog', year: '2015', month: '01'}, 149 | content: '---\ntitle: I\n---\niii' 150 | }, 151 | 'a/b/c/j.txt': { 152 | locals: {base: 'archives/blog', year: '2012', month: '03'}, 153 | content: '---\ntitle: J\n---\njjj' 154 | }, 155 | }); 156 | 157 | /** 158 | * Generate a list from the `posts` collection 159 | */ 160 | 161 | var list = new List(app.posts); 162 | 163 | /** 164 | * Create the permalink for each post in the `posts` list. 165 | */ 166 | 167 | list.items.forEach(function (post) { 168 | post.permalink(post.data.permalink, {title: post.data.title, base: post.locals.base}); 169 | }); 170 | 171 | /** 172 | * Paginate the `posts` list into pages containing 3 posts on each page. 173 | */ 174 | 175 | var pages = list.paginate({limit: 3}); 176 | 177 | /** 178 | * Add the paginated pages to the `archives` collection with `addIndices` using the `posts` list index template (view) 179 | */ 180 | 181 | app.archives.addIndices(pages, {index: posts}); 182 | 183 | /** 184 | * Group `posts` list based on year. 185 | * //=> { '2010': 186 | * //=> [ >, 187 | * //=> >, 188 | * //=> > ], 189 | * //=> '2011': 190 | * //=> [ >, 191 | * //=> > ], 192 | * //=> '2012': 193 | * //=> [ >, 194 | * //=> > ], 195 | * //=> '2013': [ > ], 196 | * //=> '2015': 197 | * //=> [ >, 198 | * //=> > ] } 199 | */ 200 | 201 | var yearsGroup = list.groupBy('locals.year'); 202 | 203 | /** 204 | * Group `posts` list based on year and month. 205 | * //=> { '2010': 206 | * //=> { '04': 207 | * //=> [ >, 208 | * //=> > ], 209 | * //=> '06': [ > ] }, 210 | * //=> '2011': 211 | * //=> { '11': [ > ], 212 | * //=> '08': [ > ] }, 213 | * //=> '2012': 214 | * //=> { '01': [ > ], 215 | * //=> '03': [ > ] }, 216 | * //=> '2013': { '04': [ > ] }, 217 | * //=> '2015': 218 | * //=> { '12': [ > ], 219 | * //=> '01': [ > ] } } 220 | */ 221 | 222 | var monthsGroup = list.groupBy('locals.year', 'locals.month'); 223 | 224 | /** 225 | * Iterate over the year/month groups that were generated and add 226 | * them to their archive collections 227 | */ 228 | 229 | var years = Object.keys(yearsGroup); 230 | years.forEach(function (year) { 231 | var yearGroup = yearsGroup.get(year); 232 | app.yearArchives.addIndices(yearGroup.paginate({limit: 3}), {slug: year, year: year, index: archives}); 233 | }); 234 | 235 | years = Object.keys(monthsGroup); 236 | years.forEach(function (year) { 237 | var yearGroup = monthsGroup.get(year); 238 | var months = Object.keys(yearGroup); 239 | months.forEach(function (month) { 240 | var monthGroup = monthsGroup.get([year, month].join('.')); 241 | app.monthArchives.addIndices(monthGroup.paginate({limit: 3}), {slug: path.join(year, month), year: year, month: month, index: archives}); 242 | }); 243 | }); 244 | 245 | async.series([ 246 | /** 247 | * Render and write out all the `posts` from the `posts` list. They'll be written based on their permalink url. 248 | * These are also sorted by their year and month properties to show how the next/prev links work. 249 | */ 250 | renderList.bind(null, new List(list).sortBy('locals.year', 'locals.month'), {layout: 'post'}), 251 | /** 252 | * Render and write out all of the `archives` list pages of `posts` the their permalink generated url. 253 | */ 254 | renderList.bind(null, new List(app.archives)), 255 | /** 256 | * Render and write out all of the `year archives` list pages of `posts` the their permalink generated url. 257 | */ 258 | renderList.bind(null, new List(app.yearArchives).sortBy('url')), 259 | /** 260 | * Render and write out all of the `month archives` list pages of `posts` the their permalink generated url. 261 | */ 262 | renderList.bind(null, new List(app.monthArchives).sortBy('url')), 263 | 264 | ], function (err) { 265 | if (err) return console.error(err); 266 | console.log('Finished'); 267 | }); 268 | 269 | /** 270 | * Helper function to iterate over a list of templates, render them 271 | * and write them to their generated permalink url 272 | */ 273 | 274 | function renderList (list, locals, cb) { 275 | if (typeof locals === 'function') { 276 | cb = locals; 277 | locals = {}; 278 | } 279 | 280 | async.eachSeries(list.items, function (item, next) { 281 | process.stdout.write(cyan('Rendering ') + grey(item.url) + cyan(' => ')); 282 | item.render(locals, function (err, res) { 283 | if (err) return next(err); 284 | var dest = path.join(__dirname, '../actual', res.url); 285 | process.stdout.write(grey(path.relative(process.cwd(), dest)) + '... '); 286 | writeFile(dest, res.content, function (err) { 287 | if (err) { 288 | process.stdout.write(red(error) + '\n'); 289 | return next(err); 290 | } 291 | process.stdout.write(green(success) + '\n'); 292 | next(); 293 | }); 294 | }); 295 | }, cb); 296 | } 297 | 298 | function renameKey (fp) { 299 | return fp; 300 | } 301 | --------------------------------------------------------------------------------