Release standalone PPT template workflow 0.5.0
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user