Skill: mutour/markdown-export (version 0.5.0)

--- name: markdown-export description: Convert a Markdown file or raw Markdown string into polished DOCX or HTML output from one unified skill. Supports custom DOCX reference templates, custom Pandoc HTML templates, custom CSS, and includes built-in templates for both formats. Use this skill when the user asks to export Markdown into Word, HTML, or multiple publication formats from the same source. --- # Markdown Export Use this skill when the user wants one Markdown source exported into `docx`, `html`, or both. ## What this skill provides - One unified CLI for both `docx` and `html` - Input from either a Markdown file or a raw Markdown string - Built-in templates for both output formats - Custom template support for both output formats - Optional table of contents, numbered headings, metadata, and resource-path support - HTML layout controls for optional TOC panel and plain article body rendering ## Formats - `docx` Uses a Word `reference.docx` style template - `html` Uses a Pandoc HTML template plus CSS ## Built-in template names ### DOCX - `modern-blue` - `executive-serif` - `warm-notebook` - `minimal-gray` ### HTML - `docs-slate` - `magazine-amber` - `product-midnight` - `serif-paper` List everything with: ```bash python3 scripts/export_markdown.py --list-templates ``` Only list one format with: ```bash python3 scripts/export_markdown.py --format html --list-templates ``` ## Main examples ### Markdown to DOCX ```bash python3 scripts/export_markdown.py \ --format docx \ --input-file /abs/path/source.md \ --output /abs/path/output.docx \ --builtin-template modern-blue \ --toc \ --number-sections ``` ### Markdown to HTML ```bash python3 scripts/export_markdown.py \ --format html \ --input-file /abs/path/source.md \ --output /abs/path/output.html \ --builtin-template docs-slate \ --toc \ --embed-assets ``` ### Markdown to HTML Without TOC Background Card ```bash python3 scripts/export_markdown.py \ --format html \ --input-file /abs/path/source.md \ --output /abs/path/output.html \ --builtin-template docs-slate \ --embed-assets \ --no-body-background ``` ### Raw string ```bash python3 scripts/export_markdown.py \ --format html \ --markdown '# Release Notes\n\n- Export to both HTML and DOCX with one skill' \ --output /abs/path/release.html \ --builtin-template magazine-amber \ --title 'Release Notes' ``` ## Template rules - `docx` `--template` must point to a `.docx` reference document - `html` `--template` must point to a Pandoc HTML template that contains `$body$` - For HTML, built-in CSS is automatically applied when using a built-in template - For HTML, extra `--css` files are appended after built-in CSS - For HTML, omitting `--toc` removes the TOC panel and expands the article body to full width - For HTML, `--no-body-background` removes the article panel background, border, and shadow while keeping the page theme ## Bundled files - Unified converter: `scripts/export_markdown.py` - DOCX built-in templates: `assets/docx/templates/*.docx` - HTML built-in templates: `assets/html/templates/*.html` - HTML built-in styles: `assets/html/styles/*.css` ## Notes - This skill expects `pandoc` to be installed and available on `PATH` - If the user already has a brand template, prefer passing `--template`

path: assets/examples/merged-sample.html
content: <!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>统一导出</title>
  
  
  
  <style type="text/css">:root {
--bg: #08111f;
--bg-2: #101b30;
--panel: rgba(11, 18, 33, 0.72);
--panel-2: rgba(17, 27, 48, 0.78);
--text: #e6edf7;
--muted: #92a4c4;
--line: rgba(144, 164, 196, 0.18);
--accent: #6ee7f2;
--accent-2: #7c3aed;
--quote: rgba(110, 231, 242, 0.08);
--code-bg: #050a14;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Space Grotesk", "PingFang SC", "Noto Sans CJK SC", sans-serif;
color: var(--text);
background:
radial-gradient(circle at 10% 10%, rgba(110, 231, 242, 0.14), transparent 22rem),
radial-gradient(circle at 90% 20%, rgba(124, 58, 237, 0.18), transparent 24rem),
linear-gradient(180deg, #050a14 0%, #09111f 45%, #0b1628 100%);
line-height: 1.78;
}
.ambient {
position: fixed;
inset: auto;
width: 40vw;
height: 40vw;
border-radius: 999px;
filter: blur(80px);
opacity: 0.25;
pointer-events: none;
}
.ambient-a {
top: -8vw;
right: -10vw;
background: #7c3aed;
}
.ambient-b {
left: -12vw;
top: 24vh;
background: #0891b2;
}
.midnight-page {
position: relative;
z-index: 1;
max-width: 1220px;
margin: 0 auto;
padding: 58px 24px 80px;
}
.launch-header h1 {
margin: 0;
font-size: clamp(2.9rem, 7vw, 5.8rem);
line-height: 0.94;
letter-spacing: -0.06em;
max-width: 12ch;
}
.eyebrow,
.toc-label {
margin: 0 0 12px;
color: var(--accent);
font-size: 0.82rem;
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 800;
}
.meta-row {
margin-top: 14px;
color: var(--muted);
display: flex;
gap: 14px;
flex-wrap: wrap;
}
.midnight-layout {
display: grid;
grid-template-columns: minmax(0, 270px) minmax(0, 1fr);
gap: 24px;
align-items: start;
margin-top: 28px;
}
.toc-panel,
.content-panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 30px;
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.28);
backdrop-filter: blur(22px);
}
.toc-panel {
position: sticky;
top: 24px;
padding: 20px;
}
.content-panel {
padding: 34px clamp(20px, 4vw, 46px) 48px;
}
#TOC ul,
.toc-panel ul {
list-style: none;
padding-left: 0;
}
a {
color: var(--accent);
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: #f3f7ff;
line-height: 1.1;
letter-spacing: -0.03em;
}
h2 {
margin-top: 2.3rem;
font-size: clamp(1.9rem, 4vw, 2.8rem);
}
h3 {
color: #b9c8e4;
}
blockquote {
margin: 1.7rem 0;
padding: 1rem 1.2rem;
background: var(--quote);
border-left: 4px solid var(--accent);
border-radius: 0 18px 18px 0;
}
code,
pre {
font-family: "JetBrains Mono", "SFMono-Regular", monospace;
}
code {
background: rgba(110, 231, 242, 0.12);
color: #aef8ff;
padding: 0.14rem 0.38rem;
border-radius: 0.42rem;
}
pre {
background: var(--code-bg);
color: #d9e3f0;
padding: 1rem 1.1rem;
border-radius: 20px;
overflow-x: auto;
border: 1px solid rgba(146, 164, 196, 0.14);
}
pre code {
background: transparent;
color: inherit;
padding: 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1.6rem 0;
}
th,
td {
padding: 0.9rem 0.95rem;
border-bottom: 1px solid var(--line);
text-align: left;
}
th {
color: #b8f4fa;
}
img {
max-width: 100%;
border-radius: 22px;
border: 1px solid var(--line);
}
@media (max-width: 920px) {
.midnight-layout {
grid-template-columns: 1fr;
}
.toc-panel {
position: static;
}
}
</style>
</head>
<body class="theme-product-midnight">
  <div class="ambient ambient-a"></div>
  <div class="ambient ambient-b"></div>
  <main class="midnight-page">
    <header class="launch-header">
      <p class="eyebrow">Launch Notes</p><h1>统一导出</h1>
      <div class="meta-row">
        
        
      </div>
    </header>
    <section class="midnight-layout">
            <article class="content-panel">
        <h1 id="统一导出--一个-skill--两种格式">统一导出- 一个 skill-
        两种格式</h1>
      </article>
    </section>
  </main>
</body>
</html>


path: assets/examples/sample-docx.md
content: # Markdown Export DOCX Example

This is a sample document to verify the `markdown-export` skill in `docx` mode.

## Features

- Supports Markdown file input
- Supports Markdown string input
- Supports custom `reference.docx` templates
- Built-in multiple style templates

## Table

| Template | Style | Use Case |
| --- | --- | --- |
| modern-blue | Clear & Professional | Reports, Proposals, Weekly updates |
| executive-serif | Formal & Steady | Proposals, Contract attachments, Memos |
| warm-notebook | Soft Editorial | Articles, Sharing notes, Reading notes |
| minimal-gray | Restrained & Simple | Tech docs, Internal guides |

## Code

```bash
python3 scripts/export_markdown.py \
  --format docx \
  --input-file sample.md \
  --output sample.docx \
  --builtin-template modern-blue \
  --toc
```


path: assets/examples/sample-html.md
content: # Markdown Export HTML Example

This is a sample document to verify the `markdown-export` skill in `html` mode.

## Supported Capabilities

- Markdown file input
- Markdown string input
- Custom Pandoc HTML templates
- Custom CSS overrides
- Built-in four style templates

## Example Table

| Template | Visual Direction | Use Case |
| --- | --- | --- |
| docs-slate | Documentation | Internal docs, Tech guides |
| magazine-amber | Editorial | Columns, Long forms, Content pages |
| product-midnight | Release | Release notes, Product updates |
| serif-paper | Paper | Articles, Essays, Whitepapers |

## Quote

> Templates handle the skeleton, CSS handles the visuals. Custom templates take priority over built-in ones.

## Code

```bash
python3 scripts/export_markdown.py \
  --format html \
  --input-file sample.md \
  --output sample.html \
  --builtin-template docs-slate \
  --toc \
  --embed-assets
```

## Full-Width Article Without TOC

```bash
python3 scripts/export_markdown.py \
  --format html \
  --input-file sample.md \
  --output sample-no-toc.html \
  --builtin-template docs-slate \
  --embed-assets
```

## Plain Body Without Article Card Background

```bash
python3 scripts/export_markdown.py \
  --format html \
  --input-file sample.md \
  --output sample-plain.html \
  --builtin-template docs-slate \
  --embed-assets \
  --no-body-background
```


path: assets/html/styles/docs-slate.css
content: :root {
  --bg: #f3f5f8;
  --panel: #ffffff;
  --panel-alt: #eef2f7;
  --text: #17212b;
  --muted: #5c6b7a;
  --line: #d7dee7;
  --accent: #1f4f78;
  --accent-soft: #dce8f3;
  --code-bg: #0f1720;
  --code-text: #e8edf2;
  --quote: #f7fbff;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: "Inter", "PingFang SC", "Noto Sans CJK SC", sans-serif;
  background:
    radial-gradient(circle at top left, rgba(31, 79, 120, 0.08), transparent 28rem),
    linear-gradient(180deg, #f8fafc 0%, var(--bg) 100%);
  color: var(--text);
  line-height: 1.75;
}

.page-shell {
  max-width: 1240px;
  margin: 0 auto;
  padding: 56px 24px 80px;
}

.page-header {
  padding: 28px 8px 32px;
}

.page-header h1 {
  margin: 0;
  font-size: clamp(2.4rem, 6vw, 4.5rem);
  line-height: 1.02;
  letter-spacing: -0.04em;
}

.eyebrow,
.toc-label {
  margin: 0 0 12px;
  color: var(--accent);
  font-size: 0.82rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-weight: 700;
}

.meta-row {
  margin-top: 14px;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  font-size: 0.96rem;
}

.page-main {
  display: grid;
  grid-template-columns: minmax(0, 280px) minmax(0, 1fr);
  gap: 28px;
  align-items: start;
}

.page-main.no-toc {
  grid-template-columns: minmax(0, 1fr);
}

.toc-panel,
.content-panel {
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(18px);
  border: 1px solid rgba(215, 222, 231, 0.9);
  border-radius: 28px;
  box-shadow: 0 24px 70px rgba(10, 18, 30, 0.08);
}

.toc-panel {
  position: sticky;
  top: 24px;
  padding: 22px 20px;
}

.content-panel {
  padding: 34px clamp(20px, 4vw, 48px) 48px;
}

.content-panel.plain {
  background: transparent;
  backdrop-filter: none;
  border: 0;
  box-shadow: none;
  border-radius: 0;
  padding-left: 8px;
  padding-right: 8px;
}

#TOC ul,
.toc-panel ul {
  list-style: none;
  padding-left: 0;
}

#TOC li,
.toc-panel li {
  margin: 0.4rem 0;
}

a {
  color: var(--accent);
}

h1,
h2,
h3,
h4,
h5,
h6 {
  color: #122b42;
  line-height: 1.15;
  letter-spacing: -0.03em;
  margin-top: 2.2rem;
  margin-bottom: 0.9rem;
}

h2 {
  font-size: clamp(1.8rem, 3vw, 2.4rem);
  padding-top: 0.6rem;
  border-top: 1px solid var(--line);
}

blockquote {
  margin: 1.6rem 0;
  padding: 1rem 1.2rem;
  border-left: 4px solid var(--accent);
  background: var(--quote);
  color: #243647;
  border-radius: 0 18px 18px 0;
}

code,
pre {
  font-family: "JetBrains Mono", "SFMono-Regular", monospace;
}

code {
  background: var(--accent-soft);
  padding: 0.16rem 0.38rem;
  border-radius: 0.45rem;
  font-size: 0.92em;
}

pre {
  background: var(--code-bg);
  color: var(--code-text);
  padding: 1rem 1.1rem;
  border-radius: 18px;
  overflow-x: auto;
}

pre code {
  background: transparent;
  padding: 0;
  color: inherit;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin: 1.6rem 0;
  background: var(--panel);
  border-radius: 16px;
  overflow: hidden;
}

th,
td {
  padding: 0.85rem 0.95rem;
  border-bottom: 1px solid var(--line);
  text-align: left;
}

th {
  background: var(--panel-alt);
}

img {
  max-width: 100%;
  border-radius: 18px;
  box-shadow: 0 20px 60px rgba(15, 23, 32, 0.12);
}

@media (max-width: 920px) {
  .page-main {
    grid-template-columns: 1fr;
  }

  .toc-panel {
    position: static;
  }
}


path: assets/html/styles/magazine-amber.css
content: :root {
  --bg: #fff8f0;
  --paper: rgba(255, 252, 246, 0.92);
  --card: #fffdf8;
  --text: #2d241b;
  --muted: #7a5c3c;
  --line: #eadcc9;
  --accent: #a64b00;
  --accent-2: #e07a1f;
  --quote: #fff2df;
  --code-bg: #2f2419;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  color: var(--text);
  font-family: "Avenir Next", "PingFang SC", "Noto Sans CJK SC", sans-serif;
  background:
    linear-gradient(180deg, rgba(230, 122, 31, 0.12), transparent 26rem),
    linear-gradient(135deg, #fff9f2 0%, #fff3df 42%, #fffaf6 100%);
  line-height: 1.78;
}

.hero-band {
  height: 22vh;
  min-height: 180px;
  background:
    radial-gradient(circle at 20% 30%, rgba(230, 122, 31, 0.4), transparent 18rem),
    radial-gradient(circle at 80% 10%, rgba(166, 75, 0, 0.25), transparent 18rem),
    linear-gradient(135deg, #2d241b 0%, #a64b00 100%);
}

.magazine-page {
  max-width: 980px;
  margin: -90px auto 0;
  padding: 0 22px 80px;
}

.hero-card,
.magazine-toc,
.story-card {
  background: var(--paper);
  backdrop-filter: blur(16px);
  border: 1px solid rgba(234, 220, 201, 0.8);
  box-shadow: 0 28px 90px rgba(77, 46, 17, 0.12);
}

.hero-card {
  border-radius: 34px;
  padding: 30px 30px 34px;
}

.hero-card h1 {
  margin: 0;
  font-size: clamp(2.8rem, 7vw, 5.3rem);
  line-height: 0.95;
  letter-spacing: -0.05em;
}

.kicker,
.toc-label {
  margin: 0 0 12px;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 800;
}

.meta-row {
  margin-top: 14px;
  color: var(--muted);
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}

.magazine-toc,
.story-card {
  margin-top: 20px;
  border-radius: 28px;
  padding: 24px clamp(18px, 4vw, 40px);
}

.story-card.plain {
  background: transparent;
  backdrop-filter: none;
  border: 0;
  box-shadow: none;
  padding-left: 0;
  padding-right: 0;
}

#TOC ul {
  list-style: none;
  padding-left: 0;
}

a {
  color: var(--accent);
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: "Canela", "Iowan Old Style", "Songti SC", serif;
  letter-spacing: -0.035em;
  line-height: 1.08;
  color: #2f2419;
}

h2 {
  font-size: clamp(2rem, 4vw, 2.8rem);
  margin-top: 2.3rem;
}

blockquote {
  margin: 1.8rem 0;
  padding: 1rem 1.2rem;
  background: var(--quote);
  border-left: 4px solid var(--accent-2);
  border-radius: 0 16px 16px 0;
}

code,
pre {
  font-family: "JetBrains Mono", "SFMono-Regular", monospace;
}

code {
  background: #f8e4cb;
  color: #6f3200;
  padding: 0.16rem 0.38rem;
  border-radius: 0.45rem;
}

pre {
  background: var(--code-bg);
  color: #fff7ef;
  padding: 1rem 1.1rem;
  border-radius: 20px;
  overflow-x: auto;
}

pre code {
  background: transparent;
  color: inherit;
  padding: 0;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin: 1.6rem 0;
}

th,
td {
  padding: 0.9rem 0.95rem;
  border-bottom: 1px solid var(--line);
  text-align: left;
}

th {
  color: var(--accent);
}

img {
  max-width: 100%;
  border-radius: 22px;
}


path: assets/html/styles/product-midnight.css
content: :root {
  --bg: #08111f;
  --bg-2: #101b30;
  --panel: rgba(11, 18, 33, 0.72);
  --panel-2: rgba(17, 27, 48, 0.78);
  --text: #e6edf7;
  --muted: #92a4c4;
  --line: rgba(144, 164, 196, 0.18);
  --accent: #6ee7f2;
  --accent-2: #7c3aed;
  --quote: rgba(110, 231, 242, 0.08);
  --code-bg: #050a14;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: "Space Grotesk", "PingFang SC", "Noto Sans CJK SC", sans-serif;
  color: var(--text);
  background:
    radial-gradient(circle at 10% 10%, rgba(110, 231, 242, 0.14), transparent 22rem),
    radial-gradient(circle at 90% 20%, rgba(124, 58, 237, 0.18), transparent 24rem),
    linear-gradient(180deg, #050a14 0%, #09111f 45%, #0b1628 100%);
  line-height: 1.78;
}

.ambient {
  position: fixed;
  inset: auto;
  width: 40vw;
  height: 40vw;
  border-radius: 999px;
  filter: blur(80px);
  opacity: 0.25;
  pointer-events: none;
}

.ambient-a {
  top: -8vw;
  right: -10vw;
  background: #7c3aed;
}

.ambient-b {
  left: -12vw;
  top: 24vh;
  background: #0891b2;
}

.midnight-page {
  position: relative;
  z-index: 1;
  max-width: 1220px;
  margin: 0 auto;
  padding: 58px 24px 80px;
}

.launch-header h1 {
  margin: 0;
  font-size: clamp(2.9rem, 7vw, 5.8rem);
  line-height: 0.94;
  letter-spacing: -0.06em;
  max-width: 12ch;
}

.eyebrow,
.toc-label {
  margin: 0 0 12px;
  color: var(--accent);
  font-size: 0.82rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 800;
}

.meta-row {
  margin-top: 14px;
  color: var(--muted);
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}

.midnight-layout {
  display: grid;
  grid-template-columns: minmax(0, 270px) minmax(0, 1fr);
  gap: 24px;
  align-items: start;
  margin-top: 28px;
}

.midnight-layout.no-toc {
  grid-template-columns: minmax(0, 1fr);
}

.toc-panel,
.content-panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 30px;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.28);
  backdrop-filter: blur(22px);
}

.toc-panel {
  position: sticky;
  top: 24px;
  padding: 20px;
}

.content-panel {
  padding: 34px clamp(20px, 4vw, 46px) 48px;
}

.content-panel.plain {
  background: transparent;
  border: 0;
  box-shadow: none;
  backdrop-filter: none;
  border-radius: 0;
  padding-left: 0;
  padding-right: 0;
}

#TOC ul,
.toc-panel ul {
  list-style: none;
  padding-left: 0;
}

a {
  color: var(--accent);
}

h1,
h2,
h3,
h4,
h5,
h6 {
  color: #f3f7ff;
  line-height: 1.1;
  letter-spacing: -0.03em;
}

h2 {
  margin-top: 2.3rem;
  font-size: clamp(1.9rem, 4vw, 2.8rem);
}

h3 {
  color: #b9c8e4;
}

blockquote {
  margin: 1.7rem 0;
  padding: 1rem 1.2rem;
  background: var(--quote);
  border-left: 4px solid var(--accent);
  border-radius: 0 18px 18px 0;
}

code,
pre {
  font-family: "JetBrains Mono", "SFMono-Regular", monospace;
}

code {
  background: rgba(110, 231, 242, 0.12);
  color: #aef8ff;
  padding: 0.14rem 0.38rem;
  border-radius: 0.42rem;
}

pre {
  background: var(--code-bg);
  color: #d9e3f0;
  padding: 1rem 1.1rem;
  border-radius: 20px;
  overflow-x: auto;
  border: 1px solid rgba(146, 164, 196, 0.14);
}

pre code {
  background: transparent;
  color: inherit;
  padding: 0;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin: 1.6rem 0;
}

th,
td {
  padding: 0.9rem 0.95rem;
  border-bottom: 1px solid var(--line);
  text-align: left;
}

th {
  color: #b8f4fa;
}

img {
  max-width: 100%;
  border-radius: 22px;
  border: 1px solid var(--line);
}

@media (max-width: 920px) {
  .midnight-layout {
    grid-template-columns: 1fr;
  }

  .toc-panel {
    position: static;
  }
}


path: assets/html/styles/serif-paper.css
content: :root {
  --bg: #ebe7df;
  --paper: #fffdf8;
  --text: #2b231d;
  --muted: #78695a;
  --line: #ded3c5;
  --accent: #6d4c2e;
  --accent-2: #b17a4e;
  --quote: #f8f0e6;
  --code-bg: #1f1d1a;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: "Source Serif 4", "Songti SC", "STSong", serif;
  color: var(--text);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.4), transparent 18rem),
    linear-gradient(180deg, #efebe4 0%, var(--bg) 100%);
  line-height: 1.84;
}

.paper-wrap {
  max-width: 900px;
  margin: 0 auto;
  padding: 52px 22px 84px;
}

.paper-header,
.paper-toc,
.paper-body {
  background: rgba(255, 253, 248, 0.92);
  border: 1px solid rgba(222, 211, 197, 0.9);
  box-shadow: 0 22px 70px rgba(58, 43, 30, 0.08);
}

.paper-header {
  border-radius: 22px 22px 0 0;
  padding: 28px 28px 20px;
}

.paper-toc {
  padding: 16px 28px 12px;
  border-top: 0;
}

.paper-body {
  padding: 18px 28px 42px;
  border-top: 0;
  border-radius: 0 0 22px 22px;
}

.paper-body.plain {
  background: transparent;
  border: 0;
  box-shadow: none;
  border-radius: 0;
  padding-left: 0;
  padding-right: 0;
}

.paper-header h1 {
  margin: 0;
  font-size: clamp(2.6rem, 6vw, 4.8rem);
  line-height: 1;
  letter-spacing: -0.045em;
}

.eyebrow,
.toc-label {
  margin: 0 0 10px;
  color: var(--accent);
  font-size: 0.82rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-weight: 700;
  font-family: "Inter", "PingFang SC", sans-serif;
}

.meta-row {
  margin-top: 16px;
  color: var(--muted);
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  font-family: "Inter", "PingFang SC", sans-serif;
  font-size: 0.95rem;
}

#TOC ul {
  list-style: none;
  padding-left: 0;
}

a {
  color: var(--accent);
}

h1,
h2,
h3,
h4,
h5,
h6 {
  color: #201914;
  line-height: 1.14;
}

h2 {
  margin-top: 2.4rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--line);
}

blockquote {
  margin: 1.7rem 0;
  padding: 1rem 1.2rem;
  background: var(--quote);
  border-left: 4px solid var(--accent-2);
}

code,
pre {
  font-family: "JetBrains Mono", "SFMono-Regular", monospace;
}

code {
  background: #efe2d2;
  color: #7e4d1d;
  padding: 0.14rem 0.34rem;
  border-radius: 0.4rem;
}

pre {
  background: var(--code-bg);
  color: #f8f3ed;
  padding: 1rem 1.1rem;
  border-radius: 12px;
  overflow-x: auto;
}

pre code {
  background: transparent;
  color: inherit;
  padding: 0;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin: 1.6rem 0;
}

th,
td {
  padding: 0.85rem 0.9rem;
  border-bottom: 1px solid var(--line);
  text-align: left;
}

th {
  color: var(--accent);
  font-family: "Inter", "PingFang SC", sans-serif;
}

img {
  max-width: 100%;
  border-radius: 14px;
}


path: assets/html/templates/docs-slate.html
content: $if(lang)$<!DOCTYPE html>
<html lang="$lang$">
$else$<!DOCTYPE html>
<html lang="zh-CN">
$endif$
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
  $for(author-meta)$<meta name="author" content="$author-meta$" />$endfor$
  $if(date-meta)$<meta name="date" content="$date-meta$" />$endif$
  $if(description-meta)$<meta name="description" content="$description-meta$" />$endif$
  $for(css)$<link rel="stylesheet" href="$css$" />$endfor$
</head>
<body class="theme-docs-slate">
  <div class="page-shell">
    <header class="page-header">
      $if(title)$<p class="eyebrow">Markdown to HTML</p><h1>$title$</h1>$endif$
      <div class="meta-row">
        $if(author)$<span>$for(author)$$author$$sep$ · $endfor$</span>$endif$
        $if(date)$<span>$date$</span>$endif$
      </div>
    </header>
    <main class="page-main$if(toc)$ has-toc$else$ no-toc$endif$">
      $if(toc)$
      <aside class="toc-panel">
        <p class="toc-label">目录</p>
        $table-of-contents$
      </aside>
      $endif$
      <article class="content-panel$if(plain_content)$ plain$endif$">
        $body$
      </article>
    </main>
  </div>
</body>
</html>


path: assets/html/templates/magazine-amber.html
content: $if(lang)$<!DOCTYPE html>
<html lang="$lang$">
$else$<!DOCTYPE html>
<html lang="zh-CN">
$endif$
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
  $for(author-meta)$<meta name="author" content="$author-meta$" />$endfor$
  $if(date-meta)$<meta name="date" content="$date-meta$" />$endif$
  $if(description-meta)$<meta name="description" content="$description-meta$" />$endif$
  $for(css)$<link rel="stylesheet" href="$css$" />$endfor$
</head>
<body class="theme-magazine-amber">
  <div class="hero-band"></div>
  <main class="magazine-page">
    <header class="hero-card">
      $if(title)$<p class="kicker">Feature</p><h1>$title$</h1>$endif$
      <div class="meta-row">
        $if(author)$<span>$for(author)$$author$$sep$ · $endfor$</span>$endif$
        $if(date)$<span>$date$</span>$endif$
      </div>
    </header>
    $if(toc)$
    <section class="magazine-toc">
      <p class="toc-label">On This Page</p>
      $table-of-contents$
    </section>
    $endif$
    <article class="story-card$if(plain_content)$ plain$endif$">
      $body$
    </article>
  </main>
</body>
</html>


path: assets/html/templates/product-midnight.html
content: $if(lang)$<!DOCTYPE html>
<html lang="$lang$">
$else$<!DOCTYPE html>
<html lang="zh-CN">
$endif$
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
  $for(author-meta)$<meta name="author" content="$author-meta$" />$endfor$
  $if(date-meta)$<meta name="date" content="$date-meta$" />$endif$
  $if(description-meta)$<meta name="description" content="$description-meta$" />$endif$
  $for(css)$<link rel="stylesheet" href="$css$" />$endfor$
</head>
<body class="theme-product-midnight">
  <div class="ambient ambient-a"></div>
  <div class="ambient ambient-b"></div>
  <main class="midnight-page">
    <header class="launch-header">
      $if(title)$<p class="eyebrow">Launch Notes</p><h1>$title$</h1>$endif$
      <div class="meta-row">
        $if(author)$<span>$for(author)$$author$$sep$ · $endfor$</span>$endif$
        $if(date)$<span>$date$</span>$endif$
      </div>
    </header>
    <section class="midnight-layout$if(toc)$ has-toc$else$ no-toc$endif$">
      $if(toc)$
      <aside class="toc-panel">
        <p class="toc-label">Navigate</p>
        $table-of-contents$
      </aside>
      $endif$
      <article class="content-panel$if(plain_content)$ plain$endif$">
        $body$
      </article>
    </section>
  </main>
</body>
</html>


path: assets/html/templates/serif-paper.html
content: $if(lang)$<!DOCTYPE html>
<html lang="$lang$">
$else$<!DOCTYPE html>
<html lang="zh-CN">
$endif$
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
  $for(author-meta)$<meta name="author" content="$author-meta$" />$endfor$
  $if(date-meta)$<meta name="date" content="$date-meta$" />$endif$
  $if(description-meta)$<meta name="description" content="$description-meta$" />$endif$
  $for(css)$<link rel="stylesheet" href="$css$" />$endfor$
</head>
<body class="theme-serif-paper">
  <main class="paper-wrap">
    <header class="paper-header">
      $if(title)$<p class="eyebrow">Essay</p><h1>$title$</h1>$endif$
      <div class="meta-row">
        $if(author)$<span>$for(author)$$author$$sep$ · $endfor$</span>$endif$
        $if(date)$<span>$date$</span>$endif$
      </div>
    </header>
    $if(toc)$
    <section class="paper-toc">
      <p class="toc-label">Contents</p>
      $table-of-contents$
    </section>
    $endif$
    <article class="paper-body$if(plain_content)$ plain$endif$">
      $body$
    </article>
  </main>
</body>
</html>


path: scripts/export_markdown.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path


ROOT = Path(__file__).resolve().parent.parent
DOCX_TEMPLATE_DIR = ROOT / "assets" / "docx" / "templates"
HTML_TEMPLATE_DIR = ROOT / "assets" / "html" / "templates"
HTML_STYLE_DIR = ROOT / "assets" / "html" / "styles"
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"

ET.register_namespace("w", W_NS)

DOCX_TEMPLATES = {
    "modern-blue": {
        "file": DOCX_TEMPLATE_DIR / "modern-blue.docx",
        "description": "Clean report style with strong blue headings.",
    },
    "executive-serif": {
        "file": DOCX_TEMPLATE_DIR / "executive-serif.docx",
        "description": "Formal memo style with serif typography.",
    },
    "warm-notebook": {
        "file": DOCX_TEMPLATE_DIR / "warm-notebook.docx",
        "description": "Warm editorial style with softer accent colors.",
    },
    "minimal-gray": {
        "file": DOCX_TEMPLATE_DIR / "minimal-gray.docx",
        "description": "Low-contrast documentation style.",
    },
}

DOCX_CODE_THEMES = {
    "modern-blue": {
        "code_font": "Menlo",
        "minor_hans": "PingFang SC",
        "body_size": 22,
        "code_bg": "F4F8FC",
        "code_border": "C9DAEA",
        "code_text": "17324D",
        "code_keyword": "1D4ED8",
        "code_string": "047857",
        "code_key": "7C3AED",
        "code_number": "B45309",
        "code_comment": "64748B",
        "code_operator": "0F766E",
    },
    "executive-serif": {
        "code_font": "Menlo",
        "minor_hans": "PingFang SC",
        "body_size": 23,
        "code_bg": "F7F4F1",
        "code_border": "D9CDC0",
        "code_text": "3B322C",
        "code_keyword": "274C77",
        "code_string": "0F766E",
        "code_key": "7C2D12",
        "code_number": "9A3412",
        "code_comment": "78716C",
        "code_operator": "57534E",
    },
    "warm-notebook": {
        "code_font": "Menlo",
        "minor_hans": "PingFang SC",
        "body_size": 22,
        "code_bg": "FFF6ED",
        "code_border": "F3D2B3",
        "code_text": "7C2D12",
        "code_keyword": "C2410C",
        "code_string": "15803D",
        "code_key": "9A3412",
        "code_number": "B45309",
        "code_comment": "78716C",
        "code_operator": "9A3412",
    },
    "minimal-gray": {
        "code_font": "Menlo",
        "minor_hans": "PingFang SC",
        "body_size": 21,
        "code_bg": "F5F6F7",
        "code_border": "D1D5DB",
        "code_text": "1F2937",
        "code_keyword": "374151",
        "code_string": "047857",
        "code_key": "4B5563",
        "code_number": "B45309",
        "code_comment": "6B7280",
        "code_operator": "4B5563",
    },
    "neutral": {
        "code_font": "Menlo",
        "minor_hans": "PingFang SC",
        "body_size": 22,
        "code_bg": "F6F8FA",
        "code_border": "D0D7DE",
        "code_text": "24292F",
        "code_keyword": "0550AE",
        "code_string": "0A7F42",
        "code_key": "8250DF",
        "code_number": "9A6700",
        "code_comment": "6E7781",
        "code_operator": "0A3069",
    },
}

CODE_TOKEN_COLORS = {
    "KeywordTok": "code_keyword",
    "DataTypeTok": "code_key",
    "DecValTok": "code_number",
    "BaseNTok": "code_number",
    "FloatTok": "code_number",
    "ConstantTok": "code_number",
    "CharTok": "code_string",
    "SpecialCharTok": "code_operator",
    "StringTok": "code_string",
    "VerbatimStringTok": "code_string",
    "SpecialStringTok": "code_string",
    "ImportTok": "code_text",
    "CommentTok": "code_comment",
    "DocumentationTok": "code_comment",
    "AnnotationTok": "code_comment",
    "CommentVarTok": "code_comment",
    "OtherTok": "code_operator",
    "FunctionTok": "code_operator",
    "VariableTok": "code_text",
    "ControlFlowTok": "code_keyword",
    "OperatorTok": "code_operator",
    "BuiltInTok": "code_text",
    "ExtensionTok": "code_text",
    "PreprocessorTok": "code_comment",
    "AttributeTok": "code_key",
    "RegionMarkerTok": "code_text",
    "InformationTok": "code_comment",
    "WarningTok": "code_comment",
    "AlertTok": "code_keyword",
    "ErrorTok": "code_keyword",
    "NormalTok": "code_text",
}

CODE_TOKEN_BOLD = {"KeywordTok", "ControlFlowTok", "FunctionTok", "AttributeTok", "DataTypeTok"}
CODE_TOKEN_ITALIC = {"CommentTok", "DocumentationTok", "AnnotationTok", "CommentVarTok", "PreprocessorTok"}

HTML_TEMPLATES = {
    "docs-slate": {
        "template": HTML_TEMPLATE_DIR / "docs-slate.html",
        "css": [HTML_STYLE_DIR / "docs-slate.css"],
        "description": "Technical documentation layout with clean spacing and slate tones.",
    },
    "magazine-amber": {
        "template": HTML_TEMPLATE_DIR / "magazine-amber.html",
        "css": [HTML_STYLE_DIR / "magazine-amber.css"],
        "description": "Editorial layout with warm highlights and magazine rhythm.",
    },
    "product-midnight": {
        "template": HTML_TEMPLATE_DIR / "product-midnight.html",
        "css": [HTML_STYLE_DIR / "product-midnight.css"],
        "description": "Dark narrative layout for launch notes and product storytelling.",
    },
    "serif-paper": {
        "template": HTML_TEMPLATE_DIR / "serif-paper.html",
        "css": [HTML_STYLE_DIR / "serif-paper.css"],
        "description": "Print-inspired article page with serif typography.",
    },
}


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Export Markdown files or Markdown strings into DOCX or HTML."
    )
    parser.add_argument("--format", choices=["docx", "html"], help="Target output format.")
    parser.add_argument("--input-file", type=Path, help="Absolute or relative path to a Markdown file.")
    parser.add_argument("--markdown", help="Raw Markdown string to convert.")
    parser.add_argument("--output", type=Path, help="Output path. Defaults from --input-file when possible.")
    parser.add_argument(
        "--template",
        type=Path,
        help="Custom template. DOCX expects reference.docx, HTML expects a Pandoc HTML template.",
    )
    parser.add_argument("--builtin-template", help="Built-in template name for the selected --format.")
    parser.add_argument(
        "--css",
        action="append",
        type=Path,
        default=[],
        help="Additional CSS file for HTML output. May be passed more than once.",
    )
    parser.add_argument("--metadata-file", type=Path, help="Pandoc metadata file, usually YAML.")
    parser.add_argument("--resource-path", help="Extra pandoc resource path for linked assets.")
    parser.add_argument("--title")
    parser.add_argument("--author")
    parser.add_argument("--date")
    parser.add_argument("--toc", action="store_true", help="Insert a table of contents.")
    parser.add_argument("--number-sections", action="store_true", help="Number document headings.")
    parser.add_argument(
        "--no-body-background",
        action="store_true",
        help="Remove the background, border, and shadow behind the rendered HTML article body.",
    )
    parser.add_argument(
        "--embed-assets",
        action="store_true",
        help="Embed CSS, images, and other resources into HTML when possible.",
    )
    parser.add_argument(
        "--self-contained",
        action="store_true",
        help="Compatibility alias for --embed-assets.",
    )
    parser.add_argument(
        "--highlight-style",
        default="tango",
        help="Pandoc syntax highlighting style. Default: tango.",
    )
    parser.add_argument("--list-templates", action="store_true", help="Print built-in templates and exit.")
    parser.add_argument("--print-command", action="store_true", help="Print the pandoc command before running.")
    return parser


def ensure_pandoc() -> str:
    pandoc = shutil.which("pandoc")
    if not pandoc:
        raise SystemExit("pandoc is required but was not found on PATH.")
    return pandoc


def w(tag: str) -> str:
    return f"{{{W_NS}}}{tag}"


def style_by_id(root: ET.Element, style_id: str) -> ET.Element | None:
    for style in root.findall(w("style")):
        if style.get(w("styleId")) == style_id or style.get("styleId") == style_id:
            return style
    return None


def ensure_child(parent: ET.Element, tag: str) -> ET.Element:
    node = parent.find(tag)
    if node is None:
        node = ET.SubElement(parent, tag)
    return node


def append_style(root: ET.Element, style_type: str, style_id: str, name: str) -> ET.Element:
    style = ET.SubElement(root, w("style"))
    style.set(w("type"), style_type)
    style.set(w("customStyle"), "1")
    style.set(w("styleId"), style_id)
    name_node = ET.SubElement(style, w("name"))
    name_node.set(w("val"), name)
    return style


def set_font_on_rpr(rpr: ET.Element, *, ascii_font: str | None = None, east_asia_font: str | None = None) -> None:
    fonts = ensure_child(rpr, w("rFonts"))
    if ascii_font:
        fonts.set(w("ascii"), ascii_font)
        fonts.set(w("hAnsi"), ascii_font)
    if east_asia_font:
        fonts.set(w("eastAsia"), east_asia_font)


def set_size_on_rpr(rpr: ET.Element, value: int) -> None:
    sz = ensure_child(rpr, w("sz"))
    sz.set(w("val"), str(value))
    sz_cs = ensure_child(rpr, w("szCs"))
    sz_cs.set(w("val"), str(value))


def set_color_on_rpr(rpr: ET.Element, value: str) -> None:
    color = ensure_child(rpr, w("color"))
    color.attrib.clear()
    color.set(w("val"), value)


def set_spacing_on_ppr(ppr: ET.Element, *, before: int, after: int, line: int) -> None:
    spacing = ensure_child(ppr, w("spacing"))
    spacing.set(w("before"), str(before))
    spacing.set(w("after"), str(after))
    spacing.set(w("line"), str(line))
    spacing.set(w("lineRule"), "auto")


def set_indentation(ppr: ET.Element, *, left: int, right: int) -> None:
    ind = ensure_child(ppr, w("ind"))
    ind.set(w("left"), str(left))
    ind.set(w("right"), str(right))


def set_shading(target: ET.Element, fill: str) -> None:
    shd = ensure_child(target, w("shd"))
    shd.set(w("val"), "clear")
    shd.set(w("fill"), fill)


def set_borders(ppr: ET.Element, color: str, size: int = 8, space: int = 4) -> None:
    p_bdr = ensure_child(ppr, w("pBdr"))
    for edge in ("top", "left", "bottom", "right"):
        border = ensure_child(p_bdr, w(edge))
        border.set(w("val"), "single")
        border.set(w("sz"), str(size))
        border.set(w("space"), str(space))
        border.set(w("color"), color)


def update_docx_code_styles(styles_xml: bytes, cfg: dict[str, str | int]) -> bytes:
    root = ET.fromstring(styles_xml)

    source_code = style_by_id(root, "SourceCode")
    if source_code is None:
        source_code = append_style(root, "paragraph", "SourceCode", "Source Code")
        based_on = ET.SubElement(source_code, w("basedOn"))
        based_on.set(w("val"), "Normal")
        link = ET.SubElement(source_code, w("link"))
        link.set(w("val"), "VerbatimChar")
    ppr = ensure_child(source_code, w("pPr"))
    word_wrap = ppr.find(w("wordWrap"))
    if word_wrap is not None:
        ppr.remove(word_wrap)
    set_spacing_on_ppr(ppr, before=120, after=120, line=240)
    set_indentation(ppr, left=240, right=120)
    set_shading(ppr, str(cfg["code_bg"]))
    set_borders(ppr, str(cfg["code_border"]))

    verbatim = style_by_id(root, "VerbatimChar")
    if verbatim is None:
        verbatim = append_style(root, "character", "VerbatimChar", "Verbatim Char")
        based_on = ET.SubElement(verbatim, w("basedOn"))
        based_on.set(w("val"), "BodyTextChar")
    verbatim_rpr = ensure_child(verbatim, w("rPr"))
    set_font_on_rpr(
        verbatim_rpr,
        ascii_font=str(cfg["code_font"]),
        east_asia_font=str(cfg["minor_hans"]),
    )
    set_size_on_rpr(verbatim_rpr, max(18, int(cfg["body_size"]) - 2))
    set_color_on_rpr(verbatim_rpr, str(cfg["code_text"]))

    for token_style_id, color_key in CODE_TOKEN_COLORS.items():
        token_style = style_by_id(root, token_style_id)
        if token_style is None:
            token_style = append_style(root, "character", token_style_id, token_style_id)
            based_on = ET.SubElement(token_style, w("basedOn"))
            based_on.set(w("val"), "VerbatimChar")
        token_rpr = ensure_child(token_style, w("rPr"))
        set_color_on_rpr(token_rpr, str(cfg[color_key]))
        if token_style_id in CODE_TOKEN_BOLD:
            ensure_child(token_rpr, w("b"))
        if token_style_id in CODE_TOKEN_ITALIC:
            ensure_child(token_rpr, w("i"))

    return ET.tostring(root, encoding="utf-8", xml_declaration=True)


def patch_docx_output_styles(docx_path: Path, builtin_template: str | None) -> None:
    cfg = DOCX_CODE_THEMES.get(builtin_template or "", DOCX_CODE_THEMES["neutral"])
    with tempfile.TemporaryDirectory(prefix="markdown-export-docx-") as tmpdir:
        tmpdir_path = Path(tmpdir)
        unpack_dir = tmpdir_path / "docx"
        unpack_dir.mkdir()
        with zipfile.ZipFile(docx_path) as src_zip:
            src_zip.extractall(unpack_dir)

        styles_path = unpack_dir / "word" / "styles.xml"
        styles_path.write_bytes(update_docx_code_styles(styles_path.read_bytes(), cfg))

        rewritten = tmpdir_path / "rewritten.docx"
        with zipfile.ZipFile(rewritten, "w", compression=zipfile.ZIP_DEFLATED) as out_zip:
            for path in sorted(unpack_dir.rglob("*")):
                if path.is_file():
                    out_zip.write(path, path.relative_to(unpack_dir).as_posix())
        shutil.move(str(rewritten), str(docx_path))


def list_templates(output_format: str | None) -> None:
    payload: dict[str, object] = {}
    if output_format in (None, "docx"):
        payload["docx"] = [
            {
                "name": name,
                "path": str(config["file"]),
                "description": config["description"],
                "exists": config["file"].exists(),
            }
            for name, config in DOCX_TEMPLATES.items()
        ]
    if output_format in (None, "html"):
        payload["html"] = [
            {
                "name": name,
                "template": str(config["template"]),
                "css": [str(path) for path in config["css"]],
                "description": config["description"],
                "exists": config["template"].exists() and all(path.exists() for path in config["css"]),
            }
            for name, config in HTML_TEMPLATES.items()
        ]
    print(json.dumps(payload, ensure_ascii=False, indent=2))


def validate_args(args: argparse.Namespace) -> None:
    if args.list_templates and not args.format:
        return
    if not args.format:
        raise SystemExit("--format is required unless you only use --list-templates.")
    if args.list_templates:
        return
    if bool(args.input_file) == bool(args.markdown):
        raise SystemExit("Provide exactly one of --input-file or --markdown.")
    if args.input_file and not args.input_file.exists():
        raise SystemExit(f"Input Markdown file not found: {args.input_file}")
    if args.template and not args.template.exists():
        raise SystemExit(f"Template file not found: {args.template}")
    if args.metadata_file and not args.metadata_file.exists():
        raise SystemExit(f"Metadata file not found: {args.metadata_file}")
    for css_path in args.css:
        if not css_path.exists():
            raise SystemExit(f"CSS file not found: {css_path}")
    if args.format == "docx" and args.css:
        raise SystemExit("--css is only valid with --format html.")


def resolve_output(args: argparse.Namespace) -> Path:
    if args.output:
        return args.output.expanduser().resolve()
    if args.input_file:
        suffix = ".docx" if args.format == "docx" else ".html"
        return args.input_file.expanduser().resolve().with_suffix(suffix)
    raise SystemExit("--output is required when using --markdown.")


def resolve_docx_template(args: argparse.Namespace) -> Path:
    if args.template:
        return args.template.expanduser().resolve()
    name = args.builtin_template or "modern-blue"
    if name not in DOCX_TEMPLATES:
        valid = ", ".join(sorted(DOCX_TEMPLATES))
        raise SystemExit(f"Unknown DOCX built-in template '{name}'. Valid values: {valid}")
    path = DOCX_TEMPLATES[name]["file"]
    if not path.exists():
        raise SystemExit(f"Built-in DOCX template is missing: {path}")
    return path


def resolve_html_template(args: argparse.Namespace) -> tuple[Path, list[Path], str | None]:
    if args.template:
        return args.template.expanduser().resolve(), [path.expanduser().resolve() for path in args.css], None
    name = args.builtin_template or "docs-slate"
    if name not in HTML_TEMPLATES:
        valid = ", ".join(sorted(HTML_TEMPLATES))
        raise SystemExit(f"Unknown HTML built-in template '{name}'. Valid values: {valid}")
    config = HTML_TEMPLATES[name]
    template_path = config["template"]
    if not template_path.exists():
        raise SystemExit(f"Built-in HTML template is missing: {template_path}")
    css_paths = [path.resolve() for path in config["css"]] + [path.expanduser().resolve() for path in args.css]
    return template_path, css_paths, name


def base_resource_path(args: argparse.Namespace) -> str | None:
    if args.resource_path:
        return args.resource_path
    if args.input_file:
        return str(args.input_file.expanduser().resolve().parent)
    return None


def pandoc_markdown_input() -> str:
    return (
        "markdown+yaml_metadata_block+emoji+footnotes+pipe_tables+grid_tables+task_lists+"
        "fenced_divs+fenced_code_attributes+raw_attribute+tex_math_dollars"
    )


def build_docx_command(
    pandoc: str,
    source_path: Path,
    output_path: Path,
    template_path: Path,
    args: argparse.Namespace,
) -> list[str]:
    command = [
        pandoc,
        str(source_path),
        "--from",
        pandoc_markdown_input(),
        "--to",
        "docx",
        "--standalone",
        "--reference-doc",
        str(template_path),
        "--syntax-highlighting",
        args.highlight_style,
        "--output",
        str(output_path),
    ]
    extend_common_command(command, args)
    return command


def build_html_command(
    pandoc: str,
    source_path: Path,
    output_path: Path,
    template_path: Path,
    css_paths: list[Path],
    args: argparse.Namespace,
) -> list[str]:
    command = [
        pandoc,
        str(source_path),
        "--from",
        pandoc_markdown_input(),
        "--to",
        "html5",
        "--standalone",
        "--template",
        str(template_path),
        "--syntax-highlighting",
        args.highlight_style,
        "--output",
        str(output_path),
    ]
    extend_common_command(command, args)
    if args.embed_assets or args.self_contained:
        command.append("--embed-resources")
    for css_path in css_paths:
        command.extend(["--css", str(css_path)])
    return command


def extend_common_command(command: list[str], args: argparse.Namespace) -> None:
    if args.metadata_file:
        command.extend(["--metadata-file", str(args.metadata_file.expanduser().resolve())])
    if args.title:
        command.extend(["--metadata", f"title={args.title}"])
    if args.author:
        command.extend(["--metadata", f"author={args.author}"])
    if args.date:
        command.extend(["--metadata", f"date={args.date}"])
    if args.no_body_background:
        command.extend(["--metadata", "plain_content=true"])
    resource_path = base_resource_path(args)
    if resource_path:
        command.extend(["--resource-path", resource_path])
    if args.toc:
        command.append("--toc")
    if args.number_sections:
        command.append("--number-sections")


def prepare_source(args: argparse.Namespace) -> tuple[Path, tempfile.NamedTemporaryFile[str] | None]:
    if args.input_file:
        return args.input_file.expanduser().resolve(), None
    # 使用上下文管理器确保临时文件总是被清理
    temp_source = tempfile.NamedTemporaryFile(
        mode="w",
        suffix=".md",
        prefix="markdown-export-",
        encoding="utf-8",
        delete=False,
    )
    with temp_source:
        temp_source.write(args.markdown)
        temp_source.flush()
    return Path(temp_source.name), temp_source


def run_conversion(args: argparse.Namespace) -> int:
    pandoc = ensure_pandoc()
    output_path = resolve_output(args)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    source_path, temp_source = prepare_source(args)
    try:
        if args.format == "docx":
            template_path = resolve_docx_template(args)
            command = build_docx_command(pandoc, source_path, output_path, template_path, args)
            css_paths: list[Path] = []
            builtin_name = None if args.template else (args.builtin_template or "modern-blue")
        else:
            template_path, css_paths, builtin_name = resolve_html_template(args)
            command = build_html_command(pandoc, source_path, output_path, template_path, css_paths, args)

        if args.print_command:
            print(" ".join(command), file=sys.stderr)
        completed = subprocess.run(command, check=False)
        if completed.returncode != 0:
            return completed.returncode
        if args.format == "docx":
            patch_docx_output_styles(output_path, builtin_name)

        print(
            json.dumps(
                {
                    "format": args.format,
                    "output": str(output_path),
                    "template": str(template_path),
                    "css": [str(path) for path in css_paths],
                    "builtin_template": builtin_name,
                    "source": str(source_path),
                },
                ensure_ascii=False,
                indent=2,
            )
        )
        return 0
    finally:
        if temp_source is not None:
            try:
                Path(temp_source.name).unlink(missing_ok=True)
            except OSError:
                pass


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    validate_args(args)
    if args.list_templates:
        list_templates(args.format)
        return 0
    return run_conversion(args)


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)