src/index.ts
import createDebug from 'debug';
import Metalsmith from 'metalsmith';
import multimatch from 'multimatch';
import path from 'path';
import util from 'util';
import {
normalizeOptions,
OptionsGenerator,
OptionsInterface,
} from './options';
import { parser } from './parse';
import {
createPlugin,
firstItem,
hasProp,
isFile,
isObject,
isReadonlyOrWritableArray,
} from './utils';
const debug = createDebug(require('../package.json').name);
const defineDebug = debug.extend('define');
export = (
opts: Partial<OptionsInterface> | OptionsGenerator = {},
): Metalsmith.Plugin => {
return createPlugin(async (files, metalsmith) => {
const options = await normalizeOptions(files, metalsmith, opts);
const { pattern } = options;
const dataListMap = new Map<
string,
{ filename: string; dirdata: ReturnType<typeof parser> }[]
>();
const matchedFilenameList = multimatch(
Object.keys(files),
(Array.isArray as isReadonlyOrWritableArray)(pattern)
? [...pattern]
: pattern,
);
for (const filename of matchedFilenameList) {
const filedata = files[filename];
if (!isFile(filedata)) {
continue;
}
debug('parsing file: %o', filename);
const dirdata = parser(filename, filedata);
const dirname = path.normalize(path.dirname(filename));
dataListMap.set(dirname, [
...(dataListMap.get(dirname) || []),
{ filename, dirdata },
]);
}
const dataMap = new Map(
[...dataListMap].map(([dirname, dirDataList]): [
string,
(
| { filename: string; dirdata: ReturnType<typeof parser> }
| undefined),
] => {
if (dirDataList.length > 1) {
throw new Error(
`Conflicting data files. Multiple files define metadata for one directory ${util.inspect(
dirname,
)}:\n` +
dirDataList
.map(
({ filename }) =>
`- ${util.inspect(filename)}`,
)
.join('\n'),
);
}
const data = firstItem(dirDataList);
if (data) {
debug('removing file: %o', data.filename);
delete files[data.filename];
}
return [dirname, data];
}),
);
for (const [filename, filedata] of Object.entries(files)) {
if (!isObject(filedata)) {
continue;
}
let dirname = path.normalize(filename);
const searchedDirname = new Set<string>();
do {
const data = dataMap.get(dirname);
if (data) {
debug(
'setting metadata to file: %o <- %o',
filename,
data.filename,
);
for (const [prop, value] of Object.entries(data.dirdata)) {
if (!hasProp(filedata, prop)) {
(filedata as Record<string, unknown>)[prop] = value;
defineDebug(
'assign property %o to file: %o',
prop,
filename,
);
}
}
}
searchedDirname.add(dirname);
dirname = path.normalize(path.dirname(dirname));
} while (!searchedDirname.has(dirname));
}
});
};