@threepenny/md2pdf 0.0.0-stage → 2.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.
@@ -0,0 +1,75 @@
1
+ // Obsidian wikilinks and embeds. A link to another note can't go anywhere in a single PDF, so
2
+ // [[Note]], [[Note|alias]] and [[Note#Heading]] render as plain text, the way Obsidian displays them;
3
+ // only a same-note [[#Heading]] becomes a link. ![[image.png]] renders like ![](image.png), and other
4
+ // embeds, which neither the CLI nor the website can read, become a labelled placeholder.
5
+
6
+ const IMAGE = /\.(avif|bmp|gif|jpe?g|png|svg|webp)$/i;
7
+ const FILE = /\.(3gp|canvas|flac|m4a|mkv|mov|mp3|mp4|ogg|ogv|pdf|wav|webm)$/i;
8
+ // Obsidian's ![[image.png|300]] and ![[image.png|300x200]] set the image's size.
9
+ const SIZE = /^(\d+)(?:x(\d+))?$/;
10
+
11
+ export default function wikilinks(md, { slugify }) {
12
+ md.inline.ruler.before('link', 'wikilink', (state, silent) => {
13
+ // Silent mode skips over a link's text; a rule that consumes its [ reads as a nested link and
14
+ // breaks [see [[Note]]](url). A wikilink's brackets balance, so it can be skipped one character at a time.
15
+ if (silent) return false;
16
+ const { src, pos } = state;
17
+ const embed = src.charCodeAt(pos) === 0x21 /* ! */;
18
+ const start = pos + (embed ? 1 : 0);
19
+ if (!src.startsWith('[[', start)) return false;
20
+ const end = src.indexOf(']]', start + 2);
21
+ if (end < 0) return false;
22
+ const inner = src.slice(start + 2, end);
23
+ if (!inner.trim() || /[\n[\]]/.test(inner)) return false;
24
+ // [[1]](url) is an ordinary link whose text is [1]; AI assistants write citations that way.
25
+ if (src.charCodeAt(end + 2) === 0x28 /* ( */) return false;
26
+
27
+ // In tables the pipe is escaped, [[Note\|alias]]; markdown-it unescapes it only inside table cells.
28
+ const [, target, alias = ''] = inner.match(/^(.*?)(?:\\?\|(.*))?$/);
29
+ const [page, ...rest] = target.split('#').map((s) => s.trim());
30
+ // Block references (#^id) name no heading to show.
31
+ const headings = rest.filter((h) => h && !h.startsWith('^'));
32
+ const name = [page, ...headings].filter(Boolean).join(' > ') || target.trim();
33
+ const text = alias.trim() || name;
34
+ if (!name) return false;
35
+
36
+ if (embed && IMAGE.test(page)) image(state, page, alias.trim());
37
+ // The placeholder names what was left out; Obsidian doesn't show an embed's alias either.
38
+ else if (embed) {
39
+ state.push('wikilink_embed', 'span', 0).meta = { kind: FILE.test(page) ? 'file' : 'note', text: name };
40
+ } else if (!page && headings.length) {
41
+ state.push('link_open', 'a', 1).attrs = [['href', `#${headingId(state.md, headings.at(-1))}`]];
42
+ state.push('text', '', 0).content = text;
43
+ state.push('link_close', 'a', -1);
44
+ } else {
45
+ state.push('text', '', 0).content = text;
46
+ }
47
+ state.pos = end + 2;
48
+ return true;
49
+ });
50
+
51
+ // markdown-it-anchor slugs a heading's text after the typographer and emoji rules have run, so
52
+ // `A -- B` has the id a--b. Slug the same text it does.
53
+ const headingId = (md, heading) =>
54
+ slugify(md.parseInline(heading, {})[0].children
55
+ .filter((t) => t.type === 'text' || t.type === 'code_inline')
56
+ .map((t) => t.content)
57
+ .join(''));
58
+
59
+ md.renderer.rules.wikilink_embed = (tokens, idx) => {
60
+ const { kind, text } = tokens[idx].meta;
61
+ return `<span class="markdown-embed">Embedded ${kind}: ${md.utils.escapeHtml(text)}</span>`;
62
+ };
63
+ }
64
+
65
+ function image(state, path, alias) {
66
+ const href = state.md.normalizeLink(path);
67
+ const size = alias.match(SIZE);
68
+ const alt = new state.Token('text', '', 0);
69
+ alt.content = size ? path : alias || path;
70
+ const token = state.push('image', 'img', 0);
71
+ token.attrs = [['src', state.md.validateLink(href) ? href : ''], ['alt', '']];
72
+ if (size) token.attrs.push(['width', size[1]], ...(size[2] ? [['height', size[2]]] : []));
73
+ token.children = [alt];
74
+ token.content = alt.content;
75
+ }
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@threepenny/md2pdf",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.0",
4
+ "description": "Markdown to beautifully styled PDF: the renderer and macOS CLI behind md2pdf.me",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Threepenny Labs LLC",
8
+ "homepage": "https://md2pdf.me",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/jbrecht/md2pdf.git"
12
+ },
13
+ "bin": {
14
+ "md2pdf": "bin/md2pdf.js"
15
+ },
16
+ "exports": {
17
+ ".": "./lib/render.js",
18
+ "./render": "./lib/render.js",
19
+ "./styles": "./lib/styles.js",
20
+ "./styles/*": "./styles/*",
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "lib",
26
+ "styles"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "test": "node test/run.js",
33
+ "demo": "echo 'Open http://localhost:3900/demo/' && python3 -m http.server 3900 --bind 127.0.0.1"
34
+ },
35
+ "dependencies": {
36
+ "github-markdown-css": "5.9.0",
37
+ "highlight.js": "^11.12.0",
38
+ "markdown-it": "^15.0.2",
39
+ "markdown-it-anchor": "^10.0.0",
40
+ "markdown-it-emoji": "^3.1.0",
41
+ "markdown-it-footnote": "^4.0.0",
42
+ "markdown-it-task-lists": "^2.1.1"
43
+ }
44
+ }
@@ -0,0 +1,396 @@
1
+ /* Academic: a journal-article look, with Palatino, numbered sections, booktabs tables and numbered figures. */
2
+
3
+ /* Variables a page can override, on .markdown-body itself: for all its documents, or for one with
4
+ <article class="markdown-body" style="--md2pdf-font-size: 12pt">. A value set on :root or another
5
+ ancestor doesn't reach the document. :where() gives these defaults no specificity, so the page's
6
+ rule wins wherever it comes. Every style uses the same names. */
7
+ :where(.markdown-body) {
8
+ /* Size and spacing. --md2pdf-font-size is the body text size, an absolute length such as 11pt or 15px; headings and smaller text scale with
9
+ it. --md2pdf-leading scales each line height's excess over 1, and --md2pdf-space the vertical
10
+ gaps between blocks; both are 1 as designed. */
11
+ --md2pdf-font-size: 11pt;
12
+ --md2pdf-leading: 1;
13
+ --md2pdf-space: 1;
14
+ /* Colors: --md2pdf-background the document's background; --md2pdf-text body text; --md2pdf-muted
15
+ secondary text; --md2pdf-accent links and callouts; --md2pdf-rule rules and table lines. */
16
+ --md2pdf-background: #fff;
17
+ --md2pdf-text: #111;
18
+ --md2pdf-muted: #555;
19
+ --md2pdf-accent: #1b3a73;
20
+ --md2pdf-rule: #111;
21
+ /* Fonts: --md2pdf-font body text, --md2pdf-heading-font headings (the body font unless set),
22
+ --md2pdf-code-font code. */
23
+ --md2pdf-font: Palatino, "Palatino Linotype", "Book Antiqua", "URW Palladio L", "TeX Gyre Pagella", Georgia, serif;
24
+ --md2pdf-heading-font: var(--md2pdf-font);
25
+ --md2pdf-code-font: "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
26
+ }
27
+
28
+ .markdown-body {
29
+ font-family: var(--md2pdf-font);
30
+ background: var(--md2pdf-background);
31
+ font-size: var(--md2pdf-font-size);
32
+ line-height: calc(1 + 0.42 * var(--md2pdf-leading));
33
+ color: var(--md2pdf-text);
34
+ font-kerning: normal;
35
+ counter-reset: h2 figure;
36
+ -webkit-print-color-adjust: exact;
37
+ print-color-adjust: exact;
38
+ }
39
+
40
+ .markdown-body p,
41
+ .markdown-body li {
42
+ text-align: justify;
43
+ hyphens: auto;
44
+ orphans: 2;
45
+ widows: 2;
46
+ }
47
+
48
+ .markdown-body p {
49
+ margin: 0 0 calc(0.6em * var(--md2pdf-space));
50
+ }
51
+ /* Paragraphs that follow one another are indented rather than spaced, as in print. */
52
+ .markdown-body p + p {
53
+ margin-top: calc(-0.6em * var(--md2pdf-space));
54
+ text-indent: 1.5em;
55
+ }
56
+
57
+ /* Headings: the first h1 is the title; h2–h4 are numbered 1, 1.1, 1.1.1. */
58
+
59
+ .markdown-body h1,
60
+ .markdown-body h2,
61
+ .markdown-body h3,
62
+ .markdown-body h4,
63
+ .markdown-body h5,
64
+ .markdown-body h6 {
65
+ font-family: var(--md2pdf-heading-font);
66
+ font-weight: bold;
67
+ line-height: calc(1 + 0.25 * var(--md2pdf-leading));
68
+ break-after: avoid;
69
+ }
70
+
71
+ .markdown-body h1 {
72
+ font-size: calc(17 / 11 * var(--md2pdf-font-size));
73
+ font-weight: normal;
74
+ text-align: center;
75
+ margin: 0 0 calc(1.5em * var(--md2pdf-space));
76
+ }
77
+ .markdown-body h1:not(:first-child) {
78
+ margin-top: calc(2em * var(--md2pdf-space));
79
+ }
80
+
81
+ .markdown-body h2 {
82
+ font-size: calc(13 / 11 * var(--md2pdf-font-size));
83
+ margin: calc(1.6em * var(--md2pdf-space)) 0 calc(0.6em * var(--md2pdf-space));
84
+ counter-increment: h2;
85
+ counter-reset: h3;
86
+ }
87
+ .markdown-body h3 {
88
+ font-size: calc(11.5 / 11 * var(--md2pdf-font-size));
89
+ margin: calc(1.3em * var(--md2pdf-space)) 0 calc(0.5em * var(--md2pdf-space));
90
+ counter-increment: h3;
91
+ counter-reset: h4;
92
+ }
93
+ .markdown-body h4 {
94
+ font-size: var(--md2pdf-font-size);
95
+ margin: calc(1.1em * var(--md2pdf-space)) 0 calc(0.4em * var(--md2pdf-space));
96
+ counter-increment: h4;
97
+ }
98
+ .markdown-body h5,
99
+ .markdown-body h6 {
100
+ font-size: var(--md2pdf-font-size);
101
+ font-style: italic;
102
+ font-weight: normal;
103
+ margin: calc(1em * var(--md2pdf-space)) 0 calc(0.3em * var(--md2pdf-space));
104
+ }
105
+ .markdown-body h2::before {
106
+ content: counter(h2);
107
+ margin-right: 1em;
108
+ }
109
+ .markdown-body h3::before {
110
+ content: counter(h2) "." counter(h3);
111
+ margin-right: 1em;
112
+ }
113
+ .markdown-body h4::before {
114
+ content: counter(h2) "." counter(h3) "." counter(h4);
115
+ margin-right: 1em;
116
+ }
117
+
118
+ /* Links */
119
+
120
+ .markdown-body a {
121
+ color: var(--md2pdf-accent);
122
+ text-decoration: none;
123
+ }
124
+ .markdown-body a.header-anchor {
125
+ display: none;
126
+ }
127
+
128
+ /* Lists */
129
+
130
+ .markdown-body ul,
131
+ .markdown-body ol {
132
+ margin: 0 0 calc(0.6em * var(--md2pdf-space));
133
+ padding-left: 1.8em;
134
+ }
135
+ .markdown-body li {
136
+ margin: calc(0.1em * var(--md2pdf-space)) 0;
137
+ }
138
+ .markdown-body li > p + p {
139
+ text-indent: 0;
140
+ margin-top: 0;
141
+ }
142
+ .markdown-body li > ul,
143
+ .markdown-body li > ol {
144
+ margin: calc(0.1em * var(--md2pdf-space)) 0 0;
145
+ }
146
+ .markdown-body .task-list-item {
147
+ list-style-type: none;
148
+ }
149
+ .markdown-body .task-list-item input {
150
+ margin: 0 0.4em 0.2em -1.4em;
151
+ vertical-align: middle;
152
+ accent-color: var(--md2pdf-text);
153
+ }
154
+
155
+ /* Quotes: set in from both sides, slightly smaller. */
156
+
157
+ .markdown-body blockquote {
158
+ margin: calc(0.9em * var(--md2pdf-space)) 2.2em;
159
+ padding: 0;
160
+ font-size: calc(10 / 11 * var(--md2pdf-font-size));
161
+ break-inside: avoid;
162
+ }
163
+ .markdown-body blockquote p + p {
164
+ text-indent: 0;
165
+ margin-top: 0;
166
+ }
167
+
168
+ /* Callouts (GitHub alerts and Obsidian callouts): set off by a thin rule, titled in small caps. */
169
+
170
+ .markdown-body .markdown-alert {
171
+ --md2pdf-callout: var(--md2pdf-accent);
172
+ margin: calc(0.9em * var(--md2pdf-space)) 0;
173
+ padding: 0.1em 0 0.1em 1em;
174
+ border-left: 1.5px solid var(--md2pdf-callout);
175
+ break-inside: avoid;
176
+ }
177
+ .markdown-body .markdown-alert-tip {
178
+ --md2pdf-callout: #2f5a2a;
179
+ }
180
+ .markdown-body .markdown-alert-important {
181
+ --md2pdf-callout: #5a2f6b;
182
+ }
183
+ .markdown-body .markdown-alert-warning {
184
+ --md2pdf-callout: #7a5410;
185
+ }
186
+ .markdown-body .markdown-alert-caution {
187
+ --md2pdf-callout: #8a1f1f;
188
+ }
189
+ .markdown-body .markdown-alert-quote {
190
+ --md2pdf-callout: var(--md2pdf-muted);
191
+ }
192
+ .markdown-body .markdown-alert-title {
193
+ color: var(--md2pdf-callout);
194
+ font-variant: small-caps;
195
+ font-weight: bold;
196
+ letter-spacing: 0.03em;
197
+ text-align: left;
198
+ hyphens: manual;
199
+ }
200
+ .markdown-body .markdown-alert-title .octicon {
201
+ margin-right: 0.4em;
202
+ vertical-align: -0.1em;
203
+ width: 0.8em;
204
+ height: 0.8em;
205
+ fill: currentColor;
206
+ }
207
+ .markdown-body .markdown-alert-title + p {
208
+ text-indent: 0;
209
+ }
210
+ .markdown-body .markdown-alert > :last-child {
211
+ margin-bottom: 0;
212
+ }
213
+
214
+ /* Code */
215
+
216
+ .markdown-body code,
217
+ .markdown-body kbd {
218
+ font-family: var(--md2pdf-code-font);
219
+ font-size: 0.84em;
220
+ }
221
+ .markdown-body kbd {
222
+ padding: 0 0.3em;
223
+ border: 0.75px solid var(--md2pdf-muted);
224
+ border-radius: 2px;
225
+ }
226
+ .markdown-body pre {
227
+ margin: calc(0.9em * var(--md2pdf-space)) 0;
228
+ padding: 0.6em 0;
229
+ border-top: 0.75px solid var(--md2pdf-rule);
230
+ border-bottom: 0.75px solid var(--md2pdf-rule);
231
+ line-height: calc(1 + 0.4 * var(--md2pdf-leading));
232
+ white-space: pre-wrap;
233
+ word-break: break-word;
234
+ break-inside: avoid;
235
+ }
236
+ .markdown-body pre code {
237
+ font-size: calc(8.5 / 11 * var(--md2pdf-font-size));
238
+ }
239
+ /* A highlight.js theme on the host page styles .hljs too; keep code blocks this style's own. */
240
+ .markdown-body pre code.hljs {
241
+ display: inline;
242
+ overflow: visible;
243
+ padding: 0;
244
+ color: inherit;
245
+ background: none;
246
+ }
247
+ /* And its tokens: this outranks a theme's token rules, and this style's colors come after it. */
248
+ .markdown-body [class*="hljs-"] {
249
+ color: inherit;
250
+ background: none;
251
+ font-style: inherit;
252
+ font-weight: inherit;
253
+ text-decoration: none;
254
+ }
255
+
256
+ .markdown-body .hljs-comment,
257
+ .markdown-body .hljs-quote {
258
+ color: #6b6b6b;
259
+ font-style: italic;
260
+ }
261
+ .markdown-body .hljs-keyword,
262
+ .markdown-body .hljs-selector-tag,
263
+ .markdown-body .hljs-literal,
264
+ .markdown-body .hljs-doctag,
265
+ .markdown-body .hljs-built_in {
266
+ color: #1b3a73;
267
+ font-weight: bold;
268
+ }
269
+ .markdown-body .hljs-string,
270
+ .markdown-body .hljs-regexp,
271
+ .markdown-body .hljs-addition {
272
+ color: #3d5a2a;
273
+ }
274
+ .markdown-body .hljs-number,
275
+ .markdown-body .hljs-symbol,
276
+ .markdown-body .hljs-bullet,
277
+ .markdown-body .hljs-meta,
278
+ .markdown-body .hljs-deletion {
279
+ color: #7a3b1d;
280
+ }
281
+ .markdown-body .hljs-title,
282
+ .markdown-body .hljs-section,
283
+ .markdown-body .hljs-name,
284
+ .markdown-body .hljs-type {
285
+ color: #111;
286
+ font-weight: bold;
287
+ }
288
+ .markdown-body .hljs-emphasis {
289
+ font-style: italic;
290
+ }
291
+ .markdown-body .hljs-strong {
292
+ font-weight: bold;
293
+ }
294
+
295
+ /* Math: a touch smaller than KaTeX's default so formulas sit evenly with Palatino. */
296
+
297
+ .markdown-body .katex {
298
+ font-size: 1.1em;
299
+ }
300
+ .markdown-body .katex-display {
301
+ margin: calc(0.8em * var(--md2pdf-space)) 0;
302
+ }
303
+
304
+ /* Tables: booktabs-style rules, no vertical lines. */
305
+
306
+ .markdown-body table {
307
+ border-collapse: collapse;
308
+ margin: calc(1.1em * var(--md2pdf-space)) auto;
309
+ border-top: 1.2px solid var(--md2pdf-rule);
310
+ border-bottom: 1.2px solid var(--md2pdf-rule);
311
+ font-size: calc(10 / 11 * var(--md2pdf-font-size));
312
+ font-variant-numeric: lining-nums tabular-nums;
313
+ }
314
+ .markdown-body th,
315
+ .markdown-body td {
316
+ padding: 0.3em 0.8em;
317
+ text-align: left;
318
+ }
319
+ .markdown-body th {
320
+ font-weight: normal;
321
+ border-bottom: 0.6px solid var(--md2pdf-rule);
322
+ }
323
+ .markdown-body tr {
324
+ break-inside: avoid;
325
+ }
326
+
327
+ /* Figures: diagrams are numbered, so the text can refer to "Figure 2". */
328
+
329
+ .markdown-body .mermaid-diagram {
330
+ margin: calc(1.2em * var(--md2pdf-space)) 0;
331
+ counter-increment: figure;
332
+ }
333
+ .markdown-body .mermaid-diagram::after {
334
+ content: "Figure " counter(figure) ".";
335
+ display: block;
336
+ margin-top: calc(0.5em * var(--md2pdf-space));
337
+ font-family: var(--md2pdf-font);
338
+ font-size: calc(10 / 11 * var(--md2pdf-font-size));
339
+ }
340
+
341
+ .markdown-body img {
342
+ display: block;
343
+ max-width: 100%;
344
+ margin: calc(1em * var(--md2pdf-space)) auto;
345
+ break-inside: avoid;
346
+ }
347
+
348
+ .markdown-body hr {
349
+ width: 30%;
350
+ margin: calc(1.4em * var(--md2pdf-space)) auto;
351
+ border: 0;
352
+ border-top: 0.75px solid var(--md2pdf-rule);
353
+ }
354
+
355
+ /* Footnotes: small type under a short rule, as in a journal. */
356
+
357
+ .markdown-body hr.footnotes-sep {
358
+ display: none;
359
+ }
360
+ .markdown-body .footnotes {
361
+ margin-top: calc(2.2em * var(--md2pdf-space));
362
+ font-size: calc(9 / 11 * var(--md2pdf-font-size));
363
+ line-height: calc(1 + 0.35 * var(--md2pdf-leading));
364
+ }
365
+ .markdown-body .footnotes::before {
366
+ content: "";
367
+ display: block;
368
+ width: 25%;
369
+ border-top: 0.6px solid var(--md2pdf-rule);
370
+ margin-bottom: calc(0.7em * var(--md2pdf-space));
371
+ }
372
+ .markdown-body .footnotes ol {
373
+ padding-left: 1.4em;
374
+ }
375
+ .markdown-body .footnotes p + p {
376
+ text-indent: 0;
377
+ margin-top: 0;
378
+ }
379
+ .markdown-body .footnote-ref a {
380
+ color: var(--md2pdf-text);
381
+ }
382
+ /* "Back to text" arrows are for screens. */
383
+ .markdown-body .footnote-backref {
384
+ display: none;
385
+ }
386
+
387
+ /* An Obsidian embed (![[Other note]]) that can't be shown: a muted placeholder. */
388
+ .markdown-body .markdown-embed {
389
+ color: var(--md2pdf-muted);
390
+ font-style: italic;
391
+ }
392
+
393
+ /* <div class="page-break"></div> starts a new page. */
394
+ .markdown-body .page-break {
395
+ break-after: page;
396
+ }