feat(md-print): use mm font and corner page numbers

Body font size is now specified in millimetres and defaults to 3mm. Page numbers move from the centre footer to the outer bottom corners, alternating left and right per A5 page.
This commit is contained in:
2026-10-23 11:28:22 +08:00
parent 80da5364e5
commit 1ce76dca64
12 changed files with 60 additions and 34 deletions
+3 -3
View File
@@ -116,13 +116,13 @@ Prefer the `yaml role=tag` form — it is the only form that supports template l
Paginates a markdown document into A4 landscape sheets for printing.
Each sheet holds two A5 pages side by side, and each A5 page flows the markdown into `columns` columns. `margin` and `gap` are in millimetres, `font` is in points. `pageNumbers` adds an `n / total` footer and `cutMarks` draws the centre cut line. The preview modal exports through the browser's print dialog (choose A4 landscape, no margins, 100% scale).
Each sheet holds two A5 pages side by side, and each A5 page flows the markdown into `columns` columns. `margin`, `gap`, and `font` are in millimetres (the body font defaults to 3mm). `pageNumbers` adds an `n / total` footer in the outer corner and `cutMarks` draws the centre cut line. The preview modal exports through the browser's print dialog (choose A4 landscape, no margins, 100% scale).
- Body: markdown path, optional #section
- Attributes: `columns`, `margin`, `gap`, `font`, `title`, `pageNumbers`, `cutMarks`
```markdown
:md-print[./rules.md]{columns="2" margin="10" gap="5" font="9"}
:md-print[./rules.md]{columns="2" margin="10" gap="5" font="3"}
```
````markdown
@@ -132,7 +132,7 @@ body: ./rules.md
columns: 2
margin: 10
gap: 5
font: 9
font: 3
title: 规则书
```
````
+4 -4
View File
@@ -464,7 +464,7 @@ data-config:
```markdown
:md-print[./rules.md]
:md-print[./rules.md]{columns="2" margin="10" gap="5" font="9"}
:md-print[./rules.md]{columns="2" margin="10" gap="5" font="3"}
```
**功能:**
@@ -480,9 +480,9 @@ data-config:
| `columns` | number | `2` | 每个 A5 页面的列数(1–4) |
| `margin` | number | `10` | 页面边距(毫米) |
| `gap` | number | `5` | 列间距(毫米) |
| `font` | number | `9` | 正文字号(磅) |
| `font` | number | `3` | 正文字号(毫米) |
| `title` | string | - | 页眉标题 |
| `pageNumbers` | boolean | `true` | 是否显示页码 |
| `pageNumbers` | boolean | `true` | 是否在页面外侧角落显示页码 |
| `cutMarks` | boolean | `true` | 是否显示 A4 中缝裁切线 |
**YAML 写法:**
@@ -493,7 +493,7 @@ body: ./rules.md
columns: 2
margin: 10
gap: 5
font: 9
font: 3
title: 规则书
```
+3 -3
View File
@@ -116,13 +116,13 @@ Prefer the `yaml role=tag` form — it is the only form that supports template l
Paginates a markdown document into A4 landscape sheets for printing.
Each sheet holds two A5 pages side by side, and each A5 page flows the markdown into `columns` columns. `margin` and `gap` are in millimetres, `font` is in points. `pageNumbers` adds an `n / total` footer and `cutMarks` draws the centre cut line. The preview modal exports through the browser's print dialog (choose A4 landscape, no margins, 100% scale).
Each sheet holds two A5 pages side by side, and each A5 page flows the markdown into `columns` columns. `margin`, `gap`, and `font` are in millimetres (the body font defaults to 3mm). `pageNumbers` adds an `n / total` footer in the outer corner and `cutMarks` draws the centre cut line. The preview modal exports through the browser's print dialog (choose A4 landscape, no margins, 100% scale).
- Body: markdown path, optional #section
- Attributes: `columns`, `margin`, `gap`, `font`, `title`, `pageNumbers`, `cutMarks`
```markdown
:md-print[./rules.md]{columns="2" margin="10" gap="5" font="9"}
:md-print[./rules.md]{columns="2" margin="10" gap="5" font="3"}
```
````markdown
@@ -132,7 +132,7 @@ body: ./rules.md
columns: 2
margin: 10
gap: 5
font: 9
font: 3
title: 规则书
```
````
+3 -3
View File
@@ -82,11 +82,11 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
summary:
"Paginates a markdown document into A4 landscape sheets for printing.",
details:
"Each sheet holds two A5 pages side by side, and each A5 page flows the markdown into `columns` columns. `margin` and `gap` are in millimetres, `font` is in points. `pageNumbers` adds an `n / total` footer and `cutMarks` draws the centre cut line. The preview modal exports through the browser's print dialog (choose A4 landscape, no margins, 100% scale).",
"Each sheet holds two A5 pages side by side, and each A5 page flows the markdown into `columns` columns. `margin`, `gap`, and `font` are in millimetres (the body font defaults to 3mm). `pageNumbers` adds an `n / total` footer in the outer corner and `cutMarks` draws the centre cut line. The preview modal exports through the browser's print dialog (choose A4 landscape, no margins, 100% scale).",
refKind: "md",
examples: [
':md-print[./rules.md]{columns="2" margin="10" gap="5" font="9"}',
"```yaml role=tag\ntag: md-print\nbody: ./rules.md\ncolumns: 2\nmargin: 10\ngap: 5\nfont: 9\ntitle: 规则书\n```",
':md-print[./rules.md]{columns="2" margin="10" gap="5" font="3"}',
"```yaml role=tag\ntag: md-print\nbody: ./rules.md\ncolumns: 2\nmargin: 10\ngap: 5\nfont: 3\ntitle: 规则书\n```",
],
},
{
+10 -2
View File
@@ -90,7 +90,15 @@ export function PrintPreview(props: PrintPreviewProps) {
const result = paginate(source, options(), mount);
result.pages.forEach((page, index) => {
const footer = page.querySelector(".md-print-footer");
if (footer) footer.textContent = `${index + 1} / ${result.pages.length}`;
if (footer) {
footer.textContent = `${index + 1} / ${result.pages.length}`;
// Left A5 gets the left corner, right A5 gets the right corner.
footer.classList.add(
index % 2 === 0
? "md-print-footer--left"
: "md-print-footer--right",
);
}
});
setPages(result.pages);
setWarnings(result.warnings);
@@ -224,7 +232,7 @@ export function PrintPreview(props: PrintPreviewProps) {
class="md-print-content prose"
style={{
width: `${columnWidthMm(props.config)}mm`,
"--md-print-font-size": `${props.config.font}pt`,
"--md-print-font-size": `${props.config.font}mm`,
position: "absolute",
left: "-99999px",
top: "0",
+8 -3
View File
@@ -18,11 +18,16 @@ describe("normalizePrintConfig", () => {
expect(config.columns).toBe(3);
expect(config.margin).toBe(40);
expect(config.gap).toBe(0);
expect(config.font).toBe(16);
expect(config.font).toBe(6);
expect(config.pageNumbers).toBe(false);
expect(config.cutMarks).toBe(false);
});
test("clamps font to 2..6mm", () => {
expect(normalizePrintConfig({ font: "1" }).font).toBe(2);
expect(normalizePrintConfig({ font: "20" }).font).toBe(6);
});
test("clamps columns to 1..4", () => {
expect(normalizePrintConfig({ columns: "0" }).columns).toBe(1);
expect(normalizePrintConfig({ columns: "9" }).columns).toBe(4);
@@ -33,7 +38,7 @@ describe("normalizePrintConfig", () => {
columns: 2,
margin: 12,
gap: 6,
font: 10,
font: 3,
title: "规则书",
pageNumbers: true,
cutMarks: false,
@@ -43,7 +48,7 @@ describe("normalizePrintConfig", () => {
columns: 2,
margin: 12,
gap: 6,
font: 10,
font: 3,
title: "规则书",
pageNumbers: true,
cutMarks: false,
+2 -2
View File
@@ -4,7 +4,7 @@ export const PRINT_DEFAULTS: PrintConfig = {
columns: 2,
margin: 10,
gap: 5,
font: 9,
font: 3,
title: "",
pageNumbers: true,
cutMarks: true,
@@ -43,7 +43,7 @@ export function normalizePrintConfig(
),
margin: clamp(toNumber(input.margin, PRINT_DEFAULTS.margin), 5, 40),
gap: clamp(toNumber(input.gap, PRINT_DEFAULTS.gap), 0, 20),
font: clamp(toNumber(input.font, PRINT_DEFAULTS.font), 6, 16),
font: clamp(toNumber(input.font, PRINT_DEFAULTS.font), 2, 6),
title:
typeof input.title === "string" ? input.title : PRINT_DEFAULTS.title,
pageNumbers: toBoolean(input.pageNumbers, PRINT_DEFAULTS.pageNumbers),
+1 -1
View File
@@ -12,7 +12,7 @@ customElement<PrintProps>(
columns: "2",
margin: "10",
gap: "5",
font: "9",
font: "3",
title: "",
pageNumbers: "true",
cutMarks: "true",
+1 -1
View File
@@ -54,7 +54,7 @@ export function createPageElement(opts: PaginateOptions): {
page.style.position = "relative";
page.style.overflow = "hidden";
page.style.background = "white";
page.style.setProperty("--md-print-font-size", `${opts.font}pt`);
page.style.setProperty("--md-print-font-size", `${opts.font}mm`);
if (opts.title) {
const header = document.createElement("div");
+1 -1
View File
@@ -5,7 +5,7 @@ export interface PrintConfig {
margin: number;
/** Column gap in millimetres. */
gap: number;
/** Body font size in points. */
/** Body font size in millimetres. */
font: number;
/** Running header text. Empty hides the header. */
title: string;
+4 -4
View File
@@ -17,7 +17,7 @@ title: 打印版式
**自定义版式:**
```markdown
:md-print[./rules.md]{columns="2" margin="10" gap="5" font="9"}
:md-print[./rules.md]{columns="2" margin="10" gap="5" font="3"}
```
**YAML 写法:**
@@ -28,7 +28,7 @@ body: ./rules.md
columns: 2
margin: 10
gap: 5
font: 9
font: 3
title: 规则书
```
@@ -41,9 +41,9 @@ title: 规则书
| `columns` | number | `2` | 每个 A5 页面的列数(1–4) |
| `margin` | number | `10` | 页面边距(毫米) |
| `gap` | number | `5` | 列间距(毫米) |
| `font` | number | `9` | 正文字号(磅) |
| `font` | number | `3` | 正文字号(毫米) |
| `title` | string | — | 页眉标题 |
| `pageNumbers` | boolean | `true` | 是否显示 `n / 总数` 页码 |
| `pageNumbers` | boolean | `true` | 是否在页面外侧角落显示 `n / 总数` 页码 |
| `cutMarks` | boolean | `true` | 是否显示 A4 中缝裁切线 |
## 打印设置
+20 -7
View File
@@ -204,7 +204,7 @@ icon.big .icon-label-stroke {
}
.md-print-content {
font-size: var(--md-print-font-size, 9pt);
font-size: var(--md-print-font-size, 3mm);
line-height: 1.45;
color: #000;
max-width: none;
@@ -257,11 +257,11 @@ icon.big .icon-label-stroke {
pointer-events: none;
}
.md-print-header,
.md-print-footer {
.md-print-header {
position: absolute;
left: 0;
right: 0;
top: 3mm;
text-align: center;
font-size: 8pt;
color: #6b7280;
@@ -270,12 +270,25 @@ icon.big .icon-label-stroke {
text-overflow: ellipsis;
}
.md-print-header {
top: 3mm;
.md-print-footer {
position: absolute;
bottom: 3mm;
left: 3mm;
font-size: 8pt;
color: #6b7280;
white-space: nowrap;
}
.md-print-footer {
bottom: 3mm;
.md-print-footer--left {
left: 3mm;
right: auto;
text-align: left;
}
.md-print-footer--right {
left: auto;
right: 3mm;
text-align: right;
}
@media print {