跳到內容

Python

先備知識:快速開始Document IR

要從 Django/Flask backend、Jupyter manuscript pipeline 或 Python static-site generator 解析/轉譯 .mdi,不必呼叫 CLI。illusion-markdown 經由 PyO3/maturinmdi-core 編譯成 native extension;約 60 行的 Python wrapper 沒有自己的 MDI grammar,每個 function 都直接呼叫同一份 Rust。

Terminal window
pip install illusion-markdown

PyPI distribution 名稱是 illusion-markdownimport name 是 mdi。需要 Python 3.10+;macOS(Intel/Apple Silicon)、Linux x64、Windows x64 有預建 wheel,其他 platform 會使用需要 Rust toolchain 的 source distribution。

import mdi
source = """---
title: 東京の夜
lang: ja
---
# {東京|とうきょう}の夜
第^12^話
"""
result = mdi.parse(source)
print(result["document"]["children"][0]["type"]) # "heading"
html = mdi.render_html(source)
open("book.html", "w", encoding="utf-8").write(html)
mdi.MDI_SPEC_VERSION # "2.0"
mdi.MDI_IR_VERSION # "1.0"
mdi.TextFormat # Literal["txt", "txt-ruby", "narou", "kakuyomu", "aozora", "note"]
mdi.MdiRenderError
mdi.parse(source: str) -> dict
mdi.render_html(source: str) -> str
mdi.render_text(source: str) -> str
mdi.render_text_format(source: str, format: mdi.TextFormat, indent_prefix: str = "") -> str
mdi.render_epub(source: str) -> bytes
mdi.render_docx(source: str) -> bytes
mdi.serialize_mdi(source: str) -> str
mdi.parse_mdi_syntax # deprecated alias

現時沒有 render_pdf

parse() 回傳不是 typed class/dataclass 的 plain dict[str, Any],由 JSON wire format 直接 json.loads 而來,保留camelCase key:irVersionsyntaxVersiondocumentdiagnostics,node 裡是 startByte/endByte。因此請用 result["document"]["children"][0]["type"],且 Document IR 完全適用。render_epub/render_docx 是真正的 bytes(Rust 端由 PyBytes 支援),必須以 binary mode("wb")寫檔。

一般格式不正確的 MDI 不會 raise,而以 literal fallback 處理;回傳 dict 的 diagnostics warning 完整清單見診斷

result = mdi.parse("---\nmdi: '3.0'\n---\n\n本文")
# result["diagnostics"]:
# [{'severity': 'warning', 'code': 'mdi.version.unsupported',
# 'message': 'MDI 3.0 is newer than the supported 2.0',
# 'span': {'startByte': 0, 'endByte': 18}}]

三種真實且不同的失敗模式如下:非 str input 是 PyO3 自動產生的 TypeErrormdi.render_text_format("text", "invalid")ValueError: Unsupported text format: invalid;只有 Rust 的 EPUB/DOCX archive writer 本身失敗(例如底層 I/O)才會是 mdi.MdiRenderError。一般文件不會觸發最後一種。保留 tryexcept 給這些情況,勿用它取代檢查 result["diagnostics"]

mdi.parse() 會自行將 decoded irVersionmdi.MDI_IR_VERSION 比較;若不同會 raise RuntimeError: Unsupported MDI IR version: ...。span 是 UTF-8 byte offset,不是 Python str 的 code-point index;要轉換須明確做 source.encode("utf-8")。請見診斷與 source spans

def byte_span_to_str_index(source: str, byte_offset: int) -> int:
return len(source.encode("utf-8")[:byte_offset].decode("utf-8", errors="ignore"))

以上功能都已發布並受測,並非 speculative binding。套件自己的測試會驗證 IR shape、diagnostic 格式、byte span、六種文字格式、EPUB/DOCX archive 結構與上述錯誤路徑,並強制至少 95% branch coverage。

  • **尚無 PDF function。**Python 可以 spawn subprocess,沒有 WASM 那類根本限制,但 package 今天未 expose mdi.render_pdf;Python workflow 的 PDF 暫用 CLI
  • **沒有自己的 grammar。**每個 function 都直接呼叫同一個 mdi-core;若與 CLI 或 Rust 不一致,是這個約 60 行 wrapper 的 bug,而不是另一套 parser。
  • 尚未提供 export profile 參數。render_epubrender_docx 目前只收 source。設定型 EPUB/DOCX 已在 Rust 實作,只是 Python wrapper 尚未公開 profile 與 cover 參數。

分割規則由 Rust 統一實作。固定兩行、正文50%字級、零小行間距。首個片段可使用正文行剩餘容量,後續片段使用完整行容量。容量與回傳寬度以割注字級的半個em為單位;這是字寬估算,不保證比例字型的精確均衡。

from mdi import layout_warichu
fragments = layout_warichu([{"type": "text", "value": "一二三四五六"}], 4, first_capacity=2)

結果包含 lineshtmlwidthsoverflowhardBreakAftersourcespath 是從輸入陣列起算的子節點索引路徑;startUtf8 / endUtf8 是可見文字中的半開UTF-8位元組範圍。相同 group 保留跨格式邊界的書寫素。Ruby、縱中橫及no-break保持不可拆。作者硬換行保留,自動分割不寫回canonical MDI或純文字。靜態HTML/EPUB的閱讀器重排結果可能不同。DOCX使用原生雙行群組;XML與匯入器檢查不代表Word實測。