@cenk1cenk2/md-printer 2.21.0 → 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.
- package/README.md +332 -48
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.mjs +1 -2
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.mjs +1 -2
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateParam.mjs +8 -0
- package/dist/commands/index.mjs +133 -295
- package/dist/components/Date.svelte +1 -0
- package/dist/components/Markdown.svelte +20 -0
- package/dist/components/Nodes.svelte +32 -0
- package/dist/components/Page.svelte +20 -0
- package/dist/components/PageBreak.svelte +5 -0
- package/dist/components/PageNumber.svelte +1 -0
- package/dist/components/SignatureBlock.svelte +56 -0
- package/dist/components/Title.svelte +1 -0
- package/dist/components/TotalPages.svelte +1 -0
- package/dist/components/index.ts +8 -0
- package/dist/constants/file.constants.mjs +1 -8
- package/dist/constants/index.mjs +3 -4
- package/dist/constants/template.constants.mjs +48 -10
- package/dist/interfaces/commands/index.mjs +1 -1
- package/dist/interfaces/commands/run.interface.mjs +1 -1
- package/dist/interfaces/index.mjs +1 -1
- package/dist/interfaces/markdown.interface.mjs +1 -0
- package/dist/interfaces/renderer.interface.mjs +1 -0
- package/dist/interfaces/session.interface.mjs +1 -0
- package/dist/interfaces/signature.interface.mjs +1 -1
- package/dist/interfaces/template.interface.mjs +1 -0
- package/dist/services/i18n.service.mjs +4 -8
- package/dist/services/index.mjs +9 -2
- package/dist/services/loopback.mjs +18 -0
- package/dist/services/markdown.service.mjs +123 -0
- package/dist/services/printer.module.mjs +41 -0
- package/dist/services/printer.service.mjs +171 -0
- package/dist/services/renderer/browser.mjs +41 -0
- package/dist/services/renderer/browser.service.mjs +56 -0
- package/dist/services/renderer/renderer.service.mjs +198 -0
- package/dist/services/session/Preview.svelte +238 -0
- package/dist/services/session/preview.service.mjs +187 -0
- package/dist/services/session/session.service.mjs +115 -0
- package/dist/services/signature/adapters/a-trust.adapter.mjs +27 -37
- package/dist/services/signature/index.mjs +1 -2
- package/dist/services/signature/signature.adapter.mjs +1 -2
- package/dist/services/signature/signature.service.mjs +1 -3
- package/dist/services/template/Document.svelte +21 -0
- package/dist/services/template/Margin.svelte +15 -0
- package/dist/services/template/assets.mjs +127 -0
- package/dist/services/template/render.entry.ts +32 -0
- package/dist/services/template/svelte.plugin.mjs +66 -0
- package/dist/services/template/template.service.mjs +211 -0
- package/oclif.manifest.json +45 -34
- package/package.json +64 -31
- package/templates/default/Template.svelte +83 -0
- package/templates/default/settings.json +1 -3
- package/templates/invoice/Template.svelte +247 -0
- package/templates/invoice/components/Client.svelte +42 -0
- package/templates/invoice/components/Company.svelte +41 -0
- package/templates/invoice/components/Items.svelte +66 -0
- package/templates/invoice/components/Notes.svelte +21 -0
- package/templates/invoice/components/Payment.svelte +25 -0
- package/templates/invoice/components/Signature.svelte +34 -0
- package/templates/invoice/components/Totals.svelte +47 -0
- package/templates/invoice/round.js +2 -0
- package/templates/invoice/settings.json +1 -4
- package/templates/kilic.dev/Template.svelte +102 -0
- package/templates/kilic.dev/settings.json +1 -3
- package/templates/mail/Template.svelte +166 -0
- package/templates/mail/settings.json +1 -3
- package/dist/hooks/not-found.hook.mjs +0 -7
- package/dist/hooks/update-notifier.hook.mjs +0 -7
- package/dist/interfaces/pupeteer.mjs +0 -1
- package/templates/default/main.css +0 -73
- package/templates/invoice/.gitignore +0 -1
- package/templates/invoice/footer.html +0 -18
- package/templates/invoice/tailwind.css +0 -164
- package/templates/invoice/template.html.j2 +0 -226
- package/templates/kilic.dev/footer.html +0 -26
- package/templates/kilic.dev/header.html +0 -16
- package/templates/kilic.dev/main.css +0 -74
- package/templates/mail/.gitignore +0 -1
- package/templates/mail/tailwind.css +0 -145
- package/templates/mail/template.html.j2 +0 -14
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import "../../constants/template.constants.mjs";
|
|
2
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.mjs";
|
|
3
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.mjs";
|
|
4
|
+
import { PrinterService } from "../printer.service.mjs";
|
|
5
|
+
import { dirname, join, resolve } from "path";
|
|
6
|
+
import { LoggerService } from "@cenk1cenk2/oclif-common";
|
|
7
|
+
import { Injectable } from "@nestjs/common";
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import { watch } from "chokidar";
|
|
10
|
+
import { debounce } from "perfect-debounce";
|
|
11
|
+
//#region src/services/session/session.service.ts
|
|
12
|
+
let SessionService = class SessionService extends EventEmitter {
|
|
13
|
+
logger;
|
|
14
|
+
printer;
|
|
15
|
+
watcher;
|
|
16
|
+
bundle;
|
|
17
|
+
stopped = false;
|
|
18
|
+
modules = /* @__PURE__ */ new Set();
|
|
19
|
+
watched = /* @__PURE__ */ new Set();
|
|
20
|
+
version = 0;
|
|
21
|
+
building;
|
|
22
|
+
flags;
|
|
23
|
+
ignored = /* @__PURE__ */ new Set();
|
|
24
|
+
build = debounce(() => this.stopped ? Promise.resolve() : this.building = this.run(), 50);
|
|
25
|
+
constructor(logger, printer) {
|
|
26
|
+
super();
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
this.printer = printer;
|
|
29
|
+
this.logger.setup(this.constructor.name);
|
|
30
|
+
}
|
|
31
|
+
async start(flags) {
|
|
32
|
+
this.flags = flags;
|
|
33
|
+
this.watcher = watch([], {
|
|
34
|
+
ignoreInitial: true,
|
|
35
|
+
atomic: true
|
|
36
|
+
}).on("change", (path) => this.changed(path)).on("add", (path) => this.changed(path)).on("unlink", (path) => this.changed(path));
|
|
37
|
+
await this.build();
|
|
38
|
+
}
|
|
39
|
+
async onModuleDestroy() {
|
|
40
|
+
await this.stop();
|
|
41
|
+
}
|
|
42
|
+
ignore(path) {
|
|
43
|
+
this.ignored.add(resolve(path));
|
|
44
|
+
}
|
|
45
|
+
async stop() {
|
|
46
|
+
this.stopped = true;
|
|
47
|
+
this.build.cancel();
|
|
48
|
+
await this.building;
|
|
49
|
+
await this.watcher?.close();
|
|
50
|
+
this.watcher = void 0;
|
|
51
|
+
}
|
|
52
|
+
changed(path) {
|
|
53
|
+
const file = resolve(path);
|
|
54
|
+
if (this.ignored.has(file)) return;
|
|
55
|
+
this.logger.debug("Changed: %s", file);
|
|
56
|
+
if (this.modules.has(file) || this.bundle && dirname(file) === join(this.bundle.template, "components")) this.bundle = void 0;
|
|
57
|
+
this.build();
|
|
58
|
+
}
|
|
59
|
+
async run() {
|
|
60
|
+
const started = performance.now();
|
|
61
|
+
this.emit("building");
|
|
62
|
+
try {
|
|
63
|
+
const input = await this.printer.input(this.flags);
|
|
64
|
+
const options = await this.printer.options(this.flags, input);
|
|
65
|
+
const template = join(options.template, "Template.svelte");
|
|
66
|
+
this.modules.add(template);
|
|
67
|
+
this.watch([
|
|
68
|
+
input.file,
|
|
69
|
+
template,
|
|
70
|
+
join(options.template, "settings.json"),
|
|
71
|
+
join(options.template, "i18n"),
|
|
72
|
+
join(options.template, "components"),
|
|
73
|
+
options.sign.certificate && resolve(options.sign.certificate)
|
|
74
|
+
]);
|
|
75
|
+
if (this.bundle?.template !== options.template) this.bundle = {
|
|
76
|
+
template: options.template,
|
|
77
|
+
bundle: this.printer.bundle(options)
|
|
78
|
+
};
|
|
79
|
+
const cached = this.bundle;
|
|
80
|
+
const bundle = await cached.bundle.catch((err) => {
|
|
81
|
+
if (this.bundle === cached) this.bundle = void 0;
|
|
82
|
+
throw err;
|
|
83
|
+
});
|
|
84
|
+
if (this.bundle !== cached) return;
|
|
85
|
+
this.modules = /* @__PURE__ */ new Set([template, ...bundle.modules]);
|
|
86
|
+
this.watch(bundle.modules);
|
|
87
|
+
const ctx = {
|
|
88
|
+
input,
|
|
89
|
+
options
|
|
90
|
+
};
|
|
91
|
+
await this.printer.prepare(ctx);
|
|
92
|
+
const document = await this.printer.document(ctx, bundle);
|
|
93
|
+
this.watch(document.dependencies);
|
|
94
|
+
const { content, assets } = await this.printer.output(ctx, document);
|
|
95
|
+
this.watch(assets);
|
|
96
|
+
this.emit("ready", {
|
|
97
|
+
version: ++this.version,
|
|
98
|
+
ms: Math.round(performance.now() - started),
|
|
99
|
+
ctx,
|
|
100
|
+
document,
|
|
101
|
+
output: content
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
this.emit("failed", err instanceof Error ? err : new Error(String(err)));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
watch(paths) {
|
|
108
|
+
const fresh = paths.filter((path) => path && !this.watched.has(path));
|
|
109
|
+
for (const path of fresh) this.watched.add(path);
|
|
110
|
+
if (fresh.length) this.watcher.add(fresh);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
SessionService = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [typeof LoggerService === "undefined" ? Object : LoggerService, typeof PrinterService === "undefined" ? Object : PrinterService])], SessionService);
|
|
114
|
+
//#endregion
|
|
115
|
+
export { SessionService };
|
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.mjs";
|
|
2
2
|
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.mjs";
|
|
3
|
+
import { isLoopbackRequest } from "../../loopback.mjs";
|
|
3
4
|
import { SignatureAdapter } from "../signature.adapter.mjs";
|
|
5
|
+
import { readFile } from "fs/promises";
|
|
4
6
|
import { LoggerService } from "@cenk1cenk2/oclif-common";
|
|
5
7
|
import { Injectable } from "@nestjs/common";
|
|
6
8
|
import { spawn } from "child_process";
|
|
7
|
-
import {
|
|
9
|
+
import { once } from "events";
|
|
8
10
|
import { createServer } from "http";
|
|
9
|
-
|
|
11
|
+
import getRawBody from "raw-body";
|
|
10
12
|
//#region src/services/signature/adapters/a-trust.adapter.ts
|
|
11
|
-
var _ATrustAdapter;
|
|
12
13
|
let ATrustAdapter = class ATrustAdapter extends SignatureAdapter {
|
|
13
|
-
static {
|
|
14
|
-
_ATrustAdapter = this;
|
|
15
|
-
}
|
|
16
14
|
logger;
|
|
17
|
-
static ENDPOINT = "https://service.a-trust.at/mobile/https-security-layer-request/default.aspx";
|
|
18
|
-
static NAMESPACE = "http://www.buergerkarte.at/namespaces/securitylayer/1.2#";
|
|
19
|
-
static TIMEOUT = 6e5;
|
|
20
|
-
static PAGE = "./a-trust.html";
|
|
21
15
|
name = "a-trust";
|
|
22
16
|
constructor(logger) {
|
|
23
17
|
super();
|
|
@@ -28,21 +22,20 @@ let ATrustAdapter = class ATrustAdapter extends SignatureAdapter {
|
|
|
28
22
|
return this.collect(await this.session(this.request(request)));
|
|
29
23
|
}
|
|
30
24
|
request({ pdf, excluded: [from, to] }) {
|
|
31
|
-
const data = Buffer.from(pdf).fill(0, from, to + 1);
|
|
32
25
|
return [
|
|
33
26
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
34
|
-
|
|
27
|
+
"<sl:CreateCMSSignatureRequest xmlns:sl=\"http://www.buergerkarte.at/namespaces/securitylayer/1.2#\" Structure=\"detached\" PAdESCompatibility=\"true\">",
|
|
35
28
|
"<sl:KeyboxIdentifier>SecureSignatureKeypair</sl:KeyboxIdentifier>",
|
|
36
29
|
"<sl:DataObject>",
|
|
37
30
|
"<sl:MetaInfo><sl:MimeType>application/pdf</sl:MimeType></sl:MetaInfo>",
|
|
38
|
-
`<sl:Content><sl:Base64Content>${
|
|
31
|
+
`<sl:Content><sl:Base64Content>${Buffer.from(pdf).fill(0, from, to + 1).toString("base64")}</sl:Base64Content></sl:Content>`,
|
|
39
32
|
`<sl:ExcludedByteRange><sl:From>${from}</sl:From><sl:To>${to}</sl:To></sl:ExcludedByteRange>`,
|
|
40
33
|
"</sl:DataObject>",
|
|
41
34
|
"</sl:CreateCMSSignatureRequest>"
|
|
42
35
|
].join("");
|
|
43
36
|
}
|
|
44
37
|
async session(request) {
|
|
45
|
-
let url =
|
|
38
|
+
let url = "https://service.a-trust.at/mobile/https-security-layer-request/default.aspx";
|
|
46
39
|
let response = await fetch(url, {
|
|
47
40
|
method: "POST",
|
|
48
41
|
body: new URLSearchParams([["XMLRequest", request]]),
|
|
@@ -58,13 +51,15 @@ let ATrustAdapter = class ATrustAdapter extends SignatureAdapter {
|
|
|
58
51
|
}
|
|
59
52
|
async collect(login) {
|
|
60
53
|
let resolve;
|
|
61
|
-
|
|
62
|
-
const result = new Promise((done, fail) => {
|
|
54
|
+
const result = new Promise((done) => {
|
|
63
55
|
resolve = done;
|
|
64
|
-
reject = fail;
|
|
65
56
|
});
|
|
66
|
-
const page = await readFile(new URL(
|
|
57
|
+
const page = await readFile(new URL("./a-trust.html", import.meta.url));
|
|
67
58
|
const server = createServer(async (req, res) => {
|
|
59
|
+
if (!isLoopbackRequest(req, req.method === "POST" ? `http://127.0.0.1:${server.address().port}` : void 0)) {
|
|
60
|
+
res.writeHead(403);
|
|
61
|
+
return res.end();
|
|
62
|
+
}
|
|
68
63
|
if (req.method === "GET" && req.url === "/session") {
|
|
69
64
|
res.setHeader("content-type", "application/json");
|
|
70
65
|
res.writeHead(200);
|
|
@@ -75,8 +70,14 @@ let ATrustAdapter = class ATrustAdapter extends SignatureAdapter {
|
|
|
75
70
|
res.writeHead(200);
|
|
76
71
|
return res.end(page);
|
|
77
72
|
}
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const body = await getRawBody(req, {
|
|
74
|
+
limit: "1mb",
|
|
75
|
+
encoding: true
|
|
76
|
+
}).catch((err) => {
|
|
77
|
+
res.writeHead(err.status ?? 400);
|
|
78
|
+
res.end();
|
|
79
|
+
});
|
|
80
|
+
if (body === void 0) return;
|
|
80
81
|
const cms = (body.match(/<(?:\w+:)?CMSSignature>([^<]+)</)?.[1] ?? body).replace(/\s+/g, "");
|
|
81
82
|
if (!/^MII[A-Za-z0-9+/=]+$/.test(cms)) {
|
|
82
83
|
res.writeHead(400);
|
|
@@ -86,31 +87,20 @@ let ATrustAdapter = class ATrustAdapter extends SignatureAdapter {
|
|
|
86
87
|
res.end("Signature received, you can close this tab.");
|
|
87
88
|
resolve(Buffer.from(cms, "base64"));
|
|
88
89
|
});
|
|
89
|
-
|
|
90
|
+
server.listen(0, "127.0.0.1");
|
|
91
|
+
await once(server, "listening");
|
|
90
92
|
const url = `http://127.0.0.1:${server.address().port}/`;
|
|
91
93
|
this.logger.info("Sign the document in the browser, then paste the signature back: %s", url);
|
|
92
94
|
this.open(url);
|
|
93
|
-
|
|
94
|
-
return result.finally(() => {
|
|
95
|
-
clearTimeout(timeout);
|
|
96
|
-
server.close();
|
|
97
|
-
});
|
|
95
|
+
return result.finally(() => server.close());
|
|
98
96
|
}
|
|
99
97
|
open(url) {
|
|
100
|
-
|
|
101
|
-
"cmd",
|
|
102
|
-
"/c",
|
|
103
|
-
"start",
|
|
104
|
-
"",
|
|
105
|
-
url
|
|
106
|
-
] : ["xdg-open", url];
|
|
107
|
-
spawn(command, args, {
|
|
98
|
+
spawn("xdg-open", [url], {
|
|
108
99
|
stdio: "ignore",
|
|
109
100
|
detached: true
|
|
110
101
|
}).on("error", (err) => this.logger.warn("Can not open the browser, open the link manually: %s", err.message)).unref();
|
|
111
102
|
}
|
|
112
103
|
};
|
|
113
|
-
ATrustAdapter =
|
|
114
|
-
|
|
104
|
+
ATrustAdapter = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [typeof LoggerService === "undefined" ? Object : LoggerService])], ATrustAdapter);
|
|
115
105
|
//#endregion
|
|
116
|
-
export { ATrustAdapter };
|
|
106
|
+
export { ATrustAdapter };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { SignatureAdapter } from "./signature.adapter.mjs";
|
|
2
2
|
import { ATrustAdapter } from "./adapters/a-trust.adapter.mjs";
|
|
3
3
|
import { SignatureService } from "./signature.service.mjs";
|
|
4
|
-
|
|
5
|
-
export { ATrustAdapter, SignatureAdapter, SignatureService };
|
|
4
|
+
export { ATrustAdapter, SignatureAdapter, SignatureService };
|
|
@@ -8,7 +8,6 @@ import { plainAddPlaceholder } from "@signpdf/placeholder-plain";
|
|
|
8
8
|
import { SUBFILTER_ETSI_CADES_DETACHED, findByteRange, removeTrailingNewLine } from "@signpdf/utils";
|
|
9
9
|
import { X509Certificate } from "crypto";
|
|
10
10
|
import { homedir } from "os";
|
|
11
|
-
|
|
12
11
|
//#region src/services/signature/signature.service.ts
|
|
13
12
|
var _SignatureService;
|
|
14
13
|
let SignatureService = class SignatureService {
|
|
@@ -92,6 +91,5 @@ let SignatureService = class SignatureService {
|
|
|
92
91
|
}
|
|
93
92
|
};
|
|
94
93
|
SignatureService = _SignatureService = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [typeof FileSystemService === "undefined" ? Object : FileSystemService, typeof ATrustAdapter === "undefined" ? Object : ATrustAdapter])], SignatureService);
|
|
95
|
-
|
|
96
94
|
//#endregion
|
|
97
|
-
export { SignatureService };
|
|
95
|
+
export { SignatureService };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
HTML document around the rendered template body.
|
|
4
|
+
-->
|
|
5
|
+
<script>
|
|
6
|
+
/** @type {import('@interfaces').DocumentShellProps} */
|
|
7
|
+
let { lang, title, head, body } = $props()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<html {lang}>
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="utf-8" />
|
|
13
|
+
{#if title !== undefined}
|
|
14
|
+
<title>{title}</title>
|
|
15
|
+
{/if}
|
|
16
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- styles and head elements rendered by md-printer -->
|
|
17
|
+
{@html head}
|
|
18
|
+
</head>
|
|
19
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- the template body rendered by md-printer -->
|
|
20
|
+
<body>{@html body}</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Wrapper for a header or footer snippet printed into Chrome's isolated page margin, which ignores the page stylesheet and defaults to a tiny font.
|
|
4
|
+
-->
|
|
5
|
+
<script>
|
|
6
|
+
/** @type {import('@interfaces').MarginShellProps} */
|
|
7
|
+
let { styles, body } = $props()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- styles collected by md-printer for the isolated margin document -->
|
|
11
|
+
{@html styles}
|
|
12
|
+
<div style="font-size: 10px; width: 100%; box-sizing: border-box; -webkit-print-color-adjust: exact; print-color-adjust: exact">
|
|
13
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- the header or footer snippet rendered by md-printer -->
|
|
14
|
+
{@html body}
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFile, realpath } from "fs/promises";
|
|
2
|
+
import { isAbsolute, relative, sep } from "path";
|
|
3
|
+
import { toHtml } from "hast-util-to-html";
|
|
4
|
+
import { visit } from "unist-util-visit";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
6
|
+
import { fromHtml } from "hast-util-from-html";
|
|
7
|
+
import { transform, transformStyleAttribute } from "lightningcss";
|
|
8
|
+
import mime from "mime";
|
|
9
|
+
import { parseSrcset, stringifySrcset } from "srcset";
|
|
10
|
+
//#region src/services/template/assets.ts
|
|
11
|
+
const REFERENCES = {
|
|
12
|
+
img: ["src", "srcSet"],
|
|
13
|
+
source: ["src", "srcSet"],
|
|
14
|
+
input: ["src"],
|
|
15
|
+
video: ["poster"],
|
|
16
|
+
image: ["href", "xLinkHref"]
|
|
17
|
+
};
|
|
18
|
+
function mimeType(path) {
|
|
19
|
+
return mime.getType(path) ?? "application/octet-stream";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Images and fonts, the assets that can be embedded into a page as data URIs.
|
|
23
|
+
*/
|
|
24
|
+
function isInlinableAsset(path) {
|
|
25
|
+
return /^(image|font)\//.test(mime.getType(path) ?? "");
|
|
26
|
+
}
|
|
27
|
+
function dataUri(path, content) {
|
|
28
|
+
return `data:${mimeType(path)};base64,${content.toString("base64")}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolves symlinks first, so a link inside the directory can not reach a file outside of it.
|
|
32
|
+
*/
|
|
33
|
+
async function resolveConfined(path, base) {
|
|
34
|
+
const [real, root] = await Promise.all([realpath(path), realpath(base)]).catch(() => []);
|
|
35
|
+
if (!real) return;
|
|
36
|
+
const rel = relative(root, real);
|
|
37
|
+
return rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel) ? void 0 : real;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Replaces local image references in HTML with data URIs: the image attributes, `style` attributes and `<style>` elements.
|
|
41
|
+
* Remote references are fetched as well when `remote` is set, which Chrome's isolated header/footer templates need.
|
|
42
|
+
* Document content is `confined` to its directory, the same rule the PDF renderer applies to the assets a page loads.
|
|
43
|
+
*/
|
|
44
|
+
async function inlineHtml(markup, base, options = {}) {
|
|
45
|
+
const assets = /* @__PURE__ */ new Set();
|
|
46
|
+
const inline = inliner(base, options, assets);
|
|
47
|
+
const tree = fromHtml(markup, { fragment: !/^<!doctype/i.test(markup) });
|
|
48
|
+
const pending = [];
|
|
49
|
+
visit(tree, "element", (node) => {
|
|
50
|
+
const { properties } = node;
|
|
51
|
+
for (const name of REFERENCES[node.tagName] ?? []) {
|
|
52
|
+
const value = properties[name];
|
|
53
|
+
if (typeof value !== "string") continue;
|
|
54
|
+
pending.push((async () => {
|
|
55
|
+
properties[name] = name === "srcSet" ? stringifySrcset(await Promise.all(parseSrcset(value).map(async (candidate) => ({
|
|
56
|
+
...candidate,
|
|
57
|
+
url: await inline(candidate.url) ?? candidate.url
|
|
58
|
+
})))) : await inline(value) ?? value;
|
|
59
|
+
})());
|
|
60
|
+
}
|
|
61
|
+
if (typeof properties.style === "string") {
|
|
62
|
+
const style = properties.style;
|
|
63
|
+
pending.push(stylesheet(style, inline, true).then((css) => {
|
|
64
|
+
properties.style = css;
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
if (node.tagName === "style") {
|
|
68
|
+
for (const child of node.children) if (child.type === "text") pending.push(stylesheet(child.value, inline).then((css) => {
|
|
69
|
+
child.value = css;
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
await Promise.all(pending);
|
|
74
|
+
return {
|
|
75
|
+
content: toHtml(tree, { characterReferences: { useNamedReferences: true } }),
|
|
76
|
+
assets: [...assets]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Replaces local `url()` references in CSS with data URIs, with the same options as `inlineHtml`.
|
|
81
|
+
*/
|
|
82
|
+
async function inlineCss(css, base, options = {}) {
|
|
83
|
+
const assets = /* @__PURE__ */ new Set();
|
|
84
|
+
return {
|
|
85
|
+
content: await stylesheet(css, inliner(base, options, assets)),
|
|
86
|
+
assets: [...assets]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function inliner(base, { remote = false, confined = false, cache = /* @__PURE__ */ new Map() }, assets) {
|
|
90
|
+
return async (reference) => {
|
|
91
|
+
const asset = await (cache.get(reference) ?? cache.set(reference, load(reference, base, remote, confined)).get(reference));
|
|
92
|
+
if (asset?.path) assets.add(asset.path);
|
|
93
|
+
return asset?.uri;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function stylesheet(css, inline, attribute = false) {
|
|
97
|
+
const options = {
|
|
98
|
+
filename: "style.css",
|
|
99
|
+
code: Buffer.from(css),
|
|
100
|
+
analyzeDependencies: { preserveImports: true },
|
|
101
|
+
errorRecovery: true
|
|
102
|
+
};
|
|
103
|
+
const { code, dependencies } = attribute ? transformStyleAttribute({
|
|
104
|
+
...options,
|
|
105
|
+
analyzeDependencies: true
|
|
106
|
+
}) : transform(options);
|
|
107
|
+
return (await Promise.all(dependencies.map(async (dependency) => [`"${dependency.placeholder}"`, JSON.stringify(dependency.type === "url" && await inline(dependency.url) || dependency.url)]))).reduce((result, [placeholder, value]) => result.replace(placeholder, () => value), code.toString());
|
|
108
|
+
}
|
|
109
|
+
async function load(reference, base, remote, confined) {
|
|
110
|
+
const url = reference.startsWith("#") ? void 0 : URL.parse(reference, pathToFileURL(`${base}${sep}`));
|
|
111
|
+
if (url?.protocol === "http:" || url?.protocol === "https:") {
|
|
112
|
+
if (!remote) return;
|
|
113
|
+
const response = await fetch(url).catch(() => void 0);
|
|
114
|
+
if (!response?.ok) return;
|
|
115
|
+
return { uri: `data:${response.headers.get("content-type") ?? mimeType(url.pathname)};base64,${Buffer.from(await response.arrayBuffer()).toString("base64")}` };
|
|
116
|
+
}
|
|
117
|
+
if (url?.protocol !== "file:" || url.host) return;
|
|
118
|
+
const path = fileURLToPath(url);
|
|
119
|
+
const file = confined ? await resolveConfined(path, base) : path;
|
|
120
|
+
if (!file || !isInlinableAsset(path)) return;
|
|
121
|
+
return readFile(file).then((content) => ({
|
|
122
|
+
uri: dataUri(path, content),
|
|
123
|
+
path: file
|
|
124
|
+
}), () => void 0);
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
export { dataUri, inlineCss, inlineHtml, isInlinableAsset, mimeType, resolveConfined };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Template from 'md-printer:template'
|
|
2
|
+
import components from 'md-printer:template-components'
|
|
3
|
+
import { render } from 'svelte/server'
|
|
4
|
+
|
|
5
|
+
import Document from './Document.svelte'
|
|
6
|
+
import Margin from './Margin.svelte'
|
|
7
|
+
import Nodes from '../../components/Nodes.svelte'
|
|
8
|
+
import type { DocumentShellProps, MarginShellProps, MarkdownNode, RenderedPart, TemplateProps } from '@interfaces'
|
|
9
|
+
|
|
10
|
+
export { components }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Entry of every template bundle, compiled at runtime together with the template it renders.
|
|
14
|
+
*/
|
|
15
|
+
export function part(props: TemplateProps, context: [string, unknown][]): Promise<RenderedPart> {
|
|
16
|
+
return Promise.resolve(render(Template, { props, context: new Map(context) }))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The markdown as one HTML string, for templates that place it with `{@html content}`. Plain HTML is kept as is, without Svelte's hydration markers.
|
|
21
|
+
*/
|
|
22
|
+
export function content(nodes: MarkdownNode[], context: [string, unknown][]): string {
|
|
23
|
+
return nodes.map((node) => (node.type === 'html' ? node.html : render(Nodes, { props: { nodes: [node] }, context: new Map(context) }).body)).join('')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function document(props: DocumentShellProps): string {
|
|
27
|
+
return `<!doctype html>${render(Document, { props }).body}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function margin(props: MarginShellProps): string {
|
|
31
|
+
return render(Margin, { props }).body
|
|
32
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import "../../constants/template.constants.mjs";
|
|
2
|
+
import { dataUri, isInlinableAsset } from "./assets.mjs";
|
|
3
|
+
import { readFile, readdir } from "fs/promises";
|
|
4
|
+
import { basename, dirname, extname, join } from "path";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
import { compile, preprocess } from "svelte/compiler";
|
|
9
|
+
//#region src/services/template/svelte.plugin.ts
|
|
10
|
+
/**
|
|
11
|
+
* `svelte` resolves to md-printer's copy, since templates can live outside any `node_modules`. `<style lang="postcss">` blocks are collected
|
|
12
|
+
* instead of compiled per component, so the template's stylesheet goes through Tailwind once. `md-printer:template-components` maps the shared
|
|
13
|
+
* components and the template's `components/*.svelte` by name, the template's winning.
|
|
14
|
+
*/
|
|
15
|
+
function sveltePlugin({ template, components, root, stylesheets }) {
|
|
16
|
+
const require = createRequire(root);
|
|
17
|
+
return {
|
|
18
|
+
name: "md-printer:svelte",
|
|
19
|
+
resolveId(id) {
|
|
20
|
+
if (id === "md-printer:template") return template;
|
|
21
|
+
if (id === "@cenk1cenk2/md-printer/components") return components;
|
|
22
|
+
if (id === "md-printer:template-components") return `\0${id}`;
|
|
23
|
+
if (id === "svelte" || id.startsWith("svelte/")) return {
|
|
24
|
+
id: pathToFileURL(require.resolve(id)).href,
|
|
25
|
+
external: true
|
|
26
|
+
};
|
|
27
|
+
return null;
|
|
28
|
+
},
|
|
29
|
+
async load(id) {
|
|
30
|
+
if (id === `\0md-printer:template-components`) {
|
|
31
|
+
const directory = join(dirname(template), "components");
|
|
32
|
+
const files = existsSync(directory) ? (await readdir(directory)).filter((file) => extname(file) === ".svelte").sort() : [];
|
|
33
|
+
return [
|
|
34
|
+
`import * as shared from ${JSON.stringify("@cenk1cenk2/md-printer/components")}`,
|
|
35
|
+
...files.map((file, index) => `import $${index} from ${JSON.stringify(join(directory, file))}`),
|
|
36
|
+
"const { Markdown, ...builtins } = shared",
|
|
37
|
+
`export default { ...builtins, ${files.map((file, index) => `${JSON.stringify(basename(file, ".svelte"))}: $${index}`).join(", ")} }`
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
if (!isInlinableAsset(id)) return null;
|
|
41
|
+
return `export default ${JSON.stringify(dataUri(id, await readFile(id)))}`;
|
|
42
|
+
},
|
|
43
|
+
async transform(code, id) {
|
|
44
|
+
if (extname(id) !== ".svelte") return null;
|
|
45
|
+
const { code: source } = await preprocess(code, { style: ({ content, attributes }) => {
|
|
46
|
+
if (attributes.lang !== "postcss") return;
|
|
47
|
+
stylesheets.push({
|
|
48
|
+
file: id,
|
|
49
|
+
source: content
|
|
50
|
+
});
|
|
51
|
+
return { code: "" };
|
|
52
|
+
} }, { filename: id });
|
|
53
|
+
const { js } = compile(source, {
|
|
54
|
+
filename: id,
|
|
55
|
+
generate: "server",
|
|
56
|
+
css: "injected"
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
code: js.code,
|
|
60
|
+
map: js.map
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { sveltePlugin };
|