Initial commit
This commit is contained in:
parent
e473389c04
commit
86aa021db7
12 changed files with 402 additions and 1194 deletions
5
src/css.d.ts
vendored
Normal file
5
src/css.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
declare module '*.css' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
104
src/defaults/default-style.css
Normal file
104
src/defaults/default-style.css
Normal 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;
|
||||
}
|
16
src/defaults/default-template.html
Normal file
16
src/defaults/default-template.html
Normal 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
3
src/error.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export class CLIError extends Error {
|
||||
name = "CLIError";
|
||||
}
|
197
src/index.ts
Normal file
197
src/index.ts
Normal 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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue