md-pdf-studio 2.1.0 → 2.2.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 +1 -0
- package/package.json +1 -1
- package/src/core/footnotes.js +122 -0
- package/src/core/parser.js +3 -2
- package/src/core/templates.js +6 -0
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
- 📊 **Mermaid Diagrams**: Sequence diagrams (with auto note wrapping), flowcharts, class diagrams, and gantt charts.
|
|
30
30
|
- 🌈 **Syntax Highlighting**: Fenced code blocks that name a language (` ```js `, ` ```python `, ` ```diff `, …) are coloured at build time with highlight.js — no CDN, so it also works offline and in static HTML export. Colours follow the theme's code block background. Works in the CLI and in the extension preview.
|
|
31
31
|
- 📐 **LaTeX Math Support**: Mathematical equations rendered with KaTeX.
|
|
32
|
+
- 📝 **Footnotes**: `text[^1]` with `[^1]: the note` anywhere in the file. Notes are numbered in the order they are first referenced, gathered at the end of the document, and linked both ways. Works in the CLI and in the extension preview.
|
|
32
33
|
- 💡 **GitHub Callout Box Alerts**: Support for `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!IMPORTANT]`, and `> [!CAUTION]`.
|
|
33
34
|
- 💻 **ASCII UI Mockups**: Styled dark-theme boxes for terminal output & ASCII wireframe mockups.
|
|
34
35
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-pdf-studio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A powerful CLI tool and Manifest V3 Browser Extension to convert Markdown files, text, or web pages into beautifully styled PDFs with Mermaid diagrams, LaTeX math, TOC, and themes.",
|
|
5
5
|
"main": "src/core/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown footnotes: `text[^id]` in the body, `[^id]: note` anywhere on a line of its own.
|
|
3
|
+
*
|
|
4
|
+
* Works on the Markdown text, before it is parsed, and needs nothing from Node: the same file
|
|
5
|
+
* is shipped in the extension as extension/lib/footnotes.js (a test keeps the two identical).
|
|
6
|
+
*/
|
|
7
|
+
(function (scope) {
|
|
8
|
+
const DEFINITION = /^ {0,3}\[\^([^\]\s]+)\]:[ \t]*(.*)$/;
|
|
9
|
+
const REFERENCE = /\[\^([^\]\s]+)\]/g;
|
|
10
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
11
|
+
const CONTINUATION = /^(?: {2,}|\t)\S/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Splits the text into lines tagged as code or prose. Footnote syntax inside a fenced
|
|
15
|
+
* block is an example of the syntax, not a footnote.
|
|
16
|
+
* @param {string} markdown
|
|
17
|
+
* @returns {{ text: string, code: boolean }[]}
|
|
18
|
+
*/
|
|
19
|
+
function tagLines(markdown) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
let fence = null;
|
|
22
|
+
for (const text of markdown.split(/\r?\n/)) {
|
|
23
|
+
const match = FENCE.exec(text);
|
|
24
|
+
if (fence) {
|
|
25
|
+
lines.push({ text, code: true });
|
|
26
|
+
if (match && match[1][0] === fence[0] && match[1].length >= fence.length) fence = null;
|
|
27
|
+
} else if (match) {
|
|
28
|
+
fence = match[1];
|
|
29
|
+
lines.push({ text, code: true });
|
|
30
|
+
} else {
|
|
31
|
+
lines.push({ text, code: false });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Applies `replace` to the parts of a line that are not inline code.
|
|
39
|
+
* @param {string} line
|
|
40
|
+
* @param {(prose: string) => string} replace
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function outsideInlineCode(line, replace) {
|
|
44
|
+
return line
|
|
45
|
+
.split(/(`+[^`]*`+)/)
|
|
46
|
+
.map((part, index) => (index % 2 === 1 ? part : replace(part)))
|
|
47
|
+
.join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Numbers the footnotes of a Markdown document in the order they are first referenced,
|
|
52
|
+
* replaces each reference with a superscript link, and appends the notes as a list.
|
|
53
|
+
*
|
|
54
|
+
* A reference to a note that is never defined is left as the author typed it; a note that
|
|
55
|
+
* is never referenced is left out, as GitHub does.
|
|
56
|
+
* @param {string} markdown
|
|
57
|
+
* @returns {string} Markdown, with HTML for the references and the notes section
|
|
58
|
+
*/
|
|
59
|
+
function applyFootnotes(markdown = '') {
|
|
60
|
+
if (!markdown || markdown.indexOf('[^') === -1) return markdown;
|
|
61
|
+
|
|
62
|
+
const lines = tagLines(markdown);
|
|
63
|
+
const definitions = new Map();
|
|
64
|
+
const body = [];
|
|
65
|
+
|
|
66
|
+
for (let index = 0; index < lines.length; index++) {
|
|
67
|
+
const line = lines[index];
|
|
68
|
+
const match = line.code ? null : DEFINITION.exec(line.text);
|
|
69
|
+
if (!match) {
|
|
70
|
+
body.push(line);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const parts = [match[2].trim()];
|
|
74
|
+
// Indented lines that follow belong to the note.
|
|
75
|
+
while (
|
|
76
|
+
index + 1 < lines.length &&
|
|
77
|
+
!lines[index + 1].code &&
|
|
78
|
+
CONTINUATION.test(lines[index + 1].text)
|
|
79
|
+
) {
|
|
80
|
+
index += 1;
|
|
81
|
+
parts.push(lines[index].text.trim());
|
|
82
|
+
}
|
|
83
|
+
// The first definition of an id wins, as with link reference definitions.
|
|
84
|
+
if (!definitions.has(match[1])) definitions.set(match[1], parts.join(' ').trim());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (definitions.size === 0) return markdown;
|
|
88
|
+
|
|
89
|
+
const numbers = new Map();
|
|
90
|
+
const uses = new Map();
|
|
91
|
+
const text = body
|
|
92
|
+
.map((line) => {
|
|
93
|
+
if (line.code) return line.text;
|
|
94
|
+
return outsideInlineCode(line.text, (prose) =>
|
|
95
|
+
prose.replace(REFERENCE, (whole, id) => {
|
|
96
|
+
if (!definitions.has(id)) return whole;
|
|
97
|
+
if (!numbers.has(id)) numbers.set(id, numbers.size + 1);
|
|
98
|
+
const number = numbers.get(id);
|
|
99
|
+
const use = (uses.get(id) || 0) + 1;
|
|
100
|
+
uses.set(id, use);
|
|
101
|
+
const anchor = use === 1 ? `fnref-${number}` : `fnref-${number}-${use}`;
|
|
102
|
+
return `<sup class="footnote-ref"><a href="#fn-${number}" id="${anchor}">${number}</a></sup>`;
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
})
|
|
106
|
+
.join('\n');
|
|
107
|
+
|
|
108
|
+
if (numbers.size === 0) return text;
|
|
109
|
+
|
|
110
|
+
const notes = Array.from(numbers.entries()).map(([id, number]) => {
|
|
111
|
+
const note = definitions.get(id);
|
|
112
|
+
return `${number}. <span id="fn-${number}"></span>${note} <a href="#fnref-${number}" class="footnote-backref" aria-label="Back to the text">↩</a>`;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// The blank lines let Markdown inside the section be parsed as Markdown.
|
|
116
|
+
return `${text.replace(/\s+$/, '')}\n\n<section class="footnotes">\n\n${notes.join('\n')}\n\n</section>\n`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const api = { applyFootnotes };
|
|
120
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
121
|
+
else scope.MdPdfFootnotes = api;
|
|
122
|
+
})(typeof globalThis !== 'undefined' ? globalThis : this);
|
package/src/core/parser.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
escapeHtml
|
|
11
11
|
} = require('./utils');
|
|
12
12
|
const { getHtmlTemplate } = require('./templates');
|
|
13
|
+
const { applyFootnotes } = require('./footnotes');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Builds marked custom renderer for diagrams, UI mockups, and headings
|
|
@@ -100,8 +101,8 @@ function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
|
|
|
100
101
|
cssFile: options.cssFile || frontMatter.cssFile || frontMatter.css
|
|
101
102
|
};
|
|
102
103
|
|
|
103
|
-
// 3. Preprocess Markdown (LaTeX, pagebreaks, alerts)
|
|
104
|
-
const processedMd = preprocessMarkdown(markdownContent);
|
|
104
|
+
// 3. Preprocess Markdown (footnotes, LaTeX, pagebreaks, alerts)
|
|
105
|
+
const processedMd = preprocessMarkdown(applyFootnotes(markdownContent));
|
|
105
106
|
|
|
106
107
|
// 4. Generate TOC & Cover Page
|
|
107
108
|
let tocHtml = '';
|
package/src/core/templates.js
CHANGED
|
@@ -99,6 +99,12 @@ function getHtmlTemplate({ title, author = '', bodyHtml, options = {} }) {
|
|
|
99
99
|
page-break-after: always;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
.footnote-ref { font-size: 0.75em; line-height: 0; }
|
|
103
|
+
.footnote-ref a, .footnote-backref { text-decoration: none; }
|
|
104
|
+
.footnotes { margin-top: 28px; padding-top: 10px; border-top: 1px solid currentColor; font-size: 0.85em; break-inside: avoid-page; }
|
|
105
|
+
.footnotes ol { margin: 0; padding-left: 1.6em; }
|
|
106
|
+
.footnotes li { margin: 3px 0; }
|
|
107
|
+
|
|
102
108
|
img {
|
|
103
109
|
max-width: 100% !important;
|
|
104
110
|
height: auto !important;
|