printdown 1.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/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/chunk-HX2CR2SJ.js +1006 -0
- package/dist/chunk-HX2CR2SJ.js.map +1 -0
- package/dist/chunk-INBZBWSZ.js +868 -0
- package/dist/chunk-INBZBWSZ.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +71 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/themes/default/index.d.ts +40 -0
- package/dist/themes/default/index.js +31 -0
- package/dist/themes/default/index.js.map +1 -0
- package/dist/themes/default/style.css +2133 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultThemeCss,
|
|
3
|
+
getFontDir
|
|
4
|
+
} from "./chunk-INBZBWSZ.js";
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
import fs3 from "fs";
|
|
8
|
+
import path3 from "path";
|
|
9
|
+
|
|
10
|
+
// src/markdown/index.ts
|
|
11
|
+
import MarkdownIt from "markdown-it";
|
|
12
|
+
import {
|
|
13
|
+
createHighlighter
|
|
14
|
+
} from "shiki";
|
|
15
|
+
|
|
16
|
+
// src/markdown/plugins/highlight.ts
|
|
17
|
+
function highlightPlugin(md) {
|
|
18
|
+
function tokenize(state, silent) {
|
|
19
|
+
const start = state.pos;
|
|
20
|
+
const marker = state.src.charCodeAt(start);
|
|
21
|
+
if (marker !== 61) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const scanned = state.scanDelims(state.pos, true);
|
|
25
|
+
const len = scanned.length;
|
|
26
|
+
const ch = String.fromCharCode(marker);
|
|
27
|
+
if (len < 2) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
let isColor = false;
|
|
31
|
+
let color = "blue";
|
|
32
|
+
if (scanned.can_open) {
|
|
33
|
+
const match = state.src.slice(start + len).match(/^\{([a-zA-Z0-9_-]+)\}/);
|
|
34
|
+
if (match) {
|
|
35
|
+
isColor = true;
|
|
36
|
+
color = match[1];
|
|
37
|
+
state.pos += match[0].length;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (silent) {
|
|
41
|
+
state.pos += len;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
for (let i = 0; i < Math.floor(len / 2); i++) {
|
|
45
|
+
const token = state.push("text", "", 0);
|
|
46
|
+
token.content = ch + ch;
|
|
47
|
+
state.delimiters.push({
|
|
48
|
+
marker: 61,
|
|
49
|
+
length: 0,
|
|
50
|
+
jump: i,
|
|
51
|
+
token: state.tokens.length - 1,
|
|
52
|
+
level: state.level,
|
|
53
|
+
end: -1,
|
|
54
|
+
open: scanned.can_open,
|
|
55
|
+
close: scanned.can_close,
|
|
56
|
+
color: isColor ? color : "blue"
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
state.pos += len;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
function postProcess(state) {
|
|
63
|
+
const delimiters = state.delimiters;
|
|
64
|
+
if (!delimiters) return;
|
|
65
|
+
for (let i = 0; i < delimiters.length; i++) {
|
|
66
|
+
const startDelim = delimiters[i];
|
|
67
|
+
if (startDelim.marker !== 61 || startDelim.end === -1) continue;
|
|
68
|
+
const endDelim = delimiters[startDelim.end];
|
|
69
|
+
const tokenStart = state.tokens[startDelim.token];
|
|
70
|
+
tokenStart.type = "mark_open";
|
|
71
|
+
tokenStart.tag = "mark";
|
|
72
|
+
tokenStart.nesting = 1;
|
|
73
|
+
tokenStart.markup = "==";
|
|
74
|
+
tokenStart.content = "";
|
|
75
|
+
tokenStart.attrs = [
|
|
76
|
+
["class", `printdown-highlight printdown-highlight--${startDelim.color || "blue"}`]
|
|
77
|
+
];
|
|
78
|
+
const tokenEnd = state.tokens[endDelim.token];
|
|
79
|
+
tokenEnd.type = "mark_close";
|
|
80
|
+
tokenEnd.tag = "mark";
|
|
81
|
+
tokenEnd.nesting = -1;
|
|
82
|
+
tokenEnd.markup = "==";
|
|
83
|
+
tokenEnd.content = "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
md.inline.ruler.before("emphasis", "highlight", tokenize);
|
|
87
|
+
md.inline.ruler2.before("emphasis", "highlight", (state) => {
|
|
88
|
+
const delimiters = state.delimiters;
|
|
89
|
+
if (!delimiters) return;
|
|
90
|
+
for (let i = 0; i < delimiters.length; i++) {
|
|
91
|
+
const startDelim = delimiters[i];
|
|
92
|
+
if (startDelim.marker !== 61) continue;
|
|
93
|
+
for (let j = i + 1; j < delimiters.length; j++) {
|
|
94
|
+
const endDelim = delimiters[j];
|
|
95
|
+
if (endDelim.marker !== 61) continue;
|
|
96
|
+
if (startDelim.open && endDelim.close && endDelim.end === -1 && startDelim.end === -1) {
|
|
97
|
+
startDelim.end = j;
|
|
98
|
+
endDelim.end = i;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
postProcess(state);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/markdown/plugins/task-list.ts
|
|
108
|
+
var TASK_LIST_REGEX = /^\[([ xX])\][ \t]+/;
|
|
109
|
+
function taskListPlugin(md) {
|
|
110
|
+
md.core.ruler.after("inline", "task_list", (state) => {
|
|
111
|
+
const tokens = state.tokens;
|
|
112
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
113
|
+
if (tokens[i].type !== "bullet_list_open") {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let listEnd = i + 1;
|
|
117
|
+
let depth = 1;
|
|
118
|
+
let hasTaskItems = false;
|
|
119
|
+
while (listEnd < tokens.length && depth > 0) {
|
|
120
|
+
if (tokens[listEnd].type === "bullet_list_open") {
|
|
121
|
+
depth++;
|
|
122
|
+
} else if (tokens[listEnd].type === "bullet_list_close") {
|
|
123
|
+
depth--;
|
|
124
|
+
}
|
|
125
|
+
listEnd++;
|
|
126
|
+
}
|
|
127
|
+
for (let j = i + 1; j < listEnd; j++) {
|
|
128
|
+
if (tokens[j].type === "list_item_open") {
|
|
129
|
+
const inlineTokenIndex = findFirstInlineToken(tokens, j);
|
|
130
|
+
if (inlineTokenIndex !== -1) {
|
|
131
|
+
const inlineToken = tokens[inlineTokenIndex];
|
|
132
|
+
if (isTaskListItem(inlineToken)) {
|
|
133
|
+
hasTaskItems = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!hasTaskItems) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
addClass(tokens[i], "printdown-task-list");
|
|
143
|
+
for (let j = i + 1; j < listEnd; j++) {
|
|
144
|
+
if (tokens[j].type === "list_item_open") {
|
|
145
|
+
const inlineTokenIndex = findFirstInlineToken(tokens, j);
|
|
146
|
+
if (inlineTokenIndex !== -1) {
|
|
147
|
+
const inlineToken = tokens[inlineTokenIndex];
|
|
148
|
+
const match = getTaskListMatch(inlineToken);
|
|
149
|
+
if (match) {
|
|
150
|
+
addClass(tokens[j], "printdown-task-list-item");
|
|
151
|
+
const isChecked = match[1].toLowerCase() === "x";
|
|
152
|
+
removeTaskListPrefix(inlineToken);
|
|
153
|
+
insertCheckboxToken(inlineToken, isChecked, state.Token);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function findFirstInlineToken(tokens, listItemIndex) {
|
|
162
|
+
for (let i = listItemIndex + 1; i < tokens.length; i++) {
|
|
163
|
+
if (tokens[i].type === "list_item_close" || tokens[i].type === "list_item_open") {
|
|
164
|
+
return -1;
|
|
165
|
+
}
|
|
166
|
+
if (tokens[i].type === "inline") {
|
|
167
|
+
return i;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return -1;
|
|
171
|
+
}
|
|
172
|
+
function isTaskListItem(inlineToken) {
|
|
173
|
+
if (!inlineToken.children || inlineToken.children.length === 0) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const firstChild = inlineToken.children[0];
|
|
177
|
+
return firstChild.type === "text" && TASK_LIST_REGEX.test(firstChild.content);
|
|
178
|
+
}
|
|
179
|
+
function getTaskListMatch(inlineToken) {
|
|
180
|
+
if (!inlineToken.children || inlineToken.children.length === 0) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const firstChild = inlineToken.children[0];
|
|
184
|
+
if (firstChild.type === "text") {
|
|
185
|
+
return firstChild.content.match(TASK_LIST_REGEX);
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
function removeTaskListPrefix(inlineToken) {
|
|
190
|
+
if (!inlineToken.children || inlineToken.children.length === 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const firstChild = inlineToken.children[0];
|
|
194
|
+
if (firstChild.type === "text") {
|
|
195
|
+
firstChild.content = firstChild.content.replace(TASK_LIST_REGEX, "");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function insertCheckboxToken(inlineToken, checked, TokenConstructor) {
|
|
199
|
+
const checkboxToken = new TokenConstructor("html_inline", "", 0);
|
|
200
|
+
checkboxToken.content = `<input class="printdown-task-list-checkbox" type="checkbox" disabled${checked ? ' checked=""' : ""} /> `;
|
|
201
|
+
if (!inlineToken.children) {
|
|
202
|
+
inlineToken.children = [];
|
|
203
|
+
}
|
|
204
|
+
inlineToken.children.unshift(checkboxToken);
|
|
205
|
+
}
|
|
206
|
+
function addClass(token, className) {
|
|
207
|
+
const classIndex = token.attrIndex("class");
|
|
208
|
+
if (classIndex < 0) {
|
|
209
|
+
token.attrPush(["class", className]);
|
|
210
|
+
} else if (token.attrs) {
|
|
211
|
+
const existing = token.attrs[classIndex][1];
|
|
212
|
+
if (typeof existing === "string" && !existing.split(" ").includes(className)) {
|
|
213
|
+
token.attrs[classIndex][1] = `${existing} ${className}`.trim();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/markdown/plugins/badge.ts
|
|
219
|
+
var BADGE_MARKDOWN_REGEX = /^\[badge(?:\(([^)]+)\)|\{([^}]+)\})?:([\s\S]*?)\]/i;
|
|
220
|
+
var BADGE_OPEN_TAG_REGEX = /^<badge(\s+[^>]*)?>$/i;
|
|
221
|
+
var BADGE_CLOSE_TAG_REGEX = /^<\/badge>$/i;
|
|
222
|
+
var BADGE_SELF_CLOSING_REGEX = /^<badge(\s+[^>]*)?\/>$/i;
|
|
223
|
+
function parseBadgeOptions(optionsStr) {
|
|
224
|
+
let variant = "solid";
|
|
225
|
+
let color = "blue";
|
|
226
|
+
if (!optionsStr) {
|
|
227
|
+
return { variant, color };
|
|
228
|
+
}
|
|
229
|
+
const parts = optionsStr.split(/[\s,]+/).map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
230
|
+
for (const part of parts) {
|
|
231
|
+
if (part === "solid" || part === "soft" || part === "outline") {
|
|
232
|
+
variant = part;
|
|
233
|
+
} else if (/^[a-z0-9_-]+$/.test(part)) {
|
|
234
|
+
color = part;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { variant, color };
|
|
238
|
+
}
|
|
239
|
+
function parseBadgeAttrs(attrsStr) {
|
|
240
|
+
let variant = "solid";
|
|
241
|
+
let color = "blue";
|
|
242
|
+
if (!attrsStr) {
|
|
243
|
+
return { variant, color };
|
|
244
|
+
}
|
|
245
|
+
const variantMatch = attrsStr.match(/variant=["']?([a-zA-Z0-9_-]+)["']?/i);
|
|
246
|
+
if (variantMatch) {
|
|
247
|
+
const v = variantMatch[1].toLowerCase();
|
|
248
|
+
if (v === "solid" || v === "soft" || v === "outline") {
|
|
249
|
+
variant = v;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const colorMatch = attrsStr.match(/color=["']?([a-zA-Z0-9_-]+)["']?/i);
|
|
253
|
+
if (colorMatch) {
|
|
254
|
+
color = colorMatch[1].toLowerCase();
|
|
255
|
+
}
|
|
256
|
+
return { variant, color };
|
|
257
|
+
}
|
|
258
|
+
function badgePlugin(md) {
|
|
259
|
+
function inlineBadgeRule(state, silent) {
|
|
260
|
+
const max = state.posMax;
|
|
261
|
+
const start = state.pos;
|
|
262
|
+
if (state.src.charCodeAt(start) !== 91) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
const match = state.src.slice(start).match(BADGE_MARKDOWN_REGEX);
|
|
266
|
+
if (!match) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const matchedLength = match[0].length;
|
|
270
|
+
if (start + matchedLength > max) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
if (silent) {
|
|
274
|
+
state.pos += matchedLength;
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
const rawOptions = match[1] || match[2] || "";
|
|
278
|
+
const badgeText = match[3];
|
|
279
|
+
const { variant, color } = parseBadgeOptions(rawOptions);
|
|
280
|
+
const tokenOpen = state.push("badge_open", "span", 1);
|
|
281
|
+
tokenOpen.attrs = [
|
|
282
|
+
["class", `printdown-badge printdown-badge--${variant} printdown-badge--${color}`]
|
|
283
|
+
];
|
|
284
|
+
const nestedTokens = [];
|
|
285
|
+
state.md.inline.parse(badgeText, state.md, state.env, nestedTokens);
|
|
286
|
+
for (const nested of nestedTokens) {
|
|
287
|
+
state.tokens.push(nested);
|
|
288
|
+
}
|
|
289
|
+
state.push("badge_close", "span", -1);
|
|
290
|
+
state.pos += matchedLength;
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
md.inline.ruler.before("link", "badge", inlineBadgeRule);
|
|
294
|
+
md.core.ruler.after("inline", "badge_tags", (state) => {
|
|
295
|
+
const tokens = state.tokens;
|
|
296
|
+
function processToken(token) {
|
|
297
|
+
if (token.type === "html_inline") {
|
|
298
|
+
const trimmed = token.content.trim();
|
|
299
|
+
if (BADGE_SELF_CLOSING_REGEX.test(trimmed)) {
|
|
300
|
+
const match = trimmed.match(BADGE_SELF_CLOSING_REGEX);
|
|
301
|
+
const { variant, color } = parseBadgeAttrs(match?.[1]);
|
|
302
|
+
token.content = `<span class="printdown-badge printdown-badge--${variant} printdown-badge--${color}"></span>`;
|
|
303
|
+
} else if (BADGE_OPEN_TAG_REGEX.test(trimmed)) {
|
|
304
|
+
const match = trimmed.match(BADGE_OPEN_TAG_REGEX);
|
|
305
|
+
const { variant, color } = parseBadgeAttrs(match?.[1]);
|
|
306
|
+
token.content = `<span class="printdown-badge printdown-badge--${variant} printdown-badge--${color}">`;
|
|
307
|
+
} else if (BADGE_CLOSE_TAG_REGEX.test(trimmed)) {
|
|
308
|
+
token.content = `</span>`;
|
|
309
|
+
}
|
|
310
|
+
} else if (token.type === "html_block") {
|
|
311
|
+
token.content = token.content.replace(
|
|
312
|
+
/<badge(\s+[^>]*)?>([\s\S]*?)<\/badge>/gi,
|
|
313
|
+
(_, attrs, body) => {
|
|
314
|
+
const { variant, color } = parseBadgeAttrs(attrs);
|
|
315
|
+
return `<span class="printdown-badge printdown-badge--${variant} printdown-badge--${color}">${body}</span>`;
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
321
|
+
processToken(tokens[i]);
|
|
322
|
+
if (tokens[i].children) {
|
|
323
|
+
for (let j = 0; j < tokens[i].children.length; j++) {
|
|
324
|
+
processToken(tokens[i].children[j]);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/markdown/plugins/kbd.ts
|
|
332
|
+
var KBD_MARKDOWN_REGEX = /^\[kbd(?:\(([^)]+)\))?:([\s\S]*?)\]/i;
|
|
333
|
+
var KBD_ICON_ONLY_REGEX = /^\[kbd\(([^)]+)\)\]/i;
|
|
334
|
+
var KBD_OPEN_TAG_REGEX = /^<kbd(\s+[^>]*)?>$/i;
|
|
335
|
+
var KBD_CLOSE_TAG_REGEX = /^<\/kbd>$/i;
|
|
336
|
+
var KBD_SELF_CLOSING_REGEX = /^<kbd(\s+[^>]*)?\/>$/i;
|
|
337
|
+
function parseKbdAttrs(attrsStr) {
|
|
338
|
+
if (!attrsStr) return {};
|
|
339
|
+
const iconMatch = attrsStr.match(/icon=["']?([a-zA-Z0-9_-]+)["']?/i);
|
|
340
|
+
if (iconMatch) {
|
|
341
|
+
return { icon: iconMatch[1].toLowerCase() };
|
|
342
|
+
}
|
|
343
|
+
return {};
|
|
344
|
+
}
|
|
345
|
+
function kbdPlugin(md) {
|
|
346
|
+
function inlineKbdRule(state, silent) {
|
|
347
|
+
const max = state.posMax;
|
|
348
|
+
const start = state.pos;
|
|
349
|
+
if (state.src.charCodeAt(start) !== 91) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const rest = state.src.slice(start);
|
|
353
|
+
const matchWithText = rest.match(KBD_MARKDOWN_REGEX);
|
|
354
|
+
const matchIconOnly = !matchWithText ? rest.match(KBD_ICON_ONLY_REGEX) : null;
|
|
355
|
+
const match = matchWithText || matchIconOnly;
|
|
356
|
+
if (!match) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const matchedLength = match[0].length;
|
|
360
|
+
if (start + matchedLength > max) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
if (silent) {
|
|
364
|
+
state.pos += matchedLength;
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
const icon = matchWithText ? matchWithText[1] : matchIconOnly ? matchIconOnly[1] : void 0;
|
|
368
|
+
const kbdText = matchWithText ? matchWithText[2] : "";
|
|
369
|
+
const tokenOpen = state.push("kbd_open", "kbd", 1);
|
|
370
|
+
tokenOpen.attrs = [["class", "printdown-kbd"]];
|
|
371
|
+
if (icon) {
|
|
372
|
+
const iconToken = state.push("html_inline", "", 0);
|
|
373
|
+
iconToken.content = `<span class="printdown-kbd-icon printdown-kbd-icon--${icon.toLowerCase()}"></span>`;
|
|
374
|
+
}
|
|
375
|
+
if (kbdText) {
|
|
376
|
+
const nestedTokens = [];
|
|
377
|
+
state.md.inline.parse(kbdText, state.md, state.env, nestedTokens);
|
|
378
|
+
for (const nested of nestedTokens) {
|
|
379
|
+
state.tokens.push(nested);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
state.push("kbd_close", "kbd", -1);
|
|
383
|
+
state.pos += matchedLength;
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
md.inline.ruler.before("link", "kbd", inlineKbdRule);
|
|
387
|
+
md.core.ruler.after("inline", "kbd_tags", (state) => {
|
|
388
|
+
const tokens = state.tokens;
|
|
389
|
+
function processToken(token) {
|
|
390
|
+
if (token.type === "html_inline") {
|
|
391
|
+
const trimmed = token.content.trim();
|
|
392
|
+
if (KBD_SELF_CLOSING_REGEX.test(trimmed)) {
|
|
393
|
+
const match = trimmed.match(KBD_SELF_CLOSING_REGEX);
|
|
394
|
+
const { icon } = parseKbdAttrs(match?.[1]);
|
|
395
|
+
const iconHtml = icon ? `<span class="printdown-kbd-icon printdown-kbd-icon--${icon}"></span>` : "";
|
|
396
|
+
token.content = `<kbd class="printdown-kbd">${iconHtml}</kbd>`;
|
|
397
|
+
} else if (KBD_OPEN_TAG_REGEX.test(trimmed)) {
|
|
398
|
+
const match = trimmed.match(KBD_OPEN_TAG_REGEX);
|
|
399
|
+
const { icon } = parseKbdAttrs(match?.[1]);
|
|
400
|
+
const iconHtml = icon ? `<span class="printdown-kbd-icon printdown-kbd-icon--${icon}"></span>` : "";
|
|
401
|
+
token.content = `<kbd class="printdown-kbd">${iconHtml}`;
|
|
402
|
+
} else if (KBD_CLOSE_TAG_REGEX.test(trimmed)) {
|
|
403
|
+
token.content = `</kbd>`;
|
|
404
|
+
}
|
|
405
|
+
} else if (token.type === "html_block") {
|
|
406
|
+
token.content = token.content.replace(
|
|
407
|
+
/<kbd(\s+[^>]*)?>([\s\S]*?)<\/kbd>/gi,
|
|
408
|
+
(_, attrs, body) => {
|
|
409
|
+
const { icon } = parseKbdAttrs(attrs);
|
|
410
|
+
const iconHtml = icon ? `<span class="printdown-kbd-icon printdown-kbd-icon--${icon}"></span>` : "";
|
|
411
|
+
return `<kbd class="printdown-kbd">${iconHtml}${body}</kbd>`;
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
417
|
+
processToken(tokens[i]);
|
|
418
|
+
if (tokens[i].children) {
|
|
419
|
+
for (let j = 0; j < tokens[i].children.length; j++) {
|
|
420
|
+
processToken(tokens[i].children[j]);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/markdown/index.ts
|
|
428
|
+
var DEFAULT_LANGS = [
|
|
429
|
+
"javascript",
|
|
430
|
+
"typescript",
|
|
431
|
+
"jsx",
|
|
432
|
+
"tsx",
|
|
433
|
+
"html",
|
|
434
|
+
"css",
|
|
435
|
+
"json",
|
|
436
|
+
"markdown",
|
|
437
|
+
"mdx",
|
|
438
|
+
"bash",
|
|
439
|
+
"shell",
|
|
440
|
+
"yaml",
|
|
441
|
+
"toml",
|
|
442
|
+
"sql",
|
|
443
|
+
"python",
|
|
444
|
+
"rust",
|
|
445
|
+
"go",
|
|
446
|
+
"c",
|
|
447
|
+
"cpp",
|
|
448
|
+
"diff",
|
|
449
|
+
"dockerfile",
|
|
450
|
+
"graphql",
|
|
451
|
+
"ini",
|
|
452
|
+
"java",
|
|
453
|
+
"kotlin",
|
|
454
|
+
"ruby",
|
|
455
|
+
"php",
|
|
456
|
+
"swift",
|
|
457
|
+
"xml"
|
|
458
|
+
];
|
|
459
|
+
var DEFAULT_THEME = "github-dark";
|
|
460
|
+
var cachedHighlighter = null;
|
|
461
|
+
var cachedLangs = /* @__PURE__ */ new Set();
|
|
462
|
+
var cachedThemes = /* @__PURE__ */ new Set();
|
|
463
|
+
async function getHighlighter(options) {
|
|
464
|
+
const theme = options?.theme || DEFAULT_THEME;
|
|
465
|
+
const userLangs = options?.langs || [];
|
|
466
|
+
const requiredLangs = Array.from(/* @__PURE__ */ new Set([...DEFAULT_LANGS, ...userLangs]));
|
|
467
|
+
if (!cachedHighlighter) {
|
|
468
|
+
cachedHighlighter = await createHighlighter({
|
|
469
|
+
themes: [theme],
|
|
470
|
+
langs: requiredLangs
|
|
471
|
+
});
|
|
472
|
+
for (const lang of requiredLangs) {
|
|
473
|
+
cachedLangs.add(lang);
|
|
474
|
+
}
|
|
475
|
+
cachedThemes.add(theme);
|
|
476
|
+
return cachedHighlighter;
|
|
477
|
+
}
|
|
478
|
+
if (!cachedThemes.has(theme)) {
|
|
479
|
+
await cachedHighlighter.loadTheme(theme);
|
|
480
|
+
cachedThemes.add(theme);
|
|
481
|
+
}
|
|
482
|
+
const missingLangs = requiredLangs.filter((l) => !cachedLangs.has(l));
|
|
483
|
+
if (missingLangs.length > 0) {
|
|
484
|
+
await cachedHighlighter.loadLanguage(...missingLangs);
|
|
485
|
+
for (const lang of missingLangs) {
|
|
486
|
+
cachedLangs.add(lang);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return cachedHighlighter;
|
|
490
|
+
}
|
|
491
|
+
async function createMarkdownRenderer(options = {}) {
|
|
492
|
+
const md = new MarkdownIt({
|
|
493
|
+
html: options.html ?? true,
|
|
494
|
+
linkify: options.linkify ?? true,
|
|
495
|
+
typographer: options.typographer ?? true
|
|
496
|
+
});
|
|
497
|
+
md.use(highlightPlugin);
|
|
498
|
+
md.use(taskListPlugin);
|
|
499
|
+
md.use(badgePlugin);
|
|
500
|
+
md.use(kbdPlugin);
|
|
501
|
+
const highlighter = await getHighlighter(options.shiki);
|
|
502
|
+
const theme = options.shiki?.theme || DEFAULT_THEME;
|
|
503
|
+
const defaultFence = md.renderer.rules.fence;
|
|
504
|
+
md.renderer.rules.fence = (tokens, idx, fenceOptions, env, self) => {
|
|
505
|
+
const token = tokens[idx];
|
|
506
|
+
const info = token.info ? token.info.trim() : "";
|
|
507
|
+
const lang = info ? info.split(/\s+/)[0] : "text";
|
|
508
|
+
const code = token.content;
|
|
509
|
+
try {
|
|
510
|
+
const loadedLangs = highlighter.getLoadedLanguages();
|
|
511
|
+
const targetLang = loadedLangs.includes(lang) ? lang : "text";
|
|
512
|
+
return highlighter.codeToHtml(code, {
|
|
513
|
+
lang: targetLang,
|
|
514
|
+
theme,
|
|
515
|
+
transformers: [
|
|
516
|
+
{
|
|
517
|
+
pre(node) {
|
|
518
|
+
const existingClass = node.properties.class || "";
|
|
519
|
+
node.properties.class = `printdown-code ${existingClass}`.trim();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
});
|
|
524
|
+
} catch {
|
|
525
|
+
if (defaultFence) {
|
|
526
|
+
return defaultFence(tokens, idx, fenceOptions, env, self);
|
|
527
|
+
}
|
|
528
|
+
return `<pre class="printdown-code"><code>${md.utils.escapeHtml(code)}</code></pre>
|
|
529
|
+
`;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
return md;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/renderer/index.ts
|
|
536
|
+
import fs from "fs";
|
|
537
|
+
import path from "path";
|
|
538
|
+
import { chromium } from "playwright";
|
|
539
|
+
async function setupFontRouting(context) {
|
|
540
|
+
const interDir = getFontDir("@fontsource-variable/inter/index.css");
|
|
541
|
+
const notoDir = getFontDir("@fontsource-variable/noto-sans-jp/index.css");
|
|
542
|
+
const jbDir = getFontDir("@fontsource-variable/jetbrains-mono/index.css");
|
|
543
|
+
const genDir = getFontDir("gen-interface-jp/500.css");
|
|
544
|
+
await context.route("https://printdown.local/fonts/**", async (route) => {
|
|
545
|
+
const reqUrl = route.request().url();
|
|
546
|
+
let filePath = "";
|
|
547
|
+
if (interDir && reqUrl.startsWith("https://printdown.local/fonts/inter/")) {
|
|
548
|
+
filePath = path.join(
|
|
549
|
+
interDir,
|
|
550
|
+
"files",
|
|
551
|
+
reqUrl.replace("https://printdown.local/fonts/inter/", "")
|
|
552
|
+
);
|
|
553
|
+
} else if (notoDir && reqUrl.startsWith("https://printdown.local/fonts/noto-sans-jp/")) {
|
|
554
|
+
filePath = path.join(
|
|
555
|
+
notoDir,
|
|
556
|
+
"files",
|
|
557
|
+
reqUrl.replace("https://printdown.local/fonts/noto-sans-jp/", "")
|
|
558
|
+
);
|
|
559
|
+
} else if (jbDir && reqUrl.startsWith("https://printdown.local/fonts/jetbrains-mono/")) {
|
|
560
|
+
filePath = path.join(
|
|
561
|
+
jbDir,
|
|
562
|
+
"files",
|
|
563
|
+
reqUrl.replace("https://printdown.local/fonts/jetbrains-mono/", "")
|
|
564
|
+
);
|
|
565
|
+
} else if (genDir && reqUrl.startsWith("https://printdown.local/fonts/gen-interface-jp/w/")) {
|
|
566
|
+
filePath = path.join(
|
|
567
|
+
genDir,
|
|
568
|
+
"w",
|
|
569
|
+
reqUrl.replace("https://printdown.local/fonts/gen-interface-jp/w/", "")
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
573
|
+
try {
|
|
574
|
+
const body = fs.readFileSync(filePath);
|
|
575
|
+
await route.fulfill({
|
|
576
|
+
status: 200,
|
|
577
|
+
contentType: "font/woff2",
|
|
578
|
+
body
|
|
579
|
+
});
|
|
580
|
+
return;
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
await route.abort();
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
var BrowserRenderer = class {
|
|
588
|
+
browser = null;
|
|
589
|
+
async init() {
|
|
590
|
+
if (!this.browser) {
|
|
591
|
+
this.browser = await chromium.launch({
|
|
592
|
+
headless: true,
|
|
593
|
+
args: [
|
|
594
|
+
"--disable-gpu",
|
|
595
|
+
"--no-sandbox",
|
|
596
|
+
"--disable-setuid-sandbox",
|
|
597
|
+
"--font-render-hinting=medium"
|
|
598
|
+
]
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async close() {
|
|
603
|
+
if (this.browser) {
|
|
604
|
+
await this.browser.close();
|
|
605
|
+
this.browser = null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async preparePage(html, width, height, deviceScaleFactor) {
|
|
609
|
+
let isLocalBrowser = false;
|
|
610
|
+
let browser = this.browser;
|
|
611
|
+
if (!browser) {
|
|
612
|
+
browser = await chromium.launch({
|
|
613
|
+
headless: true,
|
|
614
|
+
args: [
|
|
615
|
+
"--disable-gpu",
|
|
616
|
+
"--no-sandbox",
|
|
617
|
+
"--disable-setuid-sandbox",
|
|
618
|
+
"--font-render-hinting=medium"
|
|
619
|
+
]
|
|
620
|
+
});
|
|
621
|
+
isLocalBrowser = true;
|
|
622
|
+
}
|
|
623
|
+
const context = await browser.newContext({
|
|
624
|
+
viewport: { width, height },
|
|
625
|
+
deviceScaleFactor
|
|
626
|
+
});
|
|
627
|
+
await setupFontRouting(context);
|
|
628
|
+
const page = await context.newPage();
|
|
629
|
+
await page.setContent(html, {
|
|
630
|
+
waitUntil: "networkidle"
|
|
631
|
+
});
|
|
632
|
+
await page.evaluate(async () => {
|
|
633
|
+
if (document.fonts) {
|
|
634
|
+
await document.fonts.ready;
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
await page.evaluate(async () => {
|
|
638
|
+
const images = Array.from(document.images);
|
|
639
|
+
const pendingImages = images.filter((img) => !img.complete);
|
|
640
|
+
if (pendingImages.length > 0) {
|
|
641
|
+
await Promise.all(
|
|
642
|
+
pendingImages.map(
|
|
643
|
+
(img) => new Promise((resolve) => {
|
|
644
|
+
img.onload = img.onerror = resolve;
|
|
645
|
+
})
|
|
646
|
+
)
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return { browser, context, page, isLocalBrowser };
|
|
651
|
+
}
|
|
652
|
+
async renderToBuffer(html, format, options = {}) {
|
|
653
|
+
const width = options.width ?? (format === "pdf" ? 820 : 1200);
|
|
654
|
+
const height = options.height ?? 800;
|
|
655
|
+
const deviceScaleFactor = options.scale ?? options.deviceScaleFactor ?? 2;
|
|
656
|
+
const { browser, context, page, isLocalBrowser } = await this.preparePage(
|
|
657
|
+
html,
|
|
658
|
+
width,
|
|
659
|
+
height,
|
|
660
|
+
deviceScaleFactor
|
|
661
|
+
);
|
|
662
|
+
try {
|
|
663
|
+
if (format === "pdf") {
|
|
664
|
+
const pdfBuffer = await page.pdf({
|
|
665
|
+
printBackground: true,
|
|
666
|
+
preferCSSPageSize: true,
|
|
667
|
+
...options.pdf
|
|
668
|
+
});
|
|
669
|
+
return Buffer.from(pdfBuffer);
|
|
670
|
+
} else if (format === "png") {
|
|
671
|
+
const screenshotBuffer = await page.screenshot({
|
|
672
|
+
type: "png",
|
|
673
|
+
fullPage: options.image?.fullPage ?? true,
|
|
674
|
+
...options.image
|
|
675
|
+
});
|
|
676
|
+
return Buffer.from(screenshotBuffer);
|
|
677
|
+
} else if (format === "jpeg") {
|
|
678
|
+
const screenshotBuffer = await page.screenshot({
|
|
679
|
+
type: "jpeg",
|
|
680
|
+
quality: options.quality ?? 90,
|
|
681
|
+
fullPage: options.image?.fullPage ?? true,
|
|
682
|
+
...options.image
|
|
683
|
+
});
|
|
684
|
+
return Buffer.from(screenshotBuffer);
|
|
685
|
+
} else if (format === "webp") {
|
|
686
|
+
const screenshotBuffer = await page.screenshot({
|
|
687
|
+
type: "webp",
|
|
688
|
+
quality: options.quality ?? 90,
|
|
689
|
+
fullPage: options.image?.fullPage ?? true,
|
|
690
|
+
...options.image
|
|
691
|
+
});
|
|
692
|
+
return Buffer.from(screenshotBuffer);
|
|
693
|
+
} else {
|
|
694
|
+
throw new Error(`Unsupported output format: ${format}`);
|
|
695
|
+
}
|
|
696
|
+
} finally {
|
|
697
|
+
await page.close().catch(() => {
|
|
698
|
+
});
|
|
699
|
+
await context.close().catch(() => {
|
|
700
|
+
});
|
|
701
|
+
if (isLocalBrowser && browser) {
|
|
702
|
+
await browser.close().catch(() => {
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async renderToPageBuffers(html, format, options = {}) {
|
|
708
|
+
if (format === "pdf") {
|
|
709
|
+
const singleBuffer = await this.renderToBuffer(html, format, options);
|
|
710
|
+
return [singleBuffer];
|
|
711
|
+
}
|
|
712
|
+
const width = options.width ?? 820;
|
|
713
|
+
const pageHeight = options.pageHeight ?? Math.round(width * 297 / 210);
|
|
714
|
+
const marginTop = options.pageMarginTop ?? Math.round(20 / 297 * pageHeight);
|
|
715
|
+
const marginBottom = options.pageMarginBottom ?? Math.round(20 / 297 * pageHeight);
|
|
716
|
+
const contentHeightPerPage = Math.max(100, pageHeight - marginTop - marginBottom);
|
|
717
|
+
const deviceScaleFactor = options.scale ?? options.deviceScaleFactor ?? 2;
|
|
718
|
+
const pagesHtml = html.includes("</head>") ? html.replace(
|
|
719
|
+
"</head>",
|
|
720
|
+
"<style>.printdown { padding-top: 0 !important; padding-bottom: 0 !important; }</style></head>"
|
|
721
|
+
) : html + "<style>.printdown { padding-top: 0 !important; padding-bottom: 0 !important; }</style>";
|
|
722
|
+
const { browser, context, page, isLocalBrowser } = await this.preparePage(
|
|
723
|
+
pagesHtml,
|
|
724
|
+
width,
|
|
725
|
+
pageHeight,
|
|
726
|
+
deviceScaleFactor
|
|
727
|
+
);
|
|
728
|
+
let composePage = null;
|
|
729
|
+
try {
|
|
730
|
+
const totalContentHeight = await page.evaluate(
|
|
731
|
+
() => Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)
|
|
732
|
+
);
|
|
733
|
+
const pageCount = Math.max(1, Math.ceil(totalContentHeight / contentHeightPerPage));
|
|
734
|
+
await page.setViewportSize({
|
|
735
|
+
width,
|
|
736
|
+
height: Math.max(totalContentHeight + 100, pageHeight)
|
|
737
|
+
});
|
|
738
|
+
const fullContentScreenshot = await page.screenshot({
|
|
739
|
+
type: "png",
|
|
740
|
+
fullPage: true
|
|
741
|
+
});
|
|
742
|
+
composePage = await context.newPage();
|
|
743
|
+
await composePage.setContent(`
|
|
744
|
+
<!DOCTYPE html>
|
|
745
|
+
<html>
|
|
746
|
+
<head>
|
|
747
|
+
<style>
|
|
748
|
+
html, body { margin: 0; padding: 0; background: #ffffff; overflow: hidden; }
|
|
749
|
+
canvas { display: block; }
|
|
750
|
+
</style>
|
|
751
|
+
</head>
|
|
752
|
+
<body>
|
|
753
|
+
<canvas id="canvas" width="${width * deviceScaleFactor}" height="${pageHeight * deviceScaleFactor}" style="width:${width}px; height:${pageHeight}px;"></canvas>
|
|
754
|
+
</body>
|
|
755
|
+
</html>
|
|
756
|
+
`);
|
|
757
|
+
const base64Img = fullContentScreenshot.toString("base64");
|
|
758
|
+
const pageBuffers = [];
|
|
759
|
+
const imageType = format === "jpeg" ? "jpeg" : format === "webp" ? "webp" : "png";
|
|
760
|
+
for (let i = 0; i < pageCount; i++) {
|
|
761
|
+
const srcY = i * contentHeightPerPage;
|
|
762
|
+
const srcH = Math.min(contentHeightPerPage, totalContentHeight - srcY);
|
|
763
|
+
await composePage.evaluate(
|
|
764
|
+
async ({ base64, width: width2, marginTop: marginTop2, srcY: srcY2, srcH: srcH2, scale }) => {
|
|
765
|
+
const canvas = document.getElementById("canvas");
|
|
766
|
+
const ctx = canvas.getContext("2d");
|
|
767
|
+
if (!ctx) return;
|
|
768
|
+
ctx.fillStyle = "#ffffff";
|
|
769
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
770
|
+
const img = new Image();
|
|
771
|
+
await new Promise((resolve) => {
|
|
772
|
+
img.onload = resolve;
|
|
773
|
+
img.src = "data:image/png;base64," + base64;
|
|
774
|
+
});
|
|
775
|
+
ctx.drawImage(
|
|
776
|
+
img,
|
|
777
|
+
0,
|
|
778
|
+
srcY2 * scale,
|
|
779
|
+
width2 * scale,
|
|
780
|
+
srcH2 * scale,
|
|
781
|
+
0,
|
|
782
|
+
marginTop2 * scale,
|
|
783
|
+
width2 * scale,
|
|
784
|
+
srcH2 * scale
|
|
785
|
+
);
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
base64: base64Img,
|
|
789
|
+
width,
|
|
790
|
+
marginTop,
|
|
791
|
+
srcY,
|
|
792
|
+
srcH,
|
|
793
|
+
scale: deviceScaleFactor
|
|
794
|
+
}
|
|
795
|
+
);
|
|
796
|
+
const screenshotBuffer = await composePage.screenshot({
|
|
797
|
+
type: imageType,
|
|
798
|
+
quality: imageType !== "png" ? options.quality ?? 90 : void 0,
|
|
799
|
+
...options.image
|
|
800
|
+
});
|
|
801
|
+
pageBuffers.push(Buffer.from(screenshotBuffer));
|
|
802
|
+
}
|
|
803
|
+
return pageBuffers;
|
|
804
|
+
} finally {
|
|
805
|
+
if (composePage) {
|
|
806
|
+
await composePage.close().catch(() => {
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
await page.close().catch(() => {
|
|
810
|
+
});
|
|
811
|
+
await context.close().catch(() => {
|
|
812
|
+
});
|
|
813
|
+
if (isLocalBrowser && browser) {
|
|
814
|
+
await browser.close().catch(() => {
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
async function renderHtmlToBuffer(html, format, options = {}) {
|
|
821
|
+
const renderer = new BrowserRenderer();
|
|
822
|
+
return renderer.renderToBuffer(html, format, options);
|
|
823
|
+
}
|
|
824
|
+
async function renderHtmlToPageBuffers(html, format, options = {}) {
|
|
825
|
+
const renderer = new BrowserRenderer();
|
|
826
|
+
return renderer.renderToPageBuffers(html, format, options);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/renderer/html.ts
|
|
830
|
+
import fs2 from "fs";
|
|
831
|
+
import path2 from "path";
|
|
832
|
+
function resolveCss(cssInput, baseUrl) {
|
|
833
|
+
const inputs = Array.isArray(cssInput) ? cssInput : [cssInput];
|
|
834
|
+
const resolvedCssParts = [];
|
|
835
|
+
for (const input of inputs) {
|
|
836
|
+
if (!input || typeof input !== "string") continue;
|
|
837
|
+
let isFile = false;
|
|
838
|
+
let filePath = input;
|
|
839
|
+
if (baseUrl && !path2.isAbsolute(input)) {
|
|
840
|
+
const candidate = path2.resolve(baseUrl, input);
|
|
841
|
+
if (fs2.existsSync(candidate) && fs2.statSync(candidate).isFile()) {
|
|
842
|
+
isFile = true;
|
|
843
|
+
filePath = candidate;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (!isFile && (fs2.existsSync(input) || input.endsWith(".css"))) {
|
|
847
|
+
if (fs2.existsSync(input) && fs2.statSync(input).isFile()) {
|
|
848
|
+
isFile = true;
|
|
849
|
+
filePath = path2.resolve(input);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (isFile) {
|
|
853
|
+
try {
|
|
854
|
+
const fileContent = fs2.readFileSync(filePath, "utf-8");
|
|
855
|
+
resolvedCssParts.push(fileContent);
|
|
856
|
+
} catch (err) {
|
|
857
|
+
console.warn(`[printdown] Failed to read CSS file "${filePath}":`, err);
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
resolvedCssParts.push(input);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return resolvedCssParts.join("\n\n");
|
|
864
|
+
}
|
|
865
|
+
function escapeHtml(str) {
|
|
866
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
867
|
+
}
|
|
868
|
+
function buildHtmlDocument(bodyHtml, options = {}) {
|
|
869
|
+
const title = options.title || "Printdown Document";
|
|
870
|
+
let themeCss = "";
|
|
871
|
+
if (options.theme !== false) {
|
|
872
|
+
if (typeof options.theme === "object" && options.theme !== null && "css" in options.theme) {
|
|
873
|
+
themeCss = options.theme.css;
|
|
874
|
+
} else {
|
|
875
|
+
themeCss = defaultThemeCss;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const userCss = options.css ? resolveCss(options.css, options.baseUrl) : "";
|
|
879
|
+
const baseTag = options.baseUrl ? `<base href="${options.baseUrl.endsWith("/") ? options.baseUrl : options.baseUrl + "/"}">` : "";
|
|
880
|
+
return `<!DOCTYPE html>
|
|
881
|
+
<html lang="ja">
|
|
882
|
+
<head>
|
|
883
|
+
<meta charset="utf-8">
|
|
884
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
885
|
+
<title>${escapeHtml(title)}</title>
|
|
886
|
+
${baseTag}
|
|
887
|
+
${themeCss ? `<style id="printdown-theme">${themeCss}</style>` : ""}
|
|
888
|
+
${userCss ? `<style id="printdown-user-css">${userCss}</style>` : ""}
|
|
889
|
+
</head>
|
|
890
|
+
<body>
|
|
891
|
+
<article class="printdown">
|
|
892
|
+
${bodyHtml}
|
|
893
|
+
</article>
|
|
894
|
+
</body>
|
|
895
|
+
</html>`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/index.ts
|
|
899
|
+
function inferFormat(output, explicitFormat) {
|
|
900
|
+
if (explicitFormat) {
|
|
901
|
+
return explicitFormat;
|
|
902
|
+
}
|
|
903
|
+
if (output) {
|
|
904
|
+
const ext = path3.extname(output).toLowerCase();
|
|
905
|
+
if (ext === ".png") return "png";
|
|
906
|
+
if (ext === ".jpg" || ext === ".jpeg") return "jpeg";
|
|
907
|
+
if (ext === ".webp") return "webp";
|
|
908
|
+
if (ext === ".pdf") return "pdf";
|
|
909
|
+
}
|
|
910
|
+
return "pdf";
|
|
911
|
+
}
|
|
912
|
+
function resolvePageOutputFilePath(outputPath, pageIndex, totalPages) {
|
|
913
|
+
const pageNumber = pageIndex + 1;
|
|
914
|
+
if (outputPath.includes("%d")) {
|
|
915
|
+
return outputPath.replace(/%d/g, String(pageNumber));
|
|
916
|
+
}
|
|
917
|
+
if (totalPages === 1 && !outputPath.includes("-1")) {
|
|
918
|
+
const parsed2 = path3.parse(outputPath);
|
|
919
|
+
return path3.join(parsed2.dir, `${parsed2.name}-${pageNumber}${parsed2.ext}`);
|
|
920
|
+
}
|
|
921
|
+
const parsed = path3.parse(outputPath);
|
|
922
|
+
return path3.join(parsed.dir, `${parsed.name}-${pageNumber}${parsed.ext}`);
|
|
923
|
+
}
|
|
924
|
+
async function renderToHtml(markdown, options = {}) {
|
|
925
|
+
const md = await createMarkdownRenderer(options.markdown);
|
|
926
|
+
const bodyHtml = md.render(markdown);
|
|
927
|
+
return buildHtmlDocument(bodyHtml, options);
|
|
928
|
+
}
|
|
929
|
+
async function renderPages(markdown, options = {}) {
|
|
930
|
+
const format = inferFormat(options.output, options.format);
|
|
931
|
+
const html = await renderToHtml(markdown, options);
|
|
932
|
+
const buffers = await renderHtmlToPageBuffers(html, format, options);
|
|
933
|
+
if (options.output) {
|
|
934
|
+
const resolvedPath = path3.resolve(options.output);
|
|
935
|
+
const outputDir = path3.dirname(resolvedPath);
|
|
936
|
+
if (!fs3.existsSync(outputDir)) {
|
|
937
|
+
fs3.mkdirSync(outputDir, { recursive: true });
|
|
938
|
+
}
|
|
939
|
+
if (format === "pdf" && buffers.length === 1) {
|
|
940
|
+
fs3.writeFileSync(resolvedPath, buffers[0]);
|
|
941
|
+
} else {
|
|
942
|
+
buffers.forEach((buf, idx) => {
|
|
943
|
+
const filePath = resolvePageOutputFilePath(resolvedPath, idx, buffers.length);
|
|
944
|
+
fs3.writeFileSync(filePath, buf);
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return buffers;
|
|
949
|
+
}
|
|
950
|
+
async function renderFilePages(filePath, options = {}) {
|
|
951
|
+
const absolutePath = path3.resolve(filePath);
|
|
952
|
+
const encoding = options.encoding ?? "utf-8";
|
|
953
|
+
const markdown = fs3.readFileSync(absolutePath, encoding);
|
|
954
|
+
const fileDir = path3.dirname(absolutePath);
|
|
955
|
+
const baseUrl = options.baseUrl ?? fileDir;
|
|
956
|
+
return renderPages(markdown, {
|
|
957
|
+
baseUrl,
|
|
958
|
+
...options
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
async function render(markdown, options = {}) {
|
|
962
|
+
if (options.pages) {
|
|
963
|
+
return renderPages(markdown, options);
|
|
964
|
+
}
|
|
965
|
+
const format = inferFormat(options.output, options.format);
|
|
966
|
+
const html = await renderToHtml(markdown, options);
|
|
967
|
+
const buffer = await renderHtmlToBuffer(html, format, options);
|
|
968
|
+
if (options.output) {
|
|
969
|
+
const outputPath = path3.resolve(options.output);
|
|
970
|
+
const outputDir = path3.dirname(outputPath);
|
|
971
|
+
if (!fs3.existsSync(outputDir)) {
|
|
972
|
+
fs3.mkdirSync(outputDir, { recursive: true });
|
|
973
|
+
}
|
|
974
|
+
fs3.writeFileSync(outputPath, buffer);
|
|
975
|
+
}
|
|
976
|
+
return buffer;
|
|
977
|
+
}
|
|
978
|
+
async function renderFile(filePath, options = {}) {
|
|
979
|
+
const absolutePath = path3.resolve(filePath);
|
|
980
|
+
const encoding = options.encoding ?? "utf-8";
|
|
981
|
+
const markdown = fs3.readFileSync(absolutePath, encoding);
|
|
982
|
+
const fileDir = path3.dirname(absolutePath);
|
|
983
|
+
const baseUrl = options.baseUrl ?? fileDir;
|
|
984
|
+
if (options.pages) {
|
|
985
|
+
return renderPages(markdown, {
|
|
986
|
+
baseUrl,
|
|
987
|
+
...options
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
return render(markdown, {
|
|
991
|
+
baseUrl,
|
|
992
|
+
...options
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export {
|
|
997
|
+
createMarkdownRenderer,
|
|
998
|
+
BrowserRenderer,
|
|
999
|
+
renderHtmlToPageBuffers,
|
|
1000
|
+
renderToHtml,
|
|
1001
|
+
renderPages,
|
|
1002
|
+
renderFilePages,
|
|
1003
|
+
render,
|
|
1004
|
+
renderFile
|
|
1005
|
+
};
|
|
1006
|
+
//# sourceMappingURL=chunk-HX2CR2SJ.js.map
|