From aff3fb81a16b63864f5393dc97fb0622574b1b8b Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Tue, 27 May 2025 08:09:34 -0400 Subject: [PATCH] Add support for multiple inputs --- demo/another.md | 6 ++ demo/index.md | 3 + src/index.ts | 263 ++++++++++++++++++++++++++++++------------------ 3 files changed, 176 insertions(+), 96 deletions(-) create mode 100644 demo/another.md create mode 100644 demo/index.md diff --git a/demo/another.md b/demo/another.md new file mode 100644 index 0000000..7fdb6ca --- /dev/null +++ b/demo/another.md @@ -0,0 +1,6 @@ +Here's another file. + +It has a list! + +- See? +- Fancy! diff --git a/demo/index.md b/demo/index.md new file mode 100644 index 0000000..9af8485 --- /dev/null +++ b/demo/index.md @@ -0,0 +1,3 @@ +# Demo Data + +This is just a folder of markdown files for testing purposes. diff --git a/src/index.ts b/src/index.ts index 23ede52..95c5ea9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import defaultTemplate from "./defaults/default-template.html" with { type: "text", }; import { CLIError } from "./error"; -import { mkdir } from "node:fs/promises"; +import type { BunFile } from "bun"; const DEFAULT_TEMPLATE_FILE = "_template.html"; const DEFAULT_STYLESHEET_FILE = "_style.css"; @@ -35,15 +35,17 @@ function replacePlaceholders( } interface CLIOptions { - infile: string | null; outfile: string | null; + outdir: string | null; + stdout: boolean; templateFilePath: string | null; stylesheetFilePath: string | null; - forceStdout: boolean; help: boolean; + title: string | null; + cwd: string; } -function parseCLIArgs(): CLIOptions { +function parseCLIArgs(): { options: CLIOptions; args: string[] } { const { values: flags, positionals } = parseArgs({ args: Bun.argv.slice(2), options: { @@ -51,16 +53,28 @@ function parseCLIArgs(): CLIOptions { type: "string", short: "o", }, + outdir: { + type: "string", + short: "d", + }, + stdout: { + type: "boolean", + }, template: { type: "string", short: "t", }, + title: { + type: "string", + short: "T", + }, stylesheet: { type: "string", short: "s", }, - stdout: { - type: "boolean", + cwd: { + type: "string", + default: process.cwd(), }, help: { type: "boolean", @@ -70,20 +84,136 @@ function parseCLIArgs(): CLIOptions { 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 { - infile: positionals[0] ?? null, - outfile: flags.outfile ?? null, - templateFilePath: flags.template ?? null, - stylesheetFilePath: flags.stylesheet ?? null, - forceStdout: flags.stdout ?? false, - help: flags.help ?? false, + options: { + cwd: flags.cwd, + outfile: flags.outfile ?? null, + outdir: flags.outdir ?? null, + stdout: flags.stdout ?? false, + templateFilePath: flags.template ?? null, + stylesheetFilePath: flags.stylesheet ?? null, + help: flags.help ?? false, + title: flags.title ?? null, + }, + args: positionals, }; } -async function main() { - const args = parseCLIArgs(); +async function loadTemplate(options: CLIOptions): Promise { + 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 { + 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 { + const input = await infile.text(); + const template = await loadTemplate(options); + const stylesheet = await loadStylesheet(options); + + const { content, ...props } = EMDY.parse(input) as Record & { + 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 { + // --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( ` buildmd - Build markdown files to html with templates and metadata @@ -95,103 +225,44 @@ Usage: Options: --outfile, -o Output file path + --outdir, -d Output directory path --template, -t Template file path --stylesheet, -s Stylesheet file path - --stdout Force output to stdout + --title, -T Override title in template --help, -h Show this help message `.trim(), ); process.exit(0); } - // == INPUT == - const infile = args.infile ? Bun.file(args.infile) : Bun.stdin; - - // If using a file for input, it must exist - if (infile !== Bun.stdin && !(await infile.exists())) { - throw new CLIError(`Input file ${args.infile} does not exist.`); + // stdin mode + if (args.length === 0) { + const outfile = options.outfile ? Bun.file(options.outfile) : Bun.stdout; + await buildFile(Bun.stdin, outfile, options); + process.exit(0); } - const input = await infile.text(); + // file mode + for (const arg of args) { + const file = Bun.file(arg); - // == TEMPLATE == - let template = defaultTemplate; - - 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(); + // Ensure input files exist + if (!(await file.exists())) { + throw new CLIError(`File ${arg} does not exist.`); } + + const outfile = await getOutputFile(arg, options); + await buildFile(file, outfile, options); } - - // == STYLESHEET == - let stylesheet = defaultStylesheet; - - 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 & { - 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 { await main(); } catch (error) { - console.error(error); - process.exit(1); + if (error instanceof CLIError) { + console.error(error.message); + process.exit(1); + } + + throw error; }