Skip to content

Commit

Permalink
Closes #179: adds extname option & reworks multiple transform-per-fil…
Browse files Browse the repository at this point in the history
…e feature
  • Loading branch information
webketje committed Jul 11, 2023
1 parent 7400b27 commit 8788f47
Show file tree
Hide file tree
Showing 16 changed files with 236 additions and 80 deletions.
5 changes: 5 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export type Options = {
* @default {}
*/
engineOptions?: any;
/**
* Pass `''` to remove the extension or `'.<extname>'` to keep or rename it.
* @default transform.outputFormat
*/
extname?: string;
};
/**
* A metalsmith plugin for in-place templating
Expand Down
92 changes: 50 additions & 42 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,50 +93,52 @@ function parseFilepath(filename) {
}

/**
* Engine, renders file contents with all available transformers
* @param {string} filename
* @param {Options} opts
* @returns
*/

async function render({ filename, files, metalsmith, settings, transform }) {
export function handleExtname(filename, opts) {
const { dirname, base, extensions } = parseFilepath(filename)
const extname = opts.extname && opts.extname.slice(1)
// decouples file extension chaining order from transformer usage order
for (let i = extensions.length; i--; ) {
if (opts.transform.inputFormats.includes(extensions[i])) {
extensions.splice(i, 1)
break
}
}
const isLast = !extensions.length
if (isLast && extname) extensions.push(extname)
return [path.join(dirname, base), ...extensions].join('.')
}

async function render({ filename, files, metalsmith, options, transform }) {
const file = files[filename]
const engineOptions = Object.assign({}, settings.engineOptions)
if (settings.engineOptions.filename) {
const engineOptions = Object.assign({}, options.engineOptions)
if (options.engineOptions.filename) {
Object.assign(engineOptions, {
// set the filename in options for jstransformers requiring it (like Pug)
filename: metalsmith.path(metalsmith.source(), filename)
})
}
const metadata = metalsmith.metadata()
const isLastExtension = extensions.length === 1
debug(`rendering ${filename}`)

const ext = extensions.pop()
const locals = Object.assign({}, metadata, file)

// Stringify file contents
const contents = file.contents.toString()

// If this is the last extension, replace it with a new one
if (isLastExtension) {
debug(`last extension reached, replacing extension with ${transform.outputFormat}`)
extensions.push(transform.outputFormat)
}

// Transform the contents
debug(`rendering ${ext} extension for ${filename}`)

return transform
.renderAsync(contents, engineOptions, locals)
.then((rendered) => {
// Delete old file
delete files[filename]
const newName = handleExtname(filename, { ...options, transform })
debug('Done rendering %s', filename)
debug('Renaming "%s" to "%s"', filename, newName)

// Update files with the newly rendered file
const newName = [path.join(dirname, base), ...extensions].join('.')
files[newName] = file
if (newName !== filename) {
delete files[filename]
files[newName] = file
}
files[newName].contents = Buffer.from(rendered.body)

debug(`done rendering ${filename}, renamed to ${newName}`)
})
.catch((err) => {
err.message = `${filename}: ${err.message}`
Expand All @@ -149,18 +151,17 @@ async function render({ filename, files, metalsmith, settings, transform }) {
*/

function validate({ filename, files, transform }) {
debug(`validating ${filename}`)
const { extensions } = parseFilepath(filename)
debug(`validating ${filename} %O %O`, extensions, transform.inputFormats)

// IF the transform has inputFormats defined, invalidate the file if it has no matching extname
if (transform.inputFormats && !transform.inputFormats.includes(extensions.slice(-1)[0])) {
if (transform.inputFormats && !extensions.some((fmt) => transform.inputFormats.includes(fmt))) {
debug.warn(
'Validation failed for file "%s", transformer %s supports extensions %s.',
filename,
transform.name,
transform.inputFormats.map((i) => `.${i}`).join(', ')
)
return false
}

// Files that are not utf8 are ignored
Expand All @@ -174,30 +175,39 @@ function validate({ filename, files, transform }) {
/**
* @typedef {Object} Options
* @property {string|JsTransformer} transform Jstransformer to run: name of a node module or local JS module path (starting with `.`) whose default export is a jstransformer. As a shorthand for existing transformers you can remove the `jstransformer-` prefix: `marked` will be understood as `jstransformer-marked`. Or an actual jstransformer; an object with `name`, `inputFormats`,`outputFormat`, and at least one of the render methods `render`, `renderAsync`, `compile` or `compileAsync` described in the [jstransformer API docs](https://github.com/jstransformers/jstransformer#api)
* @property {string} [pattern='**\/*.<transform.inputFormats>'] (*optional*) One or more paths or glob patterns to limit the scope of the transform. Defaults to `'**\/*.<transform.inputFormats>'`
* @property {string} [pattern='**\/*.<transform.inputFormats>'] (*optional*) One or more paths or glob patterns to limit the scope of the transform. Defaults to `'**\/*.<transform.inputFormats>*'`
* @property {Object} [engineOptions={}] (*optional*) Pass options to the jstransformer templating engine that's rendering your files. The default is `{}`
* @property {string} [extname] (*optional*) Pass `''` to remove the extension or `'.<extname>'` to keep or rename it. Defaults to `transform.outputFormat`
**/

/** @type {Options} */
const defaultOptions = {
pattern: '**',
engineOptions: {}
/**
* Set default options based on jstransformer `transform`
* @param {JsTransformer} transform
* @returns
*/
function normalizeOptions(transform) {
const extMatch =
transform.inputFormats.length === 1 ? transform.inputFormats[0] : `{${transform.inputFormats.join(',')}}`
return {
pattern: `**/*.${extMatch}*`,
extname: `.${transform.outputFormat}`,
engineOptions: {}
}
}

/**
* A metalsmith plugin for in-place templating
* @param {Options} options
* @returns {import('metalsmith').Plugin}
*/
function initializeInPlace(options = defaultOptions) {
const settings = Object.assign({}, defaultOptions, options)
function initializeInPlace(options = {}) {
let transform

return async function inPlace(files, metalsmith, done) {
debug = metalsmith.debug('@metalsmith/in-place')

// Check whether the pattern option is valid
if (!(typeof settings.pattern === 'string' || Array.isArray(settings.pattern))) {
if (options.pattern && !(typeof options.pattern === 'string' || Array.isArray(options.pattern))) {
return done(
new Error(
'invalid pattern, the pattern option should be a string or array of strings. See https://www.npmjs.com/package/@metalsmith/in-place#pattern'
Expand All @@ -215,13 +225,11 @@ function initializeInPlace(options = defaultOptions) {
}
}

if (settings.pattern === defaultOptions.pattern) {
settings.pattern = `${settings.pattern}/*.{${transform.inputFormats.join(',')}}`
}
options = Object.assign(normalizeOptions(transform), options)

debug('Running with options %O', settings)
debug('Running with options %O', options)

const matchedFiles = metalsmith.match(settings.pattern)
const matchedFiles = metalsmith.match(options.pattern)

// Filter files by validity, pass basename to avoid dots in folder path
const validFiles = matchedFiles.filter((filename) => validate({ filename, files, transform }))
Expand All @@ -235,7 +243,7 @@ function initializeInPlace(options = defaultOptions) {
}

// Map all files that should be processed to an array of promises and call done when finished
return Promise.all(validFiles.map((filename) => render({ filename, files, metalsmith, settings, transform })))
return Promise.all(validFiles.map((filename) => render({ filename, files, metalsmith, options, transform })))
.then(() => done())
.catch((error) => done(error))
}
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/custom-ext-order/expected/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<h2 id="title">Title</h2>
<ul>
<li>one</li><li>two</li><li>three</li>
</ul>
5 changes: 5 additions & 0 deletions test/fixtures/custom-ext-order/src/index.hbs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Title

<ul>
{{#each nums }}<li>{{ . }}</li>{{/each}}
</ul>
6 changes: 6 additions & 0 deletions test/fixtures/ext-chaining/expected/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<h2 id="title">Title</h2>
<ul>
<li>one</li>
<li>two</li>
<li>three</li>
</ul>
5 changes: 5 additions & 0 deletions test/fixtures/ext-chaining/src/index.md.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Title

{{#each nums }}
- {{ . }}
{{/each}}

This file was deleted.

2 changes: 0 additions & 2 deletions test/fixtures/ignore-extensionless/expected/index

This file was deleted.

2 changes: 0 additions & 2 deletions test/fixtures/ignore-extensionless/expected/index.html

This file was deleted.

5 changes: 0 additions & 5 deletions test/fixtures/ignore-extensionless/src/index

This file was deleted.

5 changes: 0 additions & 5 deletions test/fixtures/ignore-extensionless/src/index.hbs

This file was deleted.

2 changes: 2 additions & 0 deletions test/fixtures/warn-no-matching-extension/expected/index.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<h2 id="title">Title</h2>
<h1>{{ title }}</h1>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
title: A title
---
## Title
<h1>{{ title }}</h1>
Loading

0 comments on commit 8788f47

Please sign in to comment.