#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
md2html_mathjax.py
------------------
Convert a Markdown file (with LaTeX math) into a standalone HTML file.
- Inserts MathJax (CDN) and CSS into
- Uses Python-Markdown + pymdownx.arithmatex so math survives Markdown rendering
- Default inline CSS (dark-mode aware), overridable with --css-file or --no-css
Requirements:
pip install markdown pymdown-extensions
Usage:
python md2html_mathjax.py input.md -o output.html
# Options:
python md2html_mathjax.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
"""
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, BlinkMacSystemFont, '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 = """
{title}
{css_block}
{body}
"""
def derive_title(md_text: str, fallback: str) -> str:
# 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:
# 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"" if css_text else ""
return HTML_TEMPLATE.format(lang=lang, title=title, css_block=css_block, mathjax_url=mathjax_url, body=body)
def main():
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 . 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()