Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ Options:
--stylesheet Set CSS file path used for rendering.
```

### `stdin` / `stdout` Support

md2pdf supports reading from `stdin` and writing to `stdout`.\
You can pipe markdown content directly to md2pdf, making it easy to integrate with other CLI tools:

```sh
# Convert piped input to PDF
cat input.md | md2pdf > path/to/output.pdf

# Use with curl to convert remote markdown
curl -s https://raw.githubusercontent.com/ryuapp/md2pdf/main/README.md | md2pdf > README.pdf
```

## Front matter (Experimental)

We can specify CSS file used in the front matter of markdown.
Expand Down
4 changes: 2 additions & 2 deletions src/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function createWaveAnimation(
}

const encoder = new TextEncoder();
Deno.stdout.writeSync(encoder.encode(`\r${coloredText}`));
Deno.stderr.writeSync(encoder.encode(`\r${coloredText}`));
frame++;
};

Expand All @@ -51,7 +51,7 @@ export function createWaveAnimation(
clearInterval(intervalId);
intervalId = null;
const encoder = new TextEncoder();
Deno.stdout.writeSync(
Deno.stderr.writeSync(
encoder.encode(`\r${" ".repeat(fullText.length + 2)}\r`),
);
}
Expand Down
53 changes: 31 additions & 22 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import {
underline,
yellow,
} from "@std/fmt/colors";
import { mdToPdf } from "./md-to-pdf.ts";
import { getFilename } from "./utils/filename.ts";
import { createWaveAnimation } from "./animation.ts";
import type { MdToPdfOptions } from "./types.ts";
import {
generatePdfFromMarkdown,
generatePdfFromStdin,
} from "./generate-pdf.ts";
import { getFilename } from "./utils/filename.ts";
import denoJson from "../deno.json" with { type: "json" };

function printHelp(): void {
Expand All @@ -41,23 +43,6 @@ ${yellow("Options:")}
console.log(help);
}

async function generatePdfFromMarkdown(path: string, options?: MdToPdfOptions) {
const pdfName = getFilename(path) + ".pdf";

const waveAnimation = createWaveAnimation("generating PDF from", path);
waveAnimation.start();
await mdToPdf(path, options).then(
(pdf) => {
Deno.writeFileSync(
pdfName,
pdf,
);
waveAnimation.stop();
console.log(green("✓") + " generated " + underline(pdfName));
},
);
}

// Inline

const args = parseArgs(Deno.args, {
Expand All @@ -75,6 +60,16 @@ if (args.h || args.help) {
Deno.exit(0);
}

// Check if input is piped
if (!Deno.stdin.isTerminal()) {
// Handle piped input - output to stdout
const waveAnimation = createWaveAnimation("generating PDF from", "stdin");
waveAnimation.start();
await generatePdfFromStdin(args);
waveAnimation.stop();
Deno.exit(0);
}

const paths: Array<string> = [];
if (args._) {
for await (const path of args._) {
Expand All @@ -98,6 +93,7 @@ if (paths.length < 1) {
brightRed("error")
}: Set a valid markdown file path you wanna convert to PDF\n`,
" e.g.) md2pdf README.md\n",
" e.g.) cat README.md | md2pdf\n",
);
console.error("For more information, try '--help'.");
} else {
Expand All @@ -108,7 +104,12 @@ if (paths.length < 1) {

await (async () => {
for (let i = 0; i < paths.length; i++) {
await generatePdfFromMarkdown(paths[i], args);
const pdfPath = getFilename(paths[i]) + ".pdf";
const waveAnimation = createWaveAnimation("generating PDF from", paths[i]);
waveAnimation.start();
await generatePdfFromMarkdown(paths[i], pdfPath, args);
waveAnimation.stop();
console.log(green("✓") + " generated " + underline(pdfPath));
}
})();

Expand All @@ -129,7 +130,15 @@ if (args.w || args.watch) {
) {
pastRealPath = realPath;
pastMTime = stat.mtime?.toString();
await generatePdfFromMarkdown(paths[0], args);
const pdfPath = getFilename(paths[0]) + ".pdf";
const waveAnimation = createWaveAnimation(
"generating PDF from",
paths[0],
);
waveAnimation.start();
await generatePdfFromMarkdown(paths[0], pdfPath, args);
waveAnimation.stop();
console.log(green("✓") + " generated " + underline(pdfPath));
}
}
}
Expand Down
76 changes: 76 additions & 0 deletions src/generate-pdf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { join } from "@std/path";
import { brightRed } from "@std/fmt/colors";
import { mdToPdf } from "./md-to-pdf.ts";
import type { MdToPdfOptions } from "./types.ts";

export async function generatePdfFromMarkdown(
markdownPath: string,
pdfPath: string,
options?: MdToPdfOptions,
) {
await mdToPdf(markdownPath, options).then(
(pdf) => {
Deno.writeFileSync(
pdfPath,
pdf,
);
},
);
}

export async function generatePdfFromStdin(options?: MdToPdfOptions) {
const chunks: Uint8Array[] = [];
const reader = Deno.stdin.readable.getReader();

try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
} finally {
reader.releaseLock();
}

const markdownContent = new TextDecoder().decode(
new Uint8Array(
chunks.reduce((acc, chunk) => [...acc, ...chunk], [] as number[]),
),
);

// Create temporary file using Deno's standard approach
const tempDir = await Deno.makeTempDir();
const tempPath = join(tempDir, "stdin.md");
// Cleanup
const cleanupTempDir = () => {
try {
Deno.removeSync(tempDir, { recursive: true });
} catch (error) {
console.error(
`${brightRed("warning")}: Failed to cleanup temp directory: ${error}`,
);
}
};
// Setup cleanup on SIGINT (Ctrl+C) and SIGBREAK (Ctrl + Break)
const signalHandler = () => {
console.debug("Called Signal Handler");
cleanupTempDir();
Deno.exit(1);
};

Deno.addSignalListener("SIGINT", signalHandler);
Deno.addSignalListener("SIGBREAK", signalHandler);
await Deno.writeTextFile(tempPath, markdownContent);

try {
await mdToPdf(tempPath, options).then(
(pdf) => {
Deno.stdout.writeSync(pdf);
},
);
} finally {
Deno.removeSignalListener("SIGINT", signalHandler);
Deno.removeSignalListener("SIGBREAK", signalHandler);
cleanupTempDir();
}
}
Loading