Skip to content

Commit 87b491a

Browse files
committed
Refactoring options API and documentation
* Enable `noPageOne` configuration (Refs: #5) * Enable `groupBy` configuration (Refs: #6) * Enable `pageContents` configuration (Refs: #7) Together, these options allow disabling page one rendering, grouping by custom logic (E.g. not pagination, but other file information - author, year, id, etc.) and setting default page contents (overriding the empty buffer default).
1 parent 5ae6a81 commit 87b491a

File tree

5 files changed

+220
-79
lines changed

5 files changed

+220
-79
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
coverage
22
node_modules
33
.DS_Store
4+
npm-debug.log

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ metalsmith.use(pagination({
7272
* **path** The path to render every page under.
7373
* **filter** A string or function used to filter files in pagination.
7474
* **pageMetadata** The metadata to merge with every page.
75+
* **noPageOne** Set to true to disable rendering of page one, useful in conjunction with first (default: `false`).
76+
* **pageContents** Set the contents of generated pages (default: `new Buffer('')`). Useful for [metalsmith-in-place](https://npmjs.org/package/metalsmith-in-place) (especially with `pageMetadata`).
77+
* **groupBy** Set the grouping algorithm manually (default: paginated by `perPage`). Useful for paginating by other factors, like year published (E.g. `date.getFullYear()`).
7578

7679
### Page Metadata
7780

@@ -82,6 +85,7 @@ The `pageMetadata` option is optional. The object passed as `pageMetadata` is me
8285
Within the template you specified, you will have access to pagination specific helpers:
8386

8487
* **pagination.num** The current page number.
88+
* **pagination.name** The page name from `groupBy`. It will be the page number string with the default `groupBy`.
8589
* **pagination.files** All the files for the current page (E.g. an array of `x` articles).
8690
* **pagination.pages** Links to every page in the collection (E.g. used to render pagination numbers).
8791
* **pagination.next** The immediately following page, if it exists.

metalsmith-pagination.js

Lines changed: 101 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
var toFn = require('to-function')
2-
var extend = require('extend')
2+
var extend = require('xtend')
33

44
/**
55
* Page collection defaults.
66
*
77
* @type {Object}
88
*/
99
var DEFAULTS = {
10-
perPage: 10
10+
perPage: 10,
11+
noPageOne: false,
12+
pageContents: new Buffer('')
1113
}
1214

1315
/**
1416
* Paginate based on the collection.
1517
*
16-
* @param {Object} opts
18+
* @param {Object} options
1719
* @return {Function}
1820
*/
19-
module.exports = function (opts) {
20-
var keys = Object.keys(opts)
21+
module.exports = function (options) {
22+
var keys = Object.keys(options)
2123

2224
return function (files, metalsmith, done) {
2325
var metadata = metalsmith.metadata()
@@ -26,94 +28,114 @@ module.exports = function (opts) {
2628
var complete = keys.every(function (name) {
2729
var collection
2830

31+
// Catch nested collection reference errors.
2932
try {
3033
collection = toFn(name)(metadata)
31-
} catch (e) {
32-
done(e)
34+
} catch (error) {}
3335

34-
return false
35-
}
36-
37-
// Throw an error if the collection does not exist.
3836
if (!collection) {
39-
done(new TypeError('Collection "' + name + '" does not exist'))
37+
done(new TypeError('Collection not found (' + name + ')'))
4038

4139
return false
4240
}
4341

44-
var pageOpts = extend({}, DEFAULTS, opts[name])
42+
var pageOptions = extend(DEFAULTS, options[name])
4543
var toShow = collection
44+
var groupBy = toFn(pageOptions.groupBy || groupByPagination)
4645

47-
if (typeof pageOpts.filter === 'function') {
48-
toShow = collection.filter(pageOpts.filter)
49-
} else if (pageOpts.filter) {
50-
toShow = collection.filter(toFn(pageOpts.filter))
46+
if (pageOptions.filter) {
47+
toShow = collection.filter(toFn(pageOptions.filter))
5148
}
5249

53-
var perPage = pageOpts.perPage
54-
var pages = collection.pages = []
55-
var numPages = Math.ceil(toShow.length / perPage)
50+
if (!pageOptions.template && !pageOptions.layout) {
51+
done(new TypeError('A template or layout is required (' + name + ')'))
52+
53+
return false
54+
}
5655

57-
if (!pageOpts.template && !pageOpts.layout) {
58-
done(new TypeError('Specify a template or layout for "' + name + '" pages'))
56+
if (pageOptions.template && pageOptions.layout) {
57+
done(new TypeError(
58+
'Template and layout can not be used simultaneosly (' + name + ')'
59+
))
5960

6061
return false
6162
}
6263

63-
if (pageOpts.template && pageOpts.layout) {
64-
done(new TypeError('You should not specify template and layout for "' +
65-
name + '" pages simultaneosly'))
64+
if (!pageOptions.path) {
65+
done(new TypeError('The path is required (' + name + ')'))
6666

6767
return false
6868
}
6969

70-
if (!pageOpts.path) {
71-
done(new TypeError('Specify a path for "' + name + '" pages'))
70+
// Can't specify both
71+
if (pageOptions.noPageOne && !pageOptions.first) {
72+
done(new TypeError(
73+
'When `noPageOne` is enabled, a first page must be set (' + name + ')'
74+
))
7275

7376
return false
7477
}
7578

76-
// Iterate over every page and generate a pages array.
77-
for (var i = 0; i < numPages; i++) {
78-
var pageFiles = toShow.slice(i * perPage, (i + 1) * perPage)
79+
// Put a `pages` property on the original collection.
80+
var pages = collection.pages = []
81+
var pageMap = {}
82+
83+
// Sort pages into "categories".
84+
toShow.forEach(function (file, index) {
85+
var name = groupBy(file, index, pageOptions).toString()
86+
87+
// Keep pages in the order they are returned. E.g. Allows sorting
88+
// by published year to work.
89+
if (!pageMap.hasOwnProperty(name)) {
90+
// Use the index to calculate pagination, page numbers, etc.
91+
var length = pages.length
92+
93+
var pagination = {
94+
name: name,
95+
num: length + 1,
96+
pages: pages,
97+
files: []
98+
}
99+
100+
// Generate the page data.
101+
var page = extend(pageOptions.pageMetadata, {
102+
template: pageOptions.template,
103+
layout: pageOptions.layout,
104+
contents: pageOptions.pageContents,
105+
path: interpolate(pageOptions.path, pagination),
106+
pagination: pagination
107+
})
79108

80-
// Create the pagination object for the current page.
81-
var pagination = {
82-
num: i + 1,
83-
pages: pages,
84-
files: extend(pageFiles, { metadata: collection.metadata })
85-
}
109+
// Copy collection metadata onto every page "collection".
110+
pagination.files.metadata = collection.metadata
86111

87-
// Generate a new file based on the filename with correct metadata.
88-
var page = extend({}, pageOpts.pageMetadata, {
89-
template: pageOpts.template,
90-
layout: pageOpts.layout,
91-
contents: new Buffer(''),
92-
path: interpolate(pageOpts.path, pagination),
93-
pagination: pagination
94-
})
95-
96-
// Create the file.
97-
files[page.path] = page
98-
99-
// Update next/prev references.
100-
if (i > 0) {
101-
page.pagination.previous = pages[i - 1]
102-
pages[i - 1].pagination.next = page
103-
}
112+
if (length === 0) {
113+
if (!pageOptions.noPageOne) {
114+
files[page.path] = page
115+
}
104116

105-
// When the first page option is set, render it over the top of the
106-
// canonically generated page.
107-
if (i === 0 && pageOpts.first) {
108-
page = extend({}, page, {
109-
path: interpolate(pageOpts.first, page.pagination)
110-
})
117+
if (pageOptions.first) {
118+
// Extend the "first page" over the top of "page one".
119+
page = extend(page, {
120+
path: interpolate(pageOptions.first, page.pagination)
121+
})
122+
123+
files[page.path] = page
124+
}
125+
} else {
126+
files[page.path] = page
111127

112-
files[page.path] = page
128+
page.pagination.previous = pages[length - 1]
129+
pages[length - 1].pagination.next = page
130+
}
131+
132+
pages.push(page)
133+
pageMap[name] = pagination
113134
}
114135

115-
pages.push(page)
116-
}
136+
// Files are kept sorted within their own category.
137+
pageMap[name].files.push(file)
138+
})
117139

118140
return true
119141
})
@@ -126,9 +148,23 @@ module.exports = function (opts) {
126148
* Interpolate the page path with pagination variables.
127149
*
128150
* @param {String} path
129-
* @param {Object} opts
151+
* @param {Object} data
130152
* @return {String}
131153
*/
132-
function interpolate (path, opts) {
133-
return path.replace(/:num/g, opts.num)
154+
function interpolate (path, data) {
155+
return path.replace(/:(\w+)/g, function (match, param) {
156+
return data[param]
157+
})
158+
}
159+
160+
/**
161+
* Group by pagination by default.
162+
*
163+
* @param {Object} file
164+
* @param {number} index
165+
* @param {Object} options
166+
* @return {number}
167+
*/
168+
function groupByPagination (file, index, options) {
169+
return Math.ceil((index + 1) / options.perPage)
134170
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"standard": "^2.11.0"
3737
},
3838
"dependencies": {
39-
"extend": "^2.0.0",
40-
"to-function": "^2.0.5"
39+
"to-function": "^2.0.5",
40+
"xtend": "^4.0.0"
4141
}
4242
}

0 commit comments

Comments
 (0)