# 简化 mark2pdf 外部调用方案（v3）

---

## 核心需求

1. **保留预处理逻辑**：JSON 格式化、frontmatter 处理
2. **消除路径依赖**：不再硬编码 mark2pdf 路径
3. **定制 frontmatter**：支持配置文件设置全局 frontmatter
4. **定制 template**：支持本地模板或系统模板

---

## 配置方案

### 目录结构

```
调用脚本所在目录/
├── run_mark2pdf_md2pdf.py     # 调用脚本
├── mark2pdf.yaml              # 配置文件（可选）
├── template/                 # 本地模板目录（可选）
│   └── my_template.typ
├── _l2points/                # 输入目录
│   ├── doc1.md
│   └── doc2.md
└── _l2points/pdf/            # 输出目录（自动创建）
```

---

### 配置文件 `mark2pdf.yaml`

```yaml
# ============ 模板配置 ============
# 优先级：CLI 参数 > 本地模板 > 系统模板
template:
  # 方式1：使用系统模板（mark2pdf/template/ 下的模板）
  name: "default"  # 对应 mark2pdf/template/default.typ
  
  # 方式2：使用本地模板（相对于配置文件的路径）
  # local: "template/my_template.typ"

# ============ Frontmatter 默认值 ============
frontmatter:
  toc-depth: 2
  author: "fangjun"
  date: auto  # auto = 当前日期

# ============ 按文件名模式覆盖 ============
overrides:
  - pattern: "*期中*"
    frontmatter:
      title: "期中考试复习"
  - pattern: "*期末*"
    frontmatter:
      title: "期末考试复习"
    template:
      name: "exam"  # 使用专门的考试模板
```

---

## 模板查找逻辑

```
CLI --template 参数
    ↓ 未指定
config.template.local（本地模板）
    ↓ 不存在
config.template.name（系统模板名）
    ↓ 未指定
mark2pdf 默认模板
```

---

### 实现代码

```python
def resolve_template(config: dict, script_dir: Path) -> str | None:
    """
    解析模板路径
    返回：模板绝对路径 或 None（使用默认）
    """
    template_config = config.get("template", {})
    
    # 优先使用本地模板
    if local := template_config.get("local"):
        local_path = script_dir / local
        if local_path.exists():
            return str(local_path.resolve())
        else:
            click.echo(f"警告：本地模板不存在：{local_path}", err=True)
    
    # 使用系统模板名（由 md2pdf 的 --template 参数处理）
    if name := template_config.get("name"):
        return name  # 传递给 md2pdf --template
    
    return None  # 使用默认
```

---

## 简化后的脚本（约 120 行）

```python
# /// script
# dependencies = ["click", "pyyaml"]
# ///
"""外部调用 mark2pdf md2pdf - 支持配置文件"""
import re
import json
import subprocess
import fnmatch
from pathlib import Path
from datetime import date
import click
import yaml

SCRIPT_DIR = Path(__file__).parent
CONFIG_FILE = "mark2pdf.yaml"


def load_config() -> dict:
    """加载配置文件"""
    config_path = SCRIPT_DIR / CONFIG_FILE
    if config_path.exists():
        return yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
    return {}


def resolve_template(config: dict) -> str | None:
    """解析模板路径"""
    t = config.get("template", {})
    if local := t.get("local"):
        p = SCRIPT_DIR / local
        if p.exists():
            return str(p.resolve())
    return t.get("name")


def get_frontmatter_defaults(config: dict, filename: str) -> dict:
    """获取 frontmatter 默认值（含文件名模式匹配）"""
    fm = dict(config.get("frontmatter", {}))
    
    # 处理特殊值
    if fm.get("date") == "auto":
        fm["date"] = date.today().isoformat()
    
    # 检查 overrides
    for override in config.get("overrides", []):
        if fnmatch.fnmatch(filename, override.get("pattern", "")):
            fm.update(override.get("frontmatter", {}))
    
    return fm


def ensure_frontmatter(content: str, defaults: dict) -> str:
    """确保文档有 frontmatter"""
    fm_pattern = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
    m = fm_pattern.match(content)
    
    if m:
        existing = yaml.safe_load(m.group(1)) or {}
        merged = {**defaults, **existing}  # 现有值优先
    else:
        merged = defaults
    
    fm_str = yaml.dump(merged, allow_unicode=True, default_flow_style=False).strip()
    body = content[m.end():] if m else content
    return f"---\n{fm_str}\n---\n\n{body}"


def convert_json_fences_to_blockquote(content: str) -> str:
    """将 ```json 代码块转为引用格式"""
    # ... 保留原有逻辑 ...


@click.command(context_settings=dict(ignore_unknown_options=True, allow_extra_args=True))
@click.argument("files", nargs=-1, required=True)
@click.option("--input-dir", default="_l2points")
@click.option("--template", help="覆盖模板（CLI 优先级最高）")
@click.option("--verbose", is_flag=True)
@click.pass_context
def cli(ctx, files, input_dir, template, verbose):
    """预处理 Markdown 并调用 md2pdf"""
    config = load_config()
    src_dir = Path(input_dir)
    tmp_dir = src_dir / "tmp"
    pdf_dir = src_dir / "pdf"
    tmp_dir.mkdir(exist_ok=True)
    pdf_dir.mkdir(exist_ok=True)

    # 解析模板
    resolved_template = template or resolve_template(config)

    for f in files:
        md_file = src_dir / f if not Path(f).is_absolute() else Path(f)
        if not md_file.exists():
            click.echo(f"跳过：{md_file}", err=True)
            continue

        # 预处理
        content = md_file.read_text(encoding="utf-8")
        processed = convert_json_fences_to_blockquote(content)
        
        fm_defaults = get_frontmatter_defaults(config, md_file.name)
        processed = ensure_frontmatter(processed, fm_defaults)

        tmp_path = tmp_dir / md_file.name
        tmp_path.write_text(processed, encoding="utf-8")

        # 构建命令
        cmd = [
            "md2pdf", md_file.name,
            "--indir", str(tmp_dir),
            "--outdir", str(pdf_dir),
            "-ft",
        ]
        if resolved_template:
            cmd.extend(["--template", resolved_template])
        cmd.extend(ctx.args)

        if verbose:
            click.echo(f"执行: {' '.join(cmd)}")

        result = subprocess.run(cmd, check=False)
        status = "✓" if result.returncode == 0 else "✗"
        click.echo(f"{status} {md_file.name}")


if __name__ == "__main__":
    cli()
```

---

## 使用示例

```bash
# 使用配置文件中的设置
uv run run_mark2pdf_md2pdf.py doc1.md doc2.md

# CLI 覆盖模板
uv run run_mark2pdf_md2pdf.py doc1.md --template exam

# 透传其他参数
uv run run_mark2pdf_md2pdf.py doc1.md --tc
```

---

## 执行步骤

1. **安装全局命令**（一次性）：
   ```bash
   cd /Users/fangjun/python/mark2pdf && uv pip install -e .
   ```

2. **创建配置文件**：在 `_logs/调用/` 创建 `mark2pdf.yaml`

3. **更新脚本**：用简化版替换 `run_mark2pdf_md2pdf.py`
