mdv-live 0.1.0__tar.gz → 0.1.4__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.
- {mdv_live-0.1.0 → mdv_live-0.1.4}/PKG-INFO +17 -4
- {mdv_live-0.1.0 → mdv_live-0.1.4}/README.md +16 -3
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv/cli.py +10 -10
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv/file_types.py +3 -0
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv/server.py +67 -4
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv/static/index.html +185 -2
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv_live.egg-info/PKG-INFO +17 -4
- {mdv_live-0.1.0 → mdv_live-0.1.4}/pyproject.toml +1 -1
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv/__init__.py +0 -0
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv/__main__.py +0 -0
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv/models.py +0 -0
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv_live.egg-info/SOURCES.txt +0 -0
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv_live.egg-info/dependency_links.txt +0 -0
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv_live.egg-info/entry_points.txt +0 -0
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv_live.egg-info/requires.txt +0 -0
- {mdv_live-0.1.0 → mdv_live-0.1.4}/mdv_live.egg-info/top_level.txt +0 -0
- {mdv_live-0.1.0 → mdv_live-0.1.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdv-live
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Markdown Viewer - File tree + Live preview + Hot reload
|
|
5
5
|
Author-email: PanHouse <hirono.okamoto@panhouse.jp>
|
|
6
6
|
License: MIT
|
|
@@ -42,11 +42,12 @@ Requires-Dist: markdown>=3.5.0
|
|
|
42
42
|
## Installation
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
#
|
|
45
|
+
# PyPIからインストール(推奨)
|
|
46
|
+
pip install mdv-live
|
|
47
|
+
|
|
48
|
+
# または開発版をインストール
|
|
46
49
|
git clone https://github.com/panhouse/mdv.git
|
|
47
50
|
cd mdv
|
|
48
|
-
|
|
49
|
-
# グローバルインストール
|
|
50
51
|
pip install -e .
|
|
51
52
|
```
|
|
52
53
|
|
|
@@ -59,11 +60,23 @@ mdv
|
|
|
59
60
|
# 特定のディレクトリを表示
|
|
60
61
|
mdv ./project/
|
|
61
62
|
|
|
63
|
+
# 特定のファイルを開く
|
|
64
|
+
mdv README.md
|
|
65
|
+
|
|
62
66
|
# ポート指定
|
|
63
67
|
mdv -p 9000
|
|
64
68
|
|
|
65
69
|
# ブラウザを自動で開かない
|
|
66
70
|
mdv --no-browser
|
|
71
|
+
|
|
72
|
+
# MarkdownをPDFに変換
|
|
73
|
+
mdv --pdf README.md
|
|
74
|
+
mdv --pdf README.md -o output.pdf
|
|
75
|
+
|
|
76
|
+
# サーバー管理
|
|
77
|
+
mdv -l # 稼働中のサーバー一覧
|
|
78
|
+
mdv -k -a # 全サーバー停止
|
|
79
|
+
mdv -k <PID> # 特定サーバー停止
|
|
67
80
|
```
|
|
68
81
|
|
|
69
82
|
## Requirements
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
#
|
|
17
|
+
# PyPIからインストール(推奨)
|
|
18
|
+
pip install mdv-live
|
|
19
|
+
|
|
20
|
+
# または開発版をインストール
|
|
18
21
|
git clone https://github.com/panhouse/mdv.git
|
|
19
22
|
cd mdv
|
|
20
|
-
|
|
21
|
-
# グローバルインストール
|
|
22
23
|
pip install -e .
|
|
23
24
|
```
|
|
24
25
|
|
|
@@ -31,11 +32,23 @@ mdv
|
|
|
31
32
|
# 特定のディレクトリを表示
|
|
32
33
|
mdv ./project/
|
|
33
34
|
|
|
35
|
+
# 特定のファイルを開く
|
|
36
|
+
mdv README.md
|
|
37
|
+
|
|
34
38
|
# ポート指定
|
|
35
39
|
mdv -p 9000
|
|
36
40
|
|
|
37
41
|
# ブラウザを自動で開かない
|
|
38
42
|
mdv --no-browser
|
|
43
|
+
|
|
44
|
+
# MarkdownをPDFに変換
|
|
45
|
+
mdv --pdf README.md
|
|
46
|
+
mdv --pdf README.md -o output.pdf
|
|
47
|
+
|
|
48
|
+
# サーバー管理
|
|
49
|
+
mdv -l # 稼働中のサーバー一覧
|
|
50
|
+
mdv -k -a # 全サーバー停止
|
|
51
|
+
mdv -k <PID> # 特定サーバー停止
|
|
39
52
|
```
|
|
40
53
|
|
|
41
54
|
## Requirements
|
|
@@ -165,11 +165,8 @@ def convert_to_pdf(input_path: Path, output_path: Optional[Path] = None) -> int:
|
|
|
165
165
|
print(f"Error: Not a markdown file: {input_path}")
|
|
166
166
|
return 1
|
|
167
167
|
|
|
168
|
-
# md-to-pdf
|
|
168
|
+
# md-to-pdfコマンドを構築(最新版は--out-dirをサポートしない)
|
|
169
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
170
|
|
|
174
171
|
try:
|
|
175
172
|
result = subprocess.run(
|
|
@@ -187,17 +184,20 @@ def convert_to_pdf(input_path: Path, output_path: Optional[Path] = None) -> int:
|
|
|
187
184
|
print(f"Error: {result.stderr}")
|
|
188
185
|
return 1
|
|
189
186
|
|
|
190
|
-
#
|
|
187
|
+
# 出力ファイルパスを特定(md-to-pdfは入力と同じディレクトリに生成)
|
|
191
188
|
default_output = input_path.with_suffix(".pdf")
|
|
192
189
|
if output_path and output_path != default_output:
|
|
193
|
-
#
|
|
190
|
+
# 出力先が指定されている場合、生成後に移動
|
|
194
191
|
if default_output.exists():
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
import shutil
|
|
193
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
shutil.move(str(default_output), str(output_path))
|
|
195
|
+
print(f"PDF saved: {output_path}")
|
|
197
196
|
else:
|
|
198
|
-
print(f"
|
|
197
|
+
print(f"Warning: Expected PDF not found at {default_output}")
|
|
198
|
+
return 1
|
|
199
199
|
else:
|
|
200
|
-
print(f"
|
|
200
|
+
print(f"PDF saved: {default_output}")
|
|
201
201
|
|
|
202
202
|
return 0
|
|
203
203
|
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
10
|
import mimetypes
|
|
11
|
+
import re
|
|
11
12
|
import socket
|
|
12
13
|
import webbrowser
|
|
13
14
|
from dataclasses import dataclass, field
|
|
@@ -76,13 +77,20 @@ class FileChangeHandler(FileSystemEventHandler):
|
|
|
76
77
|
"""ファイル変更を処理"""
|
|
77
78
|
if not state.current_watching_file:
|
|
78
79
|
return
|
|
79
|
-
if file_path != state.current_watching_file:
|
|
80
|
-
return
|
|
81
80
|
if not self._loop:
|
|
82
81
|
return
|
|
83
82
|
|
|
83
|
+
# パスを正規化して比較(watchdogとresolve()の形式が異なる場合がある)
|
|
84
|
+
try:
|
|
85
|
+
normalized_path = str(Path(file_path).resolve())
|
|
86
|
+
except Exception:
|
|
87
|
+
normalized_path = file_path
|
|
88
|
+
|
|
89
|
+
if normalized_path != state.current_watching_file:
|
|
90
|
+
return
|
|
91
|
+
|
|
84
92
|
asyncio.run_coroutine_threadsafe(
|
|
85
|
-
broadcast_file_update(
|
|
93
|
+
broadcast_file_update(normalized_path),
|
|
86
94
|
self._loop
|
|
87
95
|
)
|
|
88
96
|
|
|
@@ -128,10 +136,45 @@ def escape_html(text: str) -> str:
|
|
|
128
136
|
)
|
|
129
137
|
|
|
130
138
|
|
|
139
|
+
# リストアイテムのパターン
|
|
140
|
+
_LIST_ITEM_PATTERN = re.compile(r'^[ \t]*(?:\d+\.|[-*+])[ \t]+')
|
|
141
|
+
|
|
142
|
+
# YAMLフロントマターのパターン(ファイル先頭の---で囲まれた部分)
|
|
143
|
+
_FRONTMATTER_PATTERN = re.compile(r'^---\s*\n(.*?)\n---\s*(\n|$)', re.DOTALL)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _preprocess_markdown(content: str) -> str:
|
|
147
|
+
"""マークダウンの前処理(YAMLフロントマター変換、リスト前空行挿入)"""
|
|
148
|
+
# YAMLフロントマターをコードブロックに変換
|
|
149
|
+
# ---で囲まれた部分はMarkdownで<hr>として解釈されるため、
|
|
150
|
+
# 改行が失われる問題を回避
|
|
151
|
+
frontmatter_match = _FRONTMATTER_PATTERN.match(content)
|
|
152
|
+
if frontmatter_match:
|
|
153
|
+
frontmatter_content = frontmatter_match.group(1)
|
|
154
|
+
rest_of_content = content[frontmatter_match.end():]
|
|
155
|
+
content = f"```yaml\n{frontmatter_content}\n```\n{rest_of_content}"
|
|
156
|
+
|
|
157
|
+
# リストの開始前にのみ空行を自動挿入(python-markdown互換性のため)
|
|
158
|
+
lines = content.split('\n')
|
|
159
|
+
result = []
|
|
160
|
+
|
|
161
|
+
for i, line in enumerate(lines):
|
|
162
|
+
if i > 0 and _LIST_ITEM_PATTERN.match(line):
|
|
163
|
+
prev_line = lines[i - 1]
|
|
164
|
+
# 前の行が空でなく、かつリストアイテムでもない場合に空行を挿入
|
|
165
|
+
if prev_line.strip() and not _LIST_ITEM_PATTERN.match(prev_line):
|
|
166
|
+
result.append('')
|
|
167
|
+
result.append(line)
|
|
168
|
+
|
|
169
|
+
return '\n'.join(result)
|
|
170
|
+
|
|
171
|
+
|
|
131
172
|
def render_markdown(content: str) -> str:
|
|
132
173
|
"""マークダウンをHTMLに変換"""
|
|
174
|
+
content = _preprocess_markdown(content)
|
|
133
175
|
md = markdown.Markdown(
|
|
134
|
-
extensions=["fenced_code", "codehilite", "tables", "toc", "
|
|
176
|
+
extensions=["fenced_code", "codehilite", "tables", "toc", "sane_lists"],
|
|
177
|
+
tab_length=2, # 2スペースでネストしたリストを認識
|
|
135
178
|
)
|
|
136
179
|
return md.convert(content)
|
|
137
180
|
|
|
@@ -355,6 +398,15 @@ async def get_file(path: str = Query(...)) -> dict:
|
|
|
355
398
|
"imageUrl": f"/api/image?path={path}",
|
|
356
399
|
}
|
|
357
400
|
|
|
401
|
+
# PDFの場合
|
|
402
|
+
if file_info.type == "pdf":
|
|
403
|
+
return {
|
|
404
|
+
"path": path,
|
|
405
|
+
"name": file_path.name,
|
|
406
|
+
"fileType": file_info.type,
|
|
407
|
+
"pdfUrl": f"/api/pdf?path={path}",
|
|
408
|
+
}
|
|
409
|
+
|
|
358
410
|
# テキスト系ファイルを読み込み
|
|
359
411
|
try:
|
|
360
412
|
content = file_path.read_text(encoding="utf-8")
|
|
@@ -385,6 +437,17 @@ async def get_image(path: str = Query(...)) -> FastAPIFileResponse:
|
|
|
385
437
|
return FastAPIFileResponse(file_path, media_type=mime_type)
|
|
386
438
|
|
|
387
439
|
|
|
440
|
+
@app.get("/api/pdf")
|
|
441
|
+
async def get_pdf(path: str = Query(...)) -> FastAPIFileResponse:
|
|
442
|
+
"""PDFファイルを返す"""
|
|
443
|
+
file_path = validate_path(path)
|
|
444
|
+
|
|
445
|
+
if not file_path.suffix.lower() == ".pdf":
|
|
446
|
+
raise HTTPException(status_code=400, detail="Not a PDF file")
|
|
447
|
+
|
|
448
|
+
return FastAPIFileResponse(file_path, media_type="application/pdf")
|
|
449
|
+
|
|
450
|
+
|
|
388
451
|
@app.post("/api/file")
|
|
389
452
|
async def save_file(request: SaveFileRequest) -> dict:
|
|
390
453
|
"""ファイルを保存"""
|
|
@@ -305,6 +305,8 @@
|
|
|
305
305
|
.markdown-body ul, .markdown-body ol { margin-top: 0; margin-bottom: 16px; padding-left: 2em; }
|
|
306
306
|
.markdown-body li { margin-bottom: 4px; }
|
|
307
307
|
.markdown-body li + li { margin-top: 4px; }
|
|
308
|
+
.markdown-body li > p { margin-bottom: 0; }
|
|
309
|
+
.markdown-body li > p:first-child { margin-top: 0; }
|
|
308
310
|
.markdown-body blockquote { margin: 0 0 16px; padding: 0 1em; color: var(--text-muted); border-left: 4px solid var(--border); }
|
|
309
311
|
|
|
310
312
|
.markdown-body code {
|
|
@@ -328,7 +330,7 @@
|
|
|
328
330
|
border: 1px solid var(--border);
|
|
329
331
|
}
|
|
330
332
|
|
|
331
|
-
.markdown-body pre code { padding: 0; margin: 0; font-size: 100%; background: transparent; border-radius: 0; }
|
|
333
|
+
.markdown-body pre code { padding: 0; margin: 0; font-size: 100%; background: transparent; border-radius: 0; white-space: pre; }
|
|
332
334
|
.markdown-body table { margin-bottom: 16px; border-collapse: collapse; width: 100%; }
|
|
333
335
|
.markdown-body table th, .markdown-body table td { padding: 8px 16px; border: 1px solid var(--border); }
|
|
334
336
|
.markdown-body table th { background: var(--bg-secondary); font-weight: 600; }
|
|
@@ -369,6 +371,30 @@
|
|
|
369
371
|
color: var(--text-muted);
|
|
370
372
|
}
|
|
371
373
|
|
|
374
|
+
/* PDFビューア */
|
|
375
|
+
.pdf-viewer {
|
|
376
|
+
display: flex;
|
|
377
|
+
flex-direction: column;
|
|
378
|
+
height: 100%;
|
|
379
|
+
width: 100%;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.pdf-viewer iframe {
|
|
383
|
+
flex: 1;
|
|
384
|
+
width: 100%;
|
|
385
|
+
height: 100%;
|
|
386
|
+
border: none;
|
|
387
|
+
border-radius: 8px;
|
|
388
|
+
background: var(--bg-secondary);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.pdf-viewer .pdf-info {
|
|
392
|
+
padding: 8px 16px;
|
|
393
|
+
font-size: 13px;
|
|
394
|
+
color: var(--text-muted);
|
|
395
|
+
text-align: center;
|
|
396
|
+
}
|
|
397
|
+
|
|
372
398
|
/* エディタモード */
|
|
373
399
|
.editor-container {
|
|
374
400
|
display: flex;
|
|
@@ -428,6 +454,7 @@
|
|
|
428
454
|
.icon-html { color: #e34c26; }
|
|
429
455
|
.icon-css { color: #563d7c; }
|
|
430
456
|
.icon-image { color: #a074c4; }
|
|
457
|
+
.icon-pdf { color: #e74c3c; }
|
|
431
458
|
.icon-text { color: var(--text-muted); }
|
|
432
459
|
.icon-config { color: #6d8086; }
|
|
433
460
|
.icon-shell { color: #89e051; }
|
|
@@ -628,6 +655,7 @@
|
|
|
628
655
|
html: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>',
|
|
629
656
|
css: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /></svg>',
|
|
630
657
|
image: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>',
|
|
658
|
+
pdf: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>',
|
|
631
659
|
text: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>',
|
|
632
660
|
config: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>',
|
|
633
661
|
shell: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>',
|
|
@@ -980,6 +1008,16 @@
|
|
|
980
1008
|
`;
|
|
981
1009
|
},
|
|
982
1010
|
|
|
1011
|
+
renderPDF(pdfUrl, name) {
|
|
1012
|
+
const url = pdfUrl + '&t=' + Date.now();
|
|
1013
|
+
elements.content.style.padding = '0';
|
|
1014
|
+
elements.content.innerHTML = `
|
|
1015
|
+
<div class="pdf-viewer">
|
|
1016
|
+
<iframe src="${url}" title="${name}"></iframe>
|
|
1017
|
+
</div>
|
|
1018
|
+
`;
|
|
1019
|
+
},
|
|
1020
|
+
|
|
983
1021
|
showWelcome() {
|
|
984
1022
|
elements.content.innerHTML = `
|
|
985
1023
|
<div class="welcome">
|
|
@@ -1021,6 +1059,7 @@
|
|
|
1021
1059
|
raw: data.raw,
|
|
1022
1060
|
fileType: data.fileType,
|
|
1023
1061
|
imageUrl: data.imageUrl,
|
|
1062
|
+
pdfUrl: data.pdfUrl,
|
|
1024
1063
|
scrollTop: 0
|
|
1025
1064
|
});
|
|
1026
1065
|
|
|
@@ -1095,8 +1134,13 @@
|
|
|
1095
1134
|
if (state.activeTabIndex < 0 || state.activeTabIndex >= state.tabs.length) return;
|
|
1096
1135
|
const tab = state.tabs[state.activeTabIndex];
|
|
1097
1136
|
|
|
1137
|
+
// パディングをリセット(PDFで変更されるため)
|
|
1138
|
+
elements.content.style.padding = '';
|
|
1139
|
+
|
|
1098
1140
|
if (tab.fileType === 'image') {
|
|
1099
1141
|
ContentRenderer.renderImage(tab.imageUrl, tab.name);
|
|
1142
|
+
} else if (tab.fileType === 'pdf') {
|
|
1143
|
+
ContentRenderer.renderPDF(tab.pdfUrl, tab.name);
|
|
1100
1144
|
} else {
|
|
1101
1145
|
ContentRenderer.render(tab.content, tab.fileType);
|
|
1102
1146
|
}
|
|
@@ -1138,6 +1182,9 @@
|
|
|
1138
1182
|
if (state.activeTabIndex < 0) return;
|
|
1139
1183
|
const tab = state.tabs[state.activeTabIndex];
|
|
1140
1184
|
|
|
1185
|
+
// Viewの上部に見えている要素のテキストを取得
|
|
1186
|
+
const viewTopText = this.getViewTopText();
|
|
1187
|
+
|
|
1141
1188
|
elements.content.innerHTML = `
|
|
1142
1189
|
<div class="editor-container">
|
|
1143
1190
|
<textarea class="editor-textarea" id="editorTextarea" spellcheck="false">${escapeHtml(tab.raw || '')}</textarea>
|
|
@@ -1154,7 +1201,77 @@
|
|
|
1154
1201
|
elements.editorStatus.textContent = 'Modified';
|
|
1155
1202
|
elements.editorStatus.className = 'editor-status modified';
|
|
1156
1203
|
});
|
|
1157
|
-
|
|
1204
|
+
|
|
1205
|
+
// Viewのテキスト位置に対応するEdit位置にスクロール
|
|
1206
|
+
setTimeout(() => {
|
|
1207
|
+
textarea.focus();
|
|
1208
|
+
if (viewTopText && tab.raw) {
|
|
1209
|
+
const lineIndex = this.findLineByText(tab.raw, viewTopText);
|
|
1210
|
+
if (lineIndex >= 0) {
|
|
1211
|
+
const lineHeight = this.getTextareaLineHeight(textarea);
|
|
1212
|
+
textarea.scrollTop = lineIndex * lineHeight;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}, 0);
|
|
1216
|
+
},
|
|
1217
|
+
|
|
1218
|
+
// Viewの上部に見えているテキストを取得
|
|
1219
|
+
getViewTopText() {
|
|
1220
|
+
const contentRect = elements.content.getBoundingClientRect();
|
|
1221
|
+
const topY = contentRect.top + 10; // 少し下を見る
|
|
1222
|
+
const centerX = contentRect.left + contentRect.width / 2;
|
|
1223
|
+
|
|
1224
|
+
// 上部にある要素を探す
|
|
1225
|
+
let el = document.elementFromPoint(centerX, topY);
|
|
1226
|
+
if (!el || !elements.content.contains(el)) {
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// テキストを含む要素を探す
|
|
1231
|
+
while (el && el !== elements.content) {
|
|
1232
|
+
const text = el.textContent?.trim();
|
|
1233
|
+
if (text && text.length > 5 && text.length < 500) {
|
|
1234
|
+
// 最初の行を取得(長すぎる場合は切り詰め)
|
|
1235
|
+
const firstLine = text.split('\n')[0].trim();
|
|
1236
|
+
if (firstLine.length > 5) {
|
|
1237
|
+
return firstLine.substring(0, 100);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
el = el.parentElement;
|
|
1241
|
+
}
|
|
1242
|
+
return null;
|
|
1243
|
+
},
|
|
1244
|
+
|
|
1245
|
+
// テキストを含む行を検索
|
|
1246
|
+
findLineByText(rawContent, searchText) {
|
|
1247
|
+
const lines = rawContent.split('\n');
|
|
1248
|
+
const normalizedSearch = searchText.toLowerCase().replace(/[#*`_\[\]]/g, '').trim();
|
|
1249
|
+
|
|
1250
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1251
|
+
const normalizedLine = lines[i].toLowerCase().replace(/[#*`_\[\]]/g, '').trim();
|
|
1252
|
+
// 短すぎる行はスキップ(空行や記号のみの行を除外)
|
|
1253
|
+
if (normalizedLine.length < 5) continue;
|
|
1254
|
+
|
|
1255
|
+
// 完全一致または部分一致を確認
|
|
1256
|
+
if (normalizedLine === normalizedSearch ||
|
|
1257
|
+
normalizedLine.includes(normalizedSearch) ||
|
|
1258
|
+
(normalizedLine.length >= 10 && normalizedSearch.includes(normalizedLine))) {
|
|
1259
|
+
return i;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return -1;
|
|
1263
|
+
},
|
|
1264
|
+
|
|
1265
|
+
// textareaの行の高さを取得
|
|
1266
|
+
getTextareaLineHeight(textarea) {
|
|
1267
|
+
// scrollHeightから実際の行高さを計算(paddingの影響も含む)
|
|
1268
|
+
const lines = textarea.value.split('\n');
|
|
1269
|
+
if (lines.length > 0 && textarea.scrollHeight > 0) {
|
|
1270
|
+
return textarea.scrollHeight / lines.length;
|
|
1271
|
+
}
|
|
1272
|
+
// フォールバック: CSSのlineHeight
|
|
1273
|
+
const style = window.getComputedStyle(textarea);
|
|
1274
|
+
return parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.6;
|
|
1158
1275
|
},
|
|
1159
1276
|
|
|
1160
1277
|
hide() {
|
|
@@ -1162,15 +1279,81 @@
|
|
|
1162
1279
|
const tab = state.tabs[state.activeTabIndex];
|
|
1163
1280
|
|
|
1164
1281
|
const textarea = document.getElementById('editorTextarea');
|
|
1282
|
+
let topLineText = null;
|
|
1283
|
+
let scrollPercentage = 0;
|
|
1284
|
+
|
|
1165
1285
|
if (textarea) {
|
|
1166
1286
|
tab.raw = textarea.value;
|
|
1287
|
+
// Editの上部に見えている行のテキストを取得
|
|
1288
|
+
topLineText = this.getEditTopLineText(textarea);
|
|
1289
|
+
// フォールバック用にパーセンテージを保存
|
|
1290
|
+
const maxScroll = textarea.scrollHeight - textarea.clientHeight;
|
|
1291
|
+
if (maxScroll > 0) {
|
|
1292
|
+
scrollPercentage = textarea.scrollTop / maxScroll;
|
|
1293
|
+
}
|
|
1167
1294
|
}
|
|
1168
1295
|
|
|
1169
1296
|
elements.editorStatus.style.display = 'none';
|
|
1170
1297
|
TabManager.renderActive();
|
|
1298
|
+
|
|
1299
|
+
// Editのテキスト位置に対応するView位置にスクロール
|
|
1300
|
+
setTimeout(() => {
|
|
1301
|
+
let scrolled = false;
|
|
1302
|
+
if (topLineText) {
|
|
1303
|
+
const targetElement = this.findElementByText(topLineText);
|
|
1304
|
+
if (targetElement) {
|
|
1305
|
+
const contentRect = elements.content.getBoundingClientRect();
|
|
1306
|
+
const targetRect = targetElement.getBoundingClientRect();
|
|
1307
|
+
const offsetTop = targetRect.top - contentRect.top + elements.content.scrollTop;
|
|
1308
|
+
elements.content.scrollTop = offsetTop - 10; // 少し上に余白
|
|
1309
|
+
scrolled = true;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
// テキストマッチングに失敗した場合はパーセンテージでフォールバック
|
|
1313
|
+
if (!scrolled && scrollPercentage > 0) {
|
|
1314
|
+
const maxScroll = elements.content.scrollHeight - elements.content.clientHeight;
|
|
1315
|
+
elements.content.scrollTop = maxScroll * scrollPercentage;
|
|
1316
|
+
}
|
|
1317
|
+
}, 0);
|
|
1171
1318
|
state.hasUnsavedChanges = false;
|
|
1172
1319
|
},
|
|
1173
1320
|
|
|
1321
|
+
// Editの上部に見えている行のテキストを取得
|
|
1322
|
+
getEditTopLineText(textarea) {
|
|
1323
|
+
const lineHeight = this.getTextareaLineHeight(textarea);
|
|
1324
|
+
const topLine = Math.floor(textarea.scrollTop / lineHeight);
|
|
1325
|
+
const lines = textarea.value.split('\n');
|
|
1326
|
+
|
|
1327
|
+
// 空行をスキップして最初の意味のある行を取得
|
|
1328
|
+
for (let i = topLine; i < Math.min(topLine + 5, lines.length); i++) {
|
|
1329
|
+
const line = lines[i]?.trim();
|
|
1330
|
+
if (line && line.length > 3) {
|
|
1331
|
+
// markdownの装飾を除去
|
|
1332
|
+
return line.replace(/^#+\s*/, '').replace(/[*`_\[\]]/g, '').substring(0, 100);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return null;
|
|
1336
|
+
},
|
|
1337
|
+
|
|
1338
|
+
// テキストを含む要素を検索
|
|
1339
|
+
findElementByText(searchText) {
|
|
1340
|
+
const normalizedSearch = searchText.toLowerCase().trim();
|
|
1341
|
+
const markdownBody = elements.content.querySelector('.markdown-body');
|
|
1342
|
+
if (!markdownBody) return null;
|
|
1343
|
+
|
|
1344
|
+
// 見出し、段落、リストアイテムなどを検索
|
|
1345
|
+
const candidates = markdownBody.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, td, th, pre');
|
|
1346
|
+
|
|
1347
|
+
for (const el of candidates) {
|
|
1348
|
+
const text = el.textContent?.toLowerCase().trim() || '';
|
|
1349
|
+
if (text.includes(normalizedSearch.substring(0, 30)) ||
|
|
1350
|
+
normalizedSearch.includes(text.substring(0, 30))) {
|
|
1351
|
+
return el;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return null;
|
|
1355
|
+
},
|
|
1356
|
+
|
|
1174
1357
|
async save() {
|
|
1175
1358
|
if (state.activeTabIndex < 0 || !state.isEditMode) return;
|
|
1176
1359
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdv-live
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Markdown Viewer - File tree + Live preview + Hot reload
|
|
5
5
|
Author-email: PanHouse <hirono.okamoto@panhouse.jp>
|
|
6
6
|
License: MIT
|
|
@@ -42,11 +42,12 @@ Requires-Dist: markdown>=3.5.0
|
|
|
42
42
|
## Installation
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
#
|
|
45
|
+
# PyPIからインストール(推奨)
|
|
46
|
+
pip install mdv-live
|
|
47
|
+
|
|
48
|
+
# または開発版をインストール
|
|
46
49
|
git clone https://github.com/panhouse/mdv.git
|
|
47
50
|
cd mdv
|
|
48
|
-
|
|
49
|
-
# グローバルインストール
|
|
50
51
|
pip install -e .
|
|
51
52
|
```
|
|
52
53
|
|
|
@@ -59,11 +60,23 @@ mdv
|
|
|
59
60
|
# 特定のディレクトリを表示
|
|
60
61
|
mdv ./project/
|
|
61
62
|
|
|
63
|
+
# 特定のファイルを開く
|
|
64
|
+
mdv README.md
|
|
65
|
+
|
|
62
66
|
# ポート指定
|
|
63
67
|
mdv -p 9000
|
|
64
68
|
|
|
65
69
|
# ブラウザを自動で開かない
|
|
66
70
|
mdv --no-browser
|
|
71
|
+
|
|
72
|
+
# MarkdownをPDFに変換
|
|
73
|
+
mdv --pdf README.md
|
|
74
|
+
mdv --pdf README.md -o output.pdf
|
|
75
|
+
|
|
76
|
+
# サーバー管理
|
|
77
|
+
mdv -l # 稼働中のサーバー一覧
|
|
78
|
+
mdv -k -a # 全サーバー停止
|
|
79
|
+
mdv -k <PID> # 特定サーバー停止
|
|
67
80
|
```
|
|
68
81
|
|
|
69
82
|
## Requirements
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|