diff --git a/src/index.ts b/src/index.ts index 7dba246..ab83f80 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,26 @@ import Path from "node:path"; import { parseArgs } from "node:util"; import EMDY from "@endeavorance/emdy"; -import defaultStylesheet from "./defaults/default-style.css" with { +import type { BunFile } from "bun"; +import DEFAULT_STYLESHEET from "./defaults/default-style.css" with { type: "text", }; -import defaultTemplate from "./defaults/default-template.html" with { +import DEFAULT_TEMPLATE from "./defaults/default-template.html" with { type: "text", }; import { CLIError } from "./error"; -import type { BunFile } from "bun"; import { parseMarkdown } from "./markdown"; const DEFAULT_TEMPLATE_FILE = "_template.html"; const DEFAULT_STYLESHEET_FILE = "_style.css"; +/** + * Given a file path, attempt to read the text of the file + * @param filePath - The path to the file to read + * @returns The text content of the file + * + * @throws CLIError if the file does not exist + */ async function readFile(filePath: string): Promise { const file = Bun.file(filePath); const exists = await file.exists(); @@ -23,7 +30,13 @@ async function readFile(filePath: string): Promise { return file.text(); } -function replacePlaceholders( +/** + * Renders the provided template with the given data. + * @param template - The template string containing placeholders like %key% + * @param data - An object containing key-value pairs to replace in the template + * @returns The rendered string with all placeholders replaced by their corresponding values + */ +function renderTemplate( template: string, data: Record, ): string { @@ -34,17 +47,39 @@ function replacePlaceholders( return output; } +/** + * A collection of options parsed from the command line arguments. + */ interface CLIOptions { + /** A path to the output file */ outfile: string | null; + + /** A path to the output directory */ outdir: string | null; + + /** If true, force output to stdout */ stdout: boolean; + + /** The path to the template file */ templateFilePath: string | null; + + /** The path to the stylesheet file */ stylesheetFilePath: string | null; + + /** If true, show help message */ help: boolean; + + /** If provided, overrides the document title */ title: string | null; + + /** If provided, overrides the current working directory (default: .) */ cwd: string; } +/** + * Parse the command line arguments and return the options and positional arguments. + * @returns An object containing the parsed options and positional arguments + */ function parseCLIArgs(): { options: CLIOptions; args: string[] } { const { values: flags, positionals } = parseArgs({ args: Bun.argv.slice(2), @@ -109,57 +144,69 @@ function parseCLIArgs(): { options: CLIOptions; args: string[] } { }; } +/** + * Attempt to load a custom template, falling back to a default + * @param options - The CLI options containing the template file path + * @returns The template string + */ async function loadTemplate(options: CLIOptions): Promise { if (options.templateFilePath) { return readFile(options.templateFilePath); } - const defaultTemplateFilePath = Path.join(options.cwd, DEFAULT_TEMPLATE_FILE); - const defaultTemplateFile = Bun.file(defaultTemplateFilePath); + const checkTemplateFile = Bun.file( + Path.join(options.cwd, DEFAULT_TEMPLATE_FILE), + ); - if (await defaultTemplateFile.exists()) { - return defaultTemplateFile.text(); + if (await checkTemplateFile.exists()) { + return checkTemplateFile.text(); } - return defaultTemplate; + return DEFAULT_TEMPLATE; } +/** + * Attempt to load a custom stylesheet, falling back to a default + * @param options - The CLI options containing the stylesheet file path + * @returns The stylesheet string + */ async function loadStylesheet(options: CLIOptions): Promise { if (options.stylesheetFilePath) { return readFile(options.stylesheetFilePath); } - const defaultStylesheetFilePath = Path.join( - options.cwd, - DEFAULT_STYLESHEET_FILE, + const checkStylesheetFile = Bun.file( + Path.join(options.cwd, DEFAULT_STYLESHEET_FILE), ); - const defaultStylesheetFile = Bun.file(defaultStylesheetFilePath); - if (await defaultStylesheetFile.exists()) { - return defaultStylesheetFile.text(); + if (await checkStylesheetFile.exists()) { + return checkStylesheetFile.text(); } - return defaultStylesheet; + return DEFAULT_STYLESHEET; } +/** + * Build and write a file + * @param infile - The input file to read from + * @param outfile - The output file to write to + * @param options - The CLI options containing template and stylesheet paths + */ async function buildFile( infile: BunFile, outfile: BunFile, options: CLIOptions, -): Promise { +): 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 parseMarkdown(content); - - const title = [options.title, props.title].find((t) => t) || "Untitled"; - - const replacers = { + const title = options.title ?? props.title ?? "Untitled"; + const templateData = { content: html, title, stylesheet: stylesheet, @@ -168,13 +215,15 @@ async function buildFile( ...props, }; - const output = replacePlaceholders(template, replacers); - - Bun.write(outfile, output); - - return output; + await Bun.write(outfile, renderTemplate(template, templateData)); } +/** + * Get the output path based on the input path and options + * @param inputPath - The path of the input file + * @param options - The CLI options containing output directory + * @returns The resolved output path + */ async function getOutputPath(inputPath: string, options: CLIOptions) { const inputDirname = Path.dirname(inputPath); const inputBasename = Path.basename(inputPath); @@ -189,6 +238,12 @@ async function getOutputPath(inputPath: string, options: CLIOptions) { return Path.join(inputDirname, outputBasename); } +/** + * Get the output file based on the input path and options + * @param inputPath - The path of the input file + * @param options - The CLI options containing output file and directory + * @returns The BunFile object representing the output file + */ async function getOutputFile( inputPath: string, options: CLIOptions,