md2pdf2 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # MD2PDF
2
+
3
+ A CLI tool that converts Markdown to PDF using customizable templates — think Astro, but for PDFs.
4
+
5
+ ## Features
6
+
7
+ - Convert Markdown to PDF with a single command
8
+ - Customizable templates with layout, styling, and components
9
+ - Supports multiple output formats (letter, a4, etc.)
10
+ - Template inheritance and partials
11
+ - Frontmatter support for metadata
12
+ - Built-in template engine (Handlebars-like syntax)
13
+ - CLI flags for quick customization
14
+ - Watch mode for development
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ # Install globally (once published)
20
+ npm install -g md2pdf
21
+
22
+ # Or use npx
23
+ npx md2pdf convert input.md -o output.pdf
24
+
25
+ # With a custom template
26
+ md2pdf convert input.md --template my-template.hbs -o output.pdf
27
+ ```
28
+
29
+ ## Project Structure
30
+
31
+ ```
32
+ my-doc/
33
+ ├── content/
34
+ │ └── my-doc.md
35
+ ├── templates/
36
+ │ ├── default.hbs
37
+ │ ├── parts/
38
+ │ │ └── header.hbs
39
+ │ └── styles.css
40
+ ├── md2pdf.config.js
41
+ └── package.json
42
+ ```
43
+
44
+ ## Templates
45
+
46
+ Templates use handlebars-like syntax for placeholders and partials:
47
+
48
+ ```handlebars
49
+ <!DOCTYPE html>
50
+ <html>
51
+ <head>
52
+ <meta charset="UTF-8">
53
+ <style>{{{styles}}}</style>
54
+ </head>
55
+ <body>
56
+ {{> header}}
57
+ <main>
58
+ {{{content}}}
59
+ </main>
60
+ </body>
61
+ </html>
62
+ ```
63
+
64
+ ## Configuration
65
+
66
+ `md2pdf.config.js`:
67
+
68
+ ```js
69
+ export default {
70
+ template: './templates/default.hbs',
71
+ style: './templates/styles.css',
72
+ pdfOptions: {
73
+ format: 'A4',
74
+ margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Development
80
+
81
+ ```bash
82
+ # Clone and setup
83
+ git clone https://github.com/areai51/md2pdf.git
84
+ cd md2pdf
85
+ npm install
86
+
87
+ # Build
88
+ npm run build
89
+
90
+ # Test
91
+ npm test
92
+
93
+ # Link for global use
94
+ npm link
95
+ ```
96
+
97
+ ## License
98
+
99
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { readFileAsync, writeFileAsync, fileExists, resolvePath, getFileName } from './utils.js';
4
+ import { Converter } from './converter.js';
5
+ import { TemplateEngine } from './template-engine.js';
6
+ import { PDFGenerator } from './pdf-generator.js';
7
+ import { DEFAULT_CONFIG } from './types.js';
8
+ const program = new Command();
9
+ program
10
+ .name('md2pdf')
11
+ .description('Convert Markdown to PDF using customizable templates')
12
+ .version('0.1.0');
13
+ program
14
+ .command('convert')
15
+ .description('Convert a Markdown file to PDF')
16
+ .argument('<input>', 'Input Markdown file')
17
+ .option('-o, --output <file>', 'Output PDF file')
18
+ .option('-t, --template <file>', 'Custom template file (Handlebars)')
19
+ .option('-s, --style <file>', 'Custom CSS file')
20
+ .option('-c, --config <file>', 'Configuration file (md2pdf.config.js)')
21
+ .option('--no-pdf', 'Only generate HTML, do not create PDF')
22
+ .action(async (input, options) => {
23
+ try {
24
+ // Load configuration
25
+ const config = await loadConfig(options);
26
+ // Override with CLI options
27
+ if (options.template)
28
+ config.template = options.template;
29
+ if (options.style)
30
+ config.style = options.style;
31
+ // Validate input file
32
+ if (!(await fileExists(input))) {
33
+ console.error(`Error: Input file not found: ${input}`);
34
+ process.exit(1);
35
+ }
36
+ // Read input markdown
37
+ const markdown = await readFileAsync(input);
38
+ // Convert to HTML
39
+ const converter = new Converter();
40
+ const { html, frontMatter } = converter.convert(markdown);
41
+ // Load styles
42
+ let styles = '';
43
+ if (config.style && await fileExists(config.style)) {
44
+ styles = await readFileAsync(config.style);
45
+ }
46
+ // Load and render template
47
+ const engine = new TemplateEngine(config);
48
+ if (config.template && await fileExists(config.template)) {
49
+ await engine.loadTemplate(config.template);
50
+ }
51
+ else {
52
+ // Use built-in default template
53
+ engine.loadDefaultTemplate();
54
+ }
55
+ const renderedHtml = engine.render(html, frontMatter, styles);
56
+ // Determine output path
57
+ const outputPath = options.output || (await getOutputPath(input));
58
+ const htmlPath = outputPath.replace(/\.pdf$/, '.html');
59
+ // Save HTML for debugging
60
+ await writeFileAsync(htmlPath, renderedHtml);
61
+ console.log(`✓ HTML generated: ${htmlPath}`);
62
+ // Generate PDF unless disabled
63
+ if (!options.pdf) {
64
+ const generator = new PDFGenerator();
65
+ await generator.generate(renderedHtml, outputPath, config.pdfOptions);
66
+ console.log(`✓ PDF generated: ${outputPath}`);
67
+ }
68
+ }
69
+ catch (error) {
70
+ console.error('Error:', error instanceof Error ? error.message : error);
71
+ process.exit(1);
72
+ }
73
+ });
74
+ async function loadConfig(options) {
75
+ const config = { ...DEFAULT_CONFIG };
76
+ if (options.config && await fileExists(options.config)) {
77
+ const configModule = await import(resolvePath(process.cwd(), options.config));
78
+ const userConfig = configModule.default || configModule;
79
+ Object.assign(config, userConfig);
80
+ }
81
+ return config;
82
+ }
83
+ function getOutputPath(inputPath) {
84
+ const baseName = getFileName(inputPath);
85
+ return `${baseName}.pdf`;
86
+ }
87
+ program.parse();
88
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { FrontMatter } from './types.js';
2
+ export declare class Converter {
3
+ constructor();
4
+ convert(markdown: string): {
5
+ html: string;
6
+ frontMatter: FrontMatter;
7
+ };
8
+ convertFile(filePath: string): Promise<{
9
+ html: string;
10
+ frontMatter: FrontMatter;
11
+ }>;
12
+ }
13
+ //# sourceMappingURL=converter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../src/converter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,qBAAa,SAAS;;IASpB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,WAAW,CAAA;KAAE;IAS/D,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,WAAW,CAAA;KAAE,CAAC;CAKzF"}
@@ -0,0 +1,25 @@
1
+ import fm from 'front-matter';
2
+ import { marked } from 'marked';
3
+ export class Converter {
4
+ constructor() {
5
+ // Configure marked options
6
+ marked.setOptions({
7
+ gfm: true,
8
+ breaks: true
9
+ });
10
+ }
11
+ convert(markdown) {
12
+ const { attributes, body } = fm(markdown);
13
+ const html = marked(body, { async: false });
14
+ return {
15
+ html,
16
+ frontMatter: attributes
17
+ };
18
+ }
19
+ async convertFile(filePath) {
20
+ // Will be implemented with file reading
21
+ const content = await (await import('./utils.js')).readFileAsync(filePath);
22
+ return this.convert(content);
23
+ }
24
+ }
25
+ //# sourceMappingURL=converter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converter.js","sourceRoot":"","sources":["../src/converter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,cAAc,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,MAAM,OAAO,SAAS;IACpB;QACE,2BAA2B;QAC3B,MAAM,CAAC,UAAU,CAAC;YAChB,GAAG,EAAE,IAAI;YACT,MAAM,EAAE,IAAI;SACb,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,QAAgB;QACtB,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAW,CAAC;QACtD,OAAO;YACL,IAAI;YACJ,WAAW,EAAE,UAAyB;SACvC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,QAAgB;QAChC,wCAAwC;QACxC,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,5 @@
1
+ import { PDFOptions } from './types.js';
2
+ export declare class PDFGenerator {
3
+ generate(html: string, outputPath: string, pdfOptions?: PDFOptions): Promise<void>;
4
+ }
5
+ //# sourceMappingURL=pdf-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf-generator.d.ts","sourceRoot":"","sources":["../src/pdf-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,qBAAa,YAAY;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;CAmCzF"}
@@ -0,0 +1,36 @@
1
+ import puppeteer from 'puppeteer';
2
+ export class PDFGenerator {
3
+ async generate(html, outputPath, pdfOptions) {
4
+ const browser = await puppeteer.launch({
5
+ headless: true,
6
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
7
+ });
8
+ try {
9
+ const page = await browser.newPage();
10
+ // Set HTML content
11
+ await page.setContent(html, { waitUntil: ['networkidle0', 'domcontentloaded'] });
12
+ // Set PDF options
13
+ const options = {
14
+ format: pdfOptions?.format || 'A4',
15
+ printBackground: pdfOptions?.printBackground ?? true,
16
+ margin: {
17
+ top: pdfOptions?.margin?.top || '1cm',
18
+ right: pdfOptions?.margin?.right || '1cm',
19
+ bottom: pdfOptions?.margin?.bottom || '1cm',
20
+ left: pdfOptions?.margin?.left || '1cm'
21
+ },
22
+ ...(pdfOptions?.landscape && { landscape: pdfOptions.landscape }),
23
+ ...(pdfOptions?.width && { width: pdfOptions.width }),
24
+ ...(pdfOptions?.height && { height: pdfOptions.height })
25
+ };
26
+ await page.pdf({
27
+ path: outputPath,
28
+ ...options
29
+ });
30
+ }
31
+ finally {
32
+ await browser.close();
33
+ }
34
+ }
35
+ }
36
+ //# sourceMappingURL=pdf-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf-generator.js","sourceRoot":"","sources":["../src/pdf-generator.ts"],"names":[],"mappings":"AAAA,OAAO,SAAgD,MAAM,WAAW,CAAC;AAGzE,MAAM,OAAO,YAAY;IACvB,KAAK,CAAC,QAAQ,CAAC,IAAY,EAAE,UAAkB,EAAE,UAAuB;QACtE,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC;YACrC,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,CAAC,cAAc,EAAE,0BAA0B,CAAC;SACnD,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;YAErC,mBAAmB;YACnB,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,cAAc,EAAE,kBAAkB,CAAC,EAAE,CAAC,CAAC;YAEjF,kBAAkB;YAClB,MAAM,OAAO,GAAe;gBAC1B,MAAM,EAAE,UAAU,EAAE,MAAM,IAAI,IAAI;gBAClC,eAAe,EAAE,UAAU,EAAE,eAAe,IAAI,IAAI;gBACpD,MAAM,EAAE;oBACN,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,KAAK;oBACrC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,IAAI,KAAK;oBACzC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,IAAI,KAAK;oBAC3C,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,IAAI,KAAK;iBACxC;gBACD,GAAG,CAAC,UAAU,EAAE,SAAS,IAAI,EAAE,SAAS,EAAE,UAAU,CAAC,SAAS,EAAE,CAAC;gBACjE,GAAG,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,CAAC;gBACrD,GAAG,CAAC,UAAU,EAAE,MAAM,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC;aACzD,CAAC;YAEF,MAAM,IAAI,CAAC,GAAG,CAAC;gBACb,IAAI,EAAE,UAAU;gBAChB,GAAG,OAAO;aACY,CAAC,CAAC;QAC5B,CAAC;gBAAS,CAAC;YACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,14 @@
1
+ import { MD2PDFConfig } from './types.js';
2
+ export declare class TemplateEngine {
3
+ private template;
4
+ private partials;
5
+ constructor(config: MD2PDFConfig);
6
+ private registerHelpers;
7
+ private loadPartialsFromConfig;
8
+ loadTemplate(templatePath: string): Promise<void>;
9
+ loadDefaultTemplate(): void;
10
+ private compileTemplate;
11
+ registerPartial(name: string, content: string): void;
12
+ render(content: string, frontMatter: Record<string, any>, styles: string): string;
13
+ }
14
+ //# sourceMappingURL=template-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-engine.d.ts","sourceRoot":"","sources":["../src/template-engine.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,QAAQ,CAAkD;gBAEtD,MAAM,EAAE,YAAY;IAUhC,OAAO,CAAC,eAAe;YAMT,sBAAsB;IAW9B,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvD,mBAAmB,IAAI,IAAI;IAiD3B,OAAO,CAAC,eAAe;IAIvB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAIpD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;CAQlF"}
@@ -0,0 +1,97 @@
1
+ import Handlebars from 'handlebars';
2
+ import { readFileAsync } from './utils.js';
3
+ export class TemplateEngine {
4
+ template;
5
+ partials = {};
6
+ constructor(config) {
7
+ // Register built-in helpers
8
+ this.registerHelpers();
9
+ // Load partials from config if provided
10
+ if (config.partials) {
11
+ this.loadPartialsFromConfig(config.partials);
12
+ }
13
+ }
14
+ registerHelpers() {
15
+ Handlebars.registerHelper('hasPartial', (name) => {
16
+ return !!this.partials[name];
17
+ });
18
+ }
19
+ async loadPartialsFromConfig(partialsConfig) {
20
+ for (const [name, path] of Object.entries(partialsConfig)) {
21
+ try {
22
+ const content = await readFileAsync(path);
23
+ this.partials[name] = Handlebars.compile(content);
24
+ }
25
+ catch (error) {
26
+ console.warn(`Warning: Could not load partial "${name}" from ${path}: ${error}`);
27
+ }
28
+ }
29
+ }
30
+ async loadTemplate(templatePath) {
31
+ const templateContent = await readFileAsync(templatePath);
32
+ this.template = this.compileTemplate(templateContent);
33
+ }
34
+ loadDefaultTemplate() {
35
+ const defaultTemplate = `<!DOCTYPE html>
36
+ <html>
37
+ <head>
38
+ <meta charset="UTF-8">
39
+ <style>
40
+ body {
41
+ font-family: system-ui, -apple-system, sans-serif;
42
+ line-height: 1.6;
43
+ max-width: 800px;
44
+ margin: 0 auto;
45
+ padding: 2rem;
46
+ }
47
+ h1, h2, h3, h4, h5, h6 {
48
+ margin-top: 2rem;
49
+ margin-bottom: 1rem;
50
+ line-height: 1.25;
51
+ }
52
+ p { margin-bottom: 1rem; }
53
+ code {
54
+ background: #f4f4f4;
55
+ padding: 0.2em 0.4em;
56
+ border-radius: 3px;
57
+ font-family: monospace;
58
+ }
59
+ pre {
60
+ background: #f4f4f4;
61
+ padding: 1rem;
62
+ border-radius: 5px;
63
+ overflow-x: auto;
64
+ }
65
+ blockquote {
66
+ border-left: 4px solid #ddd;
67
+ padding-left: 1rem;
68
+ margin-left: 0;
69
+ color: #666;
70
+ }
71
+ @media print { body { padding: 1cm; } }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ {{#if (hasPartial "header")}}{{> header}}{{/if}}
76
+ <main>{{{content}}}</main>
77
+ {{#if (hasPartial "footer")}}{{> footer}}{{/if}}
78
+ </body>
79
+ </html>`;
80
+ this.template = this.compileTemplate(defaultTemplate);
81
+ }
82
+ compileTemplate(content) {
83
+ return Handlebars.compile(content, { strict: true });
84
+ }
85
+ registerPartial(name, content) {
86
+ this.partials[name] = Handlebars.compile(content);
87
+ }
88
+ render(content, frontMatter, styles) {
89
+ return this.template({
90
+ content,
91
+ frontMatter,
92
+ styles,
93
+ partials: this.partials
94
+ });
95
+ }
96
+ }
97
+ //# sourceMappingURL=template-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-engine.js","sourceRoot":"","sources":["../src/template-engine.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,OAAO,cAAc;IACjB,QAAQ,CAA8B;IACtC,QAAQ,GAA+C,EAAE,CAAC;IAElE,YAAY,MAAoB;QAC9B,4BAA4B;QAC5B,IAAI,CAAC,eAAe,EAAE,CAAC;QAEvB,wCAAwC;QACxC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACpB,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAEO,eAAe;QACrB,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,CAAC,IAAY,EAAE,EAAE;YACvD,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,sBAAsB,CAAC,cAAsC;QACzE,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;YAC1D,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;gBAC1C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACpD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,IAAI,CAAC,oCAAoC,IAAI,UAAU,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC;YACnF,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,YAAoB;QACrC,MAAM,eAAe,GAAG,MAAM,aAAa,CAAC,YAAY,CAAC,CAAC;QAC1D,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC;IACxD,CAAC;IAED,mBAAmB;QACjB,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA4CpB,CAAC;QACL,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC;IACxD,CAAC;IAEO,eAAe,CAAC,OAAe;QACrC,OAAO,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,eAAe,CAAC,IAAY,EAAE,OAAe;QAC3C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,CAAC,OAAe,EAAE,WAAgC,EAAE,MAAc;QACtE,OAAO,IAAI,CAAC,QAAQ,CAAC;YACnB,OAAO;YACP,WAAW;YACX,MAAM;YACN,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,32 @@
1
+ export interface MD2PDFConfig {
2
+ template?: string;
3
+ style?: string;
4
+ pdfOptions?: PDFOptions;
5
+ partials?: Record<string, string>;
6
+ }
7
+ export interface PDFOptions {
8
+ format?: string;
9
+ width?: string;
10
+ height?: string;
11
+ margin?: {
12
+ top?: string;
13
+ right?: string;
14
+ bottom?: string;
15
+ left?: string;
16
+ };
17
+ printBackground?: boolean;
18
+ landscape?: boolean;
19
+ }
20
+ export declare const DEFAULT_CONFIG: MD2PDFConfig;
21
+ export interface FrontMatter {
22
+ title?: string;
23
+ author?: string;
24
+ date?: string;
25
+ [key: string]: any;
26
+ }
27
+ export interface ConversionResult {
28
+ success: boolean;
29
+ outputPath?: string;
30
+ error?: string;
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QACP,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAGD,eAAO,MAAM,cAAc,EAAE,YAa5B,CAAC;AAGF,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAGD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
package/dist/types.js ADDED
@@ -0,0 +1,16 @@
1
+ // Default configuration
2
+ export const DEFAULT_CONFIG = {
3
+ template: './templates/default.hbs',
4
+ style: './templates/styles.css',
5
+ pdfOptions: {
6
+ format: 'A4',
7
+ margin: {
8
+ top: '1cm',
9
+ right: '1cm',
10
+ bottom: '1cm',
11
+ left: '1cm'
12
+ },
13
+ printBackground: true
14
+ }
15
+ };
16
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAsBA,wBAAwB;AACxB,MAAM,CAAC,MAAM,cAAc,GAAiB;IAC1C,QAAQ,EAAE,yBAAyB;IACnC,KAAK,EAAE,wBAAwB;IAC/B,UAAU,EAAE;QACV,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE;YACN,GAAG,EAAE,KAAK;YACV,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,KAAK;SACZ;QACD,eAAe,EAAE,IAAI;KACtB;CACF,CAAC"}
@@ -0,0 +1,7 @@
1
+ export declare function readFileAsync(path: string): Promise<string>;
2
+ export declare function writeFileAsync(path: string, content: string): Promise<void>;
3
+ export declare function fileExists(path: string): Promise<boolean>;
4
+ export declare function resolvePath(from: string, to: string): string;
5
+ export declare function getFileName(path: string): string;
6
+ export declare function slugify(text: string): string;
7
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAOA,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjE;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjF;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO/D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIhD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO5C"}
package/dist/utils.js ADDED
@@ -0,0 +1,39 @@
1
+ import { readFile, stat, writeFile, mkdir } from 'fs/promises';
2
+ import { dirname, resolve, extname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = dirname(__filename);
6
+ export async function readFileAsync(path) {
7
+ return await readFile(path, 'utf-8');
8
+ }
9
+ export async function writeFileAsync(path, content) {
10
+ const dir = dirname(path);
11
+ await mkdir(dir, { recursive: true });
12
+ await writeFile(path, content, 'utf-8');
13
+ }
14
+ export async function fileExists(path) {
15
+ try {
16
+ await stat(path);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ export function resolvePath(from, to) {
24
+ return resolve(dirname(from), to);
25
+ }
26
+ export function getFileName(path) {
27
+ const name = resolve(path).split('/').pop() || 'output';
28
+ const ext = extname(name);
29
+ return ext ? name.slice(0, -ext.length) : name;
30
+ }
31
+ export function slugify(text) {
32
+ return text
33
+ .toLowerCase()
34
+ .trim()
35
+ .replace(/[^\w\s-]/g, '')
36
+ .replace(/[\s_-]+/g, '-')
37
+ .replace(/^-+|-+$/g, '');
38
+ }
39
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAQ,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY;IAC9C,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,OAAe;IAChE,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,MAAM,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC3C,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,EAAU;IAClD,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,QAAQ,CAAC;IACxD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,IAAI;SACR,WAAW,EAAE;SACb,IAAI,EAAE;SACN,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;SACxB,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAC7B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "md2pdf2",
3
+ "version": "0.1.0",
4
+ "description": "Convert Markdown to PDF using customizable templates",
5
+ "type": "module",
6
+ "main": "dist/cli.js",
7
+ "bin": {
8
+ "md2pdf": "dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/cli.js",
14
+ "test": "node dist/cli.js convert examples/demo.md -o demo.pdf",
15
+ "lint": "eslint src --ext .ts",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "markdown",
20
+ "pdf",
21
+ "converter",
22
+ "cli",
23
+ "templates"
24
+ ],
25
+ "author": "Vinci Rufus",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/areai51/md2pdf.git"
30
+ },
31
+ "dependencies": {
32
+ "commander": "^12.1.0",
33
+ "front-matter": "^4.0.2",
34
+ "handlebars": "^4.7.8",
35
+ "marked": "^11.1.1",
36
+ "puppeteer": "^23.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.11.0",
40
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
41
+ "@typescript-eslint/parser": "^6.19.0",
42
+ "eslint": "^8.56.0",
43
+ "typescript": "^5.3.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "files": [
49
+ "dist",
50
+ "templates"
51
+ ]
52
+ }
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{#if frontMatter.title}}{{frontMatter.title}}{{else}}Document{{/if}}</title>
7
+ <style>
8
+ {{{styles}}}
9
+ </style>
10
+ </head>
11
+ <body>
12
+ {{#if (hasPartial "header")}}
13
+ {{> header}}
14
+ {{/if}}
15
+
16
+ <main>
17
+ {{{content}}}
18
+ </main>
19
+
20
+ {{#if (hasPartial "footer")}}
21
+ {{> footer}}
22
+ {{/if}}
23
+ </body>
24
+ </html>
@@ -0,0 +1,4 @@
1
+ <footer style="border-top: 1px solid #ddd; padding-top: 1rem; margin-top: 3rem; font-size: 0.85rem; color: #888;">
2
+ <p style="margin: 0;">Generated with md2pdf</p>
3
+ <p style="margin: 0.25rem 0 0 0;">Page <span class="pageNumber"></span></p>
4
+ </footer>
@@ -0,0 +1,9 @@
1
+ <header style="border-bottom: 1px solid #ddd; padding-bottom: 1rem; margin-bottom: 2rem;">
2
+ <h1 style="margin: 0;">{{#if frontMatter.title}}{{frontMatter.title}}{{else}}Document{{/if}}</h1>
3
+ {{#if frontMatter.author}}
4
+ <p style="color: #666; margin: 0.5rem 0 0 0;">By {{frontMatter.author}}</p>
5
+ {{/if}}
6
+ {{#if frontMatter.date}}
7
+ <p style="color: #888; font-size: 0.9rem; margin: 0.25rem 0 0 0;">{{frontMatter.date}}</p>
8
+ {{/if}}
9
+ </header>
@@ -0,0 +1,95 @@
1
+ /* Default styles for md2pdf */
2
+ body {
3
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
4
+ line-height: 1.7;
5
+ font-size: 12pt;
6
+ color: #333;
7
+ max-width: 800px;
8
+ margin: 0 auto;
9
+ }
10
+
11
+ h1, h2, h3, h4, h5, h6 {
12
+ font-weight: 600;
13
+ line-height: 1.25;
14
+ margin-top: 2rem;
15
+ margin-bottom: 1rem;
16
+ color: #111;
17
+ }
18
+
19
+ h1 { font-size: 2.25rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem; }
20
+ h2 { font-size: 1.75rem; }
21
+ h3 { font-size: 1.5rem; }
22
+
23
+ p {
24
+ margin-bottom: 1rem;
25
+ }
26
+
27
+ a {
28
+ color: #0066cc;
29
+ text-decoration: none;
30
+ }
31
+
32
+ a:hover {
33
+ text-decoration: underline;
34
+ }
35
+
36
+ code {
37
+ background: #f4f4f4;
38
+ padding: 0.2em 0.4em;
39
+ border-radius: 3px;
40
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
41
+ font-size: 0.9em;
42
+ }
43
+
44
+ pre {
45
+ background: #f4f4f4;
46
+ padding: 1rem;
47
+ border-radius: 5px;
48
+ overflow-x: auto;
49
+ font-size: 0.85rem;
50
+ line-height: 1.5;
51
+ }
52
+
53
+ pre code {
54
+ background: none;
55
+ padding: 0;
56
+ }
57
+
58
+ blockquote {
59
+ border-left: 4px solid #ddd;
60
+ padding-left: 1rem;
61
+ margin-left: 0;
62
+ color: #555;
63
+ font-style: italic;
64
+ }
65
+
66
+ table {
67
+ width: 100%;
68
+ border-collapse: collapse;
69
+ margin-bottom: 1.5rem;
70
+ }
71
+
72
+ th, td {
73
+ border: 1px solid #ddd;
74
+ padding: 0.5rem;
75
+ text-align: left;
76
+ }
77
+
78
+ th {
79
+ background: #f9f9f9;
80
+ font-weight: 600;
81
+ }
82
+
83
+ img {
84
+ max-width: 100%;
85
+ height: auto;
86
+ }
87
+
88
+ /* Print-specific adjustments */
89
+ @media print {
90
+ body {
91
+ font-size: 10pt;
92
+ }
93
+ h1 { font-size: 1.8rem; }
94
+ h2 { font-size: 1.4rem; }
95
+ }