Skip to content

Commit

Permalink
Resolves #65: adds globalRefs option
Browse files Browse the repository at this point in the history
  • Loading branch information
webketje committed Feb 2, 2023
1 parent 2ea44cd commit 77e6f20
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 5 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A Metalsmith plugin to render markdown files to HTML, using [Marked](https://git

- Compiles `.md` and `.markdown` files in `metalsmith.source()` to HTML.
- Enables rendering file metadata keys to HTML through the [keys option](#rendering-file-metadata)
- Define a dictionary of markdown globalRefs (for links, images) available to all render targets
- Supports using the markdown library of your choice through the [render option](#using-another-markdown-library)

## Installation
Expand Down Expand Up @@ -70,6 +71,7 @@ metalsmith.use(

- `keys`: Key names of file metadata to render to HTML in addition to its `contents` - can be nested key paths
- `wildcard` _(default: `false`)_ - Expand `*` wildcards in `keys` option keypaths
- `globalRefs` - An object of `{ refname: 'link' }` pairs that will be available for all markdown files and keys, or a `metalsmith.metadata()` keypath containing such object
- `render` - Specify a custom render function with the signature `(source, engineOptions, context) => string`. `context` is an object with the signature `{ path:string, key:string }` where the `path` key contains the current file path, and `key` contains the target metadata key.
- `engineOptions` Options to pass to the markdown engine (default [marked](https://github.com/markedjs/marked))

Expand Down Expand Up @@ -134,6 +136,43 @@ would be transformed into:
- If a wildcard keypath matches a key whose value is not a string, it will be ignored.
- It is set to `false` by default because it can incur some overhead if it is applied too broadly.

### Defining a dictionary of markdown globalRefs

Markdown allows users to define links in [reference style](https://www.markdownguide.org/basic-syntax/#reference-style-links) (`[]:`).
In a Metalsmith build it may be especially desirable to be able to refer to some links globally. The `globalRefs` options allows this:

```js
metalsmith.use(
markdown({
globalRefs: {
twitter_link: 'https://twitter.com/johndoe',
github_link: 'https://github.com/johndoe',
photo: '/assets/img/me.png'
}
})
)
```

Now _contents of any file or metadata key_ processed by @metalsmith/markdown will be able to refer to these links as `[My Twitter][twitter_link]` or `![Me][photo]`. You can also store the globalRefs object of the previous example in a `metalsmith.metadata()` key and pass its keypath as `globalRefs` option instead.

This enables a flow where you can load the refs into global metadata from a source file with [@metalsmith/metadata](https://github.com/metalsmith/metadata), and use them both in markdown and templating plugins like [@metalsmith/layouts](https://github.com/metalsmith/layouts):

```js
metalsith
.metadata({
global: {
links: {
twitter: 'https://twitter.com/johndoe',
github: 'https://github.com/johndoe'
}
}
})
// eg in a markdown file: [My Twitter profile][twitter]
.use(markdown({ globalRefs: 'global.links' }))
// eg in a handlebars layout: {{ global.links.twitter }}
.use(layouts())
```

### Custom markdown rendering

You can use a custom renderer by using `marked.Renderer()`
Expand Down
5 changes: 5 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export type Options<E = marked.MarkedOptions> = {
* - Expand `*` wildcards in keypaths
*/
wildcard?: boolean;
/**
* An object of `{ refname: 'link' }` pairs that will be available for all markdown files and keys,
* or a `metalsmith.metadata()` keypath containing such object
*/
globalRefs?: { [key:string]: string };
/**
* - Specify a custom render function with the signature `(source, engineOptions, context) => string`.
* `context` is an object with a `path` key containing the current file path, and `key` containing the target key.
Expand Down
3 changes: 3 additions & 0 deletions src/expand-wildcard-keypath.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import get from 'dlv'

// istanbul ignore next
function error(name, msg) {
const err = new Error(msg)
err.name = name
Expand Down Expand Up @@ -32,9 +33,11 @@ function isObject(arg) {
* @returns {Array<string|number>[]}
*/
function expandWildcardKeypath(root, keypaths, char) {
// istanbul ignore if
if (!isObject(root)) {
throw error('EINVALID_ARGUMENT', 'root must be an object or array')
}
// istanbul ignore if
if (!isArray(keypaths) || keypaths.filter((keypath) => !isString(keypath) && !isArray(keypath)).length) {
throw error('EINVALID_ARGUMENT', 'keypaths must be strings or arrays of strings')
}
Expand Down
38 changes: 33 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ function defaultRender(source, options) {
return marked(source, options)
}

function refsObjectToMarkdown(refsObject) {
return Object.entries(refsObject)
.map(([refname, value]) => `[${refname}]: ${value}`)
.join('\n')
}

/**
* @callback Render
* @param {string} source
Expand All @@ -19,6 +25,8 @@ function defaultRender(source, options) {
* @typedef Options
* @property {string[]} [keys] - Key names of file metadata to render to HTML - can be nested
* @property {boolean} [wildcard=false] - Expand `*` wildcards in keypaths
* @property {string|Object<string, string>} [globalRefs] An object of `{ refname: 'link' }` pairs that will be made available for all markdown files and keys,
* or a `metalsmith.metadata()` keypath containing such object
* @property {Render} [render] - Specify a custom render function with the signature `(source, engineOptions, context) => string`.
* `context` is an object with a `path` key containing the current file path, and `key` containing the target key.
* @property {Object} [engineOptions] Options to pass to the markdown engine (default [marked](https://github.com/markedjs/marked))
Expand All @@ -28,7 +36,8 @@ const defaultOptions = {
keys: [],
wildcard: false,
render: defaultRender,
engineOptions: {}
engineOptions: {},
globalRefs: {}
}

/**
Expand All @@ -44,8 +53,6 @@ function markdown(options = defaultOptions) {
}

return function markdown(files, metalsmith, done) {
setImmediate(done)

const debug = metalsmith.debug('@metalsmith/markdown')
const matches = metalsmith.match('**/*.{md,markdown}', Object.keys(files))

Expand All @@ -65,14 +72,33 @@ function markdown(options = defaultOptions) {
debug('Processing %s markdown file(s)', matches.length)
}

let globalRefsMarkdown = ''
if (typeof options.globalRefs === 'string') {
const found = get(metalsmith.metadata(), options.globalRefs)
if (found) {
globalRefsMarkdown = refsObjectToMarkdown(found)
} else {
const err = new Error(`globalRefs not found in metalsmith.metadata().${options.globalRefs}`)
err.name = 'Error @metalsmith/markdown'
done(err)
}
} else if (typeof options.globalRefs === 'object' && options.globalRefs !== null) {
globalRefsMarkdown = refsObjectToMarkdown(options.globalRefs)
}

if (globalRefsMarkdown.length) globalRefsMarkdown += '\n\n'

matches.forEach(function (file) {
const data = files[file]
const dir = dirname(file)
let html = basename(file, extname(file)) + '.html'
if ('.' != dir) html = join(dir, html)

debug.info('Rendering file "%s" as "%s"', file, html)
const str = options.render(data.contents.toString(), options.engineOptions, { path: file, key: 'contents' })
const str = options.render(globalRefsMarkdown + data.contents.toString(), options.engineOptions, {
path: file,
key: 'contents'
})
data.contents = Buffer.from(str)

let keys = options.keys
Expand All @@ -83,7 +109,7 @@ function markdown(options = defaultOptions) {
const value = get(data, key)
if (typeof value === 'string') {
debug.info('Rendering key "%s" of file "%s"', key.join ? key.join('.') : key, file)
set(data, key, options.render(value, options.engineOptions, { path: file, key }))
set(data, key, options.render(globalRefsMarkdown + value, options.engineOptions, { path: file, key }))
} else {
debug.warn('Couldn\'t render key %s of file "%s": not a string', key.join ? key.join('.') : key, file)
}
Expand All @@ -92,6 +118,8 @@ function markdown(options = defaultOptions) {
delete files[file]
files[html] = data
})

done()
}
}

Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/globalrefs-meta/expected/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p><a href="https://github.com/metalsmith/layouts">@metalsmith/layouts</a>
<a href="https://github.com/metalsmith/in-place">@metalsmith/in-place</a>
<a href="https://github.com/metalsmith/collections"></a>
<a href="https://github.com/metalsmith/markdown" title="with title">markdown</a></p>
4 changes: 4 additions & 0 deletions test/fixtures/globalrefs-meta/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[@metalsmith/layouts][core_plugin_layouts]
[@metalsmith/in-place][core_plugin_in-place]
[][core_plugin_collections]
[markdown][core_plugin_markdown]
4 changes: 4 additions & 0 deletions test/fixtures/globalrefs/expected/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p><a href="https://github.com/metalsmith/layouts">@metalsmith/layouts</a>
<a href="https://github.com/metalsmith/in-place">@metalsmith/in-place</a>
<a href="https://github.com/metalsmith/collections"></a>
<a href="https://github.com/metalsmith/markdown" title="with title">markdown</a></p>
8 changes: 8 additions & 0 deletions test/fixtures/globalrefs/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
frontmatter_w_markdown: |
[markdown][core_plugin_markdown]
---
[@metalsmith/layouts][core_plugin_layouts]
[@metalsmith/in-place][core_plugin_in-place]
[][core_plugin_collections]
[markdown][core_plugin_markdown]
96 changes: 96 additions & 0 deletions test/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,72 @@ describe('@metalsmith/markdown', function () {
})
})

it('should make globalRefs available to all files', function (done) {
msCommon('test/fixtures/globalrefs')
.use(markdown({
keys: ['frontmatter_w_markdown'],
globalRefs:{
'core_plugin_layouts': 'https://github.com/metalsmith/layouts',
'core_plugin_in-place': 'https://github.com/metalsmith/in-place',
'core_plugin_collections': 'https://github.com/metalsmith/collections',
'core_plugin_markdown': 'https://github.com/metalsmith/markdown "with title"'
}
}))
.build((err, files) => {
if (err) done(err)
try {
assert.strictEqual(files['index.html'].frontmatter_w_markdown, '<p><a href="https://github.com/metalsmith/markdown" title="with title">markdown</a></p>\n')
equal('test/fixtures/globalrefs/build', 'test/fixtures/globalrefs/expected')
done()
} catch(err) {
done(err)
}
})
})

it('should load globalRefs from a JSON source file', function (done) {
msCommon('test/fixtures/globalrefs-meta')
.metadata({
global: {
links: {
'core_plugin_layouts': 'https://github.com/metalsmith/layouts',
'core_plugin_in-place': 'https://github.com/metalsmith/in-place',
'core_plugin_collections': 'https://github.com/metalsmith/collections',
'core_plugin_markdown': 'https://github.com/metalsmith/markdown "with title"'
}
}
})
.use(markdown({
globalRefs: 'global.links'
}))
.build((err) => {
if (err) done(err)
try {
equal('test/fixtures/globalrefs-meta/build', 'test/fixtures/globalrefs-meta/expected')
done()
} catch(err) {
done(err)
}
})
})

it('should throw when the globalRefs metadata key is not found', function (done) {
msCommon('test/fixtures/globalrefs-meta')
.use(markdown({
globalRefs: 'not_found'
}))
.process((err) => {
try {
assert(err instanceof Error)
assert(err.name, 'Error @metalsmith/markdown')
assert(err.message, 'globalRefs not found in metalsmith.metadata().not_found')
done()
} catch (err) {
done(err)
}
})
})

it('should allow using any markdown parser through the render option', function (done) {
/** @type {import('markdown-it')} */
let mdIt
Expand Down Expand Up @@ -161,6 +227,36 @@ describe('@metalsmith/markdown', function () {
})
})

it('should log a warning when a key is not renderable (= not a string)', done => {
const ms = msCommon('test/fixtures/default')
const output = []
const Debugger = () => { }
Object.assign(Debugger, {
info: () => { },
warn: (...args) => { output.push(['warn', ...args]) },
error: () => { }
})

ms
.use(() => {
ms.debug = () => Debugger
})
.use(markdown({
keys: ['not_a_string']
}))
.process((err) => {
if (err) done(err)
try {
assert.deepStrictEqual(output.slice(0,1), [
['warn', 'Couldn\'t render key %s of file "%s": not a string', 'not_a_string', 'index.md']
])
done()
} catch (err) {
done(err)
}
})
})

it('< v2.0.0 should move legacy engine options in object root to options.engineOptions', done => {
const ms = msCommon('test/fixtures/basic')
const output = []
Expand Down

0 comments on commit 77e6f20

Please sign in to comment.