mdv-live 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdv-live
3
+ Version: 0.1.0
4
+ Summary: Markdown Viewer - File tree + Live preview + Hot reload
5
+ Author-email: PanHouse <hirono.okamoto@panhouse.jp>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/panhouse/mdv
8
+ Project-URL: Repository, https://github.com/panhouse/mdv
9
+ Keywords: markdown,viewer,live-reload,preview,hot-reload
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: fastapi>=0.104.0
24
+ Requires-Dist: uvicorn>=0.24.0
25
+ Requires-Dist: websockets>=12.0
26
+ Requires-Dist: watchdog>=3.0.0
27
+ Requires-Dist: markdown>=3.5.0
28
+
29
+ # MDV - Markdown Viewer
30
+
31
+ ファイルツリー + ライブプレビュー付きマークダウンビューア
32
+
33
+ ## Features
34
+
35
+ - 📁 左側にフォルダツリー表示
36
+ - 📄 マークダウンをHTMLでレンダリング
37
+ - 🔄 ファイル更新時に自動リロード(WebSocket)
38
+ - 🎨 シンタックスハイライト(highlight.js)
39
+ - 📊 Mermaid図のレンダリング対応
40
+ - 🌙 ダークテーマ
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ # リポジトリをクローン
46
+ git clone https://github.com/panhouse/mdv.git
47
+ cd mdv
48
+
49
+ # グローバルインストール
50
+ pip install -e .
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ```bash
56
+ # カレントディレクトリを表示
57
+ mdv
58
+
59
+ # 特定のディレクトリを表示
60
+ mdv ./project/
61
+
62
+ # ポート指定
63
+ mdv -p 9000
64
+
65
+ # ブラウザを自動で開かない
66
+ mdv --no-browser
67
+ ```
68
+
69
+ ## Requirements
70
+
71
+ - Python 3.9+
72
+ - FastAPI
73
+ - uvicorn
74
+ - watchdog
75
+ - markdown
@@ -0,0 +1,47 @@
1
+ # MDV - Markdown Viewer
2
+
3
+ ファイルツリー + ライブプレビュー付きマークダウンビューア
4
+
5
+ ## Features
6
+
7
+ - 📁 左側にフォルダツリー表示
8
+ - 📄 マークダウンをHTMLでレンダリング
9
+ - 🔄 ファイル更新時に自動リロード(WebSocket)
10
+ - 🎨 シンタックスハイライト(highlight.js)
11
+ - 📊 Mermaid図のレンダリング対応
12
+ - 🌙 ダークテーマ
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ # リポジトリをクローン
18
+ git clone https://github.com/panhouse/mdv.git
19
+ cd mdv
20
+
21
+ # グローバルインストール
22
+ pip install -e .
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ # カレントディレクトリを表示
29
+ mdv
30
+
31
+ # 特定のディレクトリを表示
32
+ mdv ./project/
33
+
34
+ # ポート指定
35
+ mdv -p 9000
36
+
37
+ # ブラウザを自動で開かない
38
+ mdv --no-browser
39
+ ```
40
+
41
+ ## Requirements
42
+
43
+ - Python 3.9+
44
+ - FastAPI
45
+ - uvicorn
46
+ - watchdog
47
+ - markdown
@@ -0,0 +1,6 @@
1
+ """
2
+ MDV - Markdown Viewer
3
+ ファイルツリー + ライブプレビュー付きマークダウンビューア
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,9 @@
1
+ """
2
+ MDV - Markdown Viewer
3
+ python -m mdv で実行可能にする
4
+ """
5
+
6
+ from .cli import main
7
+
8
+ if __name__ == '__main__':
9
+ main()
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MDV - Markdown Viewer CLI
4
+ どこからでも呼び出せるマークダウンビューア
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import subprocess
11
+ import sys
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import List, Optional
15
+
16
+
17
+
18
+ @dataclass
19
+ class ProcessInfo:
20
+ """プロセス情報"""
21
+ pid: str
22
+ port: str
23
+ command: str
24
+
25
+
26
+ def get_mdv_processes() -> List[ProcessInfo]:
27
+ """稼働中のMDVサーバープロセスを取得"""
28
+ try:
29
+ result = subprocess.run(
30
+ ["lsof", "-i", "-P", "-n"],
31
+ capture_output=True,
32
+ text=True,
33
+ )
34
+
35
+ processes = []
36
+ for line in result.stdout.strip().split("\n"):
37
+ if "python" not in line.lower() or "LISTEN" not in line:
38
+ continue
39
+
40
+ parts = line.split()
41
+ if len(parts) < 9:
42
+ continue
43
+
44
+ pid = parts[1]
45
+
46
+ # プロセスのコマンドラインを確認
47
+ try:
48
+ cmd_result = subprocess.run(
49
+ ["ps", "-p", pid, "-o", "command="],
50
+ capture_output=True,
51
+ text=True,
52
+ )
53
+ cmd = cmd_result.stdout.strip()
54
+
55
+ if "mdv" not in cmd.lower():
56
+ continue
57
+
58
+ # ポート番号を抽出
59
+ port_info = parts[8] if len(parts) > 8 else ""
60
+ port = ""
61
+ if ":" in port_info:
62
+ port = port_info.split(":")[-1].split("->")[0]
63
+
64
+ # コマンドを短縮
65
+ display_cmd = cmd[:80] + "..." if len(cmd) > 80 else cmd
66
+
67
+ processes.append(ProcessInfo(
68
+ pid=pid,
69
+ port=port,
70
+ command=display_cmd,
71
+ ))
72
+ except subprocess.SubprocessError:
73
+ pass
74
+
75
+ return processes
76
+
77
+ except Exception as e:
78
+ print(f"Error getting processes: {e}")
79
+ return []
80
+
81
+
82
+ def list_servers() -> int:
83
+ """稼働中のMDVサーバーを一覧表示"""
84
+ processes = get_mdv_processes()
85
+
86
+ if not processes:
87
+ print("稼働中のMDVサーバーはありません")
88
+ return 0
89
+
90
+ print(f"稼働中のMDVサーバー: {len(processes)}件")
91
+ print("-" * 60)
92
+ print(f"{'PID':<8} {'Port':<8} {'Command'}")
93
+ print("-" * 60)
94
+
95
+ for proc in processes:
96
+ print(f"{proc.pid:<8} {proc.port:<8} {proc.command}")
97
+
98
+ print("-" * 60)
99
+ print("\n停止: mdv -k -a (全停止) / mdv -k <PID> (個別停止)")
100
+ return 0
101
+
102
+
103
+ def kill_server_by_pid(pid: str) -> int:
104
+ """特定のPIDのサーバーを停止"""
105
+ try:
106
+ subprocess.run(["kill", pid], check=True)
107
+ print(f"PID {pid} を停止しました")
108
+ return 0
109
+ except ValueError:
110
+ print(f"無効なPID: {pid}")
111
+ return 1
112
+ except subprocess.CalledProcessError:
113
+ print(f"PID {pid} の停止に失敗しました")
114
+ return 1
115
+
116
+
117
+ def kill_all_servers() -> int:
118
+ """全サーバーを停止"""
119
+ processes = get_mdv_processes()
120
+
121
+ if not processes:
122
+ print("稼働中のMDVサーバーはありません")
123
+ return 0
124
+
125
+ print(f"{len(processes)}件のMDVサーバーを停止します...")
126
+
127
+ killed = 0
128
+ for proc in processes:
129
+ try:
130
+ subprocess.run(["kill", proc.pid], check=True)
131
+ print(f" PID {proc.pid} (port {proc.port}) を停止")
132
+ killed += 1
133
+ except subprocess.CalledProcessError:
134
+ print(f" PID {proc.pid} の停止に失敗")
135
+
136
+ print(f"\n完了: {killed}/{len(processes)} 件を停止しました")
137
+ return 0 if killed == len(processes) else 1
138
+
139
+
140
+ def kill_servers(target: Optional[str] = None, kill_all: bool = False) -> int:
141
+ """MDVサーバーを停止"""
142
+ if target:
143
+ return kill_server_by_pid(target)
144
+
145
+ if not kill_all:
146
+ print("全サーバーを停止するには -a オプションが必要です")
147
+ print(" mdv -k -a 全サーバーを停止")
148
+ print(" mdv -k <PID> 特定のサーバーを停止")
149
+ return 1
150
+
151
+ return kill_all_servers()
152
+
153
+
154
+ def convert_to_pdf(input_path: Path, output_path: Optional[Path] = None) -> int:
155
+ """MarkdownファイルをPDFに変換(md-to-pdfを使用)"""
156
+ if not input_path.exists():
157
+ print(f"Error: File not found: {input_path}")
158
+ return 1
159
+
160
+ if not input_path.is_file():
161
+ print(f"Error: Not a file: {input_path}")
162
+ return 1
163
+
164
+ if input_path.suffix.lower() not in [".md", ".markdown"]:
165
+ print(f"Error: Not a markdown file: {input_path}")
166
+ return 1
167
+
168
+ # md-to-pdfコマンドを構築
169
+ cmd = ["npx", "md-to-pdf", str(input_path)]
170
+ if output_path:
171
+ # md-to-pdfは--out-dirオプションを使用
172
+ cmd.extend(["--out-dir", str(output_path.parent)])
173
+
174
+ try:
175
+ result = subprocess.run(
176
+ cmd,
177
+ capture_output=True,
178
+ text=True,
179
+ cwd=str(input_path.parent),
180
+ )
181
+
182
+ if result.returncode != 0:
183
+ if "npx: command not found" in result.stderr or "not found" in result.stderr:
184
+ print("Error: npx (Node.js) is required for PDF conversion")
185
+ print("Install Node.js: https://nodejs.org/")
186
+ return 1
187
+ print(f"Error: {result.stderr}")
188
+ return 1
189
+
190
+ # 出力ファイルパスを特定
191
+ default_output = input_path.with_suffix(".pdf")
192
+ if output_path and output_path != default_output:
193
+ # リネームが必要な場合
194
+ if default_output.exists():
195
+ default_output.rename(output_path)
196
+ print(f"✅ PDF saved: {output_path}")
197
+ else:
198
+ print(f"✅ PDF saved: {output_path}")
199
+ else:
200
+ print(f"✅ PDF saved: {default_output}")
201
+
202
+ return 0
203
+
204
+ except FileNotFoundError:
205
+ print("Error: npx (Node.js) is required for PDF conversion")
206
+ print("Install Node.js: https://nodejs.org/")
207
+ return 1
208
+ except Exception as e:
209
+ print(f"Error creating PDF: {e}")
210
+ return 1
211
+
212
+
213
+ def start_viewer(
214
+ path: str = ".",
215
+ port: int = 8642,
216
+ open_browser: bool = True
217
+ ) -> None:
218
+ """MDVサーバーを起動"""
219
+ target_path = Path(path).resolve()
220
+
221
+ if not target_path.exists():
222
+ print(f"Error: Path does not exist: {target_path}")
223
+ sys.exit(1)
224
+
225
+ # ファイルが指定された場合、親ディレクトリをルートにして、そのファイルを開く
226
+ initial_file: Optional[str] = None
227
+ if target_path.is_file():
228
+ initial_file = target_path.name
229
+ target_path = target_path.parent
230
+
231
+ from .server import start_server
232
+ start_server(
233
+ root_path=str(target_path),
234
+ port=port,
235
+ open_browser=open_browser,
236
+ initial_file=initial_file,
237
+ )
238
+
239
+
240
+ def create_parser() -> argparse.ArgumentParser:
241
+ """引数パーサーを作成"""
242
+ parser = argparse.ArgumentParser(
243
+ prog="mdv",
244
+ description="MDV - Markdown Viewer with file tree + live preview",
245
+ formatter_class=argparse.RawDescriptionHelpFormatter,
246
+ epilog="""
247
+ Examples:
248
+ mdv Start viewer in current directory
249
+ mdv /path/to/dir Start viewer in specified directory
250
+ mdv README.md Open specific file
251
+ mdv --pdf README.md Convert markdown to PDF
252
+ mdv -p 3000 Start on port 3000
253
+ mdv -l List running servers
254
+ mdv -k -a Stop all servers
255
+ """,
256
+ )
257
+
258
+ # サーバー管理オプション
259
+ parser.add_argument(
260
+ "-l", "--list",
261
+ action="store_true",
262
+ help="List running MDV servers",
263
+ )
264
+ parser.add_argument(
265
+ "-k", "--kill",
266
+ nargs="?",
267
+ const="__no_pid__",
268
+ metavar="PID",
269
+ help="Stop server (-k -a for all, -k <PID> for specific)",
270
+ )
271
+ parser.add_argument(
272
+ "-a", "--all",
273
+ action="store_true",
274
+ help="Use with -k to stop all servers",
275
+ )
276
+
277
+ # ビューア起動オプション
278
+ parser.add_argument(
279
+ "path",
280
+ nargs="?",
281
+ default=".",
282
+ help="Directory or file path to view (default: current directory)",
283
+ )
284
+ parser.add_argument(
285
+ "-p", "--port",
286
+ type=int,
287
+ default=8642,
288
+ help="Server port (default: 8642)",
289
+ )
290
+ parser.add_argument(
291
+ "--no-browser",
292
+ action="store_true",
293
+ help="Don't open browser automatically",
294
+ )
295
+
296
+ # PDF変換オプション
297
+ parser.add_argument(
298
+ "--pdf",
299
+ action="store_true",
300
+ help="Convert markdown file to PDF",
301
+ )
302
+ parser.add_argument(
303
+ "-o", "--output",
304
+ type=str,
305
+ metavar="FILE",
306
+ help="Output PDF file path (default: same name as input with .pdf extension)",
307
+ )
308
+
309
+ return parser
310
+
311
+
312
+ def main() -> None:
313
+ """メインエントリーポイント"""
314
+ parser = create_parser()
315
+ args = parser.parse_args()
316
+
317
+ # -l: サーバー一覧
318
+ if args.list:
319
+ sys.exit(list_servers())
320
+
321
+ # -k: サーバー停止
322
+ if args.kill is not None:
323
+ if args.kill != "__no_pid__":
324
+ sys.exit(kill_servers(target=args.kill))
325
+ else:
326
+ sys.exit(kill_servers(kill_all=args.all))
327
+
328
+ # --pdf: PDF変換
329
+ if args.pdf:
330
+ input_path = Path(args.path).resolve()
331
+ output_path = Path(args.output).resolve() if args.output else None
332
+ sys.exit(convert_to_pdf(input_path, output_path))
333
+
334
+ # デフォルト: ビューア起動
335
+ start_viewer(args.path, args.port, not args.no_browser)
336
+
337
+
338
+ if __name__ == "__main__":
339
+ main()
@@ -0,0 +1,103 @@
1
+ """
2
+ ファイルタイプの定義
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Dict, Optional
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class FileTypeInfo:
11
+ """ファイルタイプ情報"""
12
+ type: str # 'markdown', 'text', 'code', 'image'
13
+ icon: str
14
+ lang: Optional[str] = None
15
+
16
+
17
+ # ファイル拡張子とタイプのマッピング
18
+ FILE_TYPES: Dict[str, FileTypeInfo] = {
19
+ # Markdown
20
+ ".md": FileTypeInfo("markdown", "markdown"),
21
+ ".markdown": FileTypeInfo("markdown", "markdown"),
22
+
23
+ # Text
24
+ ".txt": FileTypeInfo("text", "text"),
25
+ ".text": FileTypeInfo("text", "text"),
26
+ ".rst": FileTypeInfo("text", "text"),
27
+ ".log": FileTypeInfo("text", "text"),
28
+
29
+ # Config files
30
+ ".json": FileTypeInfo("code", "json", "json"),
31
+ ".yaml": FileTypeInfo("code", "yaml", "yaml"),
32
+ ".yml": FileTypeInfo("code", "yaml", "yaml"),
33
+ ".toml": FileTypeInfo("code", "toml", "toml"),
34
+ ".ini": FileTypeInfo("code", "config", "ini"),
35
+ ".cfg": FileTypeInfo("code", "config", "ini"),
36
+ ".conf": FileTypeInfo("code", "config", "ini"),
37
+ ".env": FileTypeInfo("code", "config", "bash"),
38
+
39
+ # Code files
40
+ ".py": FileTypeInfo("code", "python", "python"),
41
+ ".js": FileTypeInfo("code", "javascript", "javascript"),
42
+ ".ts": FileTypeInfo("code", "typescript", "typescript"),
43
+ ".jsx": FileTypeInfo("code", "react", "javascript"),
44
+ ".tsx": FileTypeInfo("code", "react", "typescript"),
45
+ ".html": FileTypeInfo("code", "html", "html"),
46
+ ".css": FileTypeInfo("code", "css", "css"),
47
+ ".scss": FileTypeInfo("code", "css", "scss"),
48
+ ".less": FileTypeInfo("code", "css", "less"),
49
+ ".java": FileTypeInfo("code", "java", "java"),
50
+ ".c": FileTypeInfo("code", "c", "c"),
51
+ ".cpp": FileTypeInfo("code", "cpp", "cpp"),
52
+ ".h": FileTypeInfo("code", "c", "c"),
53
+ ".hpp": FileTypeInfo("code", "cpp", "cpp"),
54
+ ".go": FileTypeInfo("code", "go", "go"),
55
+ ".rs": FileTypeInfo("code", "rust", "rust"),
56
+ ".rb": FileTypeInfo("code", "ruby", "ruby"),
57
+ ".php": FileTypeInfo("code", "php", "php"),
58
+ ".swift": FileTypeInfo("code", "swift", "swift"),
59
+ ".kt": FileTypeInfo("code", "kotlin", "kotlin"),
60
+ ".sh": FileTypeInfo("code", "shell", "bash"),
61
+ ".bash": FileTypeInfo("code", "shell", "bash"),
62
+ ".zsh": FileTypeInfo("code", "shell", "bash"),
63
+ ".sql": FileTypeInfo("code", "database", "sql"),
64
+ ".xml": FileTypeInfo("code", "xml", "xml"),
65
+ ".graphql": FileTypeInfo("code", "graphql", "graphql"),
66
+ ".vue": FileTypeInfo("code", "vue", "html"),
67
+ ".svelte": FileTypeInfo("code", "svelte", "html"),
68
+
69
+ # Images
70
+ ".png": FileTypeInfo("image", "image"),
71
+ ".jpg": FileTypeInfo("image", "image"),
72
+ ".jpeg": FileTypeInfo("image", "image"),
73
+ ".gif": FileTypeInfo("image", "image"),
74
+ ".svg": FileTypeInfo("image", "image"),
75
+ ".webp": FileTypeInfo("image", "image"),
76
+ ".ico": FileTypeInfo("image", "image"),
77
+ ".bmp": FileTypeInfo("image", "image"),
78
+ }
79
+
80
+ # サポートする拡張子のセット
81
+ SUPPORTED_EXTENSIONS = frozenset(FILE_TYPES.keys())
82
+
83
+ # スキップするディレクトリ
84
+ SKIP_DIRECTORIES = frozenset([
85
+ "node_modules",
86
+ "__pycache__",
87
+ "venv",
88
+ ".venv",
89
+ ".git",
90
+ "dist",
91
+ "build",
92
+ ".next",
93
+ ".nuxt",
94
+ ".cache",
95
+ "coverage",
96
+ ".pytest_cache",
97
+ ".mypy_cache",
98
+ ])
99
+
100
+
101
+ def get_file_type(extension: str) -> Optional[FileTypeInfo]:
102
+ """拡張子からファイルタイプ情報を取得"""
103
+ return FILE_TYPES.get(extension.lower())
@@ -0,0 +1,11 @@
1
+ """
2
+ リクエスト/レスポンスモデル
3
+ """
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class SaveFileRequest(BaseModel):
9
+ """ファイル保存リクエスト"""
10
+ path: str
11
+ content: str