#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Pandoc専用:MarkdownのWord/PowerPoint/HTML変換とテンプレート生成。
- AI処理は一切ありません(tkai_lib不要)
- 例:
# テンプレート生成
python pandoc_convert.py --make_template docx --template template_textbook.docx
python pandoc_convert.py --make_template pptx --template template_slide.pptx
# 変換
python pandoc_convert.py --convert docx --infile lecture_textbook.md --template template_textbook.docx
python pandoc_convert.py --convert pptx --infile lecture_slide.md --template template_slide.pptx
python pandoc_convert.py --convert html --infile lecture_textbook.md --mathml
python pandoc_convert.py --convert html --infile lecture_textbook.md --css custom_style.css
"""
import os
import sys
import argparse
from pathlib import Path
import re
import yaml
import shutil
import platform
import subprocess
from pathlib import Path
import chardet
pause = 0
[ドキュメント]
def terminate():
if pause:
input("\nPress ENTER to terminate\n")
exit()
[ドキュメント]
def find_pandoc() -> str | None:
"""
pandoc 実行ファイルのパスを探す。
優先順位:
1. 環境変数 pandoc_path
2. 環境変数 tkProg_path に基づく既定配置
- Windows: {tkProg_path}/tkApp_Win/pandoc/pandoc.exe
- Linux: {tkProg_path}/tkApp_Linux/pandoc/pandoc
3. PATH 環境変数から探す
見つからなければ None を返す。
"""
# 1. 環境変数 pandoc_path
env_pandoc = os.environ.get("pandoc_path")
if env_pandoc and os.path.isfile(env_pandoc):
return env_pandoc
# 2. tkProg_path 内
tkprog = os.environ.get("tkProg_path")
if tkprog is None:
script_dir = Path(sys.argv[0]).resolve().parent
tkprog = (script_dir / "../../").resolve()
if tkprog:
if platform.system() == "Windows":
candidate = os.path.join(tkprog, "tkApp_Win", "pandoc", "pandoc.exe")
else:
candidate = os.path.join(tkprog, "tkApp_Linux", "pandoc", "pandoc")
if os.path.isfile(candidate):
return candidate
# 3. PATH から探す
exe_name = "pandoc.exe" if platform.system() == "Windows" else "pandoc"
path = shutil.which(exe_name)
if path:
return path
return "pandoc"
[ドキュメント]
def find_template(template_path: str) -> str | None:
"""
template_path で与えられたファイル名を以下の順に探す:
1. 実行ディレクトリ (カレントディレクトリ)
2. スクリプトのあるディレクトリ (sys.argv[0] 基準)
見つかればフルパスを返し、見つからなければ None を返す。
"""
if not template_path:
return None
# 1. 実行ディレクトリ(カレント)
candidate1 = Path.cwd() / template_path
if candidate1.is_file():
return str(candidate1.resolve())
# 2. スクリプトのあるディレクトリ
script_dir = Path(sys.argv[0]).resolve().parent
candidate2 = script_dir / template_path
if candidate2.is_file():
return str(candidate2.resolve())
# 見つからなければ None
return template_path
[ドキュメント]
def parse_args():
pandoc_path = find_pandoc()
print(f"✅ pandoc found: {pandoc_path}")
p = argparse.ArgumentParser(
description="Pandocユーティリティ(テンプレート生成 & 変換専用)",
formatter_class=argparse.RawTextHelpFormatter
)
p.add_argument('--pandoc_path', "-p", default=pandoc_path, help=f'pandoc 実行ファイルのパス (デフォルト {pandoc_path})')
p.add_argument('--infile', "-i", help='変換元Markdown(--convert使用時に指定)')
p.add_argument('--outfile', "-o", type=str, default = None, help='出力ファイル名n(デフォルト --convertから拡張子を設定)')
p.add_argument('--template', "-t", default = None, help='変換に使用、または作成するテンプレートファイルのパス')
p.add_argument('--convert', "-c", choices=['docx', 'pptx', 'html', ''], help='Pandoc変換を実行')
p.add_argument('--toc', type=int, default=0, help='入力を markdown-yaml_metadata_block として解釈する')
p.add_argument('--mathml', action='store_true', help='HTML変換時にMathMLを使って数式を表示')
p.add_argument('--css', help='HTML変換時のカスタムCSS')
p.add_argument('--make_template', choices=['docx', 'pptx'], help='デフォルトテンプレートを出力して終了')
p.add_argument('--smart_conversion', type=int, default=0, help='自動変換 (--を-に変換するなど) する')
p.add_argument('--no-yaml', action='store_true', help='入力を markdown-yaml_metadata_block として解釈する')
p.add_argument('--verbose', action='store_true', help='詳細出力')
p.add_argument("--pause", type=int, default=0, help="終了時にENTERキー入力を要求するか (デフォルト: 0)")
"""
p.add_argument(
'--pandoc_path',
default=None,
help='pandoc 実行ファイルのパス'
)
p.add_argument(
'--template',
default=None,
help='pandoc 用テンプレート (docx/pptx)'
)
p.add_argument(
'--toc',
action='store_true',
help='pandoc: --toc'
)
p.add_argument(
'--css',
default=None,
help='pandoc: HTML 用 CSS'
)
"""
args = p.parse_args()
if args.template:
args.template = find_template(args.template)
print(f"✅ template found: {args.template}")
return p, args
[ドキュメント]
def run_command(cmd: list) -> bool:
print(f"⚡️ 実行中: {' '.join(cmd)}")
try:
_ = subprocess.run(cmd, check=True, capture_output=True, text=True, encoding='utf-8')
print("✅ コマンドは正常に完了しました。")
return True
except FileNotFoundError:
print(f"❌ '{cmd[0]}' が見つかりません。Pandocはインストール済みでPATHが通っていますか?", file=sys.stderr)
except subprocess.CalledProcessError as e:
print(f"❌ Pandoc実行エラー (終了コード: {e.returncode})", file=sys.stderr)
print(f"--- stderr ---\n{e.stderr}\n--------------", file=sys.stderr)
return False
[ドキュメント]
def is_valid_yaml(yaml_text: str) -> bool:
"""
YAML として正しいかどうかを判定する。
"""
try:
yaml.safe_load(yaml_text)
return True
except Exception:
return False
[ドキュメント]
def read_text_with_chardet(path):
# まずバイナリで読み込んで文字コードを推定
if not os.path.isfile(path): return None
with open(path, "rb") as fb:
raw = fb.read()
result = chardet.detect(raw)
encoding = result["encoding"]
confidence = result["confidence"]
if encoding is None:
print(f"Failed to detect encoding: {encoding}")
print(f" Try utf-8")
encoding = 'utf-8'
else:
print(f"Detected encoding: {encoding} (confidence={confidence}) for {path}")
# 推定した文字コードでデコード(失敗しにくいように errors='replace' も可)
try:
text = raw.decode(encoding, errors="replace")
except:
print(f"Error in read_text_with_chardet(): Failed to encode with {encoding}")
return None
return text
[ドキュメント]
def correct_markdown_pandoc_error(md_text: str, base_dir: Path = None) -> str:
"""
pandoc がエラーを出す典型的な Markdown の問題を自動修正する。
- YAML と誤判定される `---` の前後に空行を追加(ただし本物の YAML は保持)
- 存在しない画像・リンクを無害化(コード化)
- $$ inline $$ のような1行ブロック数式を、pandoc が確実に読めるブロック形式に正規化
- 生成AIがよく出す $[ ... ]$ を $$ ... $$ に変換
"""
# ============================================================
# 1. YAML フロントマターの判定
# ============================================================
yaml_block = extract_yaml_front_matter(md_text)
if yaml_block is not None:
if is_valid_yaml(yaml_block):
# 本物の YAML → 何もしない
pass
else:
# YAML として不正 → pandoc が誤判定するので空行を追加して無効化
md_text = md_text.replace("---", "\n---\n", 1)
# ============================================================
# 2. 単独の --- の前後に空行を追加(YAML 以外)
# ============================================================
lines = md_text.splitlines()
fixed_lines = []
for i, line in enumerate(lines):
if line.strip() == "---":
# YAML フロントマター以外の --- を安全化
if i > 0 and lines[i-1].strip() != "":
fixed_lines.append("")
fixed_lines.append(line)
if i < len(lines)-1 and lines[i+1].strip() != "":
fixed_lines.append("")
else:
fixed_lines.append(line)
md_text = "\n".join(fixed_lines)
# ============================================================
# 3. 存在しない画像・リンクを無害化
# ============================================================
def replace_if_missing(match):
full = match.group(1)
path = match.group(2)
if base_dir is None:
return full
file_path = (base_dir / path).resolve()
if not file_path.exists():
# pandoc が落ちるのでコード化
return f"`{full}`"
return full
pattern = r'(!?\[.*?\]\((.*?)\))'
md_text = re.sub(pattern, replace_if_missing, md_text)
# ============================================================
# 4. $$ inline $$ → ブロック数式に正規化
# ============================================================
def normalize_inline_math(match):
content = match.group(1).strip()
# すでに複数行ならそのままブロック化
return f"$$\n{content}\n$$"
math_pattern = r'\$\$(.+?)\$\$'
md_text = re.sub(math_pattern, normalize_inline_math, md_text, flags=re.DOTALL)
# ============================================================
# 5. $[ ... ]$ → $$ ... $$ に変換(生成AI対策)
# ============================================================
def convert_ai_math(match):
content = match.group(1).strip()
return f"$$\n{content}\n$$"
# ai_math_pattern = r'\$\[\s*(.+?)\s*\]\$' # $[ ... ]$
# md_text = re.sub(ai_math_pattern, convert_ai_math, md_text, flags=re.DOTALL)
# ai_math_pattern = r'\$\(\s*(.+?)\s*\)\$' # $( ... )$
# md_text = re.sub(ai_math_pattern, convert_ai_math, md_text, flags=re.DOTALL)
ai_math_pattern = r'\\\[\s*(.+?)\s*\\\]' # \[ ... \]
md_text = re.sub(ai_math_pattern, convert_ai_math, md_text, flags=re.DOTALL)
ai_math_pattern = r'\\\(\s*(.+?)\s*\\\)' # \( ... \)
md_text = re.sub(ai_math_pattern, convert_ai_math, md_text, flags=re.DOTALL)
return md_text
[ドキュメント]
def make_pandoc_template(pandoc_path: str, fmt: str, template_file: str):
print(f"🎨 {fmt.upper()} 用のPandocテンプレートを作成します...")
data_file = 'reference.docx' if fmt == 'docx' else 'reference.pptx'
cmd = [pandoc_path, '-o', template_file, f'--print-default-data-file={data_file}']
if run_command(cmd):
print(f"📄 テンプレート '{template_file}' を作成しました。編集してスタイルを調整してください。")
[ドキュメント]
def correct_html(outfile):
with open(outfile, 'r', encoding='utf-8') as f:
body_content = f.read()
# MathJax対応のHTMLとしてラップ
html_template = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/mml-chtml.js"></script>
</head>
<body>
{body_content}
</body>
</html>
"""
with open(outfile, 'w', encoding='utf-8') as f:
f.write(html_template)
[ドキュメント]
def convert_with_pandoc(pandoc_path: str, target: str, infile_md: str, outfile: str,
template: str = None, css: str = None, no_yaml: bool = False, args = None):
print()
print(f"🔄 '{infile_md}' → {target.upper()} 変換を開始します...")
if not Path(infile_md).exists():
print(f"❌ 入力Markdown '{infile_md}' が存在しません。", file=sys.stderr)
terminate()
print(f"target : {target}")
print(f"template: {template}")
if outfile is None or outfile == '':
outfile = str(Path(infile_md).with_suffix(f'.{target}'))
# ベース引数
if no_yaml:
cmd = [pandoc_path, infile_md, '-o', outfile, '-f', 'markdown-yaml_metadata_block']
else:
cmd = [pandoc_path, infile_md, '-o', outfile]
if args.toc and outfile.endswith(".docx"):
cmd.append('--toc')
if args.mathml:
cmd.append('--mathml')
if args.verbose:
cmd.append('--verbose')
if not args.smart_conversion:
cmd.extend(['-f', 'markdown-smart'])
# 出力形式ごとの追加
if target == 'html':
if css:
cmd.extend(['--css', css])
elif target in ('docx', 'pptx'):
if not template:
pass
# print("❌ --convert docx/pptx では --template が必須です。", file=sys.stderr)
# sys.exit(1)
elif f".{target}" in template:
cmd.extend(['--reference-doc', template])
# if target == 'pptx':
# cmd.extend(['-t', 'pptx'])
cmd.extend(['-t', target])
print("cmd:", cmd)
ret = run_command(cmd)
if ret:
if args.convert == 'html':
correct_html(outfile)
print(f"📑 変換後のファイル '{outfile}' を作成しました。")
[ドキュメント]
def convert_md(
infile_md,
target, # 'docx' | 'pptx' | 'html'
pandoc_path, # ← 明示的に渡す
outfile=None,
template=None,
toc=False,
css=None,
mathml=False,
no_yaml=False,
verbose=False,
smart_conversion=False,
):
"""
Library API for md -> docx/pptx/html
"""
class DummyArgs:
pass
args = DummyArgs()
args.toc = toc
args.mathml = mathml
args.verbose = verbose
args.convert = target
args.smart_conversion = smart_conversion
# read & normalize markdown
md_text = read_text_with_chardet(infile_md)
if md_text is None:
raise RuntimeError(f"Failed to read {infile_md}")
corrected = correct_markdown_pandoc_error(
md_text,
base_dir=Path(infile_md).parent
)
corrected_path = Path(infile_md).with_suffix(".corrected.md")
corrected_path.write_text(corrected, encoding="utf-8")
convert_with_pandoc(
pandoc_path=pandoc_path,
target=target,
infile_md=str(corrected_path),
outfile=outfile,
template=template,
css=css,
no_yaml=no_yaml,
args=args,
)
return True
[ドキュメント]
def main():
global pause
parser, args = parse_args()
pause = args.pause
print()
print(f"{args.pandoc_path=}")
if args.pandoc_path == "":
print(f"Error: pandoc実行ファイルを引数 pandoc_path で設定してください")
terminate()
if not os.path.exists(args.pandoc_path):
print(f"Error: pandoc実行ファイル [{args.pandoc_path}] が見つかりません")
terminate()
if args.convert == '':
target = os.path.splitext(args.outfile)[1].lstrip('.')
else:
target = args.convert
print(f"{args.infile=}")
print(f"{args.outfile=}")
print(f"{args.template=}")
print(f"{target=}")
print(f"{args.toc=}")
print(f"{args.pause=}")
# 1) テンプレート生成(最優先)
if args.make_template:
if not args.template:
parser.error("--make_template には --template で出力先ファイル名が必要です。")
make_pandoc_template(args.pandoc_path, args.make_template, args.template)
terminate()
# 2) 変換
if args.convert or args.outfile:
if not args.infile:
parser.error("--convert を使うときは --infile(Markdown)を指定してください。")
md_text = read_text_with_chardet(args.infile)
if md_text:
text_corrected = correct_markdown_pandoc_error(md_text, base_dir = None)
corrected_path = Path(args.infile).with_suffix(".corrected.md")
with open(corrected_path, "w", encoding="utf-8") as fp:
fp.write(text_corrected)
else:
corrected_path = args.infile
convert_with_pandoc(
args.pandoc_path,
target,
str(corrected_path),
args.outfile,
template=args.template,
css=args.css,
no_yaml=args.no_yaml,
args = args,
)
terminate()
# 3) どちらも指定がない場合
print("\nError: --convertか--outfileを指定してください")
parser.print_help()
if __name__ == "__main__":
main()
terminate()