-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f68cc67
commit e6ad84a
Showing
64 changed files
with
5,703 additions
and
56 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,57 +1,305 @@ | ||
var path = require('path'); | ||
var extname = path.extname; | ||
var yaml = require('js-yaml'); | ||
'use strict'; | ||
|
||
const {promises: {readFile, readdir}} = require('fs'); | ||
const path = require('path'); | ||
const extension = path.extname; | ||
const yaml = require('js-yaml'); | ||
const toml = require('toml'); | ||
|
||
|
||
/** | ||
* @typedef Options | ||
* @property {String} key | ||
*/ | ||
|
||
/** @type {Options} */ | ||
const defaults = {} | ||
|
||
/** | ||
* Normalize plugin options | ||
* @param {Options} [options] | ||
* @returns {Object} | ||
*/ | ||
function normalizeOptions(options) { | ||
return Object.assign({}, defaults, options || {}); | ||
} | ||
|
||
/** | ||
* Expose `plugin`. | ||
* YAML to JSON | ||
* @param {*} string - YAML file | ||
* @returns .json string | ||
*/ | ||
function yamlToJSON(string) { | ||
try { | ||
return yaml.load(string); | ||
} catch (e) { | ||
console.log(e); | ||
} | ||
} | ||
|
||
module.exports = plugin; | ||
/** | ||
* TOML to JSON | ||
* @param {*} string - TOML file | ||
* @returns .json string | ||
*/ | ||
function tomlToJSON(string) { | ||
try { | ||
return JSON.parse(JSON.stringify(toml.parse(string))); | ||
} catch (e) { | ||
console.log(e); | ||
} | ||
} | ||
|
||
/** | ||
* Supported metadata parsers. | ||
* getExternalFile | ||
* Reads file content in either .json, .yaml, .yml or .toml format | ||
* @param {*} filePath | ||
* @returns Content of the file in .json | ||
*/ | ||
async function getExternalFile(filePath) { | ||
const fileExtension = extension(filePath); | ||
const fileBuffer = await readFile(filePath); | ||
let fileContent; | ||
|
||
switch (fileExtension) { | ||
case ".yaml" : | ||
case ".yml" : | ||
fileContent = yamlToJSON(fileBuffer); | ||
break; | ||
case ".toml" : | ||
fileContent = tomlToJSON(fileBuffer); | ||
break; | ||
case ".json" : | ||
fileContent = JSON.parse(fileBuffer.toString()); // remove line breaks etc from the filebuffer | ||
break; | ||
default: | ||
fileContent = JSON.parse(fileBuffer.toString()); | ||
} | ||
|
||
var parsers = { | ||
'.json': JSON.parse, | ||
'.yaml': yaml.safeLoad, | ||
'.yml': yaml.safeLoad | ||
return fileContent; | ||
}; | ||
|
||
/** | ||
* Metalsmith plugin to hide drafts from the output. | ||
* | ||
* @param {Object} opts | ||
* @return {Function} | ||
* getDirectoryFiles | ||
* @param {*} directoryPath | ||
* @returns List of all files in the directory | ||
*/ | ||
async function getDirectoryFiles(directoryPath) { | ||
const fileList = await readdir(directoryPath); | ||
return await getDirectoryFilesContent(directoryPath, fileList); | ||
|
||
function plugin(opts){ | ||
opts = opts || {}; | ||
}; | ||
|
||
return function(files, metalsmith, done){ | ||
var metadata = metalsmith.metadata(); | ||
var exts = Object.keys(parsers); | ||
for (var key in opts) { | ||
var file = opts[key].replace(/(\/|\\)/g, path.sep); | ||
var ext = extname(file); | ||
if (!~exts.indexOf(ext)) throw new Error('unsupported metadata type "' + ext + '"'); | ||
if (!metadata[key] || files[file]) { | ||
if (!files[file]) throw new Error('file "' + file + '" not found'); | ||
/** | ||
* getDirectoryFilesContent | ||
* @param {*} directoryPath | ||
* @param {*} fileList | ||
* @returns The content of all files in a directory | ||
*/ | ||
async function getDirectoryFilesContent(directoryPath, fileList) { | ||
const fileContent = await fileList.map(async file => { | ||
return await getExternalFile(path.join(path.join(directoryPath, file))); | ||
}); | ||
return await Promise.all(fileContent); | ||
}; | ||
|
||
var parse = parsers[ext]; | ||
var str = files[file].contents.toString(); | ||
delete files[file]; | ||
/** | ||
* getFileObject | ||
* @param {*} filePath | ||
* @param {*} optionKey | ||
* @param {*} allMetadata | ||
* @returns promise to push metafile object to metalsmith metadata object | ||
*/ | ||
async function getFileObject(filePath, optionKey, allMetadata) { | ||
return getExternalFile(filePath) | ||
.then(fileBuffer => { | ||
allMetadata[optionKey] = fileBuffer; | ||
}); | ||
} | ||
|
||
try { | ||
var data = parse(str); | ||
} catch (e) { | ||
return done(new Error('malformed data in "' + file + '"')); | ||
/** | ||
* getDirectoryObject | ||
* @param {*} directoryPath | ||
* @param {*} optionKey | ||
* @param {*} allMetadata | ||
* @returns promise to push concatenated metafile object of all directory files to metalsmith metadata object | ||
*/ | ||
async function getDirectoryObject(directoryPath, optionKey, allMetadata) { | ||
return getDirectoryFiles(directoryPath) | ||
.then(fileBuffers => { | ||
const groupMetadata = []; | ||
fileBuffers.forEach(fileBuffer => { | ||
groupMetadata.push(JSON.parse(JSON.stringify(fileBuffer))); | ||
}) | ||
|
||
if (groupMetadata.length) { | ||
allMetadata[optionKey] = groupMetadata; | ||
} | ||
else { | ||
console.log(`No files found in this directory "${key}"`); | ||
} | ||
|
||
}) | ||
.catch(error => { | ||
console.error(error.message); | ||
process.exit(1); | ||
}); | ||
}; | ||
|
||
|
||
/** | ||
* A Metalsmith plugin to read files with metadata | ||
* | ||
* Files containing metadata must be located in the Metalsmith root directory. | ||
* Content of files located in the Metalsmith source directory (local files) is readily available | ||
* in the files object while files outside the source directory (external files) are read fropm disk. | ||
* | ||
* Files are specified via option entries like: site: "./data/siteMetadata.json" | ||
* The resulting meta object will then be something like this: | ||
* { | ||
* site: { | ||
* "title":"New MetalsmithStarter", | ||
* "description":"Metalsmith Starter Website", | ||
* "author":"werner@glinka.co", | ||
* "siteURL":"https://newmsnunjucks.netlify.app/", | ||
* ... | ||
* } | ||
* | ||
* Directories may also be specified like this: example: "./data/example". In this case | ||
* the plugin will read all files in the directory and concatenate them into a single file object. | ||
* | ||
* | ||
* @param {Options} options | ||
* @returns {import('metalsmith').Plugin} | ||
*/ | ||
|
||
function initMetameta(options){ | ||
options = normalizeOptions(options); | ||
|
||
return function metameta(files, metalsmith, done){ | ||
const allMetadata = metalsmith.metadata(); | ||
|
||
// array to hold all active promises during external file reads. Will be | ||
// used with Promise.allSettled to invoke done() | ||
const allPromises = []; | ||
|
||
// loop over all options/metadata files/directories | ||
Object.keys(options).forEach(function(optionKey) { | ||
|
||
// check if file is located inside the metalsmith source directory | ||
const metaFilePath = options[optionKey]; | ||
|
||
// convention: "./" inside, "../" outside of metasmith source | ||
const isLocal = metaFilePath.startsWith("./"); | ||
const isExternal = metaFilePath.startsWith("../"); | ||
|
||
// flag to be reset when valid filepath is detected | ||
let validFilepath = false; | ||
|
||
/* | ||
* if file or directory is local we can get the metadata from the metalsmith file object | ||
*/ | ||
if (isLocal) { | ||
// get object key from the options | ||
const key = metaFilePath.slice(2); | ||
let metadata; | ||
|
||
// check if the optionKey element has a file exension | ||
const fileExtension = extension(metaFilePath); | ||
if ( fileExtension ) { | ||
if ( fileExtension === ".json" || fileExtension === ".yaml" || fileExtension === ".yml" || fileExtension === ".toml") { | ||
// get the data from file object | ||
try { | ||
metadata = files[key].contents.toString(); | ||
} catch (error) { | ||
console.log("Could not find file in files object"); | ||
return done(error); | ||
} | ||
|
||
if ( fileExtension === ".yaml" || fileExtension === ".yml" ) { | ||
metadata = JSON.stringify(yamlToJSON(metadata)); | ||
} | ||
|
||
if ( fileExtension === ".toml" ) { | ||
metadata = JSON.stringify(toml.parse(metadata)); | ||
} | ||
|
||
|
||
|
||
// to temp meta object | ||
allMetadata[optionKey] = JSON.parse(metadata); | ||
// ... and remove this file from the metalsmith build process | ||
delete files[key]; | ||
|
||
// indicate filepath is valid | ||
validFilepath = true; | ||
} | ||
} else { | ||
// assume this is a directory, all files in this directory will be concatenated into one | ||
// metadata object | ||
const groupMetadata = []; | ||
Object.keys(files).forEach(function(file) { | ||
if (file.includes(key)) { | ||
groupMetadata.push(JSON.parse(files[file].contents.toString())); | ||
} | ||
}); | ||
|
||
if (groupMetadata.length) { | ||
allMetadata[optionKey] = groupMetadata; | ||
} | ||
else { | ||
console.log(`No files found in this directory "${key}"`); | ||
} | ||
|
||
// indicate filepath is valid | ||
validFilepath = true; | ||
} | ||
} | ||
|
||
/* | ||
* if file or directory is external we get the metadata from respective files | ||
*/ | ||
if (isExternal) { | ||
// get object key | ||
const key = metaFilePath.slice(3); | ||
|
||
metadata[key] = data; | ||
// check if the optionKey has a file exension | ||
const fileExtension = extension(metaFilePath); | ||
if ( fileExtension ) { | ||
if ( fileExtension === ".json" || fileExtension === ".yaml" || fileExtension === ".yml" || fileExtension === ".toml") { | ||
// read external file content and store in metadata object | ||
const filePath = path.join(metalsmith._directory, key); | ||
const extFilePromise = getFileObject(filePath, optionKey, allMetadata) | ||
|
||
// add this promise to allPromises array. Will be later used with Promise.allSettled to invoke done() | ||
allPromises.push(extFilePromise); | ||
|
||
// indicate filepath is valid | ||
validFilepath = true; | ||
} | ||
} else { | ||
// assume this is a directory | ||
// get content of all files in this directory, concatenated into one metadata object | ||
const directoryPath = path.join(metalsmith._directory, key); | ||
const extDirectoryPromise = getDirectoryObject(directoryPath, optionKey, allMetadata); | ||
|
||
// add this promise to allPromises array. Will be later used with Promise.allSettled to invoke done() | ||
allPromises.push(extDirectoryPromise); | ||
|
||
// indicate filepath is valid | ||
validFilepath = true; | ||
} | ||
} | ||
} | ||
|
||
done(); | ||
if (!validFilepath) { | ||
const error = `${metaFilePath} is not a valid metafile path. Path must be relative to Metalsmith root`; | ||
done(error); | ||
} | ||
}); | ||
|
||
// Promise.allSettled is used to invoke done() | ||
Promise.allSettled(allPromises).then(() => done()); | ||
}; | ||
} | ||
|
||
module.exports = initMetameta; |
Oops, something went wrong.