#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
md2html.py
----------
Markdownファイル(LaTeX数式を含む)をスタンドアロンのHTMLファイルに変換します。
このスクリプトは、指定されたMarkdownファイルを読み込み、Python-Markdownライブラリと
pymdownx.arithmatex拡張機能を使用してHTMLに変換します。
生成されたHTMLファイルには、MathJax (CDN) スクリプトと、デフォルトまたはユーザー指定のCSSが`<head>`セクションに挿入されます。
これにより、数式が正しくレンダリングされ、基本的なスタイルが適用されたHTMLドキュメントが生成されます。
デフォルトのCSSはダークモードに対応しており、`--css-file`オプションで独自のCSSファイルを指定するか、
`--no-css`オプションでCSSの埋め込みを無効にすることができます。
Requirements:
pip install markdown pymdown-extensions
Usage:
python md2html.py input.md -o output.html
# Options:
python md2html.py input.md -o output.html \
--title "My Notes" \
--lang ja \
--mathjax-url https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js \
--css-file style.css
License: MIT
Author: ChatGPT
:doc:`md2html_usage`
"""
import argparse
import re
from pathlib import Path
from typing import Optional
import markdown
DEFAULT_CSS = r"""
/* Minimal typesetting */
:root{--fg:#111;--bg:#fff;--muted:#666;--code-bg:#f6f8fa;--border:#e5e7eb;--link:#2563eb;}
@media (prefers-color-scheme: dark){
:root{--fg:#e5e7eb;--bg:#0b121b;--muted:#9ca3af;--code-bg:#111827;--border:#1f2937;--link:#60a5fa;}
}
html {font-size: 16px;}
body {margin: 2rem auto; max-width: 880px; padding: 0 1rem; color: var(--fg); background: var(--bg); line-height: 1.75;
font-family: -apple-system, BlinkMacMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans JP', 'Hiragino Sans', 'Yu Gothic',
'Helvetica Neue', Arial, 'Apple Color Emoji','Segoe UI Emoji';}
h1,h2,h3,h4{line-height:1.3}
pre, code {font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', 'Courier New', monospace;}
pre {background: var(--code-bg); padding: 1rem; overflow:auto; border-radius: 8px; border:1px solid var(--border);}
code {background: var(--code-bg); padding: .15rem .35rem; border-radius: 6px;}
a {color: var(--link); text-decoration: none;}
a:hover {text-decoration: underline;}
table {border-collapse: collapse; width: 100%; margin: 1rem 0}
th, td {border: 1px solid var(--border); padding: .5rem .75rem; text-align: left;}
blockquote {border-left: 4px solid var(--border); margin: 1rem 0; padding: .25rem 1rem; color: var(--muted);}
hr {border: 0; border-top:1px solid var(--border); margin: 2rem 0;}
/* Ensure math spans render inline nicely */
.arithmatex { font-size: 1em; }
"""
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="{lang}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content="md2html_mathjax.py" />
<title>{title}</title>
{css_block}
<script>
// MathJax config: process only elements with class 'arithmatex' (emitted by pymdownx.arithmatex)
window.MathJax = {{
tex: {{
inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
processEscapes: true,
tags: 'ams'
}},
options: {{
processHtmlClass: 'arithmatex',
ignoreHtmlClass: '.*'
}}
}};
</script>
<script id="MathJax-script" defer src="{mathjax_url}"></script>
</head>
<body>
{body}
</body>
</html>
"""
[ドキュメント]
def derive_title(md_text: str, fallback: str) -> str:
"""
MarkdownテキストからHTMLのタイトルを生成します。
Markdownテキストの内容に基づいて、以下の優先順位でタイトルを抽出します。
1. MarkdownのYAMLフロントマターにある`title:`フィールドを検索します。
2. 見つからない場合、最初のATXヘディング(`# H1`)を検索します。
3. どちらも見つからない場合、提供されたフォールバック文字列を使用します。
:param md_text: str: 入力Markdownテキスト。
:param fallback: str: タイトルが見つからなかった場合にデフォルトとして使用される文字列。
:returns: str: 抽出または生成されたHTMLタイトル。
"""
# 1) YAML metadata title: `title: ...` (very loose)
m = re.search(r'(?mi)^title:\s*(.+)$', md_text)
if m:
return m.group(1).strip()
# 2) First ATX heading (# H1)
m = re.search(r'(?m)^#\s+(.+?)\s*$', md_text)
if m:
return m.group(1).strip()
return fallback
[ドキュメント]
def build_html(md_text: str, title: str, lang: str, mathjax_url: str, css_text: Optional[str]) -> str:
"""
MarkdownテキストをHTMLコンテンツに変換し、MathJaxスクリプトとCSSを組み込みます。
`python-markdown`ライブラリと`pymdownx.arithmatex`拡張機能を使用してMarkdownをHTMLボディに変換します。
これにより、LaTeX数式が適切に処理されます。
`extra`, `toc`, `sane_lists`, `attr_list`, `pymdownx.arithmatex`, `codehilite`などの
拡張機能が有効になっています。
指定されたタイトル、言語、MathJax URL、およびCSSスタイルを使用して完全なHTMLドキュメントを構築します。
:param md_text: str: HTMLに変換するMarkdownテキスト。
:param title: str: HTMLドキュメントの`<title>`タグに設定するタイトル。
:param lang: str: HTMLドキュメントの`lang`属性に設定する言語コード(例: "ja", "en")。
:param mathjax_url: str: MathJaxのCDNスクリプトのURL。
:param css_text: Optional[str]: HTMLドキュメントに埋め込むCSSスタイルテキスト。`None`の場合、CSSは埋め込まれません。
:returns: str: 生成された完全なHTMLドキュメント文字列。
"""
# Render Markdown with math passthrough using pymdownx.arithmatex
exts = [
"extra", # tables, etc.
"toc", # optional TOC ids
"sane_lists",
"attr_list",
"pymdownx.arithmatex",
"codehilite", # highlight wrapper (no pygments by default)
]
ext_cfg = {
"pymdownx.arithmatex": {"generic": True},
"codehilite": {"guess_lang": False, "use_pygments": False},
"toc": {"permalink": True},
}
body = markdown.markdown(md_text, extensions=exts, extension_configs=ext_cfg, output_format="xhtml")
css_block = f"<style>\n{css_text}\n</style>" if css_text else ""
return HTML_TEMPLATE.format(lang=lang, title=title, css_block=css_block, mathjax_url=mathjax_url, body=body)
[ドキュメント]
def main():
"""
コマンドライン引数を解析し、指定されたMarkdownファイルをHTMLに変換して出力します。
`argparse`を使用して入力Markdownファイル、出力HTMLファイル、タイトル、言語、MathJax URL、
CSSファイルなどのオプションを処理します。
入力ファイルが存在しない場合はエラーで終了します。
タイトルが指定されない場合は、`derive_title`関数を使用してMarkdownの内容からタイトルを自動生成します。
CSSオプションに基づいて、デフォルトのCSSを使用するか、外部CSSファイルを読み込むか、
またはCSSを埋め込まないかを決定します。
`build_html`関数を呼び出してHTMLコンテンツを生成し、指定された出力ファイルに書き込みます。
:returns: None: 成功した場合はメッセージを出力し、エラーの場合は`SystemExit`で終了します。
"""
ap = argparse.ArgumentParser(description="Markdown (with LaTeX) -> HTML with MathJax & inline CSS.")
ap.add_argument("input_md", help="Input Markdown file path")
ap.add_argument("-o", "--output", required=True, help="Output HTML file path")
ap.add_argument("--title", default=None, help="HTML <title>. Default: from Markdown H1 or file name")
ap.add_argument("--lang", default="ja", help="HTML lang attribute (default: ja)")
ap.add_argument("--mathjax-url", default="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js",
help="MathJax v3 bundle URL (default: tex-chtml.js)")
ap.add_argument("--css-file", default=None, help="Use CSS from file instead of the built-in default")
ap.add_argument("--no-css", action="store_true", help="Do not embed any CSS")
args = ap.parse_args()
inp = Path(args.input_md).expanduser().resolve()
out = Path(args.output).expanduser().resolve()
if not inp.exists():
raise SystemExit(f"[ERR] Markdown not found: {inp}")
md_text = inp.read_text(encoding="utf-8")
title = args.title or derive_title(md_text, inp.stem)
css_text = None
if not args.no_css:
if args.css_file:
css_text = Path(args.css_file).read_text(encoding="utf-8")
else:
css_text = DEFAULT_CSS
html = build_html(md_text, title=title, lang=args.lang, mathjax_url=args.mathjax_url, css_text=css_text)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(html, encoding="utf-8")
print(f"[OK] Wrote: {out}")
if __name__ == "__main__":
main()