Initial commit

This commit is contained in:
Endeavorance 2025-05-24 13:19:39 -04:00
parent e473389c04
commit 86aa021db7
12 changed files with 402 additions and 1194 deletions

5
src/css.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
declare module '*.css' {
const content: string;
export default content;
}

View file

@ -0,0 +1,104 @@
:root {
--color-bg: #1d1d1d;
--color-text: #eee;
--color-link: #fff;
--color-strong: #ff8f8f;
--color-emphasis: #ff8f00;
--color-heading: #accfff;
--font-size: 18px;
--font-family: Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans",
source-sans-pro, sans-serif;
--font-weight: 400;
--font-weight-bold: 500;
--line-height: 1.4;
--max-width: 700px;
}
body {
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size);
font-family: var(--font-family);
font-weight: var(--font-weight);
line-height: var(--line-height);
}
article {
max-width: var(--max-width);
margin: 0 auto;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-family);
font-weight: var(--font-weight-bold);
color: var(--color-heading);
margin: 1rem 0;
}
h1 {
font-size: 3rem;
}
h2 {
font-size: 2.75rem;
}
h3 {
font-size: 2.15rem;
}
h4 {
font-size: 1.75rem;
}
h5 {
font-size: 1.5rem;
}
h6 {
font-size: 1.25rem;
}
em,
i {
color: var(--color-emphasis);
}
strong,
b {
color: var(--color-strong);
font-weight: var(--font-weight-bold);
}
a {
color: var(--color-link);
}
table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
font-size: 1em;
background-color: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-text);
}
th,
td {
padding: 0.75em 1em;
text-align: left;
border-bottom: 1px solid var(--color-text);
}
th {
background: var(--color-bg);
font-weight: 500;
}

View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>%title%</title>
<style>
%stylesheet%
</style>
</head>
<body>
<article>
%content%
</article>
</body>
</html>

3
src/error.ts Normal file
View file

@ -0,0 +1,3 @@
export class CLIError extends Error {
name = "CLIError";
}

197
src/index.ts Normal file
View file

@ -0,0 +1,197 @@
import Path from "node:path";
import { parseArgs } from "node:util";
import EMDY from "@endeavorance/emdy";
import { marked } from "marked";
import defaultStylesheet from "./defaults/default-style.css" with {
type: "text",
};
import defaultTemplate from "./defaults/default-template.html" with {
type: "text",
};
import { CLIError } from "./error";
import { mkdir } from "node:fs/promises";
const DEFAULT_TEMPLATE_FILE = "_template.html";
const DEFAULT_STYLESHEET_FILE = "_style.css";
async function readFile(filePath: string): Promise<string> {
const file = Bun.file(filePath);
const exists = await file.exists();
if (!exists) {
throw new CLIError(`File ${filePath} does not exist.`);
}
return file.text();
}
function replacePlaceholders(
template: string,
data: Record<string, unknown>,
): string {
let output = template;
for (const [key, value] of Object.entries(data)) {
output = output.replace(new RegExp(`%${key}%`, "g"), String(value));
}
return output;
}
interface CLIOptions {
infile: string | null;
outfile: string | null;
templateFilePath: string | null;
stylesheetFilePath: string | null;
forceStdout: boolean;
help: boolean;
}
function parseCLIArgs(): CLIOptions {
const { values: flags, positionals } = parseArgs({
args: Bun.argv.slice(2),
options: {
outfile: {
type: "string",
short: "o",
},
template: {
type: "string",
short: "t",
},
stylesheet: {
type: "string",
short: "s",
},
stdout: {
type: "boolean",
},
help: {
type: "boolean",
short: "h",
},
},
allowPositionals: true,
});
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,
};
}
async function main() {
const args = parseCLIArgs();
if (args.help) {
console.log(
`
buildmd - Build markdown files to html with templates and metadata
Usage:
buildmd file.md
buildmd file.md -o output.html
echo "some markdown" | buildmd
Options:
--outfile, -o <file> Output file path
--template, -t <file> Template file path
--stylesheet, -s <file> Stylesheet file path
--stdout Force output to stdout
--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.`);
}
const input = await infile.text();
// == 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();
}
}
// == 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<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 {
await main();
} catch (error) {
console.error(error);
process.exit(1);
}