Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for metalsmith.get/set() helpers #385

Open
webketje opened this issue Dec 21, 2022 · 1 comment
Open

Proposal for metalsmith.get/set() helpers #385

webketje opened this issue Dec 21, 2022 · 1 comment

Comments

@webketje
Copy link
Member

webketje commented Dec 21, 2022

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) and set(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 and get('blog/index.md') and set('blog/index.md', { contents: Buffer.from('overwritten') }) operate on files.
  • 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) 
@webketje
Copy link
Member Author

webketje commented Oct 1, 2023

Upon rethinking this, the better signature for get/set is an extension of a combo of metalsmith.match() + a keypath getter/setter. For all matched files, it would get/set the properties.

Example:

/** @returns {void} */
// for all files matched set type 'post' to the metadata
metalsmith.set('posts/*.html', 'type', 'post')

/** @returns {Array<*>} */
// for all files matched get the type
metalsmith.get('{posts,services,products}/*.html', 'type')

Signatures are:

get(pattern, property) => Object<{ [matchedFilePath]: [matchedProperty] }>
set(pattern, property, value) => void

The result returned is always similar to the Metalsmith.Files object.
This could be translated to a CLI command get/set to easily update front-matter with a static website:

metalsmith set  "posts/*.html" type post

metalsmith get "{posts,services,products}/*.html" type

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant