@cenk1cenk2/md-printer 2.20.12 → 3.0.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.
Files changed (83) hide show
  1. package/README.md +332 -48
  2. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.mjs +1 -2
  3. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.mjs +1 -2
  4. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateParam.mjs +8 -0
  5. package/dist/commands/index.mjs +136 -257
  6. package/dist/components/Date.svelte +1 -0
  7. package/dist/components/Markdown.svelte +20 -0
  8. package/dist/components/Nodes.svelte +32 -0
  9. package/dist/components/Page.svelte +20 -0
  10. package/dist/components/PageBreak.svelte +5 -0
  11. package/dist/components/PageNumber.svelte +1 -0
  12. package/dist/components/SignatureBlock.svelte +56 -0
  13. package/dist/components/Title.svelte +1 -0
  14. package/dist/components/TotalPages.svelte +1 -0
  15. package/dist/components/index.ts +8 -0
  16. package/dist/constants/file.constants.mjs +1 -8
  17. package/dist/constants/index.mjs +3 -4
  18. package/dist/constants/template.constants.mjs +48 -10
  19. package/dist/interfaces/commands/index.mjs +1 -1
  20. package/dist/interfaces/commands/run.interface.mjs +1 -1
  21. package/dist/interfaces/index.mjs +1 -1
  22. package/dist/interfaces/markdown.interface.mjs +1 -0
  23. package/dist/interfaces/renderer.interface.mjs +1 -0
  24. package/dist/interfaces/session.interface.mjs +1 -0
  25. package/dist/interfaces/signature.interface.mjs +1 -0
  26. package/dist/interfaces/template.interface.mjs +1 -0
  27. package/dist/services/i18n.service.mjs +4 -8
  28. package/dist/services/index.mjs +13 -2
  29. package/dist/services/loopback.mjs +18 -0
  30. package/dist/services/markdown.service.mjs +123 -0
  31. package/dist/services/printer.module.mjs +41 -0
  32. package/dist/services/printer.service.mjs +171 -0
  33. package/dist/services/renderer/browser.mjs +41 -0
  34. package/dist/services/renderer/browser.service.mjs +56 -0
  35. package/dist/services/renderer/renderer.service.mjs +198 -0
  36. package/dist/services/session/Preview.svelte +238 -0
  37. package/dist/services/session/preview.service.mjs +187 -0
  38. package/dist/services/session/session.service.mjs +115 -0
  39. package/dist/services/signature/adapters/a-trust.adapter.mjs +106 -0
  40. package/dist/services/signature/adapters/a-trust.html +77 -0
  41. package/dist/services/signature/index.mjs +4 -0
  42. package/dist/services/signature/signature.adapter.mjs +4 -0
  43. package/dist/services/signature/signature.service.mjs +95 -0
  44. package/dist/services/template/Document.svelte +21 -0
  45. package/dist/services/template/Margin.svelte +15 -0
  46. package/dist/services/template/assets.mjs +127 -0
  47. package/dist/services/template/render.entry.ts +32 -0
  48. package/dist/services/template/svelte.plugin.mjs +66 -0
  49. package/dist/services/template/template.service.mjs +211 -0
  50. package/oclif.manifest.json +64 -33
  51. package/package.json +70 -35
  52. package/templates/default/Template.svelte +83 -0
  53. package/templates/default/settings.json +1 -3
  54. package/templates/invoice/Template.svelte +247 -0
  55. package/templates/invoice/components/Client.svelte +42 -0
  56. package/templates/invoice/components/Company.svelte +41 -0
  57. package/templates/invoice/components/Items.svelte +66 -0
  58. package/templates/invoice/components/Notes.svelte +21 -0
  59. package/templates/invoice/components/Payment.svelte +25 -0
  60. package/templates/invoice/components/Signature.svelte +34 -0
  61. package/templates/invoice/components/Totals.svelte +47 -0
  62. package/templates/invoice/i18n/de.yaml +6 -0
  63. package/templates/invoice/i18n/en.yaml +6 -0
  64. package/templates/invoice/round.js +2 -0
  65. package/templates/invoice/settings.json +1 -4
  66. package/templates/kilic.dev/Template.svelte +102 -0
  67. package/templates/kilic.dev/settings.json +1 -3
  68. package/templates/mail/Template.svelte +166 -0
  69. package/templates/mail/settings.json +1 -3
  70. package/dist/hooks/not-found.hook.mjs +0 -7
  71. package/dist/hooks/update-notifier.hook.mjs +0 -7
  72. package/dist/interfaces/pupeteer.mjs +0 -1
  73. package/templates/default/main.css +0 -73
  74. package/templates/invoice/.gitignore +0 -1
  75. package/templates/invoice/footer.html +0 -18
  76. package/templates/invoice/tailwind.css +0 -164
  77. package/templates/invoice/template.html.j2 +0 -189
  78. package/templates/kilic.dev/footer.html +0 -26
  79. package/templates/kilic.dev/header.html +0 -16
  80. package/templates/kilic.dev/main.css +0 -74
  81. package/templates/mail/.gitignore +0 -1
  82. package/templates/mail/tailwind.css +0 -145
  83. package/templates/mail/template.html.j2 +0 -14
@@ -1,311 +1,190 @@
1
- import { DEFAULT_LANGUAGE, I18N_DIRECTORY, RequiredTemplateFiles, TEMPLATE_DIRECTORY, TemplateFiles } from "../constants/template.constants.mjs";
2
1
  import { InputFileType, OutputFileType } from "../constants/file.constants.mjs";
3
- import { I18nService } from "../services/i18n.service.mjs";
2
+ import { BrowserService } from "../services/renderer/browser.service.mjs";
3
+ import { PrinterService } from "../services/printer.service.mjs";
4
+ import { PreviewService } from "../services/session/preview.service.mjs";
5
+ import { SessionService } from "../services/session/session.service.mjs";
6
+ import { PrinterModule } from "../services/printer.module.mjs";
4
7
  import "../services/index.mjs";
5
- import tailwind from "@tailwindcss/postcss";
6
- import { watch } from "chokidar";
7
- import graymatter from "gray-matter";
8
- import { defaultConfig } from "md-to-pdf/dist/lib/config.js";
9
- import { convertMdToPdf } from "md-to-pdf/dist/lib/md-to-pdf.js";
10
- import { serveDirectory } from "md-to-pdf/dist/lib/serve-dir.js";
11
- import Nunjucks from "nunjucks";
12
- import { basename, dirname, extname, isAbsolute, join } from "path";
13
- import postcss from "postcss";
14
- import * as Puppeteer from "puppeteer";
15
- import puppeteer from "puppeteer";
16
- import showdown from "showdown";
17
- import { Args, Command, ConfigService, FileSystemService, Flags, JsonParser, MergeStrategy, ParserService, YamlParser, merge } from "@cenk1cenk2/oclif-common";
18
-
8
+ import { mkdir } from "fs/promises";
9
+ import { dirname } from "path";
10
+ import { text } from "stream/consumers";
11
+ import writeFileAtomic from "write-file-atomic";
12
+ import { Args, CliModule, Command, Flags, JsonParser, ParserService, YamlParser } from "@cenk1cenk2/oclif-common";
19
13
  //#region src/commands/index.ts
20
- const { parsePDFOptions, unitToPixels } = Puppeteer;
21
14
  var MDPrinter = class extends Command {
22
- static description = "Generates a PDF from the given markdown file with the selected HTML template.";
15
+ static description = "Renders a markdown or yaml file through a Svelte template into a PDF or HTML document.";
23
16
  static flags = {
24
- stdin: Flags.string({
17
+ stdin: Flags.boolean({
25
18
  char: "I",
26
- description: "Read the input from stdin.",
27
- required: false,
28
- exclusive: ["file"],
29
- allowStdin: "only"
19
+ description: "Read the input from stdin."
30
20
  }),
31
21
  ["input-filetype"]: Flags.string({
32
22
  char: "f",
33
23
  options: Object.values(InputFileType),
34
- description: "File type to be processed. By default it is detected by the file extension.",
35
- default: "md"
24
+ description: "Input file type. Detected from the file extension, markdown for stdin and unknown extensions."
36
25
  }),
37
26
  ["output-filetype"]: Flags.string({
38
27
  char: "F",
39
28
  options: Object.values(OutputFileType),
40
- description: "File type to be processed. By default it is detected by the file extension.",
41
- default: "pdf"
29
+ description: "Output file type, defaults to pdf. Overwrites the one defined in front-matter."
42
30
  }),
43
31
  stdout: Flags.boolean({
44
32
  char: "O",
45
- description: "Write to stdout instead of a file."
33
+ description: "Write the output to stdout instead of a file."
46
34
  }),
47
35
  template: Flags.string({
48
36
  char: "t",
49
- default: "default",
50
- description: "HTML template for the generated PDF file."
37
+ description: "Built-in template name or path to a template directory, defaults to \"default\". Overwrites the one defined in front-matter."
51
38
  }),
52
39
  title: Flags.string({
53
40
  char: "T",
54
- description: "Overwrite document title."
41
+ description: "Document title. Overwrites the one defined in front-matter or the template."
55
42
  }),
56
43
  language: Flags.string({
57
44
  char: "l",
58
- description: "Language code for template translations. Overwrites the one defined in front-matter and template settings."
45
+ description: "Language for template translations. Overwrites the one defined in front-matter and template settings."
59
46
  }),
60
47
  browser: Flags.string({
61
48
  char: "b",
62
- description: "Browser path that is going to be used for the PDF generation.",
63
- default: "/usr/bin/brave"
49
+ description: "Browser executable used for printing. Discovered automatically when omitted (Brave, Chrome, Chromium, Edge)."
50
+ }),
51
+ continuous: Flags.boolean({ description: "Render the PDF as a single continuous page without page breaks." }),
52
+ preview: Flags.boolean({
53
+ char: "p",
54
+ description: "Rebuild whenever the input, the template or its assets change, show the HTML and the PDF in a live preview and rewrite the output file when there is one."
64
55
  }),
65
- watch: Flags.boolean({
66
- char: "w",
67
- description: "Watch the changes on the given file."
56
+ host: Flags.string({
57
+ description: "Loopback host for the preview server, it serves the files next to the input and is never exposed to the network.",
58
+ options: [
59
+ "127.0.0.1",
60
+ "localhost",
61
+ "::1"
62
+ ],
63
+ default: "127.0.0.1"
68
64
  }),
69
- dev: Flags.boolean({
70
- char: "d",
71
- description: "Run with Chrome browser instead of publishing the file."
65
+ port: Flags.integer({
66
+ description: "Port for the preview server, random when 0.",
67
+ default: 0
72
68
  }),
73
- continuous: Flags.boolean({ description: "Render PDF as a single continuous page without page breaks." })
69
+ open: Flags.boolean({
70
+ description: "Open the preview window, --no-open keeps rebuilding and writing without one.",
71
+ default: true,
72
+ allowNo: true
73
+ }),
74
+ sign: Flags.boolean({ description: "Sign the PDF with a qualified electronic signature through ID Austria (A-Trust)." }),
75
+ ["sign-provider"]: Flags.string({ description: "Signature provider, defaults to a-trust. Overwrites the one defined in front-matter and template settings." }),
76
+ ["sign-certificate"]: Flags.string({ description: "Signer certificate (DER or PEM) used for the visible signature block and to verify the returned signature." })
74
77
  };
75
78
  static args = {
76
- file: Args.string({ description: "File to be processed." }),
79
+ file: Args.string({
80
+ description: "Markdown or yaml file to render.",
81
+ ignoreStdin: true
82
+ }),
77
83
  output: Args.string({
78
- description: "Output file that will be generated. Overwrites the one define in front-matter.",
79
- required: false
84
+ description: "Output file. Overwrites the one defined in front-matter.",
85
+ required: false,
86
+ ignoreStdin: true
80
87
  })
81
88
  };
82
- nunjucks = Nunjucks.configure({
83
- autoescape: false,
84
- throwOnUndefined: true,
85
- trimBlocks: true,
86
- lstripBlocks: false
87
- });
88
- cs;
89
- fs;
90
- i18n;
89
+ printer;
91
90
  browser;
92
- register(cli) {
93
- return {
94
- ...cli,
95
- providers: [...cli.providers ?? [], I18nService]
96
- };
91
+ register(_cli, options) {
92
+ const cli = CliModule.forRoot({
93
+ ...options,
94
+ logger: { stderr: true }
95
+ });
96
+ cli.imports.push(PrinterModule.forRoot({ browser: this.flags.browser }));
97
+ return cli;
97
98
  }
98
99
  async shouldRunBefore() {
99
- this.cs = this.app.get(ConfigService);
100
- this.fs = this.app.get(FileSystemService);
101
- this.i18n = this.app.get(I18nService);
102
100
  await this.app.get(ParserService).register(JsonParser, YamlParser);
103
- this.nunjucks.addFilter("markdown_to_html", (markdown) => {
104
- return new showdown.Converter().makeHtml(markdown);
105
- });
106
- this.tasks.options = { silentRendererCondition: true };
107
- if (!this.args.file && !this.flags.stdin) throw new Error("Either --stdin or file argument must be provided.");
101
+ this.printer = this.app.get(PrinterService);
102
+ this.browser = this.app.get(BrowserService);
108
103
  }
109
104
  async run() {
110
- this.tasks.add([{ task: async (ctx) => {
111
- if (this.args.file) {
112
- ctx.file = isAbsolute(this.args.file) ? this.args.file : join(process.cwd(), this.args.file);
113
- if (!this.fs.exists(ctx.file)) throw new Error(`File does not exists: ${ctx.file}`);
114
- this.logger.info("Loading file: %s", ctx.file);
115
- ctx.content = await this.fs.read(ctx.file);
116
- } else ctx.content = this.flags.stdin;
117
- switch (this.flags["input-filetype"] ? this.flags["input-filetype"] : extname(ctx.file).replace(/^\./, "")) {
118
- case "md": {
119
- const data = graymatter(ctx.content);
120
- ctx.content = data.content;
121
- ctx.metadata = data.data;
122
- break;
123
- }
124
- case "yaml":
125
- case "yml":
126
- ctx.metadata = await this.app.get(ParserService).parse(ctx.file, ctx.content);
127
- break;
128
- default: throw new Error("File type is not accepted.");
129
- }
130
- } }, { task: async (ctx) => {
131
- const template = ctx.metadata?.template ?? this.flags.template;
132
- ctx.templates = (/* @__PURE__ */ new RegExp(/\.\.?\//)).test(template) ? join(process.cwd(), template) : join(this.config.root, TEMPLATE_DIRECTORY, template);
133
- this.logger.info("Loading template: %s from %s", template, ctx.templates);
134
- await Promise.all(RequiredTemplateFiles.map(async (file) => {
135
- const current = join(ctx.templates, file);
136
- if (!this.fs.exists(current)) throw new Error(`Template does not exists: ${current}`);
137
- }));
138
- const paths = {
139
- ["settings.json"]: join(ctx.templates, "settings.json"),
140
- ["main.css"]: join(ctx.templates, "main.css"),
141
- ["tailwind.css"]: join(ctx.templates, "tailwind.css"),
142
- ["header.html"]: join(ctx.templates, "header.html"),
143
- ["footer.html"]: join(ctx.templates, "footer.html"),
144
- ["template.html.j2"]: join(ctx.templates, "template.html.j2")
145
- };
146
- const title = ctx.metadata?.document_title ?? this.flags.title;
147
- ctx.options = await this.cs.extend([paths["settings.json"], {
148
- dest: this.args?.output ?? ctx.metadata?.dest ?? `${basename(this.args.file, extname(this.args.file))}.${this.flags["output-filetype"]}`,
149
- ...title ? { document_title: title } : {},
150
- launch_options: { executablePath: this.flags.browser }
151
- }]);
152
- ctx.renderContinuous = this.flags.continuous || ctx.metadata?.continuous === true || ctx.options.continuous === true;
153
- this.flags.stdout ??= ctx.metadata.stdout;
154
- if (this.flags.stdout) ctx.options.dest = "stdout";
155
- if (this.flags["output-filetype"] === "html" || ctx.metadata["output-filetype"] === "html") ctx.options.as_html = true;
156
- this.logger.debug("Options: %o", ctx.options);
157
- if (this.fs.exists(paths["header.html"])) {
158
- this.logger.debug("Header exists for template.");
159
- ctx.options.pdf_options.headerTemplate = await this.fs.read(paths["header.html"]);
160
- }
161
- if (this.fs.exists(paths["footer.html"])) {
162
- this.logger.debug("Footer exists for template.");
163
- ctx.options.pdf_options.footerTemplate = await this.fs.read(paths["footer.html"]);
164
- }
165
- if (this.fs.exists(paths["template.html.j2"])) {
166
- this.logger.debug("Design template exists for template.");
167
- ctx.template = await this.fs.read(paths["template.html.j2"]);
168
- this.logger.debug("Metadata: %o", ctx.metadata);
169
- }
170
- if (this.fs.exists(paths["main.css"])) {
171
- this.logger.debug("CSS exists for template.");
172
- ctx.options.css = await this.fs.read(paths["main.css"]);
173
- }
174
- if (this.fs.exists(paths["tailwind.css"])) {
175
- this.logger.debug("Tailwind CSS exists for template: %s -> %s", paths["tailwind.css"]);
176
- ctx.options.css = await postcss([tailwind({ base: ctx.templates })]).process(await this.fs.read(paths["tailwind.css"]), { from: paths["tailwind.css"] }).then((result) => result.css);
177
- }
178
- await this.applyTranslations(ctx);
179
- await this.applyDocumentTitle(ctx);
180
- } }]);
181
- }
182
- async shouldRunAfter(ctx) {
183
- if (this.flags.watch) {
184
- this.logger.info("Running in watch mode.");
185
- watch([
186
- TEMPLATE_DIRECTORY,
187
- this.args.file,
188
- join(ctx.templates, "**/*")
189
- ]).on("change", async () => {
190
- await this.run();
191
- const ctx = await this.runTasks();
192
- this.logger.info("Waiting for the next change.");
193
- return this.runMd2Pdf(ctx).catch((err) => {
194
- this.logger.error(err);
195
- });
196
- });
197
- return this.runMd2Pdf(ctx);
198
- }
199
- await this.runMd2Pdf(ctx);
200
- if (this.browser) await this.browser.close();
105
+ const flags = {
106
+ file: this.flags.stdin ? void 0 : this.args.file,
107
+ output: this.flags.stdin ? this.args.output ?? this.args.file : this.args.output,
108
+ stdin: this.flags.stdin ? await text(process.stdin) : void 0,
109
+ inputFiletype: this.flags["input-filetype"],
110
+ outputFiletype: this.flags["output-filetype"],
111
+ stdout: this.flags.stdout,
112
+ template: this.flags.template,
113
+ title: this.flags.title,
114
+ language: this.flags.language,
115
+ continuous: this.flags.continuous,
116
+ sign: this.flags.sign,
117
+ signProvider: this.flags["sign-provider"],
118
+ signCertificate: this.flags["sign-certificate"]
119
+ };
120
+ if (this.flags.preview) await this.session(flags);
121
+ else await this.print(flags);
201
122
  }
202
- context(ctx) {
203
- const context = {
204
- ...ctx.metadata ?? {},
205
- content: ctx.content
123
+ async print(flags) {
124
+ const input = await this.printer.input(flags);
125
+ const ctx = {
126
+ input,
127
+ options: await this.printer.options(flags, input)
206
128
  };
207
- if (ctx.t) {
208
- context.t = ctx.t;
209
- context.language = ctx.language;
129
+ this.logger.debug("Options: %o", ctx.options);
130
+ await this.printer.prepare(ctx);
131
+ const document = await this.printer.document(ctx, await this.printer.bundle(ctx.options));
132
+ let { content } = await this.printer.output(ctx, document);
133
+ if (ctx.signature) {
134
+ content = await this.printer.sign(ctx, content);
135
+ this.logger.info("Document is signed.");
210
136
  }
211
- return context;
212
- }
213
- async applyDocumentTitle(ctx) {
214
- const title = ctx.options.document_title ?? this.args.file;
215
- if (!title) return;
216
- ctx.options.document_title = this.nunjucks.renderString(title, this.context(ctx));
217
- this.logger.info("Document title will be: %s", ctx.options.document_title);
137
+ await this.write(ctx, content);
218
138
  }
219
- async applyTranslations(ctx) {
220
- ctx.language = this.flags.language ?? ctx.metadata?.language ?? ctx.options.i18n?.language ?? "en";
221
- ctx.t = await this.i18n.load(join(ctx.templates, I18N_DIRECTORY), {
222
- language: ctx.language,
223
- fallback: ctx.options.i18n?.fallback
139
+ async session(flags) {
140
+ if (flags.stdout) throw new Error("Writing to stdout is not supported with --preview.");
141
+ const session = this.app.get(SessionService);
142
+ const preview = this.app.get(PreviewService);
143
+ let writes = Promise.resolve();
144
+ session.on("building", () => preview.building());
145
+ session.on("failed", (err) => {
146
+ this.logger.error(err.message);
147
+ preview.failed(err);
224
148
  });
225
- if (ctx.t) this.logger.info("Rendering with language: %s", ctx.language);
226
- }
227
- continuousBrowser(browser) {
228
- return new Proxy(browser, { get: (target, property, receiver) => {
229
- if (property !== "newPage") {
230
- const value = Reflect.get(target, property, receiver);
231
- return typeof value === "function" ? value.bind(target) : value;
232
- }
233
- return async () => {
234
- const page = await browser.newPage();
235
- const pdf = page.pdf.bind(page);
236
- page.pdf = async (pdfOptions = {}) => {
237
- const parsed = parsePDFOptions(pdfOptions, "cm");
238
- const width = parsed.landscape ? parsed.height : parsed.width;
239
- const margin = parsed.margin;
240
- const viewport = page.viewport();
241
- await page.setViewport({
242
- width: Math.ceil(Math.max(width - margin.left - margin.right, 1) * unitToPixels.cm),
243
- height: viewport?.height ?? 800,
244
- deviceScaleFactor: viewport?.deviceScaleFactor ?? 1
245
- });
246
- const contentHeight = await page.evaluate(async () => {
247
- if ("fonts" in document) await document.fonts.ready;
248
- await Promise.all(Array.from(document.images).map((image) => {
249
- if (image.complete) return;
250
- return new Promise((resolve) => {
251
- image.addEventListener("load", resolve, { once: true });
252
- image.addEventListener("error", resolve, { once: true });
253
- });
254
- }));
255
- const bodyTop = document.body.getBoundingClientRect().top;
256
- const bottom = Math.max(document.body.getBoundingClientRect().bottom, ...Array.from(document.body.querySelectorAll("*")).map((element) => element.getBoundingClientRect().bottom));
257
- return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.getBoundingClientRect().height, document.documentElement.getBoundingClientRect().height, bottom - bodyTop);
258
- });
259
- const height = parsePDFOptions({ height: Math.ceil(contentHeight + unitToPixels.cm) }, "cm").height + margin.top + margin.bottom;
260
- const options = {
261
- ...pdfOptions,
262
- width: `${width}cm`,
263
- height: `${height}cm`,
264
- landscape: false
265
- };
266
- delete options.format;
267
- return pdf(options);
268
- };
269
- return page;
270
- };
271
- } });
272
- }
273
- async runMd2Pdf(ctx) {
274
- const options = merge(MergeStrategy.EXTEND, { basedir: process.cwd() }, defaultConfig, ctx.options, { devtools: this.flags.dev });
275
- if (!this.browser) {
276
- const browser = await puppeteer.launch({
277
- devtools: options.devtools,
278
- ...options.launch_options
279
- });
280
- this.browser = ctx.renderContinuous ? this.continuousBrowser(browser) : browser;
281
- }
282
- const server = await serveDirectory(ctx.options);
283
- options.port = server.address().port;
284
- let output;
285
- if (ctx.template) {
286
- this.logger.info("Rendering as template.");
287
- output = await convertMdToPdf({ content: this.nunjucks.renderString(ctx.template, this.context(ctx)) }, options, { browser: this.browser });
288
- } else {
289
- this.logger.info("Rendering as plain file.");
290
- output = await convertMdToPdf({ content: ctx.content }, options, { browser: this.browser });
291
- }
292
- await new Promise((resolve, reject) => server.close((err) => {
293
- if (err) return reject(err);
294
- resolve(null);
295
- }));
296
- if (output) {
297
- if (this.flags.stdout) {
298
- process.stdout.write(output.content);
149
+ session.on("ready", async (build) => {
150
+ preview.ready(build);
151
+ if (!build.ctx.options.dest) {
152
+ this.logger.info("Rebuilt in %dms.", build.ms);
299
153
  return;
300
154
  }
301
- if (!output.filename) throw new Error("Output should either be defined with the variable or front-matter.");
302
- this.logger.info("Output file will be: %s", output.filename);
303
- await this.fs.mkdir(dirname(output.filename));
304
- this.logger.info("Writing file to output: %s", output.filename);
305
- await this.fs.write(output.filename, output.content);
155
+ session.ignore(build.ctx.options.dest);
156
+ writes = writes.then(() => this.write(build.ctx, build.output)).catch((err) => this.logger.error(err.message));
157
+ await writes;
158
+ });
159
+ if (flags.sign) this.logger.warn("Signing is skipped while previewing, run without --preview to sign the document.");
160
+ process.removeAllListeners("SIGINT");
161
+ process.removeAllListeners("SIGTERM");
162
+ const stopped = new Promise((resolve) => {
163
+ process.once("SIGINT", () => resolve());
164
+ process.once("SIGTERM", () => resolve());
165
+ });
166
+ await session.start(flags);
167
+ const url = await preview.listen({
168
+ host: this.flags.host,
169
+ port: this.flags.port
170
+ });
171
+ this.logger.info("Preview is running at %s, press Ctrl+C to stop.", url);
172
+ await Promise.race([stopped, this.flags.open ? preview.open(this.browser.executable(), url) : new Promise(() => void 0)]);
173
+ await preview.close();
174
+ await session.stop();
175
+ await writes;
176
+ }
177
+ async write(ctx, content) {
178
+ const { dest } = ctx.options;
179
+ if (!dest) {
180
+ if (!this.flags.stdout) throw new Error("No output file to write, pass an output argument, set dest in front-matter or use --stdout.");
181
+ process.stdout.write(content);
182
+ return;
306
183
  }
184
+ await mkdir(dirname(dest), { recursive: true });
185
+ await writeFileAtomic(dest, content);
186
+ this.logger.info("Written: %s", dest);
307
187
  }
308
188
  };
309
-
310
189
  //#endregion
311
- export { MDPrinter as default };
190
+ export { MDPrinter as default };
@@ -0,0 +1 @@
1
+ <span class="date"></span>
@@ -0,0 +1,20 @@
1
+ <!--
2
+ @component
3
+ The markdown document, with directives rendered as components. Styled by github-markdown-css when the template imports it.
4
+ -->
5
+ <script>
6
+ import { getContext } from 'svelte'
7
+
8
+ import Nodes from './Nodes.svelte'
9
+
10
+ /** @type {{ class?: string }} */
11
+ let { class: className = '' } = $props()
12
+ </script>
13
+
14
+ <article class="markdown-body {className}"><Nodes nodes={getContext('md-printer:markdown')} /></article>
15
+
16
+ <style>
17
+ :global(.page-break) {
18
+ break-after: page;
19
+ }
20
+ </style>
@@ -0,0 +1,32 @@
1
+ <!--
2
+ @component
3
+ Resolved markdown: HTML as is, directives as the components registered for them, and the elements around a directive as elements.
4
+ -->
5
+ <script>
6
+ import { getContext } from 'svelte'
7
+
8
+ // eslint-disable-next-line import/no-self-import -- a Svelte 5 component recurses by importing itself
9
+ import Nodes from './Nodes.svelte'
10
+
11
+ /** @type {{ nodes: import('../interfaces/markdown.interface.js').MarkdownNode[] }} */
12
+ let { nodes } = $props()
13
+
14
+ /** @type {Record<string, import('svelte').Component<any>>} */
15
+ const components = getContext('md-printer:components')
16
+ </script>
17
+
18
+ {#each nodes as node, index (index)}
19
+ {#if node.type === 'html'}
20
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- markdown input is trusted, same as the document itself -->
21
+ {@html node.html}
22
+ {:else if node.type === 'element'}
23
+ <svelte:element this={node.tag} {...node.attributes}><Nodes nodes={node.children} /></svelte:element>
24
+ {:else}
25
+ {@const Component = components[node.name]}
26
+ {#if node.children.length}
27
+ <Component {...node.props}><Nodes nodes={node.children} /></Component>
28
+ {:else}
29
+ <Component {...node.props} />
30
+ {/if}
31
+ {/if}
32
+ {/each}
@@ -0,0 +1,20 @@
1
+ <!--
2
+ @component
3
+ Page layout: renders `children` as the document body and the `header` / `footer` snippets into the printed header and footer of every page.
4
+ -->
5
+ <script>
6
+ import { getContext } from 'svelte'
7
+
8
+ /** @type {{ header?: import('svelte').Snippet, footer?: import('svelte').Snippet, children?: import('svelte').Snippet }} */
9
+ let { header, footer, children } = $props()
10
+
11
+ const mode = getContext('md-printer:mode')
12
+ </script>
13
+
14
+ {#if mode === 'header'}
15
+ {@render header?.()}
16
+ {:else if mode === 'footer'}
17
+ {@render footer?.()}
18
+ {:else}
19
+ {@render children?.()}
20
+ {/if}
@@ -0,0 +1,5 @@
1
+ <!--
2
+ @component
3
+ Starts a new page in the printed PDF. In markdown, `<div class="page-break"></div>` does the same.
4
+ -->
5
+ <div class="page-break" style="break-after: page"></div>
@@ -0,0 +1 @@
1
+ <span class="pageNumber"></span>
@@ -0,0 +1,56 @@
1
+ <!--
2
+ @component
3
+ Visible block for a digitally signed document, filled from the `signature` prop md-printer passes to templates when signing is enabled.
4
+ Styled with Tailwind classes, so the template needs a `<style lang="postcss">` block importing Tailwind.
5
+ -->
6
+ <script>
7
+ /**
8
+ * @type {{
9
+ * signature: { date: string, name?: string, issuer?: string, serial?: string },
10
+ * labels?: { title?: string, by?: string, date?: string, issuer?: string, verify?: string },
11
+ * class?: string
12
+ * }}
13
+ */
14
+ let { signature, labels = {}, class: className = '' } = $props()
15
+
16
+ const text = $derived({
17
+ title: 'Digitally signed',
18
+ by: 'Signed by',
19
+ date: 'Date',
20
+ issuer: 'Certificate',
21
+ verify: 'This document carries a qualified electronic signature. Verify it at',
22
+ ...labels
23
+ })
24
+ </script>
25
+
26
+ <div class="w-full overflow-hidden rounded-xl border-y-2 shadow-md shadow-gray-300 border-gray-900! text-xs {className}">
27
+ <div class="flex items-center gap-3 px-5 py-3 bg-gray-900 text-white">
28
+ <svg class="w-5 h-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
29
+ <path d="M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6l7-3z" />
30
+ <path d="M9 12l2 2 4-4" />
31
+ </svg>
32
+ <span class="font-extrabold uppercase tracking-wider">{text.title}</span>
33
+ </div>
34
+ <div class="flex flex-col px-5 py-2">
35
+ {#if signature.name}
36
+ <div class="flex justify-between gap-4 py-2">
37
+ <span class="font-semibold text-gray-400">{text.by}</span>
38
+ <span class="font-semibold text-right">{signature.name}</span>
39
+ </div>
40
+ {/if}
41
+ <div class="flex justify-between gap-4 py-2 border-t border-t-gray-100/30">
42
+ <span class="font-semibold text-gray-400">{text.date}</span>
43
+ <span class="text-right">{signature.date}</span>
44
+ </div>
45
+ {#if signature.issuer}
46
+ <div class="flex justify-between gap-4 py-2 border-t border-t-gray-100/30">
47
+ <span class="font-semibold text-gray-400">{text.issuer}</span>
48
+ <span class="text-right text-[0.6rem]">{signature.issuer} · {signature.serial}</span>
49
+ </div>
50
+ {/if}
51
+ </div>
52
+ <p class="px-5 py-3 bg-gray-50 text-[0.6rem] text-gray-400 leading-snug">
53
+ {text.verify}
54
+ <a href="https://www.signaturpruefung.gv.at" class="font-semibold text-gray-900 underline">signaturpruefung.gv.at</a>
55
+ </p>
56
+ </div>
@@ -0,0 +1 @@
1
+ <span class="title"></span>
@@ -0,0 +1 @@
1
+ <span class="totalPages"></span>
@@ -0,0 +1,8 @@
1
+ export { default as Date } from './Date.svelte'
2
+ export { default as Markdown } from './Markdown.svelte'
3
+ export { default as Page } from './Page.svelte'
4
+ export { default as PageBreak } from './PageBreak.svelte'
5
+ export { default as PageNumber } from './PageNumber.svelte'
6
+ export { default as SignatureBlock } from './SignatureBlock.svelte'
7
+ export { default as Title } from './Title.svelte'
8
+ export { default as TotalPages } from './TotalPages.svelte'
@@ -1,10 +1,4 @@
1
1
  //#region src/constants/file.constants.ts
2
- const OUTPUT_FILE_ACCEPTED_TYPES = [".pdf"];
3
- const INPUT_FILE_ACCEPTED_TYPES = [
4
- ".md",
5
- ".yml",
6
- ".yaml"
7
- ];
8
2
  let OutputFileType = /* @__PURE__ */ function(OutputFileType) {
9
3
  OutputFileType["PDF"] = "pdf";
10
4
  OutputFileType["HTML"] = "html";
@@ -16,6 +10,5 @@ let InputFileType = /* @__PURE__ */ function(InputFileType) {
16
10
  InputFileType["YAML_SHORT"] = "yml";
17
11
  return InputFileType;
18
12
  }({});
19
-
20
13
  //#endregion
21
- export { INPUT_FILE_ACCEPTED_TYPES, InputFileType, OUTPUT_FILE_ACCEPTED_TYPES, OutputFileType };
14
+ export { InputFileType, OutputFileType };
@@ -1,4 +1,3 @@
1
- import { DEFAULT_LANGUAGE, I18N_ACCEPTED_TYPES, I18N_DIRECTORY, RequiredTemplateFiles, TEMPLATE_DIRECTORY, TemplateFiles } from "./template.constants.mjs";
2
- import { INPUT_FILE_ACCEPTED_TYPES, InputFileType, OUTPUT_FILE_ACCEPTED_TYPES, OutputFileType } from "./file.constants.mjs";
3
-
4
- export { DEFAULT_LANGUAGE, I18N_ACCEPTED_TYPES, I18N_DIRECTORY, INPUT_FILE_ACCEPTED_TYPES, InputFileType, OUTPUT_FILE_ACCEPTED_TYPES, OutputFileType, RequiredTemplateFiles, TEMPLATE_DIRECTORY, TemplateFiles };
1
+ import { ContextKey, DEFAULT_HIGHLIGHT_STYLE, DEFAULT_LANGUAGE, DEFAULT_PDF_OPTIONS, DEFAULT_TEMPLATE, DIRECTIVE_TAG, Directory, I18N_ACCEPTED_TYPES, MediaType, ModuleSpecifier, PAGE_BREAK_CLASS, PRINTER_MODULE_OPTIONS, RenderMode, TAILWIND_STYLE_LANG, TemplateFiles } from "./template.constants.mjs";
2
+ import { InputFileType, OutputFileType } from "./file.constants.mjs";
3
+ export { ContextKey, DEFAULT_HIGHLIGHT_STYLE, DEFAULT_LANGUAGE, DEFAULT_PDF_OPTIONS, DEFAULT_TEMPLATE, DIRECTIVE_TAG, Directory, I18N_ACCEPTED_TYPES, InputFileType, MediaType, ModuleSpecifier, OutputFileType, PAGE_BREAK_CLASS, PRINTER_MODULE_OPTIONS, RenderMode, TAILWIND_STYLE_LANG, TemplateFiles };