@metalsmith/collections 1.1.0 → 1.2.0

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +11 -5
  2. package/README.md +177 -94
  3. package/lib/index.js +146 -199
  4. package/package.json +7 -8
package/CHANGELOG.md CHANGED
@@ -1,18 +1,24 @@
1
1
  ### Changelog
2
2
 
3
- All notable changes to this project will be documented in this file. Dates are displayed in UTC.
3
+ #### [v1.2.0](https://github.com/metalsmith/collections/compare/v1.2.0...v1.1.0) / 2022-01-29
4
4
 
5
- Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
5
+ - Feature: sortBy now also understands nested metadata properties, e.g. `sortBy: 'meta.display.order'`
6
+ - Fixed JSDoc typo that made type hints unavailable
7
+ - Documented limit & refer options
8
+ - Improved README.md with more elaborate examples
9
+ - Refactored to cleaner code
10
+ - Removed dependencies: `extend`,`uniq`
11
+ - Added dependency `lodash.get`
12
+ - Added core-plugin tests
13
+ - Updated devDependencies release-it, prettier, eslint
6
14
 
7
- #### [v1.1.0](https://github.com/metalsmith/collections/compare/v1.0.0...v1.1.0)
15
+ #### [v1.1.0](https://github.com/metalsmith/collections/compare/v1.0.0...v1.1.0) / 2021-15-12
8
16
 
9
17
  - Added standardised code formatting and QA [`#86`](https://github.com/metalsmith/collections/pull/86)
10
18
  - Updated History with v1 PRs [`#85`](https://github.com/metalsmith/collections/pull/85)
11
19
  - Added better JSDoc types, return named plugin function [`3aa3443`](https://github.com/metalsmith/collections/commit/3aa3443802c2f814c90cf39c7b43de8fc3d3ff13)
12
20
  - Updated multimatch to 4.0.0, debug to 4.3.3 [`71d6f65`](https://github.com/metalsmith/collections/commit/71d6f65b9ec5572196e17dfebf5cff2361853f9d)
13
21
 
14
- <!-- auto-changelog-above -->
15
-
16
22
  # [1.0.0][] / 2018-10-17
17
23
 
18
24
  - Fixed API and merged many PRs
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @metalsmith/collections
2
2
 
3
- A [Metalsmith](https://github.com/metalsmith/metalsmith) plugin that lets you group files together into an ordered collection, like blog posts. That way you can loop over them to generate an index, or add 'next' and 'previous' links between them.
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
- There are two ways to create collections (they can be used together):
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
- metalsmith.use(
47
- collections({
48
- articles: '*.md'
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
- Which is just a shorthand. You could also add additional options:
69
+ _Note: all examples in the readme use the same collections definitions under [Defining collections](#defining-collections)_
54
70
 
55
- ```js
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
- But you can also match based on a `collection` property in each file's metadata by omitting a pattern, and adding the property to your files:
73
+ All options are _optional_
68
74
 
69
- ```js
70
- metalsmith.use(
71
- collections({
72
- articles: {
73
- sortBy: 'date',
74
- reverse: true
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
- ```markdown
81
- ---
82
- title: My Article
83
- collection: articles
84
- date: 2021-12-01
85
- ---
83
+ ### Defining collections
86
84
 
87
- My article contents...
88
- ```
85
+ There are 2 ways to create collections & they can be used together:
89
86
 
90
- Multiple collections can also be assigned per file:
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
- ```markdown
93
- ---
94
- title: My Article
95
- collection:
96
- - articles
97
- - news
98
- date: 2021-12-01
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
- My article contents...
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
- All of the files with a matching `collection` will be added to an array that is exposed as a key of the same name on the global Metalsmith `metadata`.
105
- You can omit passing any options to the plugin when matching based on a `collection` property.
125
+ `something-happened.md`
106
126
 
107
- Adds a `path` property to the collection item's data which contains the file path of the generated file. For example, this can be used in mustache templates to create links:
127
+ ```md
128
+ title: Something happened
129
+ collection:
108
130
 
109
- ```html
110
- <h1><a href="/{{ path }}">{{ title }}</a></h1>
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.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
- The sorting method can be overridden with a custom function in order to sort the files in any order you prefer. For instance, this function sorts the "subpages" collection by a numerical "index" property but places unindexed items last.
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
- 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.
221
+ _Note: the `sortBy` option also understands nested keypaths, e.g. `display.order`_
141
222
 
142
- ### Collection Metadata
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
- metalsmith.use(
148
- collections({
149
- articles: {
150
- sortBy: 'date',
151
- reverse: true,
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
- Collection metadata can also be assigned from a `json` or `yaml` file.
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
- articles: {
238
+ recentArticles: {
239
+ pattern: 'articles/**/*.html',
167
240
  sortBy: 'date',
168
- reverse: true,
169
- metadata: 'path/to/file.json'
241
+ limit: 10
242
+ },
243
+ archives: {
244
+ pattern: 'archives/**/*.html',
245
+ sortBy: 'date'
170
246
  }
171
247
  })
172
248
  )
173
249
  ```
174
250
 
175
- On each collection definition, it's possible to add a `limit` option so that the
176
- collection length is not higher than the given limit:
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
- lastArticles: {
182
- sortBy: 'date',
183
- limit: 10
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
- By adding `refer: false` to your options, it will skip adding the "next" and
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
- refer: false
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://app.travis-ci.com/github/metalsmith/collections.svg?branch=master
244
- [ci-url]: https://app.travis-ci.com/github/metalsmith/collections
326
+ [ci-badge]: https://app.travis-ci.com/metalsmith/collections.svg?branch=master
327
+ [ci-url]: https://app.travis-ci.com/metalsmith/collections
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,183 @@
1
- var debug = require('debug')('@metalsmith/collections')
2
- var multimatch = require('multimatch')
3
- var unique = require('uniq')
4
- var loadMetadata = require('read-metadata').sync
1
+ const debug = require('debug')('@metalsmith/collections')
2
+ const multimatch = require('multimatch')
3
+ const loadMetadata = require('read-metadata').sync
4
+ const get = require('lodash.get')
5
+ // for backwards-compatibility only, date makes as little sense as "pubdate" or any custom key
6
+ const defaultSort = sortBy('date')
7
+ const defaultFilter = () => true
5
8
 
6
9
  /**
7
- * Expose `plugin`.
10
+ * @typedef {Object} CollectionConfig
11
+ * @property {string|string[]} pattern - One or more glob patterns to match files to a collection
12
+ * @property {string|(a,b) => 0|1|-1} sortBy - A key to sort by (e.g. `date`,`title`, ..) or a custom sort function
13
+ * @property {number} limit - Limit the amount of items in a collection to `limit`
14
+ * @property {boolean} refer - Adds `next` and `previous` keys to file metadata of matched files
15
+ * @property {boolean} reverse - Whether to invert the sorting function results (asc/descending)
16
+ * @property {Function} filterBy - A function that gets a `Metalsmith.File` as first argument and returns `true` for every file to include in the collection
17
+ * @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
18
  */
9
19
 
10
- module.exports = plugin
20
+ /** @type {CollectionConfig} */
21
+ const defaultOptions = {
22
+ pattern: null,
23
+ reverse: false,
24
+ metadata: null,
25
+ limit: Infinity,
26
+ refer: true,
27
+ sortBy: defaultSort,
28
+ filterBy: defaultFilter
29
+ }
30
+
31
+ function sortBy(key) {
32
+ let getKey = (x) => x[key]
33
+ if (key.includes('.')) {
34
+ getKey = (x) => get(x, key)
35
+ }
36
+ return function defaultSort(a, b) {
37
+ a = getKey(a)
38
+ b = getKey(b)
39
+ if (!a && !b) return 0
40
+ if (!a) return -1
41
+ if (!b) return 1
42
+ if (b > a) return -1
43
+ if (a > b) return 1
44
+ return 0
45
+ }
46
+ }
11
47
 
12
48
  /**
13
- * @typedef {Object} CollectionConfig
14
- * @property {String|String[]} pattern - One or more glob patterns to match files to a collection
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
49
+ * Normalize options
50
+ * @param {Object.<string,CollectionConfig>} options
19
51
  */
52
+ function normalizeOptions(options) {
53
+ options = options || {}
54
+
55
+ for (const config in options) {
56
+ let normalized = options[config]
57
+ if (typeof normalized === 'string' || Array.isArray(normalized)) {
58
+ normalized = { pattern: normalized }
59
+ }
60
+ normalized = Object.assign({}, defaultOptions, normalized)
61
+ if (typeof normalized.metadata === 'string') {
62
+ normalized.metadata = loadMetadata(normalized.metadata)
63
+ }
64
+ if (typeof normalized.sortBy === 'string') {
65
+ normalized.sortBy = sortBy(normalized.sortBy)
66
+ }
67
+ options[config] = normalized
68
+ }
69
+
70
+ return options
71
+ }
20
72
 
21
73
  /**
22
74
  * Metalsmith plugin that adds `collections` of files to the global
23
75
  * metadata as a sorted array.
24
76
  *
25
- * @param {Object.<String,CollectionConfig|String>} opts
77
+ * @param {Object.<string,CollectionConfig|string>} options
26
78
  * @return {import('metalsmith').Plugin}
27
79
  */
28
-
29
- function plugin(opts) {
30
- opts = normalize(opts)
31
- var keys = Object.keys(opts)
32
- var match = matcher(opts)
80
+ function initializeCollections(options) {
81
+ options = normalizeOptions(options)
82
+ const collectionNames = Object.keys(options)
83
+ const mapped = collectionNames.map((name) => {
84
+ return Object.assign({ name: name }, options[name])
85
+ })
33
86
 
34
87
  return function collections(files, metalsmith, done) {
35
- var metadata = metalsmith.metadata()
36
-
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
88
+ const metadata = metalsmith.metadata()
89
+ const fileNames = Object.keys(files)
61
90
 
62
- const matches = match(file, data)
63
- if (matches.length) {
64
- debug('processing file: %s', file)
91
+ metadata.collections = {}
65
92
 
66
- matches.forEach(function(key) {
67
- if (key && keys.indexOf(key) < 0) {
68
- opts[key] = {}
69
- keys.push(key)
70
- }
71
-
72
- metadata[key] = metadata[key] || []
73
- // Check if the user supplied a filter function. If so, pass the file metadata to it and
74
- // only add files that pass the filter test.
75
- if (typeof opts[key].filterBy == 'function') {
76
- var filterFunc = opts[key].filterBy
77
- if (filterFunc(data)) {
78
- metadata[key].push(data)
79
- }
80
- } else {
81
- // If no filter function is provided, add every file to the collection.
82
- metadata[key].push(data)
93
+ fileNames.forEach((filePath) => {
94
+ // add path property to file metadata for convenience
95
+ // this is for backward-compatibility only and is pretty useless
96
+ const file = files[filePath]
97
+ file.path = file.path || filePath
98
+
99
+ // dynamically add collections with default options when encountered in file metadata,
100
+ // and not explicitly defined in plugin options
101
+ if (file.collection) {
102
+ ;(Array.isArray(file.collection) ? file.collection : [file.collection]).forEach((name) => {
103
+ if (!collectionNames.includes(name)) {
104
+ collectionNames.push(name)
105
+ const normalized = Object.assign({}, defaultOptions)
106
+ mapped.push(Object.assign({ name }, normalized))
83
107
  }
84
108
  })
85
109
  }
86
110
  })
87
111
 
88
- /**
89
- * Ensure that a default empty collection exists.
90
- */
91
-
92
- keys.forEach(function(key) {
93
- metadata[key] = metadata[key] || []
94
- })
95
-
96
- /**
97
- * Sort the collections.
98
- */
99
-
100
- keys.forEach(function(key) {
101
- debug('sorting collection: %s', key)
102
- var settings = opts[key]
103
- var sort = settings.sortBy || 'date'
104
- var col = metadata[key]
105
-
106
- if ('function' == typeof sort) {
107
- col.sort(sort)
108
- } else {
109
- col.sort(function(a, b) {
110
- a = a[sort]
111
- b = b[sort]
112
- if (!a && !b) return 0
113
- if (!a) return -1
114
- if (!b) return 1
115
- if (b > a) return -1
116
- if (a > b) return 1
117
- return 0
118
- })
112
+ debug('Identified %s collections: %s', mapped.length, collectionNames.join())
113
+
114
+ mapped.forEach((collection) => {
115
+ const { pattern, filterBy, sortBy, reverse, refer, limit } = collection
116
+ const name = collection.name
117
+ const matches = []
118
+ debug('Processing collection %s with options %s:', name, collection)
119
+
120
+ // first match by pattern if provided
121
+ if (pattern) {
122
+ matches.push.apply(
123
+ matches,
124
+ multimatch(fileNames, pattern).map((filepath) => {
125
+ const data = files[filepath]
126
+ // pattern-matched files might or might not have a "collection" property defined in front-matter
127
+ // and might also be included in multiple collections
128
+ if (!data.collection) {
129
+ data.collection = collection.name
130
+ } else {
131
+ data.collection = [data.collection, collection.name]
132
+ }
133
+ return data
134
+ })
135
+ )
119
136
  }
120
137
 
121
- if (settings.reverse) col.reverse()
122
- })
138
+ // next match by "collection" key, but only push if the files haven't been added through pattern matching first
139
+ matches.push.apply(
140
+ matches,
141
+ Object.values(files).filter((file) => {
142
+ const patternMatched = matches.includes(file)
143
+ const isInCollection = Array.isArray(file.collection)
144
+ ? file.collection.includes(collection.name)
145
+ : file.collection === collection.name
146
+ return !patternMatched && isInCollection
147
+ })
148
+ )
123
149
 
124
- /**
125
- * Add `next` and `previous` references and apply the `limit` option
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
150
+ if (Object.prototype.hasOwnProperty.call(metadata, name)) {
151
+ debug('Warning: overwriting previously set metadata property %s', name)
136
152
  }
137
- if (settings.refer === false) return
138
- col.forEach(function(file, i) {
139
- if (0 != i) file.previous = col[i - 1]
140
- if (last != i) file.next = col[i + 1]
141
- })
142
- })
153
+ // apply sort, reverse, filter, limit options in this order
154
+ metadata[name] = matches.sort(sortBy)
143
155
 
144
- /**
145
- * Add collection metadata
146
- */
156
+ if (reverse) {
157
+ metadata[name].reverse()
158
+ }
147
159
 
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
- })
160
+ metadata[name] = metadata[name].filter(filterBy).slice(0, limit)
154
161
 
155
- /**
156
- * Add them grouped together to the global metadata.
157
- */
162
+ if (collection.metadata) {
163
+ metadata[name].metadata = collection.metadata
164
+ }
165
+ if (refer) {
166
+ metadata[name].forEach((file, i) => {
167
+ Object.assign(file, {
168
+ previous: i > 0 ? metadata[name][i - 1] : null,
169
+ next: i < metadata[name].length - 1 ? metadata[name][i + 1] : null
170
+ })
171
+ })
172
+ }
158
173
 
159
- metadata.collections = {}
160
- keys.forEach(function(key) {
161
- return (metadata.collections[key] = metadata[key])
174
+ metadata.collections[name] = metadata[name]
175
+ debug('Added %s files to collection %s', metadata[name].length, name)
162
176
  })
163
-
164
177
  done()
165
178
  }
166
179
  }
167
180
 
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)
205
- }
206
- }
207
- })
208
-
209
- return function(file, data) {
210
- var matches = []
211
-
212
- if (data.collection) {
213
- var collection = data.collection
214
- if (!Array.isArray(collection)) {
215
- collection = [collection]
216
- }
217
- collection.forEach(function(key) {
218
- matches.push(key)
219
-
220
- if (key && keys.indexOf(key) < 0) {
221
- debug('adding new collection through metadata: %s', key)
222
- }
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
- }
231
- }
181
+ initializeCollections.defaults = defaultOptions
232
182
 
233
- data.collection = unique(matches)
234
- return data.collection
235
- }
236
- }
183
+ module.exports = initializeCollections
package/package.json CHANGED
@@ -15,27 +15,26 @@
15
15
  "type": "git",
16
16
  "url": "https://github.com/metalsmith/collections.git"
17
17
  },
18
- "version": "1.1.0",
18
+ "version": "1.2.0",
19
19
  "license": "MIT",
20
20
  "main": "lib/index.js",
21
21
  "dependencies": {
22
22
  "debug": "^4.3.3",
23
- "extend": "^3.0.0",
23
+ "lodash.get": "^4.4.2",
24
24
  "multimatch": "^4.0.0",
25
- "read-metadata": "^1.0.0",
26
- "uniq": "^1.0.1"
25
+ "read-metadata": "^1.0.0"
27
26
  },
28
27
  "devDependencies": {
29
28
  "auto-changelog": "^2.3.0",
30
29
  "coveralls": "^3.1.1",
31
- "eslint": "^8.4.1",
30
+ "eslint": "^8.8.0",
32
31
  "eslint-config-prettier": "^8.3.0",
33
32
  "metalsmith": "^2.3.0",
34
33
  "mocha": "^7.2.0",
35
34
  "nodemon": "^1.18.4",
36
35
  "nyc": "^15.1.0",
37
- "prettier": "^1.19.1",
38
- "release-it": "^14.11.8"
36
+ "prettier": "^2.5.1",
37
+ "release-it": "^14.12.4"
39
38
  },
40
39
  "directories": {
41
40
  "lib": "lib",
@@ -50,7 +49,7 @@
50
49
  "coverage": "nyc report --reporter=text-lcov > ./coverage.info",
51
50
  "coveralls": "npm run coverage && cat ./coverage.info | coveralls",
52
51
  "format": "prettier --write \"**/*.{yml,md,js,json}\"",
53
- "lint": "eslint --cache --fix-dry-run .",
52
+ "lint": "eslint --cache --fix-dry-run \"**/*.js\"",
54
53
  "dev": "nodemon --exec 'npm test'",
55
54
  "release": "release-it ."
56
55
  },