@metalsmith/collections 1.1.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -5
- package/README.md +177 -94
- package/lib/index.js +157 -199
- package/package.json +17 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
### Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
#### [v1.2.2](https://github.com/metalsmith/collections/compare/v1.2.2...v1.2.1) / 2022-07-28
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- Resolves [`#102`](https://github.com/metalsmith/collections/issues/99): removes multimatch dependency, uses metalsmith.match instead
|
|
6
|
+
- Drops support for Metalsmith < 2.4.1
|
|
7
|
+
- Drops support for Node < 12
|
|
6
8
|
|
|
7
|
-
#### [v1.1
|
|
9
|
+
#### [v1.2.1](https://github.com/metalsmith/collections/compare/v1.2.1...v1.2.0) / 2022-02-03
|
|
10
|
+
|
|
11
|
+
- Fixes [`#99`](https://github.com/metalsmith/collections/issues/99): collection key on file metadata - no dupes, no nested arrays
|
|
12
|
+
- Fixes regression: incorrect previous & next refs when reverse: true
|
|
13
|
+
- Fixes typo's in README
|
|
14
|
+
|
|
15
|
+
#### [v1.2.0](https://github.com/metalsmith/collections/compare/v1.2.0...v1.1.0) / 2022-01-29
|
|
16
|
+
|
|
17
|
+
- Feature: sortBy now also understands nested metadata properties, e.g. `sortBy: 'meta.display.order'`
|
|
18
|
+
- Fixed JSDoc typo that made type hints unavailable
|
|
19
|
+
- Documented limit & refer options
|
|
20
|
+
- Improved README.md with more elaborate examples
|
|
21
|
+
- Refactored to cleaner code
|
|
22
|
+
- Removed dependencies: `extend`,`uniq`
|
|
23
|
+
- Added dependency `lodash.get`
|
|
24
|
+
- Added core-plugin tests
|
|
25
|
+
- Updated devDependencies release-it, prettier, eslint
|
|
26
|
+
|
|
27
|
+
#### [v1.1.0](https://github.com/metalsmith/collections/compare/v1.0.0...v1.1.0) / 2021-15-12
|
|
8
28
|
|
|
9
29
|
- Added standardised code formatting and QA [`#86`](https://github.com/metalsmith/collections/pull/86)
|
|
10
30
|
- Updated History with v1 PRs [`#85`](https://github.com/metalsmith/collections/pull/85)
|
|
11
31
|
- Added better JSDoc types, return named plugin function [`3aa3443`](https://github.com/metalsmith/collections/commit/3aa3443802c2f814c90cf39c7b43de8fc3d3ff13)
|
|
12
32
|
- Updated multimatch to 4.0.0, debug to 4.3.3 [`71d6f65`](https://github.com/metalsmith/collections/commit/71d6f65b9ec5572196e17dfebf5cff2361853f9d)
|
|
13
33
|
|
|
14
|
-
<!-- auto-changelog-above -->
|
|
15
|
-
|
|
16
34
|
# [1.0.0][] / 2018-10-17
|
|
17
35
|
|
|
18
36
|
- Fixed API and merged many PRs
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @metalsmith/collections
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A Metalsmith plugin that lets you group files together into ordered collections, like blog posts. That way you can loop over them to generate index pages, add 'next' and 'previous' links between them, and more
|
|
4
4
|
|
|
5
5
|
[![metalsmith: core plugin][metalsmith-badge]][metalsmith-url]
|
|
6
6
|
[![npm version][npm-badge]][npm-url]
|
|
@@ -10,10 +10,10 @@ A [Metalsmith](https://github.com/metalsmith/metalsmith) plugin that lets you gr
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- can match files by `collection` metadata
|
|
13
|
+
- can match files by `collection` file metadata
|
|
14
14
|
- can match files by pattern
|
|
15
15
|
- can limit the number of files in a collection
|
|
16
|
-
- can filter files in a collection based on metadata
|
|
16
|
+
- can filter files in a collection based on file metadata
|
|
17
17
|
- adds collections to global metadata
|
|
18
18
|
- adds `next` and `previous` references to each file in the collection
|
|
19
19
|
|
|
@@ -21,102 +21,183 @@ A [Metalsmith](https://github.com/metalsmith/metalsmith) plugin that lets you gr
|
|
|
21
21
|
|
|
22
22
|
NPM:
|
|
23
23
|
|
|
24
|
-
```
|
|
24
|
+
```bash
|
|
25
25
|
npm install @metalsmith/collections
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Yarn:
|
|
29
29
|
|
|
30
|
-
```
|
|
30
|
+
```bash
|
|
31
31
|
yarn add @metalsmith/collections
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Usage
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- **by pattern** - this is just passing a globbing pattern that will group any files that match into the same collection. The passed pattern can be a single pattern (as a string) or an array of globing patterns. For more information read the [multimatch patterns documentation](https://www.npmjs.com/package/multimatch#how-multiple-patterns-work).
|
|
39
|
-
- **by metadata** - this is adding a specific `collection` metadata field to each item that you want to add to a collection.
|
|
40
|
-
|
|
41
|
-
The simplest way to create a collection is to use a pattern to match the files you want to group together:
|
|
36
|
+
Pass options to `@metalsmith/collections` in the plugin chain:
|
|
42
37
|
|
|
43
38
|
```js
|
|
39
|
+
const Metalsmith = require('metalsmith')
|
|
40
|
+
const markdown = require('@metalsmith/markdown')
|
|
44
41
|
const collections = require('@metalsmith/collections')
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
// defaults, only create collections based on file metadata
|
|
44
|
+
Metalsmith(__dirname)
|
|
45
|
+
.use(markdown())
|
|
46
|
+
.use(collections())
|
|
47
|
+
|
|
48
|
+
// defaults for a "news" collection, except pattern option
|
|
49
|
+
Metalsmith(__dirname)
|
|
50
|
+
.use(markdown())
|
|
51
|
+
.use(collections({
|
|
52
|
+
news: { pattern: 'news/**/*.html' }
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
// explicit defaults for a "news" collection, except pattern option
|
|
56
|
+
Metalsmith(__dirname)
|
|
57
|
+
.use(markdown())
|
|
58
|
+
.use(collections({
|
|
59
|
+
pattern: { pattern: 'news/**/*.html' },
|
|
60
|
+
metadata: null,
|
|
61
|
+
filterBy: () => true,
|
|
62
|
+
sortBy: defaultSort,
|
|
63
|
+
reverse: false,
|
|
64
|
+
limit: Infinity,
|
|
65
|
+
refer: true
|
|
49
66
|
})
|
|
50
|
-
)
|
|
51
67
|
```
|
|
52
68
|
|
|
53
|
-
|
|
69
|
+
_Note: all examples in the readme use the same collections definitions under [Defining collections](#defining-collections)_
|
|
54
70
|
|
|
55
|
-
|
|
56
|
-
metalsmith.use(
|
|
57
|
-
collections({
|
|
58
|
-
articles: {
|
|
59
|
-
pattern: '*.md',
|
|
60
|
-
sortBy: 'date',
|
|
61
|
-
reverse: true
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
)
|
|
65
|
-
```
|
|
71
|
+
### Options
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
All options are _optional_
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
})
|
|
77
|
-
)
|
|
78
|
-
```
|
|
75
|
+
- **pattern** `string|string[]` - one or more glob patterns to group files into a collection
|
|
76
|
+
- **filterBy** `Function` - a function that returns `false` for files that should be filtered _out_ of the collection
|
|
77
|
+
- **limit** `number` - restrict the number of files in a collection to at most `limit`
|
|
78
|
+
- **sortBy** `string|Function` - a file metadata key to sort by (for example `date` or `pubdate` or `title`), or a custom sort function
|
|
79
|
+
- **reverse** `boolean` - whether the sort should be reversed (e.g., for a news/blog collection, you typically want `reverse: true`)
|
|
80
|
+
- **metadata** `Object|string` - metadata to attach to the collection. Will be available as `metalsmith.metadata().collections.<name>.metadata`. This can be used for example to attach metadata for index pages. _If a string is passed, it will be interpreted as a file path to an external `JSON` or `YAML` metadata file_
|
|
81
|
+
- **refer** `boolean` - will add `previous` and `next` keys to each file in a collection. `true` by default
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
title: My Article
|
|
83
|
-
collection: articles
|
|
84
|
-
date: 2021-12-01
|
|
85
|
-
---
|
|
83
|
+
### Defining collections
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
```
|
|
85
|
+
There are 2 ways to create collections & they can be used together:
|
|
89
86
|
|
|
90
|
-
|
|
87
|
+
- **by pattern** - for example, this is how you would create multiple pattern-based collections, based on the folders `photos`, `news`, and `services`:
|
|
91
88
|
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
```js
|
|
90
|
+
metalsmith.use(
|
|
91
|
+
collections({
|
|
92
|
+
gallery: 'photos/**/*.{jpg,png}',
|
|
93
|
+
news: {
|
|
94
|
+
metadata: {
|
|
95
|
+
title: 'Latest news',
|
|
96
|
+
description: 'All the latest in politics & world news',
|
|
97
|
+
slug: 'news'
|
|
98
|
+
},
|
|
99
|
+
pattern: 'news/**/*.html',
|
|
100
|
+
sortBy: 'pubdate',
|
|
101
|
+
reverse: true
|
|
102
|
+
},
|
|
103
|
+
services: 'services/**/*.html'
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
```
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
- **by file metadata** - add a `collection` property to the front-matter of each file that you want to add to a collection. The markdown file below will be included in the `news` collection even if it's not in the `news` folder (see previous example)
|
|
109
|
+
|
|
110
|
+
`something-happened.md`
|
|
111
|
+
|
|
112
|
+
```md
|
|
113
|
+
---
|
|
114
|
+
title: Something happened
|
|
115
|
+
collection: news
|
|
116
|
+
pubdate: 2021-12-01
|
|
117
|
+
layout: news.hbs
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
...contents
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Note that you can also add the same file to multiple collections, which is useful for example if you want to use `@metalsmith/collections` as a _category_ system:
|
|
103
124
|
|
|
104
|
-
|
|
105
|
-
You can omit passing any options to the plugin when matching based on a `collection` property.
|
|
125
|
+
`something-happened.md`
|
|
106
126
|
|
|
107
|
-
|
|
127
|
+
```md
|
|
128
|
+
title: Something happened
|
|
129
|
+
collection:
|
|
108
130
|
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
- news
|
|
132
|
+
- category_politics
|
|
133
|
+
- category_world
|
|
134
|
+
pubdate: 2021-12-01
|
|
135
|
+
layout: news.hbs
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
...contents
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Rendering collection items
|
|
143
|
+
|
|
144
|
+
Here is an example of using [@metalsmith/layouts](https://github.com/metalsmith/layouts) with [jstransformer-handlebars](https://github.com/jstransformers/jstransformer-handlebars) to render the `something-happened.md` news item, with links to the next and previous news items (using `refer: true` options):
|
|
145
|
+
|
|
146
|
+
`layouts/news.njk`
|
|
147
|
+
|
|
148
|
+
```handlebars
|
|
149
|
+
<h1>{{ title }}</h1> {{!-- something-happened.md title --}}
|
|
150
|
+
<a href="/{{ collections.news.metadata.slug }}">Back to news</a> {{!-- news collection metadata.slug --}}
|
|
151
|
+
{{ contents | safe }}
|
|
152
|
+
<hr>
|
|
153
|
+
{{!-- previous & next are added by @metalsmith/collections --}}
|
|
154
|
+
{{#if previous}}
|
|
155
|
+
Read the previous news:
|
|
156
|
+
<a href="/{{ previous.path }}">{{ previous.title }}</a>
|
|
157
|
+
{{/if}}
|
|
158
|
+
{{#if next}}
|
|
159
|
+
Read the next news:
|
|
160
|
+
<a href="/{{ next.path }}">{{ next.title }}</a>
|
|
161
|
+
{{/if}}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
_Note: If you don't need the `next` and `previous` references, you can pass the option `refer: false`_
|
|
165
|
+
|
|
166
|
+
### Rendering collection index
|
|
167
|
+
|
|
168
|
+
All matched files are added to an array that is exposed as a key of metalsmith global metadata, for example the `news` collection would be accessible at `Metalsmith.metadata().collections.news `. Below is an example of how you could render an index page for the `news` collection:
|
|
169
|
+
|
|
170
|
+
`layouts/news-index.hbs`
|
|
171
|
+
|
|
172
|
+
```handlebars
|
|
173
|
+
<h1>{{ title }}</h1> {{!-- news collection metadata.title --}}
|
|
174
|
+
<p>{{ description }}</p> {{!-- news collection metadata.description --}}
|
|
175
|
+
<hr>
|
|
176
|
+
{{!-- previous & next are added by @metalsmith/collections --}}
|
|
177
|
+
{{#if collections.news.length }}
|
|
178
|
+
<ul>
|
|
179
|
+
{{#each collections.news}}
|
|
180
|
+
<li>
|
|
181
|
+
<h3><a href="/{{path}}">{{ title }}</a></h3>
|
|
182
|
+
<p>{{ excerpt }}</p>
|
|
183
|
+
</li>
|
|
184
|
+
{{/each}}
|
|
185
|
+
</ul>
|
|
186
|
+
{{/each}}
|
|
187
|
+
{{else}}
|
|
188
|
+
No news at the moment...
|
|
189
|
+
{{/if}}
|
|
111
190
|
```
|
|
112
191
|
|
|
113
|
-
|
|
192
|
+
### Custom sorting, filtering and limiting
|
|
193
|
+
|
|
194
|
+
You could define an `order` property on a set of files and pass `sortBy: "order"` to `@metalsmith/collections` for example, or you could override the sort with a custom function (for example to do multi-level sorting). For instance, this function sorts the "subpages" collection by a numerical "index" property but places unindexed items last.
|
|
114
195
|
|
|
115
196
|
```js
|
|
116
197
|
metalsmith.use(
|
|
117
198
|
collections({
|
|
118
199
|
subpages: {
|
|
119
|
-
sortBy: function(a, b) {
|
|
200
|
+
sortBy: function (a, b) {
|
|
120
201
|
let aNum, bNum
|
|
121
202
|
|
|
122
203
|
aNum = +a.index
|
|
@@ -137,63 +218,65 @@ metalsmith.use(
|
|
|
137
218
|
)
|
|
138
219
|
```
|
|
139
220
|
|
|
140
|
-
|
|
221
|
+
_Note: the `sortBy` option also understands nested keypaths, e.g. `display.order`_
|
|
141
222
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Additional metadata can be added to the collection object.
|
|
223
|
+
The `filterBy` function is passed a single argument which corresponds to each file's metadata. You can use the metadata to perform comparisons or carry out other decision-making logic. If the function you supply evaluates to `true`, the file will be added to the collection. If it evaluates to `false`, the file will not be added. The filterBy function below could work for a collection named `thisYearsNews` as it would filter out all the items that are older than this year:
|
|
145
224
|
|
|
146
225
|
```js
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
metadata: {
|
|
153
|
-
name: 'Articles',
|
|
154
|
-
description: 'The Articles listed here...'
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
)
|
|
226
|
+
function filterBy(file) {
|
|
227
|
+
const today = new Date()
|
|
228
|
+
const pubdate = new Date(file.pubdate)
|
|
229
|
+
return pubdate.getFullYear() === today.getFullYear()
|
|
230
|
+
}
|
|
159
231
|
```
|
|
160
232
|
|
|
161
|
-
|
|
233
|
+
Add a `limit` option to a collection config, for example to separate recent articles from archives:
|
|
162
234
|
|
|
163
235
|
```js
|
|
164
236
|
metalsmith.use(
|
|
165
237
|
collections({
|
|
166
|
-
|
|
238
|
+
recentArticles: {
|
|
239
|
+
pattern: 'articles/**/*.html',
|
|
167
240
|
sortBy: 'date',
|
|
168
|
-
|
|
169
|
-
|
|
241
|
+
limit: 10
|
|
242
|
+
},
|
|
243
|
+
archives: {
|
|
244
|
+
pattern: 'archives/**/*.html',
|
|
245
|
+
sortBy: 'date'
|
|
170
246
|
}
|
|
171
247
|
})
|
|
172
248
|
)
|
|
173
249
|
```
|
|
174
250
|
|
|
175
|
-
|
|
176
|
-
|
|
251
|
+
_Note: the collection is first sorted, reversed, filtered, and then limited, if applicable._
|
|
252
|
+
|
|
253
|
+
### Collection Metadata
|
|
254
|
+
|
|
255
|
+
Additional metadata can be added to the collection object:
|
|
177
256
|
|
|
178
257
|
```js
|
|
179
258
|
metalsmith.use(
|
|
180
259
|
collections({
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
260
|
+
news: {
|
|
261
|
+
metadata: {
|
|
262
|
+
title: 'Latest news',
|
|
263
|
+
description: 'All the latest in politics & world news',
|
|
264
|
+
slug: 'news'
|
|
265
|
+
}
|
|
184
266
|
}
|
|
185
267
|
})
|
|
186
268
|
)
|
|
187
269
|
```
|
|
188
270
|
|
|
189
|
-
|
|
190
|
-
"previous" links to your articles.
|
|
271
|
+
Collection metadata can be loaded from a `json` or `yaml` file (path relative to `Metalsmith.directory()`):
|
|
191
272
|
|
|
192
273
|
```js
|
|
193
274
|
metalsmith.use(
|
|
194
275
|
collections({
|
|
195
276
|
articles: {
|
|
196
|
-
|
|
277
|
+
sortBy: 'date',
|
|
278
|
+
reverse: true,
|
|
279
|
+
metadata: 'path/to/file.json'
|
|
197
280
|
}
|
|
198
281
|
})
|
|
199
282
|
)
|
|
@@ -240,8 +323,8 @@ Add the `@metalsmith/collections` key to your `metalsmith.json` `plugins` key:
|
|
|
240
323
|
|
|
241
324
|
[npm-badge]: https://img.shields.io/npm/v/@metalsmith/collections.svg
|
|
242
325
|
[npm-url]: https://www.npmjs.com/package/@metalsmith/collections
|
|
243
|
-
[ci-badge]: https://
|
|
244
|
-
[ci-url]: https://
|
|
326
|
+
[ci-badge]: https://github.com/metalsmith/collections/actions/workflows/test.yml/badge.svg
|
|
327
|
+
[ci-url]: https://github.com/metalsmith/collections/actions/workflows/test.yml
|
|
245
328
|
[metalsmith-badge]: https://img.shields.io/badge/metalsmith-plugin-green.svg?longCache=true
|
|
246
329
|
[metalsmith-url]: http://metalsmith.io
|
|
247
330
|
[codecov-badge]: https://img.shields.io/coveralls/github/metalsmith/collections
|
package/lib/index.js
CHANGED
|
@@ -1,236 +1,194 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
const debug = require('debug')('@metalsmith/collections')
|
|
2
|
+
const loadMetadata = require('read-metadata').sync
|
|
3
|
+
const get = require('lodash.get')
|
|
4
|
+
// for backwards-compatibility only, date makes as little sense as "pubdate" or any custom key
|
|
5
|
+
const defaultSort = sortBy('date')
|
|
6
|
+
const defaultFilter = () => true
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* @typedef {Object} CollectionConfig
|
|
10
|
+
* @property {string|string[]} pattern - One or more glob patterns to match files to a collection
|
|
11
|
+
* @property {string|(a,b) => 0|1|-1} sortBy - A key to sort by (e.g. `date`,`title`, ..) or a custom sort function
|
|
12
|
+
* @property {number} limit - Limit the amount of items in a collection to `limit`
|
|
13
|
+
* @property {boolean} refer - Adds `next` and `previous` keys to file metadata of matched files
|
|
14
|
+
* @property {boolean} reverse - Whether to invert the sorting function results (asc/descending)
|
|
15
|
+
* @property {Function} filterBy - A function that gets a `Metalsmith.File` as first argument and returns `true` for every file to include in the collection
|
|
16
|
+
* @property {Object|string} metadata - An object with metadata to attach to the collection, or a `json`/`yaml`filepath string to load data from (relative to `Metalsmith.directory`)
|
|
8
17
|
*/
|
|
9
18
|
|
|
10
|
-
|
|
19
|
+
/** @type {CollectionConfig} */
|
|
20
|
+
const defaultOptions = {
|
|
21
|
+
pattern: null,
|
|
22
|
+
reverse: false,
|
|
23
|
+
metadata: null,
|
|
24
|
+
limit: Infinity,
|
|
25
|
+
refer: true,
|
|
26
|
+
sortBy: defaultSort,
|
|
27
|
+
filterBy: defaultFilter
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sortBy(key) {
|
|
31
|
+
let getKey = (x) => x[key]
|
|
32
|
+
if (key.includes('.')) {
|
|
33
|
+
getKey = (x) => get(x, key)
|
|
34
|
+
}
|
|
35
|
+
return function defaultSort(a, b) {
|
|
36
|
+
a = getKey(a)
|
|
37
|
+
b = getKey(b)
|
|
38
|
+
if (!a && !b) return 0
|
|
39
|
+
if (!a) return -1
|
|
40
|
+
if (!b) return 1
|
|
41
|
+
if (b > a) return -1
|
|
42
|
+
if (a > b) return 1
|
|
43
|
+
return 0
|
|
44
|
+
}
|
|
45
|
+
}
|
|
11
46
|
|
|
12
47
|
/**
|
|
13
|
-
*
|
|
14
|
-
* @
|
|
15
|
-
* @property {'date'|Function} sortBy
|
|
16
|
-
* @property {Boolean} reverse - Whether to invert the sorting function results (asc/descending)
|
|
17
|
-
* @property {Function} filterBy - A function that gets a `Metalsmith.File` as first argument and returns `true` for every file to include in the collection
|
|
18
|
-
* @property {Object} metadata - An object with metadata to attach to the collection
|
|
48
|
+
* Normalize options
|
|
49
|
+
* @param {Object.<string,CollectionConfig>} options
|
|
19
50
|
*/
|
|
51
|
+
function normalizeOptions(options) {
|
|
52
|
+
options = options || {}
|
|
53
|
+
|
|
54
|
+
for (const config in options) {
|
|
55
|
+
let normalized = options[config]
|
|
56
|
+
if (typeof normalized === 'string' || Array.isArray(normalized)) {
|
|
57
|
+
normalized = { pattern: normalized }
|
|
58
|
+
}
|
|
59
|
+
normalized = Object.assign({}, defaultOptions, normalized)
|
|
60
|
+
if (typeof normalized.metadata === 'string') {
|
|
61
|
+
normalized.metadata = loadMetadata(normalized.metadata)
|
|
62
|
+
}
|
|
63
|
+
if (typeof normalized.sortBy === 'string') {
|
|
64
|
+
normalized.sortBy = sortBy(normalized.sortBy)
|
|
65
|
+
}
|
|
66
|
+
options[config] = normalized
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return options
|
|
70
|
+
}
|
|
20
71
|
|
|
21
72
|
/**
|
|
22
73
|
* Metalsmith plugin that adds `collections` of files to the global
|
|
23
74
|
* metadata as a sorted array.
|
|
24
75
|
*
|
|
25
|
-
* @param {Object.<
|
|
76
|
+
* @param {Object.<string,CollectionConfig|string>} options
|
|
26
77
|
* @return {import('metalsmith').Plugin}
|
|
27
78
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
79
|
+
function initializeCollections(options) {
|
|
80
|
+
options = normalizeOptions(options)
|
|
81
|
+
const collectionNames = Object.keys(options)
|
|
82
|
+
const mappedCollections = collectionNames.map((name) => {
|
|
83
|
+
return Object.assign({ name: name }, options[name])
|
|
84
|
+
})
|
|
33
85
|
|
|
34
86
|
return function collections(files, metalsmith, done) {
|
|
35
|
-
|
|
87
|
+
const metadata = metalsmith.metadata()
|
|
88
|
+
const fileNames = Object.keys(files)
|
|
36
89
|
|
|
37
|
-
|
|
38
|
-
* Clear collections (to prevent multiple additions of the same file)
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
keys.forEach(function(key) {
|
|
42
|
-
metadata[key] = []
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Clear collections (to prevent multiple additions of the same file when running via metalsmith-browser-sync)
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
keys.forEach(function(key) {
|
|
50
|
-
metadata[key] = []
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Find the files in each collection.
|
|
55
|
-
*/
|
|
56
|
-
|
|
57
|
-
Object.keys(files).forEach(function(file) {
|
|
58
|
-
var data = files[file]
|
|
59
|
-
|
|
60
|
-
data.path = data.path || file
|
|
61
|
-
|
|
62
|
-
const matches = match(file, data)
|
|
63
|
-
if (matches.length) {
|
|
64
|
-
debug('processing file: %s', file)
|
|
65
|
-
|
|
66
|
-
matches.forEach(function(key) {
|
|
67
|
-
if (key && keys.indexOf(key) < 0) {
|
|
68
|
-
opts[key] = {}
|
|
69
|
-
keys.push(key)
|
|
70
|
-
}
|
|
90
|
+
metadata.collections = {}
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
fileNames.forEach((filePath) => {
|
|
93
|
+
// add path property to file metadata for convenience
|
|
94
|
+
// this is for backward-compatibility only and is pretty useless
|
|
95
|
+
const file = files[filePath]
|
|
96
|
+
file.path = file.path || filePath
|
|
97
|
+
|
|
98
|
+
// dynamically add collections with default options when encountered in file metadata,
|
|
99
|
+
// and not explicitly defined in plugin options
|
|
100
|
+
if (file.collection) {
|
|
101
|
+
;(Array.isArray(file.collection) ? file.collection : [file.collection]).forEach((name) => {
|
|
102
|
+
if (!collectionNames.includes(name)) {
|
|
103
|
+
collectionNames.push(name)
|
|
104
|
+
const normalized = Object.assign({}, defaultOptions)
|
|
105
|
+
mappedCollections.push(Object.assign({ name }, normalized))
|
|
83
106
|
}
|
|
84
107
|
})
|
|
85
108
|
}
|
|
86
109
|
})
|
|
87
110
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (b > a) return -1
|
|
116
|
-
if (a > b) return 1
|
|
117
|
-
return 0
|
|
118
|
-
})
|
|
111
|
+
debug('Identified %s collections: %s', mappedCollections.length, collectionNames.join())
|
|
112
|
+
|
|
113
|
+
mappedCollections.forEach((collection) => {
|
|
114
|
+
const { pattern, filterBy, sortBy, reverse, refer, limit } = collection
|
|
115
|
+
const name = collection.name
|
|
116
|
+
const matches = []
|
|
117
|
+
debug('Processing collection %s with options %s:', name, collection)
|
|
118
|
+
|
|
119
|
+
// first match by pattern if provided
|
|
120
|
+
if (pattern) {
|
|
121
|
+
matches.push.apply(
|
|
122
|
+
matches,
|
|
123
|
+
metalsmith.match(pattern, fileNames).map((filepath) => {
|
|
124
|
+
const data = files[filepath]
|
|
125
|
+
// pattern-matched files might or might not have a "collection" property defined in front-matter
|
|
126
|
+
// and might also be included in multiple collections
|
|
127
|
+
if (!data.collection) {
|
|
128
|
+
data.collection = []
|
|
129
|
+
} else if (typeof data.collection === 'string') {
|
|
130
|
+
data.collection = [data.collection]
|
|
131
|
+
}
|
|
132
|
+
if (!data.collection.includes(collection.name)) {
|
|
133
|
+
data.collection = [...data.collection, collection.name]
|
|
134
|
+
}
|
|
135
|
+
return data
|
|
136
|
+
})
|
|
137
|
+
)
|
|
119
138
|
}
|
|
120
139
|
|
|
121
|
-
if
|
|
122
|
-
|
|
140
|
+
// next match by "collection" key, but only push if the files haven't been added through pattern matching first
|
|
141
|
+
matches.push.apply(
|
|
142
|
+
matches,
|
|
143
|
+
Object.values(files).filter((file) => {
|
|
144
|
+
const patternMatched = matches.includes(file)
|
|
145
|
+
const isInCollection = Array.isArray(file.collection)
|
|
146
|
+
? file.collection.includes(collection.name)
|
|
147
|
+
: file.collection === collection.name
|
|
148
|
+
return !patternMatched && isInCollection
|
|
149
|
+
})
|
|
150
|
+
)
|
|
123
151
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
*/
|
|
127
|
-
|
|
128
|
-
keys.forEach(function(key) {
|
|
129
|
-
debug('referencing collection: %s', key)
|
|
130
|
-
var settings = opts[key]
|
|
131
|
-
var col = metadata[key]
|
|
132
|
-
var last = col.length - 1
|
|
133
|
-
if (opts[key].limit && opts[key].limit < col.length) {
|
|
134
|
-
col = metadata[key] = col.slice(0, opts[key].limit)
|
|
135
|
-
last = opts[key].limit - 1
|
|
152
|
+
if (Object.prototype.hasOwnProperty.call(metadata, name)) {
|
|
153
|
+
debug('Warning: overwriting previously set metadata property %s', name)
|
|
136
154
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (0 != i) file.previous = col[i - 1]
|
|
140
|
-
if (last != i) file.next = col[i + 1]
|
|
141
|
-
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Add collection metadata
|
|
146
|
-
*/
|
|
147
|
-
|
|
148
|
-
keys.forEach(function(key) {
|
|
149
|
-
debug('adding metadata: %s', key)
|
|
150
|
-
var settings = opts[key]
|
|
151
|
-
var col = metadata[key]
|
|
152
|
-
col.metadata = typeof settings.metadata === 'string' ? loadMetadata(settings.metadata) : settings.metadata
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Add them grouped together to the global metadata.
|
|
157
|
-
*/
|
|
155
|
+
// apply sort, reverse, filter, limit options in this order
|
|
156
|
+
metadata[name] = matches.sort(sortBy)
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return (metadata.collections[key] = metadata[key])
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
done()
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Normalize an `options` dictionary.
|
|
170
|
-
*
|
|
171
|
-
* @param {Object.<string,CollectionConfig>} options
|
|
172
|
-
*/
|
|
173
|
-
|
|
174
|
-
function normalize(options) {
|
|
175
|
-
options = options || {}
|
|
176
|
-
|
|
177
|
-
for (var key in options) {
|
|
178
|
-
var val = options[key]
|
|
179
|
-
if ('string' == typeof val) options[key] = { pattern: val }
|
|
180
|
-
if (val instanceof Array) options[key] = { pattern: val }
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return options
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Generate a matching function for a given set of `collections`.
|
|
188
|
-
*
|
|
189
|
-
* @param {Object.<String, CollectionConfig>} collections
|
|
190
|
-
* @return {Function}
|
|
191
|
-
*/
|
|
192
|
-
|
|
193
|
-
function matcher(cols) {
|
|
194
|
-
var keys = Object.keys(cols)
|
|
195
|
-
var matchers = {}
|
|
196
|
-
|
|
197
|
-
keys.forEach(function(key) {
|
|
198
|
-
var opts = cols[key]
|
|
199
|
-
if (!opts.pattern) {
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
matchers[key] = {
|
|
203
|
-
match: function(file) {
|
|
204
|
-
return multimatch(file, opts.pattern)
|
|
158
|
+
if (reverse) {
|
|
159
|
+
metadata[name].reverse()
|
|
205
160
|
}
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
161
|
|
|
209
|
-
|
|
210
|
-
var matches = []
|
|
162
|
+
metadata[name] = metadata[name].filter(filterBy).slice(0, limit)
|
|
211
163
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!Array.isArray(collection)) {
|
|
215
|
-
collection = [collection]
|
|
164
|
+
if (collection.metadata) {
|
|
165
|
+
metadata[name].metadata = collection.metadata
|
|
216
166
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
167
|
+
if (refer) {
|
|
168
|
+
if (reverse) {
|
|
169
|
+
metadata[name].forEach((file, i) => {
|
|
170
|
+
Object.assign(file, {
|
|
171
|
+
next: i > 0 ? metadata[name][i - 1] : null,
|
|
172
|
+
previous: i < metadata[name].length - 1 ? metadata[name][i + 1] : null
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
} else {
|
|
176
|
+
metadata[name].forEach((file, i) => {
|
|
177
|
+
Object.assign(file, {
|
|
178
|
+
previous: i > 0 ? metadata[name][i - 1] : null,
|
|
179
|
+
next: i < metadata[name].length - 1 ? metadata[name][i + 1] : null
|
|
180
|
+
})
|
|
181
|
+
})
|
|
222
182
|
}
|
|
223
|
-
})
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
for (var key in matchers) {
|
|
227
|
-
var m = matchers[key]
|
|
228
|
-
if (m.match(file).length) {
|
|
229
|
-
matches.push(key)
|
|
230
183
|
}
|
|
231
|
-
}
|
|
232
184
|
|
|
233
|
-
|
|
234
|
-
|
|
185
|
+
metadata.collections[name] = metadata[name]
|
|
186
|
+
debug('Added %s files to collection %s', metadata[name].length, name)
|
|
187
|
+
})
|
|
188
|
+
done()
|
|
235
189
|
}
|
|
236
190
|
}
|
|
191
|
+
|
|
192
|
+
initializeCollections.defaults = defaultOptions
|
|
193
|
+
|
|
194
|
+
module.exports = initializeCollections
|
package/package.json
CHANGED
|
@@ -15,27 +15,25 @@
|
|
|
15
15
|
"type": "git",
|
|
16
16
|
"url": "https://github.com/metalsmith/collections.git"
|
|
17
17
|
},
|
|
18
|
-
"version": "1.
|
|
18
|
+
"version": "1.2.2",
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"main": "lib/index.js",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"multimatch": "^4.0.0",
|
|
25
|
-
"read-metadata": "^1.0.0",
|
|
26
|
-
"uniq": "^1.0.1"
|
|
22
|
+
"lodash.get": "^4.4.2",
|
|
23
|
+
"read-metadata": "^1.0.0"
|
|
27
24
|
},
|
|
28
25
|
"devDependencies": {
|
|
29
|
-
"auto-changelog": "^2.
|
|
26
|
+
"auto-changelog": "^2.4.0",
|
|
30
27
|
"coveralls": "^3.1.1",
|
|
31
|
-
"
|
|
32
|
-
"eslint
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
28
|
+
"debug": "^4.3.4",
|
|
29
|
+
"eslint": "^8.20.0",
|
|
30
|
+
"eslint-config-prettier": "^8.5.0",
|
|
31
|
+
"metalsmith": "^2.4.1",
|
|
32
|
+
"mocha": "^9.2.2",
|
|
33
|
+
"nodemon": "^2.0.19",
|
|
36
34
|
"nyc": "^15.1.0",
|
|
37
|
-
"prettier": "^
|
|
38
|
-
"release-it": "^
|
|
35
|
+
"prettier": "^2.7.1",
|
|
36
|
+
"release-it": "^15.2.0"
|
|
39
37
|
},
|
|
40
38
|
"directories": {
|
|
41
39
|
"lib": "lib",
|
|
@@ -50,7 +48,9 @@
|
|
|
50
48
|
"coverage": "nyc report --reporter=text-lcov > ./coverage.info",
|
|
51
49
|
"coveralls": "npm run coverage && cat ./coverage.info | coveralls",
|
|
52
50
|
"format": "prettier --write \"**/*.{yml,md,js,json}\"",
|
|
53
|
-
"
|
|
51
|
+
"format:check": "prettier --list-different \"**/*.{yml,md,js,json}\"",
|
|
52
|
+
"lint": "eslint --fix .",
|
|
53
|
+
"lint:check": "eslint --fix-dry-run .",
|
|
54
54
|
"dev": "nodemon --exec 'npm test'",
|
|
55
55
|
"release": "release-it ."
|
|
56
56
|
},
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"access": "public"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"metalsmith": "^2.
|
|
61
|
+
"metalsmith": "^2.4.1"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
64
|
-
"node": ">=
|
|
64
|
+
"node": ">=12"
|
|
65
65
|
}
|
|
66
66
|
}
|