#!/usr/bin/env python3 """ PPT Master - Source to Markdown Dispatcher Shared routing and backend command construction for source-to-Markdown tools. Usage: Imported by scripts/source_to_md.py and scripts/project_manager.py Examples: build_conversion_command("report.pdf", "report.md") Dependencies: None """ from __future__ import annotations import shutil import sys from dataclasses import dataclass from pathlib import Path from urllib.parse import urlparse SOURCE_TO_MD_DIR = Path(__file__).resolve().parent WECHAT_HOST_KEYWORDS = ("mp.weixin.qq.com", "weixin.qq.com") DOC_SUFFIXES = { ".docx", ".doc", ".odt", ".rtf", # Office documents ".epub", # eBooks ".html", ".htm", # Web pages ".tex", ".latex", ".rst", ".org", # Academic / technical ".ipynb", ".typ", # Notebooks / Typst } EXCEL_SUFFIXES = {".xlsx", ".xlsm"} LEGACY_EXCEL_SUFFIXES = {".xls"} MARKDOWN_SUFFIXES = {".md", ".markdown"} PDF_SUFFIXES = {".pdf"} PRESENTATION_SUFFIXES = {".pptx", ".pptm", ".ppsx", ".ppsm", ".potx", ".potm"} TEXT_SUFFIXES = {".txt", ".text"} @dataclass class ConversionCommand: """Backend command plus metadata for one conversion route.""" command: list[str] script_name: str conversion_type: str output_path: Path | None def is_url(value: str) -> bool: """Return whether a string looks like an HTTP(S) URL.""" parsed = urlparse(value) return parsed.scheme in {"http", "https"} and bool(parsed.netloc) def detect_source_type(input_arg: str) -> str: """Detect a conversion type from a URL, file, or directory.""" if is_url(input_arg): return "web" path = Path(input_arg) if not path.exists(): return "unknown" if path.is_dir(): return "directory" suffix = path.suffix.lower() if suffix in PDF_SUFFIXES: return "pdf" if suffix in DOC_SUFFIXES: return "doc" if suffix in EXCEL_SUFFIXES or suffix in LEGACY_EXCEL_SUFFIXES: return "excel" if suffix in PRESENTATION_SUFFIXES: return "pptx" if suffix in MARKDOWN_SUFFIXES: return "markdown" if suffix in TEXT_SUFFIXES: return "text" return "unknown" def default_markdown_path(input_arg: str) -> Path: """Return the conventional Markdown output path for a local input.""" path = Path(input_arg) return path.parent / f"{path.stem}.md" def _curl_cffi_available() -> bool: """Return whether curl_cffi is importable.""" try: import curl_cffi # noqa: F401 return True except ImportError: return False def _web_script_command( url: str, output_path: Path | None, python_executable: str, allow_node_fallback: bool, ) -> tuple[str, list[str]]: """Return the web backend script name and base command.""" host = urlparse(url).netloc.lower() is_tls_sensitive = any(keyword in host for keyword in WECHAT_HOST_KEYWORDS) node_backend = SOURCE_TO_MD_DIR / "web_to_md.cjs" if ( allow_node_fallback and is_tls_sensitive and not _curl_cffi_available() and node_backend.is_file() and shutil.which("node") ): command = ["node", str(node_backend), url] script_name = "web_to_md.cjs" else: command = [python_executable, str(SOURCE_TO_MD_DIR / "web_to_md.py"), url] script_name = "web_to_md.py" if output_path is not None: command.extend(["-o", str(output_path)]) return script_name, command def build_conversion_command( input_arg: str, output_path: str | Path | None, *, forced_type: str | None = None, extra_args: list[str] | None = None, pdf_image_mode: str | None = None, render_vector_figures: bool = False, python_executable: str | None = None, allow_node_web_fallback: bool = True, ) -> ConversionCommand: """Build the backend CLI command for one source-to-Markdown conversion.""" conversion_type = forced_type or detect_source_type(input_arg) output = Path(output_path) if output_path is not None else None extra = extra_args or [] python = python_executable or sys.executable if conversion_type == "web": if not is_url(input_arg): raise ValueError("web conversion requires an http:// or https:// URL") script_name, command = _web_script_command( input_arg, output, python, allow_node_web_fallback, ) command.extend(extra) return ConversionCommand(command, script_name, conversion_type, output) if conversion_type in {"markdown", "text", "directory", "unknown"}: raise ValueError(f"conversion type {conversion_type!r} has no backend command") script_by_type = { "pdf": "pdf_to_md.py", "doc": "doc_to_md.py", "excel": "excel_to_md.py", "pptx": "ppt_to_md.py", } script_name = script_by_type.get(conversion_type) if script_name is None: raise ValueError(f"unsupported conversion type: {conversion_type}") if output is None: output = default_markdown_path(input_arg) command = [ python, str(SOURCE_TO_MD_DIR / script_name), input_arg, "-o", str(output), ] if conversion_type == "pdf" and pdf_image_mode: command.extend(["--images", pdf_image_mode]) if conversion_type == "pdf" and render_vector_figures: command.append("--render-vector-figures") command.extend(extra) return ConversionCommand(command, script_name, conversion_type, output)