@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,198 @@
|
|
|
1
|
+
import { PAGE_BREAK_CLASS } from "../../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 { BrowserService } from "./browser.service.mjs";
|
|
5
|
+
import { mimeType, resolveConfined } from "../template/assets.mjs";
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
import { LoggerService } from "@cenk1cenk2/oclif-common";
|
|
9
|
+
import { Injectable } from "@nestjs/common";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
11
|
+
//#region src/services/renderer/renderer.service.ts
|
|
12
|
+
var _RendererService;
|
|
13
|
+
let RendererService = class RendererService {
|
|
14
|
+
static {
|
|
15
|
+
_RendererService = this;
|
|
16
|
+
}
|
|
17
|
+
logger;
|
|
18
|
+
browser;
|
|
19
|
+
static ORIGIN = "http://md-printer.local";
|
|
20
|
+
static DOCUMENT = "/__document.html";
|
|
21
|
+
static SECTION = "data-md-printer-section";
|
|
22
|
+
constructor(logger, browser) {
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
this.browser = browser;
|
|
25
|
+
this.logger.setup(this.constructor.name);
|
|
26
|
+
}
|
|
27
|
+
async render(rendered, options) {
|
|
28
|
+
const page = await (await this.browser.get()).newPage();
|
|
29
|
+
try {
|
|
30
|
+
const assets = await this.load(page, rendered, options);
|
|
31
|
+
const sections = await this.split(page);
|
|
32
|
+
const pdf = options.continuous ? await this.continuous(page, options, sections) : await this.paginated(page, rendered, options);
|
|
33
|
+
return {
|
|
34
|
+
pdf: Buffer.from(pdf),
|
|
35
|
+
assets
|
|
36
|
+
};
|
|
37
|
+
} finally {
|
|
38
|
+
await page.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async load(page, rendered, options) {
|
|
42
|
+
const assets = /* @__PURE__ */ new Set();
|
|
43
|
+
const base = resolve(options.directory);
|
|
44
|
+
await page.setRequestInterception(true);
|
|
45
|
+
page.on("request", (request) => {
|
|
46
|
+
this.intercept(request, rendered, base, assets).catch(async (err) => {
|
|
47
|
+
this.logger.debug("Request handling failed for %s: %s", request.url(), err);
|
|
48
|
+
await request.abort().catch(() => void 0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
await page.emulateMediaType(options.media);
|
|
52
|
+
await page.goto(_RendererService.ORIGIN + _RendererService.DOCUMENT, { waitUntil: "load" });
|
|
53
|
+
await page.evaluate(async () => {
|
|
54
|
+
for (const image of Array.from(document.images)) image.loading = "eager";
|
|
55
|
+
await document.fonts.ready;
|
|
56
|
+
await Promise.all(Array.from(document.images).map((image) => image.decode().catch(() => void 0)));
|
|
57
|
+
});
|
|
58
|
+
return [...assets];
|
|
59
|
+
}
|
|
60
|
+
async intercept(request, rendered, base, assets) {
|
|
61
|
+
const url = new URL(request.url());
|
|
62
|
+
if (url.origin !== _RendererService.ORIGIN) await request.continue();
|
|
63
|
+
else if (url.pathname === "/favicon.ico") await request.respond({
|
|
64
|
+
status: 204,
|
|
65
|
+
body: ""
|
|
66
|
+
});
|
|
67
|
+
else if (url.pathname === _RendererService.DOCUMENT) await request.respond({
|
|
68
|
+
status: 200,
|
|
69
|
+
contentType: "text/html; charset=utf-8",
|
|
70
|
+
body: rendered.html
|
|
71
|
+
});
|
|
72
|
+
else {
|
|
73
|
+
const path = fileURLToPath(new URL(`.${url.pathname}`, pathToFileURL(`${base}/`)));
|
|
74
|
+
const confined = await resolveConfined(path, base);
|
|
75
|
+
const body = confined && await readFile(confined).catch(() => void 0);
|
|
76
|
+
if (body) {
|
|
77
|
+
assets.add(path);
|
|
78
|
+
await request.respond({
|
|
79
|
+
status: 200,
|
|
80
|
+
contentType: mimeType(path),
|
|
81
|
+
body
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
this.logger.warn("Can not load asset: %s", path);
|
|
85
|
+
await request.respond({
|
|
86
|
+
status: 404,
|
|
87
|
+
body: ""
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Cuts the body into top-level sections at every page break, cloning the containers a break sits in, so a break works anywhere: inside
|
|
94
|
+
* flex or grid layouts, where CSS fragmentation ignores it, and in continuous mode, where every section becomes a page of its own height.
|
|
95
|
+
*/
|
|
96
|
+
split(page) {
|
|
97
|
+
return page.evaluate((selector, attribute) => {
|
|
98
|
+
const markers = Array.from(document.body.querySelectorAll(selector));
|
|
99
|
+
if (!markers.length) return 1;
|
|
100
|
+
const sections = markers.map((marker) => {
|
|
101
|
+
const range = document.createRange();
|
|
102
|
+
const section = document.createElement("div");
|
|
103
|
+
range.setStart(document.body, 0);
|
|
104
|
+
range.setEndBefore(marker);
|
|
105
|
+
section.append(range.extractContents());
|
|
106
|
+
marker.remove();
|
|
107
|
+
return section;
|
|
108
|
+
});
|
|
109
|
+
sections.push(document.createElement("div"));
|
|
110
|
+
sections.at(-1).append(...Array.from(document.body.childNodes));
|
|
111
|
+
sections.forEach((section, index) => section.setAttribute(attribute, String(index)));
|
|
112
|
+
document.body.replaceChildren(...sections);
|
|
113
|
+
const style = document.createElement("style");
|
|
114
|
+
style.textContent = `[${attribute}]:not(:last-child) { break-after: page; }`;
|
|
115
|
+
document.head.append(style);
|
|
116
|
+
return sections.length;
|
|
117
|
+
}, `.${PAGE_BREAK_CLASS}`, _RendererService.SECTION);
|
|
118
|
+
}
|
|
119
|
+
paginated(page, rendered, options) {
|
|
120
|
+
return page.pdf({
|
|
121
|
+
...options.pdf,
|
|
122
|
+
displayHeaderFooter: options.pdf.displayHeaderFooter ?? Boolean(rendered.header || rendered.footer),
|
|
123
|
+
headerTemplate: rendered.header ?? "<span></span>",
|
|
124
|
+
footerTemplate: rendered.footer ?? "<span></span>"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Prints everything as one tall page. Chrome itself resolves the paper size (from a one page probe print) and the margins (as CSS lengths),
|
|
129
|
+
* so md-printer never has to know paper formats or length units.
|
|
130
|
+
*/
|
|
131
|
+
async continuous(page, options, sections) {
|
|
132
|
+
const [, points] = Buffer.from(await page.pdf({
|
|
133
|
+
...options.pdf,
|
|
134
|
+
displayHeaderFooter: false,
|
|
135
|
+
pageRanges: "1"
|
|
136
|
+
})).toString("latin1").match(/\/MediaBox\s*\[\s*[\d.]+\s+[\d.]+\s+([\d.]+)/);
|
|
137
|
+
const width = Math.round(Number(points) * 96 / 72);
|
|
138
|
+
const margin = await page.evaluate((lengths) => {
|
|
139
|
+
const probe = document.createElement("div");
|
|
140
|
+
probe.style.position = "absolute";
|
|
141
|
+
probe.style.visibility = "hidden";
|
|
142
|
+
Object.assign(probe.style, {
|
|
143
|
+
marginTop: lengths?.top,
|
|
144
|
+
marginRight: lengths?.right,
|
|
145
|
+
marginBottom: lengths?.bottom,
|
|
146
|
+
marginLeft: lengths?.left
|
|
147
|
+
});
|
|
148
|
+
document.body.append(probe);
|
|
149
|
+
const style = getComputedStyle(probe);
|
|
150
|
+
const resolved = {
|
|
151
|
+
top: parseFloat(style.marginTop),
|
|
152
|
+
right: parseFloat(style.marginRight),
|
|
153
|
+
bottom: parseFloat(style.marginBottom),
|
|
154
|
+
left: parseFloat(style.marginLeft)
|
|
155
|
+
};
|
|
156
|
+
probe.remove();
|
|
157
|
+
return resolved;
|
|
158
|
+
}, Object.fromEntries(Object.entries(options.pdf.margin ?? {}).map(([side, length]) => [side, typeof length === "number" ? `${length}px` : length])));
|
|
159
|
+
await page.setViewport({
|
|
160
|
+
width: Math.max(Math.round(width - margin.left - margin.right), 1),
|
|
161
|
+
height: 800
|
|
162
|
+
});
|
|
163
|
+
const totals = (await page.evaluate(async (attribute, single) => {
|
|
164
|
+
await document.fonts.ready;
|
|
165
|
+
return (single ? [document.body] : Array.from(document.body.querySelectorAll(`:scope > [${attribute}]`))).map((element) => {
|
|
166
|
+
const top = element.getBoundingClientRect().top;
|
|
167
|
+
return Math.max(element.scrollHeight, single ? document.documentElement.scrollHeight : 0, ...Array.from(element.querySelectorAll("*")).map((child) => child.getBoundingClientRect().bottom - top));
|
|
168
|
+
});
|
|
169
|
+
}, _RendererService.SECTION, sections === 1)).map((height) => Math.ceil(height + margin.top + margin.bottom + 96 / 2.54));
|
|
170
|
+
if (totals.some((total) => total * .75 > 65535)) {
|
|
171
|
+
this.logger.warn("Content is %dpx tall, which is more than a single PDF page can hold, falling back to paginated output.", Math.max(...totals));
|
|
172
|
+
return page.pdf({
|
|
173
|
+
...options.pdf,
|
|
174
|
+
displayHeaderFooter: false
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const pdf = {
|
|
178
|
+
...options.pdf,
|
|
179
|
+
displayHeaderFooter: false
|
|
180
|
+
};
|
|
181
|
+
delete pdf.format;
|
|
182
|
+
delete pdf.landscape;
|
|
183
|
+
if (sections === 1) return page.pdf({
|
|
184
|
+
...pdf,
|
|
185
|
+
width: `${width}px`,
|
|
186
|
+
height: `${totals[0]}px`,
|
|
187
|
+
pageRanges: "1"
|
|
188
|
+
});
|
|
189
|
+
await page.addStyleTag({ content: totals.map((total, index) => `@page md-printer-${index} { size: ${width}px ${total}px; } [${_RendererService.SECTION}="${index}"] { page: md-printer-${index}; }`).join("\n") });
|
|
190
|
+
return page.pdf({
|
|
191
|
+
...pdf,
|
|
192
|
+
preferCSSPageSize: true
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
RendererService = _RendererService = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [typeof LoggerService === "undefined" ? Object : LoggerService, typeof BrowserService === "undefined" ? Object : BrowserService])], RendererService);
|
|
197
|
+
//#endregion
|
|
198
|
+
export { RendererService };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Shell page of the live preview: the HTML and the printed PDF side by side, updated over server-sent events.
|
|
4
|
+
-->
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="utf-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
9
|
+
<title>md-printer preview</title>
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
color-scheme: light dark;
|
|
13
|
+
--bg: #f4f4f5;
|
|
14
|
+
--panel: #ffffff;
|
|
15
|
+
--text: #18181b;
|
|
16
|
+
--muted: #71717a;
|
|
17
|
+
--accent: #cd0043;
|
|
18
|
+
--border: #e4e4e7;
|
|
19
|
+
}
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root {
|
|
22
|
+
--bg: #17191e;
|
|
23
|
+
--panel: #22282f;
|
|
24
|
+
--text: #e4e4e7;
|
|
25
|
+
--muted: #979eab;
|
|
26
|
+
--border: #2c333d;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
* {
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
}
|
|
32
|
+
body {
|
|
33
|
+
margin: 0;
|
|
34
|
+
height: 100vh;
|
|
35
|
+
display: grid;
|
|
36
|
+
grid-template-rows: auto 1fr;
|
|
37
|
+
font:
|
|
38
|
+
13px system-ui,
|
|
39
|
+
sans-serif;
|
|
40
|
+
background: var(--bg);
|
|
41
|
+
color: var(--text);
|
|
42
|
+
}
|
|
43
|
+
header {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 12px;
|
|
47
|
+
padding: 8px 16px;
|
|
48
|
+
background: var(--panel);
|
|
49
|
+
border-bottom: 1px solid var(--border);
|
|
50
|
+
}
|
|
51
|
+
header strong {
|
|
52
|
+
margin-right: auto;
|
|
53
|
+
}
|
|
54
|
+
nav button {
|
|
55
|
+
padding: 4px 12px;
|
|
56
|
+
border: 1px solid var(--border);
|
|
57
|
+
background: transparent;
|
|
58
|
+
color: inherit;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
nav button:first-child {
|
|
62
|
+
border-radius: 6px 0 0 6px;
|
|
63
|
+
}
|
|
64
|
+
nav button:last-child {
|
|
65
|
+
border-radius: 0 6px 6px 0;
|
|
66
|
+
}
|
|
67
|
+
nav button[aria-pressed='true'] {
|
|
68
|
+
background: var(--text);
|
|
69
|
+
color: var(--bg);
|
|
70
|
+
}
|
|
71
|
+
#status {
|
|
72
|
+
color: var(--muted);
|
|
73
|
+
font-variant-numeric: tabular-nums;
|
|
74
|
+
}
|
|
75
|
+
#badge {
|
|
76
|
+
display: none;
|
|
77
|
+
padding: 2px 8px;
|
|
78
|
+
border-radius: 999px;
|
|
79
|
+
background: var(--accent);
|
|
80
|
+
color: #fff;
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
}
|
|
83
|
+
main {
|
|
84
|
+
display: grid;
|
|
85
|
+
gap: 1px;
|
|
86
|
+
background: var(--border);
|
|
87
|
+
min-height: 0;
|
|
88
|
+
}
|
|
89
|
+
main[data-view='html'] #pdf,
|
|
90
|
+
main[data-view='pdf'] #html {
|
|
91
|
+
display: none;
|
|
92
|
+
}
|
|
93
|
+
main[data-view='split'] {
|
|
94
|
+
grid-template-columns: 1fr 1fr;
|
|
95
|
+
}
|
|
96
|
+
section {
|
|
97
|
+
position: relative;
|
|
98
|
+
min-height: 0;
|
|
99
|
+
background: var(--panel);
|
|
100
|
+
}
|
|
101
|
+
iframe {
|
|
102
|
+
position: absolute;
|
|
103
|
+
inset: 0;
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: 100%;
|
|
106
|
+
border: 0;
|
|
107
|
+
background: #fff;
|
|
108
|
+
}
|
|
109
|
+
iframe[hidden] {
|
|
110
|
+
visibility: hidden;
|
|
111
|
+
}
|
|
112
|
+
#overlay {
|
|
113
|
+
display: none;
|
|
114
|
+
position: fixed;
|
|
115
|
+
inset: 48px 16px auto;
|
|
116
|
+
max-height: 70vh;
|
|
117
|
+
overflow: auto;
|
|
118
|
+
padding: 16px;
|
|
119
|
+
border-left: 4px solid var(--accent);
|
|
120
|
+
border-radius: 8px;
|
|
121
|
+
background: var(--panel);
|
|
122
|
+
box-shadow: 0 8px 32px rgb(0 0 0 / 30%);
|
|
123
|
+
white-space: pre-wrap;
|
|
124
|
+
font:
|
|
125
|
+
12px ui-monospace,
|
|
126
|
+
monospace;
|
|
127
|
+
}
|
|
128
|
+
@media (max-width: 900px) {
|
|
129
|
+
main[data-view='split'] {
|
|
130
|
+
grid-template-columns: 1fr;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
134
|
+
</head>
|
|
135
|
+
<body>
|
|
136
|
+
<header>
|
|
137
|
+
<strong>md-printer preview</strong>
|
|
138
|
+
<span id="badge">unsigned preview</span>
|
|
139
|
+
<span id="status">connecting</span>
|
|
140
|
+
<nav>
|
|
141
|
+
<button data-view="html">HTML</button><button data-view="split">Split</button><button data-view="pdf">PDF</button>
|
|
142
|
+
</nav>
|
|
143
|
+
</header>
|
|
144
|
+
<main data-view="split">
|
|
145
|
+
<section id="html"><iframe title="HTML"></iframe><iframe title="HTML" hidden></iframe></section>
|
|
146
|
+
<section id="pdf"><iframe title="PDF"></iframe></section>
|
|
147
|
+
</main>
|
|
148
|
+
<pre id="overlay"></pre>
|
|
149
|
+
<script>
|
|
150
|
+
const main = document.querySelector('main')
|
|
151
|
+
const status = document.getElementById('status')
|
|
152
|
+
const badge = document.getElementById('badge')
|
|
153
|
+
const overlay = document.getElementById('overlay')
|
|
154
|
+
const pdf = document.querySelector('#pdf iframe')
|
|
155
|
+
let [visible, buffer] = document.querySelectorAll('#html iframe')
|
|
156
|
+
let pdfOutput = true
|
|
157
|
+
|
|
158
|
+
const setView = (view) => {
|
|
159
|
+
main.dataset.view = pdfOutput ? view : 'html'
|
|
160
|
+
for (const button of document.querySelectorAll('nav button')) {
|
|
161
|
+
button.setAttribute('aria-pressed', String(button.dataset.view === main.dataset.view))
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
localStorage.setItem('md-printer:view', view)
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const button of document.querySelectorAll('nav button')) {
|
|
169
|
+
button.addEventListener('click', () => setView(button.dataset.view))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const currentPage = () => {
|
|
173
|
+
const doc = visible.contentDocument?.documentElement
|
|
174
|
+
const pages = Number(pdf.dataset.pages || 1)
|
|
175
|
+
|
|
176
|
+
if (!doc || doc.scrollHeight <= doc.clientHeight) {
|
|
177
|
+
return 1
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return Math.min(pages, Math.floor((doc.scrollTop / doc.scrollHeight) * pages) + 1)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const reloadHtml = (version) => {
|
|
184
|
+
const scroll = visible.contentDocument?.documentElement.scrollTop ?? 0
|
|
185
|
+
|
|
186
|
+
buffer.onload = () => {
|
|
187
|
+
buffer.contentDocument.documentElement.scrollTop = scroll
|
|
188
|
+
buffer.hidden = false
|
|
189
|
+
visible.hidden = true
|
|
190
|
+
;[visible, buffer] = [buffer, visible]
|
|
191
|
+
}
|
|
192
|
+
buffer.src = `/document.html?v=${version}`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const events = new EventSource('/__events')
|
|
196
|
+
|
|
197
|
+
events.addEventListener('building', () => {
|
|
198
|
+
status.textContent = 'building'
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
events.addEventListener('ready', ({ data }) => {
|
|
202
|
+
const build = JSON.parse(data)
|
|
203
|
+
|
|
204
|
+
overlay.style.display = 'none'
|
|
205
|
+
status.textContent = `v${build.version}, ${build.pages ? `${build.pages} pages, ` : ''}${build.ms}ms`
|
|
206
|
+
badge.style.display = build.signed ? 'inline' : 'none'
|
|
207
|
+
pdfOutput = build.pdf
|
|
208
|
+
|
|
209
|
+
reloadHtml(build.version)
|
|
210
|
+
|
|
211
|
+
if (build.pdf) {
|
|
212
|
+
pdf.dataset.pages = build.pages
|
|
213
|
+
pdf.src = `/document.pdf?v=${build.version}#page=${currentPage()}`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
setView(main.dataset.view)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
events.addEventListener('failed', ({ data }) => {
|
|
220
|
+
const error = JSON.parse(data)
|
|
221
|
+
|
|
222
|
+
status.textContent = 'failed'
|
|
223
|
+
overlay.textContent = [error.file, error.message, error.frame].filter(Boolean).join('\n\n')
|
|
224
|
+
overlay.style.display = 'block'
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
events.addEventListener('error', () => {
|
|
228
|
+
status.textContent = 'disconnected'
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
let stored
|
|
232
|
+
try {
|
|
233
|
+
stored = localStorage.getItem('md-printer:view')
|
|
234
|
+
} catch {}
|
|
235
|
+
setView(stored ?? (window.innerWidth > 900 ? 'split' : 'pdf'))
|
|
236
|
+
</script>
|
|
237
|
+
</body>
|
|
238
|
+
</html>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import "../../constants/file.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 { resolveConfined } from "../template/assets.mjs";
|
|
5
|
+
import { isLoopbackRequest } from "../loopback.mjs";
|
|
6
|
+
import { TemplateService } from "../template/template.service.mjs";
|
|
7
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { LoggerService } from "@cenk1cenk2/oclif-common";
|
|
10
|
+
import { Injectable } from "@nestjs/common";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import { spawn } from "child_process";
|
|
14
|
+
import { once } from "events";
|
|
15
|
+
import { createServer } from "http";
|
|
16
|
+
import serveStatic from "serve-static";
|
|
17
|
+
//#region src/services/session/preview.service.ts
|
|
18
|
+
let PreviewService = class PreviewService {
|
|
19
|
+
logger;
|
|
20
|
+
template;
|
|
21
|
+
server;
|
|
22
|
+
clients = /* @__PURE__ */ new Set();
|
|
23
|
+
build;
|
|
24
|
+
error;
|
|
25
|
+
profile;
|
|
26
|
+
window;
|
|
27
|
+
constructor(logger, template) {
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
this.template = template;
|
|
30
|
+
this.logger.setup(this.constructor.name);
|
|
31
|
+
}
|
|
32
|
+
async listen(options) {
|
|
33
|
+
const shell = await this.template.page(fileURLToPath(new URL("./Preview.svelte", import.meta.url)));
|
|
34
|
+
this.server = createServer((req, res) => {
|
|
35
|
+
this.handle(req, res, shell).catch((err) => {
|
|
36
|
+
this.logger.debug("Preview request failed: %s", err);
|
|
37
|
+
res.writeHead(500);
|
|
38
|
+
res.end();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
this.server.listen(options.port, options.host);
|
|
42
|
+
await once(this.server, "listening");
|
|
43
|
+
const { port } = this.server.address();
|
|
44
|
+
return `http://${options.host.includes(":") ? `[${options.host}]` : options.host}:${port}/`;
|
|
45
|
+
}
|
|
46
|
+
building() {
|
|
47
|
+
this.send("building", {});
|
|
48
|
+
}
|
|
49
|
+
ready(build) {
|
|
50
|
+
this.build = build;
|
|
51
|
+
this.error = void 0;
|
|
52
|
+
this.send("ready", this.summary(build));
|
|
53
|
+
}
|
|
54
|
+
failed(err) {
|
|
55
|
+
this.error = {
|
|
56
|
+
message: err.message,
|
|
57
|
+
frame: err.frame,
|
|
58
|
+
file: err.filename
|
|
59
|
+
};
|
|
60
|
+
this.send("failed", this.error);
|
|
61
|
+
}
|
|
62
|
+
async open(executable, url) {
|
|
63
|
+
this.profile = await mkdtemp(join(tmpdir(), "md-printer-preview-"));
|
|
64
|
+
this.window = spawn(executable, [
|
|
65
|
+
`--app=${url}`,
|
|
66
|
+
`--user-data-dir=${this.profile}`,
|
|
67
|
+
"--no-first-run",
|
|
68
|
+
"--no-default-browser-check"
|
|
69
|
+
], { stdio: "ignore" });
|
|
70
|
+
await new Promise((done) => {
|
|
71
|
+
this.window.on("exit", () => done());
|
|
72
|
+
this.window.on("error", (err) => {
|
|
73
|
+
this.logger.warn("Can not open the preview window, open it manually: %s (%s)", url, err.message);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async onModuleDestroy() {
|
|
78
|
+
await this.close();
|
|
79
|
+
}
|
|
80
|
+
async close() {
|
|
81
|
+
const { server, window, profile } = this;
|
|
82
|
+
this.server = void 0;
|
|
83
|
+
this.window = void 0;
|
|
84
|
+
this.profile = void 0;
|
|
85
|
+
for (const client of this.clients) client.end();
|
|
86
|
+
this.clients.clear();
|
|
87
|
+
if (window && window.exitCode === null && window.signalCode === null && window.kill()) await once(window, "exit");
|
|
88
|
+
if (server?.listening) {
|
|
89
|
+
server.close();
|
|
90
|
+
await once(server, "close");
|
|
91
|
+
}
|
|
92
|
+
if (profile) await rm(profile, {
|
|
93
|
+
recursive: true,
|
|
94
|
+
force: true
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async handle(req, res, shell) {
|
|
98
|
+
if (!isLoopbackRequest(req)) {
|
|
99
|
+
res.writeHead(403);
|
|
100
|
+
res.end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
switch (new URL(req.url, "http://localhost").pathname) {
|
|
104
|
+
case "/":
|
|
105
|
+
res.writeHead(200, {
|
|
106
|
+
["content-type"]: "text/html; charset=utf-8",
|
|
107
|
+
["cache-control"]: "no-store"
|
|
108
|
+
});
|
|
109
|
+
res.end(shell);
|
|
110
|
+
return;
|
|
111
|
+
case "/__events":
|
|
112
|
+
res.writeHead(200, {
|
|
113
|
+
["content-type"]: "text/event-stream",
|
|
114
|
+
["cache-control"]: "no-store",
|
|
115
|
+
connection: "keep-alive"
|
|
116
|
+
});
|
|
117
|
+
res.write("retry: 1000\n\n");
|
|
118
|
+
this.clients.add(res);
|
|
119
|
+
req.on("close", () => this.clients.delete(res));
|
|
120
|
+
if (this.error) res.write(this.event("failed", this.error));
|
|
121
|
+
else if (this.build) res.write(this.event("ready", this.summary(this.build)));
|
|
122
|
+
return;
|
|
123
|
+
case "/document.html":
|
|
124
|
+
if (!this.build) {
|
|
125
|
+
res.writeHead(404);
|
|
126
|
+
res.end();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
res.writeHead(200, {
|
|
130
|
+
["content-type"]: "text/html; charset=utf-8",
|
|
131
|
+
["cache-control"]: "no-store"
|
|
132
|
+
});
|
|
133
|
+
res.end(this.build.document.html);
|
|
134
|
+
return;
|
|
135
|
+
case "/document.pdf":
|
|
136
|
+
if (this.build?.ctx.options.output !== "pdf") {
|
|
137
|
+
res.writeHead(404);
|
|
138
|
+
res.end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
res.writeHead(200, {
|
|
142
|
+
["content-type"]: "application/pdf",
|
|
143
|
+
["cache-control"]: "no-store"
|
|
144
|
+
});
|
|
145
|
+
res.end(this.build.output);
|
|
146
|
+
return;
|
|
147
|
+
default: return this.asset(req, res);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async asset(req, res) {
|
|
151
|
+
const reject = (status = 404) => {
|
|
152
|
+
res.writeHead(status);
|
|
153
|
+
res.end();
|
|
154
|
+
};
|
|
155
|
+
if (!this.build) return reject();
|
|
156
|
+
const base = this.build.ctx.input.directory;
|
|
157
|
+
const url = URL.parse(req.url, "file:///");
|
|
158
|
+
const path = url && join(base, fileURLToPath(url));
|
|
159
|
+
if (!path || !await resolveConfined(path, base)) return reject();
|
|
160
|
+
serveStatic(base, {
|
|
161
|
+
dotfiles: "ignore",
|
|
162
|
+
index: false,
|
|
163
|
+
fallthrough: false,
|
|
164
|
+
cacheControl: false,
|
|
165
|
+
setHeaders: (response) => response.setHeader("cache-control", "no-store")
|
|
166
|
+
})(req, res, (err) => reject(err?.status));
|
|
167
|
+
}
|
|
168
|
+
summary(build) {
|
|
169
|
+
const pdf = build.ctx.options.output === "pdf";
|
|
170
|
+
return {
|
|
171
|
+
version: build.version,
|
|
172
|
+
ms: build.ms,
|
|
173
|
+
pdf,
|
|
174
|
+
pages: pdf ? (build.output.toString("latin1").match(/\/Type\s*\/Page[^s]/g) ?? []).length : 0,
|
|
175
|
+
signed: Boolean(build.ctx.signature)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
send(event, data) {
|
|
179
|
+
for (const client of this.clients) client.write(this.event(event, data));
|
|
180
|
+
}
|
|
181
|
+
event(event, data) {
|
|
182
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
PreviewService = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [typeof LoggerService === "undefined" ? Object : LoggerService, typeof TemplateService === "undefined" ? Object : TemplateService])], PreviewService);
|
|
186
|
+
//#endregion
|
|
187
|
+
export { PreviewService };
|