摘要
在macOS上使用kimi-cli的markdown-to-pdf技能,通过Typst排版引擎将Markdown文档转换为精美排版的PDF,采用ViTAX风格的中文字体配置.
声明
本文人类为第一作者, 龙虾为通讯作者.本文有AI生成内容.
markdown-to-pdf技能
SKILL.md
<hr>
<p>name: markdown-to-pdf</p>
<h2 id="description-convert-markdown-documents-to-beautifully-styled-pdf-using-typst-with-vitax-inspired-typography-use-when-the-user-needs-to-1-convert-md-files-to-pdf-2-create-documents-with-chinese-text-and-code-blocks-3-apply-modern-blog-style-typography-to-documents-defaults-to-noto-sans-cjk-sc-for-chinese-and-noto-sans-for-latin-text-">description: Convert Markdown documents to beautifully styled PDF using Typst, with ViTAX-inspired typography. Use when the user needs to (1) convert .md files to PDF, (2) create documents with Chinese text and code blocks, (3) apply modern blog-style typography to documents. Defaults to Noto Sans CJK SC for Chinese and Noto Sans for Latin text.</h2>
<h1 id="markdown-to-pdf-skill">Markdown to PDF Skill</h1>
<p>Convert Markdown documents to PDF using a local <code>markdown2typst</code> converter and the Typst typesetting engine, with a ViTAX-inspired visual style.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li><strong>Node.js</strong> (for running <code>markdown2typst</code>)</li>
<li><strong>Typst</strong> installed at <code>/home/qsbye/.cargo/bin/typst</code></li>
<li><strong>Fonts available</strong>:<ul>
<li><strong>Noto Sans CJK SC</strong> — Chinese sans-serif at <code>~/.local/share/fonts/noto-cjk/NotoSansCJK.ttc</code></li>
<li><strong>Noto Sans</strong> — Latin sans-serif at <code>~/.local/share/fonts/noto/NotoSans-*.ttf</code></li>
<li><strong>Noto Sans Mono CJK SC</strong> — Chinese monospace</li>
</ul>
</li>
</ul>
<h2 id="vitax-style-features">ViTAX Style Features</h2>
<p>The PDF output mimics the clean, modern typography of the "ViTAX: Building a Vision Transformer from Scratch" blog:</p>
<ul>
<li><strong>Headings</strong>: Bold, large sans-serif (Noto Sans) with generous top margin</li>
<li><strong>Body text</strong>: 11pt Noto Sans, justified, 1.4em line spacing</li>
<li><strong>Code blocks</strong>: White background, light gray border, rounded corners, monospace font</li>
<li><strong>Blockquotes</strong>: Left gray border, italic text</li>
<li><strong>Links</strong>: Blue color (#2885e2)</li>
<li><strong>Lists</strong>: Bullet markers (• ◦ ▪)</li>
<li><strong>Tables</strong>: Light header background, rounded borders</li>
</ul>
<h2 id="quick-start">Quick Start</h2>
<h3 id="convert-a-markdown-file-to-pdf">Convert a Markdown file to PDF</h3>
<pre><code class="lang-bash"><span class="hljs-regexp">/home/</span>qsbye<span class="hljs-regexp">/.config/</span>agents<span class="hljs-regexp">/skills/m</span>arkdown-to-pdf<span class="hljs-regexp">/scripts/m</span>d2pdf.sh <span class="hljs-regexp">/path/</span>to<span class="hljs-regexp">/document.md /</span>path<span class="hljs-regexp">/to/</span>output.pdf
</code></pre>
<h3 id="generate-a-document-from-scratch">Generate a document from scratch</h3>
<pre><code class="lang-bash">cat > /tmp/my_doc.md << <span class="hljs-string">'MDEOF'</span>
# 我的文档标题这是一段中文正文,支持**粗体**和*斜体*。## 代码示例```python
def hello():print(<span class="hljs-string">"Hello, 世界!"</span>)
</code></pre>
<blockquote>
<p>这是一个引用块,带有左侧边框样式。
MDEOF</p>
</blockquote>
<p>/home/qsbye/.config/agents/skills/markdown-to-pdf/scripts/md2pdf.sh /tmp/my_doc.md /tmp/my_doc.pdf</p>
<pre><code>
## Step-by-Step PipelineThe conversion happens <span class="hljs-keyword">in</span> <span class="hljs-number">3</span> stages:<span class="hljs-number">1.</span> **Markdown → Raw Typst**: `md2typst.js` uses `markdown2typst.min.js` to convert Markdown syntax to Typst markup.
<span class="hljs-number">2.</span> **Apply ViTAX Style**: `apply-vitax.js` prepends the `vitax-template.typ` style rules (fonts, colors, spacing, <span class="hljs-keyword">code</span> blocks, blockquotes).
<span class="hljs-number">3.</span> **Typst → PDF**: `typst compile` renders the final PDF.## Scripts| Script | Purpose |
|--------|---------|
| `scripts/md2pdf.sh` | One-shot Markdown → PDF wrapper |
| `scripts/md2typst.js` | Markdown → raw Typst |
| `scripts/apply-vitax.js` | Inject ViTAX style template into raw Typst |
| `scripts/vitax-template.typ` | Typst show-rules defining the visual style |
| `scripts/markdown2typst.min.js` | Local copy <span class="hljs-keyword">of</span> the markdown2typst library |## Default Font SetupThe template automatically configures:```typst
#set text(font: <span class="hljs-string">"Noto Sans"</span>, lang: <span class="hljs-string">"zh"</span>, region: <span class="hljs-string">"cn"</span>)
</code></pre><p>For Chinese documents, no extra font configuration is needed.</p>
<h2 id="parameters">Parameters</h2>
<p>The wrapper script accepts:</p>
<pre><code class="lang-bash">md2pdf<span class="hljs-selector-class">.sh</span> <<span class="hljs-selector-tag">input</span>.md> <output.pdf>
</code></pre>
<p>No additional flags are required. Intermediate <code>.typ</code> files are written to <code>$TMPDIR</code> and cleaned up automatically by the OS.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<h3 id="-maximum-show-rule-depth-exceeded-when-converting-files-with-horizontal-rules"><code>maximum show rule depth exceeded</code> when converting files with horizontal rules</h3>
<ul>
<li><strong>Symptom</strong>: Typst compilation fails with <code>maximum show rule depth exceeded</code>.</li>
<li><strong>Root cause</strong>: A <code>#show line</code> rule matches the <code>line()</code> call inside its own body, creating infinite recursion.</li>
<li><strong>Fix</strong>: Restrict the selector so it does not match the inner call. In <code>vitax-template.typ</code>, change:<pre><code class="lang-typst"><span class="hljs-meta">#show <span class="hljs-meta-keyword">line</span>: it => { ... }</span>
</code></pre>
to:<pre><code class="lang-typst">#show line.<span class="hljs-keyword">where</span>(length: <span class="hljs-number">60</span>%): it => { ... }
</code></pre>
</li>
<li><strong>Note</strong>: <code>line.where(block: true)</code> does <strong>not</strong> work because the <code>line</code> element has no <code>block</code> field.</li>
</ul>
scripts/md2pdf.sh
#!/bin/bash
set -eSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"if [ $# -lt 2 ]; thenecho "Usage: md2pdf.sh <input.md> <output.pdf>"exit 1
fiINPUT_MD="$1"
OUTPUT_PDF="$2"
BASENAME="$(basename "$INPUT_MD" .md)"
TMPDIR="${TMPDIR:-/tmp}"
RAW_TYP="$TMPDIR/${BASENAME}_raw.typ"
STYLED_TYP="$TMPDIR/${BASENAME}_styled.typ"# Step 1: Markdown -> Raw Typst
node "$SCRIPT_DIR/md2typst.js" "$INPUT_MD" "$RAW_TYP"# Step 2: Apply ViTAX style template
node "$SCRIPT_DIR/apply-vitax.js" "$RAW_TYP" "$STYLED_TYP"# Step 3: Typst -> PDF
typst compile "$STYLED_TYP" "$OUTPUT_PDF"echo "PDF generated: $OUTPUT_PDF"
scripts/md2typst.js
const fs = require('fs');
const { markdown2typst } = require('./markdown2typst.min.js');const inputFile = process.argv[2];
const outputFile = process.argv[3];if (!inputFile || !outputFile) {console.error('Usage: node md2typst.js <input.md> <output.typ>');process.exit(1);
}const md = fs.readFileSync(inputFile, 'utf-8');
const typ = markdown2typst(md);
fs.writeFileSync(outputFile, typ, 'utf-8');
console.log(`Converted ${inputFile} -> ${outputFile}`);
scripts/apply-vitax.js
const fs = require('fs');
const path = require('path');const rawTypFile = process.argv[2];
const outputTypFile = process.argv[3];if (!rawTypFile || !outputTypFile) {console.error('Usage: node apply-vitax.js <raw.typ> <output.typ>');process.exit(1);
}const templatePath = path.join(__dirname, 'vitax-template.typ');
const template = fs.readFileSync(templatePath, 'utf-8');
const raw = fs.readFileSync(rawTypFile, 'utf-8');// Combine template + raw content
const combined = template + '\n\n// === CONTENT START ===\n\n' + raw + '\n';fs.writeFileSync(outputTypFile, combined, 'utf-8');
console.log(`Applied ViTAX style: ${rawTypFile} -> ${outputTypFile}`);
scripts/vitax-template.typ (核心样式)
// ViTAX Style Template for Typst
#let vitax-colors = (primary: rgb(24, 188, 156),secondary: rgb(40, 133, 226),dark: rgb(44, 62, 80),light-bg: rgb(248, 249, 250),code-bg: rgb(255, 255, 255),code-border: rgb(193, 193, 193),text-muted: rgb(134, 142, 150),
)#let vitax-heading-font = "Noto Sans"
#let vitax-body-font = "Noto Sans"
#let vitax-code-font = "Noto Sans Mono CJK SC"#set page(paper: "a4", margin: (top: 2.5cm, bottom: 2.5cm, left: 2.5cm, right: 2.5cm))
#set text(font: vitax-body-font, size: 11pt, lang: "zh", region: "cn", fill: vitax-colors.dark)
#set par(leading: 1.4em, justify: true)// Headings
#set heading(numbering: none)
#show heading: it => { set text(font: vitax-heading-font, weight: 700); block(above: 1.5em, below: 0.8em, it) }
#show heading.where(level: 1): it => { set text(font: vitax-heading-font, weight: 700, size: 2em); block(above: 1.5em, below: 1em, it) }// Code blocks
#show raw.where(block: true): it => {block(width: 100%, fill: vitax-colors.code-bg, stroke: 0.5pt + vitax-colors.code-border,radius: 0.5em, inset: 1em, text(font: vitax-code-font, size: 0.85em, it))
}// Tables
#show table: it => {set text(font: vitax-body-font, size: 0.95em)block(stroke: 0.5pt + vitax-colors.code-border, radius: 0.3em, inset: 0pt, it)
}
#show table.cell: it => {if it.y == 0 { set text(weight: 700); table.cell(fill: vitax-colors.light-bg, it) } else { it }
}
运行效果
- 运行
md2pdf.sh document.md output.pdf三步流水线转换 - Markdown → Typst → 应用ViTAX样式 → PDF
- 输出PDF支持中文、代码块、表格、引用等丰富排版
