@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.
Files changed (4) hide show
  1. package/CHANGELOG.md +23 -5
  2. package/README.md +177 -94
  3. package/lib/index.js +157 -199
  4. package/package.json +17 -17
package/CHANGELOG.md CHANGED
@@ -1,18 +1,36 @@
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.2](https://github.com/metalsmith/collections/compare/v1.2.2...v1.2.1) / 2022-07-28
4
4
 
5
- Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
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.0](https://github.com/metalsmith/collections/compare/v1.0.0...v1.1.0)
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 [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.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
- 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://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
- 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 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
- * Expose `plugin`.
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
- module.exports = plugin
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
- * @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
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.<String,CollectionConfig|String>} opts
76
+ * @param {Object.<string,CollectionConfig|string>} options
26
77
  * @return {import('metalsmith').Plugin}
27
78
  */
28
-
29
- function plugin(opts) {
30
- opts = normalize(opts)
31
- var keys = Object.keys(opts)
32
- var match = matcher(opts)
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
- var metadata = metalsmith.metadata()
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
- 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)
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
- * 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
- })
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 (settings.reverse) col.reverse()
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
- * 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
152
+ if (Object.prototype.hasOwnProperty.call(metadata, name)) {
153
+ debug('Warning: overwriting previously set metadata property %s', name)
136
154
  }
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
- })
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
- metadata.collections = {}
160
- keys.forEach(function(key) {
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
- return function(file, data) {
210
- var matches = []
162
+ metadata[name] = metadata[name].filter(filterBy).slice(0, limit)
211
163
 
212
- if (data.collection) {
213
- var collection = data.collection
214
- if (!Array.isArray(collection)) {
215
- collection = [collection]
164
+ if (collection.metadata) {
165
+ metadata[name].metadata = collection.metadata
216
166
  }
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)
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
- data.collection = unique(matches)
234
- return data.collection
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.1.0",
18
+ "version": "1.2.2",
19
19
  "license": "MIT",
20
20
  "main": "lib/index.js",
21
21
  "dependencies": {
22
- "debug": "^4.3.3",
23
- "extend": "^3.0.0",
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.3.0",
26
+ "auto-changelog": "^2.4.0",
30
27
  "coveralls": "^3.1.1",
31
- "eslint": "^8.4.1",
32
- "eslint-config-prettier": "^8.3.0",
33
- "metalsmith": "^2.3.0",
34
- "mocha": "^7.2.0",
35
- "nodemon": "^1.18.4",
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": "^1.19.1",
38
- "release-it": "^14.11.8"
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
- "lint": "eslint --cache --fix-dry-run .",
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.3.0"
61
+ "metalsmith": "^2.4.1"
62
62
  },
63
63
  "engines": {
64
- "node": ">=8"
64
+ "node": ">=12"
65
65
  }
66
66
  }