From 1ce76dca64c05f278875e6319fdee1f2e8b7e306 Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 23 Oct 2026 11:28:22 +0800 Subject: [PATCH] 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. --- .agents/skills/tttk-authoring/reference.md | 6 ++--- docs/markdown.md | 8 +++---- docs/reference.md | 6 ++--- src/cli/validate/known.ts | 6 ++--- src/components/md-print/PrintPreview.tsx | 12 ++++++++-- src/components/md-print/config.test.ts | 11 ++++++--- src/components/md-print/config.ts | 4 ++-- src/components/md-print/index.tsx | 2 +- src/components/md-print/paginate.ts | 2 +- src/components/md-print/types.ts | 2 +- src/doc-entries/md-print.md | 8 +++---- src/styles.css | 27 ++++++++++++++++------ 12 files changed, 60 insertions(+), 34 deletions(-) diff --git a/.agents/skills/tttk-authoring/reference.md b/.agents/skills/tttk-authoring/reference.md index ed0975d..bad7c6a 100644 --- a/.agents/skills/tttk-authoring/reference.md +++ b/.agents/skills/tttk-authoring/reference.md @@ -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: 规则书 ``` ```` diff --git a/docs/markdown.md b/docs/markdown.md index 809f5bc..d44c83c 100644 --- a/docs/markdown.md +++ b/docs/markdown.md @@ -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: 规则书 ``` diff --git a/docs/reference.md b/docs/reference.md index ed0975d..bad7c6a 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -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: 规则书 ``` ```` diff --git a/src/cli/validate/known.ts b/src/cli/validate/known.ts index c17ec69..1f46cff 100644 --- a/src/cli/validate/known.ts +++ b/src/cli/validate/known.ts @@ -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```", ], }, { diff --git a/src/components/md-print/PrintPreview.tsx b/src/components/md-print/PrintPreview.tsx index 6f55f93..dba2c20 100644 --- a/src/components/md-print/PrintPreview.tsx +++ b/src/components/md-print/PrintPreview.tsx @@ -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", diff --git a/src/components/md-print/config.test.ts b/src/components/md-print/config.test.ts index 3b13d42..77bb068 100644 --- a/src/components/md-print/config.test.ts +++ b/src/components/md-print/config.test.ts @@ -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, diff --git a/src/components/md-print/config.ts b/src/components/md-print/config.ts index f9d5c61..a4e156f 100644 --- a/src/components/md-print/config.ts +++ b/src/components/md-print/config.ts @@ -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), diff --git a/src/components/md-print/index.tsx b/src/components/md-print/index.tsx index a6dc781..82e2cc3 100644 --- a/src/components/md-print/index.tsx +++ b/src/components/md-print/index.tsx @@ -12,7 +12,7 @@ customElement( columns: "2", margin: "10", gap: "5", - font: "9", + font: "3", title: "", pageNumbers: "true", cutMarks: "true", diff --git a/src/components/md-print/paginate.ts b/src/components/md-print/paginate.ts index bc24ae1..685fd84 100644 --- a/src/components/md-print/paginate.ts +++ b/src/components/md-print/paginate.ts @@ -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"); diff --git a/src/components/md-print/types.ts b/src/components/md-print/types.ts index 10c0a5a..3f72630 100644 --- a/src/components/md-print/types.ts +++ b/src/components/md-print/types.ts @@ -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; diff --git a/src/doc-entries/md-print.md b/src/doc-entries/md-print.md index 08895e2..6bffaad 100644 --- a/src/doc-entries/md-print.md +++ b/src/doc-entries/md-print.md @@ -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 中缝裁切线 | ## 打印设置 diff --git a/src/styles.css b/src/styles.css index e6dfb91..5dc72a1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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 {