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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdv-live
3
- Version: 0.1.0
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
- default_output.rename(output_path)
196
- print(f"✅ PDF saved: {output_path}")
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" PDF saved: {output_path}")
197
+ print(f"Warning: Expected PDF not found at {default_output}")
198
+ return 1
199
199
  else:
200
- print(f"PDF saved: {default_output}")
200
+ print(f"PDF saved: {default_output}")
201
201
 
202
202
  return 0
203
203
 
@@ -75,6 +75,9 @@ FILE_TYPES: Dict[str, FileTypeInfo] = {
75
75
  ".webp": FileTypeInfo("image", "image"),
76
76
  ".ico": FileTypeInfo("image", "image"),
77
77
  ".bmp": FileTypeInfo("image", "image"),
78
+
79
+ # PDF
80
+ ".pdf": FileTypeInfo("pdf", "pdf"),
78
81
  }
79
82
 
80
83
  # サポートする拡張子のセット
@@ -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(file_path),
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", "nl2br"]
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
- textarea.focus();
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.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mdv-live"
7
- version = "0.1.0"
7
+ version = "0.1.4"
8
8
  description = "Markdown Viewer - File tree + Live preview + Hot reload"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes