Add support for multiple inputs
This commit is contained in:
parent
e84b04449e
commit
aff3fb81a1
3 changed files with 176 additions and 96 deletions
6
demo/another.md
Normal file
6
demo/another.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Here's another file.
|
||||||
|
|
||||||
|
It has a list!
|
||||||
|
|
||||||
|
- See?
|
||||||
|
- Fancy!
|
3
demo/index.md
Normal file
3
demo/index.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Demo Data
|
||||||
|
|
||||||
|
This is just a folder of markdown files for testing purposes.
|
251
src/index.ts
251
src/index.ts
|
@ -9,7 +9,7 @@ import defaultTemplate from "./defaults/default-template.html" with {
|
||||||
type: "text",
|
type: "text",
|
||||||
};
|
};
|
||||||
import { CLIError } from "./error";
|
import { CLIError } from "./error";
|
||||||
import { mkdir } from "node:fs/promises";
|
import type { BunFile } from "bun";
|
||||||
|
|
||||||
const DEFAULT_TEMPLATE_FILE = "_template.html";
|
const DEFAULT_TEMPLATE_FILE = "_template.html";
|
||||||
const DEFAULT_STYLESHEET_FILE = "_style.css";
|
const DEFAULT_STYLESHEET_FILE = "_style.css";
|
||||||
|
@ -35,15 +35,17 @@ function replacePlaceholders(
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CLIOptions {
|
interface CLIOptions {
|
||||||
infile: string | null;
|
|
||||||
outfile: string | null;
|
outfile: string | null;
|
||||||
|
outdir: string | null;
|
||||||
|
stdout: boolean;
|
||||||
templateFilePath: string | null;
|
templateFilePath: string | null;
|
||||||
stylesheetFilePath: string | null;
|
stylesheetFilePath: string | null;
|
||||||
forceStdout: boolean;
|
|
||||||
help: boolean;
|
help: boolean;
|
||||||
|
title: string | null;
|
||||||
|
cwd: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCLIArgs(): CLIOptions {
|
function parseCLIArgs(): { options: CLIOptions; args: string[] } {
|
||||||
const { values: flags, positionals } = parseArgs({
|
const { values: flags, positionals } = parseArgs({
|
||||||
args: Bun.argv.slice(2),
|
args: Bun.argv.slice(2),
|
||||||
options: {
|
options: {
|
||||||
|
@ -51,16 +53,28 @@ function parseCLIArgs(): CLIOptions {
|
||||||
type: "string",
|
type: "string",
|
||||||
short: "o",
|
short: "o",
|
||||||
},
|
},
|
||||||
|
outdir: {
|
||||||
|
type: "string",
|
||||||
|
short: "d",
|
||||||
|
},
|
||||||
|
stdout: {
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
template: {
|
template: {
|
||||||
type: "string",
|
type: "string",
|
||||||
short: "t",
|
short: "t",
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
type: "string",
|
||||||
|
short: "T",
|
||||||
|
},
|
||||||
stylesheet: {
|
stylesheet: {
|
||||||
type: "string",
|
type: "string",
|
||||||
short: "s",
|
short: "s",
|
||||||
},
|
},
|
||||||
stdout: {
|
cwd: {
|
||||||
type: "boolean",
|
type: "string",
|
||||||
|
default: process.cwd(),
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
|
@ -70,20 +84,136 @@ function parseCLIArgs(): CLIOptions {
|
||||||
allowPositionals: true,
|
allowPositionals: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
|
||||||
|
if (positionals.length > 1 && flags.outfile) {
|
||||||
|
throw new CLIError("--outfile cannot be used with multiple inputs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.outdir && flags.outfile) {
|
||||||
|
throw new CLIError("--outdir and --outfile cannot be used together.");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
infile: positionals[0] ?? null,
|
options: {
|
||||||
|
cwd: flags.cwd,
|
||||||
outfile: flags.outfile ?? null,
|
outfile: flags.outfile ?? null,
|
||||||
|
outdir: flags.outdir ?? null,
|
||||||
|
stdout: flags.stdout ?? false,
|
||||||
templateFilePath: flags.template ?? null,
|
templateFilePath: flags.template ?? null,
|
||||||
stylesheetFilePath: flags.stylesheet ?? null,
|
stylesheetFilePath: flags.stylesheet ?? null,
|
||||||
forceStdout: flags.stdout ?? false,
|
|
||||||
help: flags.help ?? false,
|
help: flags.help ?? false,
|
||||||
|
title: flags.title ?? null,
|
||||||
|
},
|
||||||
|
args: positionals,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function loadTemplate(options: CLIOptions): Promise<string> {
|
||||||
const args = parseCLIArgs();
|
if (options.templateFilePath) {
|
||||||
|
return readFile(options.templateFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
if (args.help) {
|
const defaultTemplateFilePath = Path.join(options.cwd, DEFAULT_TEMPLATE_FILE);
|
||||||
|
const defaultTemplateFile = Bun.file(defaultTemplateFilePath);
|
||||||
|
|
||||||
|
if (await defaultTemplateFile.exists()) {
|
||||||
|
return defaultTemplateFile.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStylesheet(options: CLIOptions): Promise<string> {
|
||||||
|
if (options.stylesheetFilePath) {
|
||||||
|
return readFile(options.stylesheetFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultStylesheetFilePath = Path.join(
|
||||||
|
options.cwd,
|
||||||
|
DEFAULT_STYLESHEET_FILE,
|
||||||
|
);
|
||||||
|
const defaultStylesheetFile = Bun.file(defaultStylesheetFilePath);
|
||||||
|
|
||||||
|
if (await defaultStylesheetFile.exists()) {
|
||||||
|
return defaultStylesheetFile.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultStylesheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildFile(
|
||||||
|
infile: BunFile,
|
||||||
|
outfile: BunFile,
|
||||||
|
options: CLIOptions,
|
||||||
|
): Promise<string> {
|
||||||
|
const input = await infile.text();
|
||||||
|
const template = await loadTemplate(options);
|
||||||
|
const stylesheet = await loadStylesheet(options);
|
||||||
|
|
||||||
|
const { content, ...props } = EMDY.parse(input) as Record<string, unknown> & {
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const html = await marked.parse(content, {
|
||||||
|
gfm: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = [options.title, props.title].find((t) => t) || "Untitled";
|
||||||
|
|
||||||
|
const replacers = {
|
||||||
|
content: html,
|
||||||
|
title,
|
||||||
|
stylesheet: stylesheet,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
datetime: new Date().toLocaleString(),
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
|
||||||
|
const output = replacePlaceholders(template, replacers);
|
||||||
|
|
||||||
|
Bun.write(outfile, output);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOutputPath(inputPath: string, options: CLIOptions) {
|
||||||
|
const inputDirname = Path.dirname(inputPath);
|
||||||
|
const inputBasename = Path.basename(inputPath);
|
||||||
|
const outputBasename = inputBasename.replace(/\.md$/, ".html");
|
||||||
|
|
||||||
|
if (options.outdir) {
|
||||||
|
const dirPath = Path.relative(options.cwd, inputDirname);
|
||||||
|
const outputPath = Path.join(options.outdir, dirPath, outputBasename);
|
||||||
|
return Path.resolve(outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.join(inputDirname, outputBasename);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOutputFile(
|
||||||
|
inputPath: string,
|
||||||
|
options: CLIOptions,
|
||||||
|
): Promise<BunFile> {
|
||||||
|
// --stdout option -> Force stdout
|
||||||
|
if (options.stdout) {
|
||||||
|
return Bun.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact outfile defined -> write to file
|
||||||
|
if (options.outfile) {
|
||||||
|
return Bun.file(options.outfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input path with --outdir -> calculate output path
|
||||||
|
const outputPath = await getOutputPath(inputPath, options);
|
||||||
|
return Bun.file(outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { options, args } = parseCLIArgs();
|
||||||
|
|
||||||
|
if (options.help) {
|
||||||
console.log(
|
console.log(
|
||||||
`
|
`
|
||||||
buildmd - Build markdown files to html with templates and metadata
|
buildmd - Build markdown files to html with templates and metadata
|
||||||
|
@ -95,103 +225,44 @@ Usage:
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--outfile, -o <file> Output file path
|
--outfile, -o <file> Output file path
|
||||||
|
--outdir, -d <dir> Output directory path
|
||||||
--template, -t <file> Template file path
|
--template, -t <file> Template file path
|
||||||
--stylesheet, -s <file> Stylesheet file path
|
--stylesheet, -s <file> Stylesheet file path
|
||||||
--stdout Force output to stdout
|
--title, -T <string> Override title in template
|
||||||
--help, -h Show this help message
|
--help, -h Show this help message
|
||||||
`.trim(),
|
`.trim(),
|
||||||
);
|
);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// == INPUT ==
|
// stdin mode
|
||||||
const infile = args.infile ? Bun.file(args.infile) : Bun.stdin;
|
if (args.length === 0) {
|
||||||
|
const outfile = options.outfile ? Bun.file(options.outfile) : Bun.stdout;
|
||||||
// If using a file for input, it must exist
|
await buildFile(Bun.stdin, outfile, options);
|
||||||
if (infile !== Bun.stdin && !(await infile.exists())) {
|
process.exit(0);
|
||||||
throw new CLIError(`Input file ${args.infile} does not exist.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = await infile.text();
|
// file mode
|
||||||
|
for (const arg of args) {
|
||||||
|
const file = Bun.file(arg);
|
||||||
|
|
||||||
// == TEMPLATE ==
|
// Ensure input files exist
|
||||||
let template = defaultTemplate;
|
if (!(await file.exists())) {
|
||||||
|
throw new CLIError(`File ${arg} does not exist.`);
|
||||||
if (args.templateFilePath) {
|
|
||||||
template = await readFile(args.templateFilePath);
|
|
||||||
} else {
|
|
||||||
const defaultTemplateFilePath = Path.join(
|
|
||||||
process.cwd(),
|
|
||||||
DEFAULT_TEMPLATE_FILE,
|
|
||||||
);
|
|
||||||
const defaultTemplateFile = Bun.file(defaultTemplateFilePath);
|
|
||||||
|
|
||||||
if (await defaultTemplateFile.exists()) {
|
|
||||||
template = await defaultTemplateFile.text();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// == STYLESHEET ==
|
const outfile = await getOutputFile(arg, options);
|
||||||
let stylesheet = defaultStylesheet;
|
await buildFile(file, outfile, options);
|
||||||
|
|
||||||
if (args.stylesheetFilePath) {
|
|
||||||
stylesheet = await readFile(args.stylesheetFilePath);
|
|
||||||
} else {
|
|
||||||
const defaultStylesheetFilePath = Path.join(
|
|
||||||
process.cwd(),
|
|
||||||
DEFAULT_STYLESHEET_FILE,
|
|
||||||
);
|
|
||||||
const defaultStylesheetFile = Bun.file(defaultStylesheetFilePath);
|
|
||||||
|
|
||||||
if (await defaultStylesheetFile.exists()) {
|
|
||||||
stylesheet = await defaultStylesheetFile.text();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// == RENDER ==
|
|
||||||
const { content, ...props } = EMDY.parse(input) as Record<string, unknown> & {
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const html = await marked.parse(content, {
|
|
||||||
gfm: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// == REPALCE ==
|
|
||||||
const replacers = {
|
|
||||||
content: html,
|
|
||||||
title: props.title
|
|
||||||
? props.title
|
|
||||||
: args.infile
|
|
||||||
? Path.basename(args.infile)
|
|
||||||
: "Untitled",
|
|
||||||
stylesheet: stylesheet,
|
|
||||||
...props,
|
|
||||||
};
|
|
||||||
|
|
||||||
// == OUTPUT ==
|
|
||||||
const output = replacePlaceholders(template, replacers);
|
|
||||||
|
|
||||||
if (args.forceStdout || (!args.infile && !args.outfile)) {
|
|
||||||
Bun.write(Bun.stdout, output);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the output file path
|
|
||||||
const writeFilePath =
|
|
||||||
args.outfile ?? args.infile?.replace(/\.md$/, ".html") ?? "out.html";
|
|
||||||
|
|
||||||
// Ensure the output path exists
|
|
||||||
const writeFileDirname = Path.dirname(writeFilePath);
|
|
||||||
await mkdir(writeFileDirname, { recursive: true });
|
|
||||||
|
|
||||||
// Write the output to disk
|
|
||||||
await Bun.write(writeFilePath, output);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await main();
|
await main();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
if (error instanceof CLIError) {
|
||||||
|
console.error(error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue