mdv-live 0.5.9 → 0.5.11
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.
- package/CHANGELOG.md +51 -0
- package/package.json +1 -2
- package/src/api/pdf.js +41 -79
- package/src/static/app.js +4 -119
- package/src/static/index.html +0 -20
- package/src/static/styles.css +0 -34
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.11] - 2026-05-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Marp PDF export ENOENT in fresh install** (実は 0.5.5 から潜在):
|
|
13
|
+
- `src/api/pdf.js` が marp 実行ファイルを
|
|
14
|
+
`node_modules/mdv-live/node_modules/.bin/marp` で解決していたが、npm
|
|
15
|
+
hoisting により実体は top-level の `node_modules/.bin/marp` にある
|
|
16
|
+
- dev 環境 (mdv-live リポ内) では nested の方が存在するため気づかず、
|
|
17
|
+
`npm install mdv-live` した fresh install では ENOENT
|
|
18
|
+
- `require.resolve('@marp-team/marp-cli/package.json')` から bin スクリプト
|
|
19
|
+
を解決し `node` で実行する方式に変更 (hoist/nest 両対応)
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
|
|
23
|
+
- 242 → **243 件 (+1)**: marp-cli bin entry の実在チェック regression test
|
|
24
|
+
|
|
25
|
+
## [0.5.10] - 2026-05-09
|
|
26
|
+
|
|
27
|
+
### Fixed (UX revert)
|
|
28
|
+
|
|
29
|
+
- **Markdown PDF ボタンを OS 印刷ダイアログに戻す** (本来の UX 復元):
|
|
30
|
+
- 0.5.9 (実体は 2026-01-31 `e5526f9` から) で plain Markdown も server-side
|
|
31
|
+
md-to-pdf 経由の PDF DL に切り替わっていたが、本来の UX は `Cmd+P` 相当の
|
|
32
|
+
OS 印刷ダイアログ (`window.print()`) で「PDF として保存」を選ぶフロー
|
|
33
|
+
- `src/static/app.js`: `print()` の markdown 分岐を削除 (`else` 分岐の
|
|
34
|
+
`browserPrint()` に落ちる)
|
|
35
|
+
- Marp / HTML preview の挙動は変更なし (server-side marp-cli を維持)
|
|
36
|
+
|
|
37
|
+
### Fixed (codex review)
|
|
38
|
+
|
|
39
|
+
- `/api/pdf/export` の `fs.readFile` を `try/catch` 外で実行していた問題を修正
|
|
40
|
+
(directory 指定や読み取り不可ファイルで Express デフォルトエラーに落ちて
|
|
41
|
+
controlled JSON が返らなかった)。stat による file 判定を追加
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
- `PdfStyleManager` UI モジュール (markdown が server PDF を使わなくなったため
|
|
46
|
+
orphan)、`pdfStyleToggle` / `pdfStylePanel` HTML、関連 CSS、`normalizeUserPath`
|
|
47
|
+
helper、`pdf-style-preview` クラス
|
|
48
|
+
- `md-to-pdf` runtime dependency。web UI で使わなくなったため削除。
|
|
49
|
+
`bin/mdv.js convert` は元々 `npx md-to-pdf` 経由で 0.5.9 以前と同じ挙動
|
|
50
|
+
- `/api/pdf/export` の markdown 分岐 (Marp 専用エンドポイントに整理)。
|
|
51
|
+
非 Marp ファイルは 415 で拒否
|
|
52
|
+
|
|
53
|
+
### Tests
|
|
54
|
+
|
|
55
|
+
- 241 → **242 件 (+1)**、全 PASS
|
|
56
|
+
- 追加: directory path → 404 controlled JSON (codex round 3 regression)
|
|
57
|
+
- 変更: plain markdown PDF テスト → 415 テストに置換
|
|
58
|
+
|
|
8
59
|
## [0.5.9] - 2026-05-09
|
|
9
60
|
|
|
10
61
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdv-live",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "Markdown Viewer - File tree + Live preview + Marp support + Hot reload",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
"highlight.js": "^11.10.0",
|
|
52
52
|
"markdown-it": "^14.1.0",
|
|
53
53
|
"markdown-it-task-lists": "^2.1.1",
|
|
54
|
-
"md-to-pdf": "^5.2.5",
|
|
55
54
|
"mime-types": "^2.1.35",
|
|
56
55
|
"multer": "^1.4.5-lts.1",
|
|
57
56
|
"open": "^10.1.0",
|
package/src/api/pdf.js
CHANGED
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
4
|
import os from 'os';
|
|
6
5
|
import { createRequire } from 'module';
|
|
7
6
|
import { isMarp } from '../rendering/markdown.js';
|
|
8
|
-
import { validatePath
|
|
9
|
-
import { resolvePdfOptions } from '../styles/index.js';
|
|
7
|
+
import { validatePath } from '../utils/path.js';
|
|
10
8
|
|
|
11
9
|
const require = createRequire(import.meta.url);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
// marp-cli は npm hoisting によりインストール先が変わる (top-level / nested)。
|
|
11
|
+
// require.resolve でパッケージの実体を特定し、その bin スクリプトを node で
|
|
12
|
+
// 直接実行する。`node_modules/.bin/marp` 直叩きは fresh install で nested
|
|
13
|
+
// パスが無い時に ENOENT する罠。
|
|
14
|
+
const marpEntry = (() => {
|
|
15
|
+
const pkgPath = require.resolve('@marp-team/marp-cli/package.json');
|
|
16
|
+
const pkg = require('@marp-team/marp-cli/package.json');
|
|
17
|
+
const binRel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.marp;
|
|
18
|
+
if (!binRel) {
|
|
19
|
+
throw new Error('@marp-team/marp-cli does not declare a "marp" bin entry');
|
|
20
|
+
}
|
|
21
|
+
return path.join(path.dirname(pkgPath), binRel);
|
|
22
|
+
})();
|
|
16
23
|
const PDF_EXPORT_TIMEOUT_MS = 180000;
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
|
-
* Spawn
|
|
26
|
+
* Spawn marp-cli with stdin closed.
|
|
20
27
|
*
|
|
21
|
-
* 注意: execFile は stdio オプションを受け付けない (Node 仕様)
|
|
22
|
-
* 内部で get-stdin を呼ぶため、stdin が pipe のままだと EOF を永遠に待ち続けて
|
|
23
|
-
* ハングする。spawn で stdin を 'ignore' (= /dev/null) に明示的に縛る。
|
|
28
|
+
* 注意: execFile は stdio オプションを受け付けない (Node 仕様) ため spawn を使う。
|
|
24
29
|
*/
|
|
25
|
-
function
|
|
30
|
+
function runMarp(args) {
|
|
26
31
|
return new Promise((resolve, reject) => {
|
|
27
|
-
const child = spawn(
|
|
28
|
-
cwd,
|
|
32
|
+
const child = spawn(process.execPath, [marpEntry, ...args], {
|
|
29
33
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
34
|
});
|
|
31
|
-
let stdout = '';
|
|
32
35
|
let stderr = '';
|
|
33
|
-
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
34
36
|
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
35
37
|
|
|
36
38
|
const timer = setTimeout(() => {
|
|
@@ -44,12 +46,11 @@ function runPdfTool(bin, args, { cwd } = {}) {
|
|
|
44
46
|
child.on('close', (code, signal) => {
|
|
45
47
|
clearTimeout(timer);
|
|
46
48
|
if (code === 0) {
|
|
47
|
-
resolve(
|
|
49
|
+
resolve();
|
|
48
50
|
} else {
|
|
49
|
-
const err = new Error(
|
|
51
|
+
const err = new Error(`marp exited with code=${code} signal=${signal}`);
|
|
50
52
|
err.code = code;
|
|
51
53
|
err.signal = signal;
|
|
52
|
-
err.stdout = stdout;
|
|
53
54
|
err.stderr = stderr;
|
|
54
55
|
reject(err);
|
|
55
56
|
}
|
|
@@ -58,50 +59,12 @@ function runPdfTool(bin, args, { cwd } = {}) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!relativePath) return null;
|
|
68
|
-
if (!await validatePathReal(relativePath, rootDir)) {
|
|
69
|
-
throw new Error(`Access denied: ${relativePath}`);
|
|
70
|
-
}
|
|
71
|
-
const fullPath = path.join(rootDir, relativePath);
|
|
72
|
-
const stat = await fs.stat(fullPath);
|
|
73
|
-
if (!stat.isFile()) {
|
|
74
|
-
throw new Error(`Not a file: ${relativePath}`);
|
|
75
|
-
}
|
|
76
|
-
return fullPath;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Export a regular markdown document with md-to-pdf.
|
|
81
|
-
* @param {string} inputPath - Source markdown file.
|
|
82
|
-
* @param {string} outputPath - Temporary output path.
|
|
83
|
-
* @param {string | null} stylesheetPath - Optional custom CSS file.
|
|
84
|
-
* @param {string | null} pdfOptionsPath - Optional PDF options JSON file.
|
|
85
|
-
* @returns {Promise<void>}
|
|
86
|
-
*/
|
|
87
|
-
async function exportMarkdownPdf(inputPath, outputPath, stylesheetPath, pdfOptionsPath) {
|
|
88
|
-
const pdfOptions = await resolvePdfOptions(pdfOptionsPath || undefined);
|
|
89
|
-
const args = [inputPath, '--pdf-options', JSON.stringify(pdfOptions)];
|
|
90
|
-
|
|
91
|
-
if (stylesheetPath) {
|
|
92
|
-
args.push('--stylesheet', highlightStylesheet);
|
|
93
|
-
args.push('--stylesheet', stylesheetPath);
|
|
94
|
-
args.push('--highlight-style', 'atom-one-dark');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
await runPdfTool(mdToPdfBin, args, { cwd: path.dirname(inputPath) });
|
|
98
|
-
|
|
99
|
-
const generatedPdf = inputPath.replace(/\.(md|markdown)$/i, '.pdf');
|
|
100
|
-
await fs.rename(generatedPdf, outputPath);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Setup PDF export routes
|
|
62
|
+
* Setup PDF export routes.
|
|
63
|
+
*
|
|
64
|
+
* Web UI からは Marp ファイルのみがこの経路を使う。通常 Markdown は
|
|
65
|
+
* クライアント側で window.print() (OS 印刷ダイアログ) に流す設計のため、
|
|
66
|
+
* このエンドポイントは Marp 以外を 415 で拒否する。
|
|
67
|
+
*
|
|
105
68
|
* @param {Express} app - Express application
|
|
106
69
|
* @returns {void}
|
|
107
70
|
*/
|
|
@@ -109,7 +72,7 @@ export function setupPdfRoutes(app) {
|
|
|
109
72
|
const { rootDir } = app.locals;
|
|
110
73
|
|
|
111
74
|
app.post('/api/pdf/export', async (req, res) => {
|
|
112
|
-
const { filePath
|
|
75
|
+
const { filePath } = req.body;
|
|
113
76
|
|
|
114
77
|
if (!filePath) {
|
|
115
78
|
return res.status(400).json({ error: 'filePath is required' });
|
|
@@ -120,29 +83,28 @@ export function setupPdfRoutes(app) {
|
|
|
120
83
|
}
|
|
121
84
|
|
|
122
85
|
const fullPath = path.join(rootDir, filePath);
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
await fs.access(fullPath);
|
|
126
|
-
} catch {
|
|
127
|
-
return res.status(404).json({ error: 'File not found' });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
86
|
const baseName = path.basename(fullPath, '.md');
|
|
131
87
|
const outputPath = path.join(os.tmpdir(), `mdv-${Date.now()}-${baseName}.pdf`);
|
|
132
88
|
const outputFileName = `${baseName}.pdf`;
|
|
133
89
|
|
|
134
90
|
try {
|
|
91
|
+
let stat;
|
|
92
|
+
try {
|
|
93
|
+
stat = await fs.stat(fullPath);
|
|
94
|
+
} catch {
|
|
95
|
+
return res.status(404).json({ error: 'File not found' });
|
|
96
|
+
}
|
|
97
|
+
if (!stat.isFile()) {
|
|
98
|
+
return res.status(404).json({ error: 'File not found' });
|
|
99
|
+
}
|
|
100
|
+
|
|
135
101
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
136
|
-
if (isMarp(content)) {
|
|
137
|
-
|
|
138
|
-
} else {
|
|
139
|
-
const [stylesheetPath, resolvedPdfOptionsPath] = await Promise.all([
|
|
140
|
-
resolveOptionalUserFile(stylePath, rootDir),
|
|
141
|
-
resolveOptionalUserFile(pdfOptionsPath, rootDir),
|
|
142
|
-
]);
|
|
143
|
-
await exportMarkdownPdf(fullPath, outputPath, stylesheetPath, resolvedPdfOptionsPath);
|
|
102
|
+
if (!isMarp(content)) {
|
|
103
|
+
return res.status(415).json({ error: 'Server-side PDF export supports Marp files only. Use the browser print dialog for regular Markdown.' });
|
|
144
104
|
}
|
|
145
105
|
|
|
106
|
+
await runMarp([fullPath, '-o', outputPath, '--html', '--allow-local-files', '--no-stdin']);
|
|
107
|
+
|
|
146
108
|
res.download(outputPath, outputFileName, async (err) => {
|
|
147
109
|
if (err) {
|
|
148
110
|
console.error('Download error:', err);
|
package/src/static/app.js
CHANGED
|
@@ -11,9 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const STORAGE_KEYS = {
|
|
13
13
|
THEME: 'mdv-theme',
|
|
14
|
-
SIDEBAR_WIDTH: 'mdv-sidebar-width'
|
|
15
|
-
PDF_STYLE_PATH: 'mdv-pdf-style-path',
|
|
16
|
-
PDF_OPTIONS_PATH: 'mdv-pdf-options-path'
|
|
14
|
+
SIDEBAR_WIDTH: 'mdv-sidebar-width'
|
|
17
15
|
};
|
|
18
16
|
|
|
19
17
|
const HLJS_THEMES = {
|
|
@@ -87,9 +85,7 @@
|
|
|
87
85
|
isResizing: false,
|
|
88
86
|
skipScrollRestore: false,
|
|
89
87
|
uploadTargetPath: '',
|
|
90
|
-
rootPath: ''
|
|
91
|
-
pdfStylePath: localStorage.getItem(STORAGE_KEYS.PDF_STYLE_PATH) || '',
|
|
92
|
-
pdfOptionsPath: localStorage.getItem(STORAGE_KEYS.PDF_OPTIONS_PATH) || ''
|
|
88
|
+
rootPath: ''
|
|
93
89
|
};
|
|
94
90
|
|
|
95
91
|
// ============================================================
|
|
@@ -125,12 +121,6 @@
|
|
|
125
121
|
statusText: document.getElementById('statusText'),
|
|
126
122
|
resizeHandle: document.getElementById('resizeHandle'),
|
|
127
123
|
editToggle: document.getElementById('editToggle'),
|
|
128
|
-
pdfStyleToggle: document.getElementById('pdfStyleToggle'),
|
|
129
|
-
pdfStylePanel: document.getElementById('pdfStylePanel'),
|
|
130
|
-
pdfStylePath: document.getElementById('pdfStylePath'),
|
|
131
|
-
pdfOptionsPath: document.getElementById('pdfOptionsPath'),
|
|
132
|
-
pdfStyleApply: document.getElementById('pdfStyleApply'),
|
|
133
|
-
pdfStyleClear: document.getElementById('pdfStyleClear'),
|
|
134
124
|
editLabel: document.getElementById('editLabel'),
|
|
135
125
|
editorStatus: document.getElementById('editorStatus'),
|
|
136
126
|
shutdownBtn: document.getElementById('shutdownBtn'),
|
|
@@ -173,10 +163,6 @@
|
|
|
173
163
|
});
|
|
174
164
|
}
|
|
175
165
|
|
|
176
|
-
function normalizeUserPath(path) {
|
|
177
|
-
return path.trim().replace(/^\/+/, '');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
166
|
async function apiRequest(url, options = {}) {
|
|
181
167
|
const response = await fetch(url, options);
|
|
182
168
|
const data = await response.json();
|
|
@@ -249,101 +235,6 @@
|
|
|
249
235
|
}
|
|
250
236
|
};
|
|
251
237
|
|
|
252
|
-
// ============================================================
|
|
253
|
-
// PDF Style Preview
|
|
254
|
-
// ============================================================
|
|
255
|
-
|
|
256
|
-
const PdfStyleManager = {
|
|
257
|
-
scopedCssId: 'pdf-style-preview-css',
|
|
258
|
-
|
|
259
|
-
init() {
|
|
260
|
-
elements.pdfStylePath.value = state.pdfStylePath;
|
|
261
|
-
elements.pdfOptionsPath.value = state.pdfOptionsPath;
|
|
262
|
-
elements.pdfStyleToggle.addEventListener('click', () => {
|
|
263
|
-
elements.pdfStylePanel.classList.toggle('hidden');
|
|
264
|
-
});
|
|
265
|
-
elements.pdfStyleApply.addEventListener('click', () => this.applyFromInputs());
|
|
266
|
-
elements.pdfStyleClear.addEventListener('click', () => this.clear());
|
|
267
|
-
elements.pdfStylePath.addEventListener('keydown', (event) => {
|
|
268
|
-
if (event.key === 'Enter') this.applyFromInputs();
|
|
269
|
-
});
|
|
270
|
-
elements.pdfOptionsPath.addEventListener('keydown', (event) => {
|
|
271
|
-
if (event.key === 'Enter') this.applyFromInputs();
|
|
272
|
-
});
|
|
273
|
-
this.loadPreviewCss();
|
|
274
|
-
},
|
|
275
|
-
|
|
276
|
-
getExportOptions() {
|
|
277
|
-
return {
|
|
278
|
-
stylePath: normalizeUserPath(state.pdfStylePath),
|
|
279
|
-
pdfOptionsPath: normalizeUserPath(state.pdfOptionsPath)
|
|
280
|
-
};
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
async applyFromInputs() {
|
|
284
|
-
state.pdfStylePath = normalizeUserPath(elements.pdfStylePath.value);
|
|
285
|
-
state.pdfOptionsPath = normalizeUserPath(elements.pdfOptionsPath.value);
|
|
286
|
-
elements.pdfStylePath.value = state.pdfStylePath;
|
|
287
|
-
elements.pdfOptionsPath.value = state.pdfOptionsPath;
|
|
288
|
-
localStorage.setItem(STORAGE_KEYS.PDF_STYLE_PATH, state.pdfStylePath);
|
|
289
|
-
localStorage.setItem(STORAGE_KEYS.PDF_OPTIONS_PATH, state.pdfOptionsPath);
|
|
290
|
-
await this.loadPreviewCss();
|
|
291
|
-
TabManager.renderActive();
|
|
292
|
-
},
|
|
293
|
-
|
|
294
|
-
clear() {
|
|
295
|
-
state.pdfStylePath = '';
|
|
296
|
-
state.pdfOptionsPath = '';
|
|
297
|
-
elements.pdfStylePath.value = '';
|
|
298
|
-
elements.pdfOptionsPath.value = '';
|
|
299
|
-
localStorage.removeItem(STORAGE_KEYS.PDF_STYLE_PATH);
|
|
300
|
-
localStorage.removeItem(STORAGE_KEYS.PDF_OPTIONS_PATH);
|
|
301
|
-
const oldStyle = document.getElementById(this.scopedCssId);
|
|
302
|
-
if (oldStyle) oldStyle.remove();
|
|
303
|
-
TabManager.renderActive();
|
|
304
|
-
elements.statusText.textContent = 'PDF style cleared';
|
|
305
|
-
setTimeout(() => { elements.statusText.textContent = 'Connected'; }, 1600);
|
|
306
|
-
},
|
|
307
|
-
|
|
308
|
-
async loadPreviewCss() {
|
|
309
|
-
const oldStyle = document.getElementById(this.scopedCssId);
|
|
310
|
-
if (oldStyle) oldStyle.remove();
|
|
311
|
-
if (!state.pdfStylePath) return;
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
const response = await fetch(`/raw/${state.pdfStylePath}`);
|
|
315
|
-
if (!response.ok) throw new Error('CSS file not found');
|
|
316
|
-
const cssText = await response.text();
|
|
317
|
-
const style = document.createElement('style');
|
|
318
|
-
style.id = this.scopedCssId;
|
|
319
|
-
style.textContent = this.scopeCss(cssText);
|
|
320
|
-
document.head.appendChild(style);
|
|
321
|
-
elements.statusText.textContent = 'PDF style applied';
|
|
322
|
-
setTimeout(() => { elements.statusText.textContent = 'Connected'; }, 1600);
|
|
323
|
-
} catch (error) {
|
|
324
|
-
console.error('PDF style preview error:', error);
|
|
325
|
-
elements.statusText.textContent = 'PDF style failed';
|
|
326
|
-
setTimeout(() => { elements.statusText.textContent = 'Connected'; }, 2500);
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
|
|
330
|
-
scopeCss(cssText) {
|
|
331
|
-
const scope = '.markdown-body.pdf-style-preview';
|
|
332
|
-
const withoutComments = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
333
|
-
return withoutComments.replace(/([^{}]+)\{/g, (match, selectorText) => {
|
|
334
|
-
const selectors = selectorText.trim();
|
|
335
|
-
if (!selectors || selectors.startsWith('@')) return match;
|
|
336
|
-
const scopedSelectors = selectors.split(',').map((selector) => {
|
|
337
|
-
const trimmed = selector.trim();
|
|
338
|
-
if (trimmed === ':root' || trimmed === 'body') return scope;
|
|
339
|
-
if (trimmed.startsWith(scope)) return trimmed;
|
|
340
|
-
return `${scope} ${trimmed}`;
|
|
341
|
-
});
|
|
342
|
-
return `${scopedSelectors.join(', ')} {`;
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
|
|
347
238
|
// ============================================================
|
|
348
239
|
// Sidebar Management
|
|
349
240
|
// ============================================================
|
|
@@ -880,9 +771,7 @@
|
|
|
880
771
|
render(htmlContent, fileType) {
|
|
881
772
|
const containerClass = fileType === 'code'
|
|
882
773
|
? 'markdown-body code-view-container'
|
|
883
|
-
:
|
|
884
|
-
? 'markdown-body pdf-style-preview'
|
|
885
|
-
: 'markdown-body';
|
|
774
|
+
: 'markdown-body';
|
|
886
775
|
elements.content.innerHTML = `<div class="${containerClass}">${htmlContent}</div>`;
|
|
887
776
|
|
|
888
777
|
elements.content.querySelectorAll('pre code').forEach(block => {
|
|
@@ -1709,8 +1598,6 @@
|
|
|
1709
1598
|
await this.exportPdf(tab.path);
|
|
1710
1599
|
} else if (this.isHtmlPreview()) {
|
|
1711
1600
|
this.printHtmlPreview(tab.name);
|
|
1712
|
-
} else if (tab.fileType === 'markdown') {
|
|
1713
|
-
await this.exportPdf(tab.path);
|
|
1714
1601
|
} else {
|
|
1715
1602
|
this.browserPrint(tab.name);
|
|
1716
1603
|
}
|
|
@@ -1738,9 +1625,8 @@
|
|
|
1738
1625
|
|
|
1739
1626
|
try {
|
|
1740
1627
|
statusText.textContent = 'Generating PDF...';
|
|
1741
|
-
const exportOptions = PdfStyleManager.getExportOptions();
|
|
1742
1628
|
|
|
1743
|
-
const response = await MDVApi.exportPdf({ filePath
|
|
1629
|
+
const response = await MDVApi.exportPdf({ filePath });
|
|
1744
1630
|
|
|
1745
1631
|
if (!response.ok) {
|
|
1746
1632
|
const error = await response.json();
|
|
@@ -2366,7 +2252,6 @@
|
|
|
2366
2252
|
async function init() {
|
|
2367
2253
|
// Initialize all managers
|
|
2368
2254
|
ThemeManager.init();
|
|
2369
|
-
PdfStyleManager.init();
|
|
2370
2255
|
SidebarManager.init();
|
|
2371
2256
|
ResizeHandler.init();
|
|
2372
2257
|
EditorManager.init();
|
package/src/static/index.html
CHANGED
|
@@ -95,13 +95,6 @@
|
|
|
95
95
|
PDF
|
|
96
96
|
</button>
|
|
97
97
|
|
|
98
|
-
<button class="toolbar-btn" id="pdfStyleToggle" title="PDF style settings" aria-label="PDF style settings">
|
|
99
|
-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
100
|
-
<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" />
|
|
101
|
-
</svg>
|
|
102
|
-
Style
|
|
103
|
-
</button>
|
|
104
|
-
|
|
105
98
|
<div class="toolbar-spacer"></div>
|
|
106
99
|
|
|
107
100
|
<span class="editor-status" id="editorStatus" style="display: none;">Ready</span>
|
|
@@ -117,19 +110,6 @@
|
|
|
117
110
|
</div>
|
|
118
111
|
</div>
|
|
119
112
|
|
|
120
|
-
<div class="pdf-style-panel hidden" id="pdfStylePanel">
|
|
121
|
-
<label>
|
|
122
|
-
<span>CSS</span>
|
|
123
|
-
<input type="text" id="pdfStylePath" placeholder="src/styles/report.example.css">
|
|
124
|
-
</label>
|
|
125
|
-
<label>
|
|
126
|
-
<span>PDF options</span>
|
|
127
|
-
<input type="text" id="pdfOptionsPath" placeholder="src/styles/report.pdf-options.example.json">
|
|
128
|
-
</label>
|
|
129
|
-
<button class="toolbar-btn" id="pdfStyleApply">Apply</button>
|
|
130
|
-
<button class="toolbar-btn" id="pdfStyleClear">Clear</button>
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
113
|
<!-- Tab bar -->
|
|
134
114
|
<div class="tab-bar" id="tabBar" role="tablist" aria-label="Open files"></div>
|
|
135
115
|
|
package/src/static/styles.css
CHANGED
|
@@ -241,40 +241,6 @@ body {
|
|
|
241
241
|
|
|
242
242
|
.toolbar-spacer { flex: 1; }
|
|
243
243
|
|
|
244
|
-
.pdf-style-panel {
|
|
245
|
-
display: flex;
|
|
246
|
-
align-items: center;
|
|
247
|
-
gap: 10px;
|
|
248
|
-
padding: 8px 16px;
|
|
249
|
-
border-bottom: 1px solid var(--border);
|
|
250
|
-
background: var(--bg-secondary);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
.pdf-style-panel label {
|
|
254
|
-
display: flex;
|
|
255
|
-
align-items: center;
|
|
256
|
-
gap: 6px;
|
|
257
|
-
min-width: 0;
|
|
258
|
-
color: var(--text-secondary);
|
|
259
|
-
font-size: 12px;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
.pdf-style-panel input {
|
|
263
|
-
width: min(34vw, 360px);
|
|
264
|
-
min-width: 160px;
|
|
265
|
-
padding: 6px 8px;
|
|
266
|
-
background: var(--bg-primary);
|
|
267
|
-
border: 1px solid var(--border);
|
|
268
|
-
border-radius: 6px;
|
|
269
|
-
color: var(--text-primary);
|
|
270
|
-
font-size: 12px;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.pdf-style-panel input:focus {
|
|
274
|
-
border-color: var(--accent);
|
|
275
|
-
outline: none;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
244
|
.status {
|
|
279
245
|
display: flex;
|
|
280
246
|
align-items: center;
|