Skip to content

Commit

Permalink
BREAKING: Closes #101: condenses sortBy, reverse into sort option. Re…
Browse files Browse the repository at this point in the history
…names filterBy option to filter.
  • Loading branch information
webketje committed Aug 2, 2023
1 parent d162752 commit 60ae7f5
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 145 deletions.
68 changes: 34 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,29 +44,30 @@ import { dirname } from 'path'
const __dirname = dirname(new URL(import.meta.url).pathname)

// defaults, only create collections based on file metadata
Metalsmith(__dirname)
.use(markdown())
.use(collections())
Metalsmith(__dirname).use(markdown()).use(collections())

// defaults for a "news" collection, except pattern option
Metalsmith(__dirname)
.use(markdown())
.use(collections({
news: { pattern: 'news/**/*.html' }
}))
.use(
collections({
news: { pattern: 'news/**/*.html' }
})
)

// explicit defaults for a "news" collection, except pattern option
Metalsmith(__dirname)
.use(markdown())
.use(collections({
pattern: { pattern: 'news/**/*.html' },
metadata: null,
filterBy: () => true,
sortBy: defaultSort,
reverse: false,
limit: Infinity,
refer: true
})
.use(
collections({
pattern: { pattern: 'news/**/*.html' },
metadata: null,
filter: () => true,
sort: 'date:desc',
limit: Infinity,
refer: true
})
)
```
_Note: all examples in the readme use the same collections definitions under [Defining collections](#defining-collections)_
Expand All @@ -76,10 +77,9 @@ _Note: all examples in the readme use the same collections definitions under [De
All options are _optional_
- **pattern** `string|string[]` - one or more glob patterns to group files into a collection
- **filterBy** `Function` - a function that returns `false` for files that should be filtered _out_ of the collection
- **filter** `Function` - a function that returns `false` for files that should be filtered _out_ of the collection
- **limit** `number` - restrict the number of files in a collection to at most `limit`
- **sortBy** `string|Function` - a file metadata key to sort by (for example `date` or `pubdate` or `title`), or a custom sort function
- **reverse** `boolean` - whether the sort should be reversed (e.g., for a news/blog collection, you typically want `reverse: true`)
- **sort** `string|Function` - a sort string of the format `'<key_or_keypath>:<asc|desc>'`, followed by the sort order, for example: `date` or `pubdate:desc` or `order:asc`, or a custom sort function
- **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_
- **refer** `boolean` - will add `previous` and `next` keys to each file in a collection. `true` by default
Expand All @@ -100,8 +100,7 @@ There are 2 ways to create collections & they can be used together:
slug: 'news'
},
pattern: 'news/**/*.html',
sortBy: 'pubdate',
reverse: true
sort: 'pubdate'
},
services: 'services/**/*.html'
})
Expand Down Expand Up @@ -144,7 +143,7 @@ There are 2 ways to create collections & they can be used together:
### Rendering collection items
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):
Here is an example of using [@metalsmith/permalinks](https://github.com/metalsmith/permalinks), followed by [@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):
`layouts/news.njk`
Expand All @@ -156,14 +155,17 @@ Here is an example of using [@metalsmith/layouts](https://github.com/metalsmith/
{{!-- previous & next are added by @metalsmith/collections --}}
{{#if previous}}
Read the previous news:
<a href="/{{ previous.path }}">{{ previous.title }}</a>
<a href="/{{ next.permalink }}">{{ next.title }}</a>
{{/if}}
{{#if next}}
Read the next news:
<a href="/{{ next.path }}">{{ next.title }}</a>
<a href="/{{ previous.permalink }}">{{ previous.title }}</a>
{{/if}}
```
The example above supposes an ordering of `pubdate:desc` (newest to oldest) or similar, which is why the "next news" corresponds to the `previous` property.
If one would map the news dates they would be like: `['2023-01-20', 2023-01-10', '2023-01-01']`.
_Note: If you don't need the `next` and `previous` references, you can pass the option `refer: false`_

### Rendering collection index
Expand Down Expand Up @@ -194,13 +196,13 @@ No news at the moment...

### Custom sorting, filtering and limiting

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.
You could define an `order` property on a set of files and pass `sort: 'order:asc'` 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.

```js
metalsmith.use(
collections({
subpages: {
sortBy: function (a, b) {
sort(a, b) {
let aNum, bNum

aNum = +a.index
Expand All @@ -221,9 +223,9 @@ metalsmith.use(
)
```

_Note: the `sortBy` option also understands nested keypaths, e.g. `display.order`_
_Note: the `sort` option also understands nested keypaths, e.g. `display.order:asc`_

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:
The `filter` 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 filter function below could work for a collection named `thisYearsNews` as it would filter out all the items that are older than this year:

```js
function filterBy(file) {
Expand All @@ -240,18 +242,18 @@ metalsmith.use(
collections({
recentArticles: {
pattern: 'articles/**/*.html',
sortBy: 'date',
sort: 'date',
limit: 10
},
archives: {
pattern: 'archives/**/*.html',
sortBy: 'date'
sort: 'date'
}
})
)
```

_Note: the collection is first sorted, reversed, filtered, and then limited, if applicable._
_Note: the collection is first sorted, filtered, and then limited, if applicable._

### Collection Metadata

Expand All @@ -277,8 +279,7 @@ Collection metadata can be loaded from a `json` or `yaml` file (path relative to
metalsmith.use(
collections({
articles: {
sortBy: 'date',
reverse: true,
sort: 'date:asc',
metadata: 'path/to/file.json'
}
})
Expand Down Expand Up @@ -311,8 +312,7 @@ Add the `@metalsmith/collections` key to your `metalsmith.json` `plugins` key:
{
"@metalsmith/collections": {
"articles": {
"sortBy": "date",
"reverse": true
"sort": "date:asc"
}
}
}
Expand Down
16 changes: 8 additions & 8 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ export type CollectionConfig = {
*/
pattern: string | string[];
/**
* - A key to sort by (e.g. `date`,`title`, ..) or a custom sort function
* - a sort string of the format `'<key_or_keypath>:<asc|desc>'`, followed by the sort order, or a custom sort function
* @example
* 'date'
* 'pubdate:desc'
* 'order:asc'
*/
sortBy: string | ((a: any, b: any) => 0 | 1 | -1);
sort: string | ((a: any, b: any) => 0 | 1 | -1);
/**
* - Limit the amount of items in a collection to `limit`
*/
Expand All @@ -18,14 +22,10 @@ export type CollectionConfig = {
* - Adds `next` and `previous` keys to file metadata of matched files
*/
refer: boolean;
/**
* - Whether to invert the sorting function results (asc/descending)
*/
reverse: boolean;
/**
* - A function that gets a `Metalsmith.File` as first argument and returns `true` for every file to include in the collection
*/
filterBy: Function;
filter: Function;
/**
* - An object with metadata to attach to the collection, or a `json`/`yaml`filepath string to load data from (relative to `Metalsmith.directory`)
*/
Expand All @@ -39,7 +39,7 @@ export type CollectionConfig = {
* portfolio: {
* pattern: 'portfolio/*.md',
* metadata: { title: 'My portfolio' },
* sortBy: 'order'
* sort: 'order:asc'
* }
* }))
*
Expand Down
74 changes: 35 additions & 39 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
import get from 'lodash.get'
import { relative, resolve } from 'path'

function sortBy(key) {
function sortBy(key, order) {
order = order === 'asc' ? -1 : 1
let getKey = (x) => x[key]
if (key.includes('.')) {
getKey = (x) => get(x, key)
}
return function defaultSort(a, b) {
a = getKey(a)
b = getKey(b)
if (!a && !b) return 0
if (!a) return -1
if (!b) return 1
if (b > a) return -1
if (a > b) return 1
return 0
let ret
if (!a && !b) ret = 0
else if (!a) ret = 1 * order
else if (!b) ret = -1 * order
else if (b > a) ret = 1 * order
else if (a > b) ret = -1 * order
else ret = 0

return ret
}
}

// for backwards-compatibility only, date makes as little sense as "pubdate" or any custom key
const defaultSort = sortBy('date')
const defaultSort = sortBy('date', 'desc')
const defaultFilter = () => true

/**
* @typedef {Object} CollectionConfig
* @property {string|string[]} pattern - One or more glob patterns to match files to a collection
* @property {string|(a,b) => 0|1|-1} sortBy - A key to sort by (e.g. `date`,`title`, ..) or a custom sort function
* @property {string|(a,b) => 0|1|-1} sort - a sort string of the format `'<key_or_keypath>:<asc|desc>'`, followed by the sort order, or a custom sort function
* @property {number} limit - Limit the amount of items in a collection to `limit`
* @property {boolean} refer - Adds `next` and `previous` keys to file metadata of matched files
* @property {boolean} reverse - Whether to invert the sorting function results (asc/descending)
* @property {Function} filterBy - A function that gets a `Metalsmith.File` as first argument and returns `true` for every file to include in the collection
* @property {Function} filter - A function that gets a `Metalsmith.File` as first argument and returns `true` for every file to include in the collection
* @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`)
*/

/** @type {CollectionConfig} */
const defaultOptions = {
pattern: null,
reverse: false,
metadata: null,
limit: Infinity,
refer: true,
sortBy: defaultSort,
filterBy: defaultFilter
sort: defaultSort,
filter: defaultFilter
}

/**
Expand Down Expand Up @@ -72,9 +74,16 @@ function normalizeOptions(options, files, metalsmith) {
}
normalized.metadata = matter.parse(matter.wrap(metadataFile.contents.toString()))
}
if (typeof normalized.sortBy === 'string') {
normalized.sortBy = sortBy(normalized.sortBy)

// remap sort option
let sort = normalized.sort
if (typeof sort === 'string') {
if (!sort.includes(':')) sort += ':desc'
const [key, order] = sort.split(':')
sort = sortBy(key, order)
}
normalized.sort = sort

options[config] = normalized
}

Expand All @@ -89,7 +98,7 @@ function normalizeOptions(options, files, metalsmith) {
* portfolio: {
* pattern: 'portfolio/*.md',
* metadata: { title: 'My portfolio' },
* sortBy: 'order'
* sort: 'date:desc'
* }
* }))
*
Expand Down Expand Up @@ -133,7 +142,7 @@ function initializeCollections(options) {
debug('Identified %s collections: %s', mappedCollections.length, collectionNames.join())

mappedCollections.forEach((collection) => {
const { pattern, filterBy, sortBy, reverse, refer, limit } = collection
const { pattern, filter, sort, refer, limit } = collection
const name = collection.name
const matches = []
debug('Processing collection %s with options %s:', name, collection)
Expand Down Expand Up @@ -174,34 +183,21 @@ function initializeCollections(options) {
if (Object.prototype.hasOwnProperty.call(metadata, name)) {
debug('Warning: overwriting previously set metadata property %s', name)
}
// apply sort, reverse, filter, limit options in this order
let currentCollection = (metadata.collections[name] = matches.sort(sortBy))

if (reverse) {
currentCollection.reverse()
}
// apply sort, filter, limit options in this order
let currentCollection = (metadata.collections[name] = matches.sort(sort))

currentCollection = metadata.collections[name] = currentCollection.filter(filterBy).slice(0, limit)
currentCollection = metadata.collections[name] = currentCollection.filter(filter).slice(0, limit)

if (collection.metadata) {
currentCollection.metadata = collection.metadata
}
if (refer) {
if (reverse) {
currentCollection.forEach((file, i) => {
Object.assign(file, {
next: i > 0 ? currentCollection[i - 1] : null,
previous: i < currentCollection.length - 1 ? currentCollection[i + 1] : null
})
})
} else {
currentCollection.forEach((file, i) => {
Object.assign(file, {
previous: i > 0 ? currentCollection[i - 1] : null,
next: i < currentCollection.length - 1 ? currentCollection[i + 1] : null
})
currentCollection.forEach((file, i) => {
Object.assign(file, {
previous: i > 0 ? currentCollection[i - 1] : null,
next: i < currentCollection.length - 1 ? currentCollection[i + 1] : null
})
}
})
}

debug('Added %s files to collection %s', currentCollection.length, name)
Expand Down
Loading

0 comments on commit 60ae7f5

Please sign in to comment.