Proposal for metalsmith.get/set() helpers
Context
It is a frequent requirement for plugins to access and manipulate properties on metalsmith metadata or files. Retrieving and setting these properties is not always worry-free. For example, if a plugin wishes to allow users to specify plugin options to be read from a YAML file inside metalsmith.source(), let's say @metalsmith/layouts:
# src/layouts.config.yaml
---
directory: 'src/layouts'
pattern: '**/*.html'
engineOptions:
gfm: true
---it needs to check if the file is found, and do null-safe gets to nested properties
const confFile = files['layouts.config.yaml']
if (confFile) {
if (confFile.engineOptions && confFile.engineOptions.gfm) { /* code */ }
}This could be shortened to:
if (metalsmith.get('layouts.config.yaml', 'engineOptions.gfm')) { /* code */ }Another need (or inflexibility) is that a lot of plugins "dictate" the key path in the file metadata they read config options from.
For example using @metalsmith/layouts you must specify the key layout: in metadata. This will be ok for some (if not most) users, but other users may prefer to introduce different namespaces in front-matter (eg. translations, config, seo, content). So if the user would prefer reading the layout value from config.layout they should have a way to do that, without putting the onus of implementing the mechanism on the plugin developer. Building on the @metalsmith/layouts example, this would allow
Object.keys(files).forEach(path) => {
metalsmith.set(metalsmith.get(path), 'contents', compiled)
})
// or better
const { get, set } = metalsmith
Object.keys(files).forEach(path => {
set(get(path), 'contents', compiled)
})const { get, set, metadata } = metalsmith
const blog = get('blog/index.md')
const blogTitle = get(blog, 'title')
set(metadata(), 'blog.title', blogTitle)
// copy a global metadata property to a file
const blog = get('blog/index.md')
set(blog, 'siteurl', metadata().siteurl)
// copy a file property to global metadata
const blogTitle = get(blog, 'title')
set(metadata(), 'title', blogTitle)
// add a new or overwrite an existing file
set(files, 'blog/new.md', { contents: Buffer.from('new or overwritten') })
set(blog, 'contents', Buffer.from('overwritten'))Spec
- The get/set methods have a base signature of
get(context, keypath)andset(context, keypath, value) - The get/set methods should be instance-bound so they can be destructured in plugin bodies:
const { get, set } = metalsmith. - The methods would default the context to
files, so that it can be omitted andget('blog/index.md')andset('blog/index.md', { contents: Buffer.from('overwritten') })operate onfiles. - When the context is equal to
files, the methods could accept globs and work as shortcuts for resp. (get)metalsmith.match(glob).map(path => files[path]), always returning an array, and (set)metalsmith.match(glob).forEach(path => { files[path][keypath] = value })
Pros
- allows getting a file matching a path with forward slash notation cross-platform
metalsmith.get(files, 'nested/index.md'). - allows plugin devs to provide more flexibility in specifying options `getOptions: { path: 'config/options.}
Signature
/**
* Get a file
* @param {string} path
* @returns {Metalsmith.File} file at path or undefined
**/
metalsmith.get(path)
/**
* Get a property value from context (file metadata, global metadata, or any object)
* @param {string} context
* @param {string} keypath
* @returns {*} Value of the property at context[keypath] or undefined
* @example
* metalsmith.get(metalsmith.metadata(), 'seo.title')
* metalsmith.get(files, 'index.md')
* metalsmith.get(files['index.md'], 'title')
* metalsmith.get(files['index.md'].collections, 0)
**/
metalsmith.get(context, keypath)
/**
* Set a file
* @param {string} filepath
* @param {string} file
* @param {*} value
* @returns {Metalsmith}
* @throws EPERM when keypath is non-configurable
**/
/**
* Set a value to files[filepath][keypath]
* @param {string} filepath
* @param {string} keypath
* @param {*} value
* @returns {Metalsmith}
* @throws ENOENT when filepath does not exist
* @throws EPERM when keypath or filepath is non-configurable
**/
metalsmith.set(filepath, keypath, value)
/**
* Set a property value to global metadata
* @param {string} keypath
* @param {*} value
* @returns {Metalsmith}
* @throws EPERM when keypath or filepath is non-configurable
**/
metalsmith.set(metadata, keypath, value) Source: metalsmith/metalsmith