返回 ppt-master
_dispatcher.py
根目录 / skills / ppt-master / scripts / source_to_md / _dispatcher.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Source to Markdown Dispatcher
4
5 Shared routing and backend command construction for source-to-Markdown tools.
6
7 Usage:
8 Imported by scripts/source_to_md.py and scripts/project_manager.py
9
10 Examples:
11 build_conversion_command("report.pdf", "report.md")
12
13 Dependencies:
14 None
15 """
16
17 from __future__ import annotations
18
19 import shutil
20 import sys
21 from dataclasses import dataclass
22 from pathlib import Path
23 from urllib.parse import urlparse
24
25
26 SOURCE_TO_MD_DIR = Path(__file__).resolve().parent
27 WECHAT_HOST_KEYWORDS = ("mp.weixin.qq.com", "weixin.qq.com")
28
29 DOC_SUFFIXES = {
30 ".docx", ".doc", ".odt", ".rtf", # Office documents
31 ".epub", # eBooks
32 ".html", ".htm", # Web pages
33 ".tex", ".latex", ".rst", ".org", # Academic / technical
34 ".ipynb", ".typ", # Notebooks / Typst
35 }
36 EXCEL_SUFFIXES = {".xlsx", ".xlsm"}
37 LEGACY_EXCEL_SUFFIXES = {".xls"}
38 MARKDOWN_SUFFIXES = {".md", ".markdown"}
39 PDF_SUFFIXES = {".pdf"}
40 PRESENTATION_SUFFIXES = {".pptx", ".pptm", ".ppsx", ".ppsm", ".potx", ".potm"}
41 TEXT_SUFFIXES = {".txt", ".text"}
42
43
44 @dataclass
45 class ConversionCommand:
46 """Backend command plus metadata for one conversion route."""
47
48 command: list[str]
49 script_name: str
50 conversion_type: str
51 output_path: Path | None
52
53
54 def is_url(value: str) -> bool:
55 """Return whether a string looks like an HTTP(S) URL."""
56 parsed = urlparse(value)
57 return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
58
59
60 def detect_source_type(input_arg: str) -> str:
61 """Detect a conversion type from a URL, file, or directory."""
62 if is_url(input_arg):
63 return "web"
64
65 path = Path(input_arg)
66 if not path.exists():
67 return "unknown"
68 if path.is_dir():
69 return "directory"
70
71 suffix = path.suffix.lower()
72 if suffix in PDF_SUFFIXES:
73 return "pdf"
74 if suffix in DOC_SUFFIXES:
75 return "doc"
76 if suffix in EXCEL_SUFFIXES or suffix in LEGACY_EXCEL_SUFFIXES:
77 return "excel"
78 if suffix in PRESENTATION_SUFFIXES:
79 return "pptx"
80 if suffix in MARKDOWN_SUFFIXES:
81 return "markdown"
82 if suffix in TEXT_SUFFIXES:
83 return "text"
84 return "unknown"
85
86
87 def default_markdown_path(input_arg: str) -> Path:
88 """Return the conventional Markdown output path for a local input."""
89 path = Path(input_arg)
90 return path.parent / f"{path.stem}.md"
91
92
93 def _curl_cffi_available() -> bool:
94 """Return whether curl_cffi is importable."""
95 try:
96 import curl_cffi # noqa: F401
97 return True
98 except ImportError:
99 return False
100
101
102 def _web_script_command(
103 url: str,
104 output_path: Path | None,
105 python_executable: str,
106 allow_node_fallback: bool,
107 ) -> tuple[str, list[str]]:
108 """Return the web backend script name and base command."""
109 host = urlparse(url).netloc.lower()
110 is_tls_sensitive = any(keyword in host for keyword in WECHAT_HOST_KEYWORDS)
111 node_backend = SOURCE_TO_MD_DIR / "web_to_md.cjs"
112
113 if (
114 allow_node_fallback
115 and is_tls_sensitive
116 and not _curl_cffi_available()
117 and node_backend.is_file()
118 and shutil.which("node")
119 ):
120 command = ["node", str(node_backend), url]
121 script_name = "web_to_md.cjs"
122 else:
123 command = [python_executable, str(SOURCE_TO_MD_DIR / "web_to_md.py"), url]
124 script_name = "web_to_md.py"
125
126 if output_path is not None:
127 command.extend(["-o", str(output_path)])
128 return script_name, command
129
130
131 def build_conversion_command(
132 input_arg: str,
133 output_path: str | Path | None,
134 *,
135 forced_type: str | None = None,
136 extra_args: list[str] | None = None,
137 pdf_image_mode: str | None = None,
138 render_vector_figures: bool = False,
139 python_executable: str | None = None,
140 allow_node_web_fallback: bool = True,
141 ) -> ConversionCommand:
142 """Build the backend CLI command for one source-to-Markdown conversion."""
143 conversion_type = forced_type or detect_source_type(input_arg)
144 output = Path(output_path) if output_path is not None else None
145 extra = extra_args or []
146 python = python_executable or sys.executable
147
148 if conversion_type == "web":
149 if not is_url(input_arg):
150 raise ValueError("web conversion requires an http:// or https:// URL")
151 script_name, command = _web_script_command(
152 input_arg,
153 output,
154 python,
155 allow_node_web_fallback,
156 )
157 command.extend(extra)
158 return ConversionCommand(command, script_name, conversion_type, output)
159
160 if conversion_type in {"markdown", "text", "directory", "unknown"}:
161 raise ValueError(f"conversion type {conversion_type!r} has no backend command")
162
163 script_by_type = {
164 "pdf": "pdf_to_md.py",
165 "doc": "doc_to_md.py",
166 "excel": "excel_to_md.py",
167 "pptx": "ppt_to_md.py",
168 }
169 script_name = script_by_type.get(conversion_type)
170 if script_name is None:
171 raise ValueError(f"unsupported conversion type: {conversion_type}")
172 if output is None:
173 output = default_markdown_path(input_arg)
174
175 command = [
176 python,
177 str(SOURCE_TO_MD_DIR / script_name),
178 input_arg,
179 "-o",
180 str(output),
181 ]
182 if conversion_type == "pdf" and pdf_image_mode:
183 command.extend(["--images", pdf_image_mode])
184 if conversion_type == "pdf" and render_vector_figures:
185 command.append("--render-vector-figures")
186 command.extend(extra)
187 return ConversionCommand(command, script_name, conversion_type, output)
188
188 lines PYTHON