@zkov/pi-md-viewer 0.1.0 → 0.1.2
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/dist/extensions/show-markdown.js +5 -1
- package/dist/src-ts/server.js +6 -2
- package/extensions/show-markdown.ts +5 -1
- package/package.json +1 -1
- package/src/pi_md_viewer/static/side-aura.svg +46 -0
- package/src/pi_md_viewer/static/viewer-actions.js +9 -4
- package/src/pi_md_viewer/static/viewer.css +43 -8
- package/src/pi_md_viewer/static/viewer.js +47 -22
- package/src/pi_md_viewer/templates/viewer.html +0 -5
|
@@ -24,7 +24,11 @@ export function buildInvocation(cwd, extensionFile, input, options = {}) {
|
|
|
24
24
|
const platform = options.platform ?? process.platform;
|
|
25
25
|
const pathForCwd = platform === "win32" ? path.win32 : path.posix;
|
|
26
26
|
const extensionPath = fileURLToPath(extensionFile);
|
|
27
|
-
const
|
|
27
|
+
const extensionDir = path.dirname(extensionPath);
|
|
28
|
+
const packageRoot = path.basename(path.dirname(extensionDir)) === "dist"
|
|
29
|
+
? path.resolve(extensionDir, "..", "..")
|
|
30
|
+
: path.resolve(extensionDir, "..");
|
|
31
|
+
const launcher = path.join(packageRoot, "bin", "mdview.js");
|
|
28
32
|
const resolvedPaths = input.paths.map((supplied) => {
|
|
29
33
|
const normalized = supplied.startsWith("@") ? supplied.slice(1) : supplied;
|
|
30
34
|
if (!normalized)
|
package/dist/src-ts/server.js
CHANGED
|
@@ -124,7 +124,7 @@ export class ViewerServer {
|
|
|
124
124
|
const template = readFileSync(path.join(legacyAssetRoot, "templates", "viewer.html"), "utf8");
|
|
125
125
|
this.sendBytes(response, 200, Buffer.from(template.replace("{{CSRF_TOKEN}}", this.csrfToken)), "text/html; charset=utf-8");
|
|
126
126
|
}
|
|
127
|
-
else if (["/static/viewer.css", "/static/viewer.js", "/static/viewer-actions.js"].includes(parsed.pathname)) {
|
|
127
|
+
else if (["/static/viewer.css", "/static/viewer.js", "/static/viewer-actions.js", "/static/side-aura.svg"].includes(parsed.pathname)) {
|
|
128
128
|
await this.serveAsset(parsed.pathname.replace("/static/", ""), response);
|
|
129
129
|
}
|
|
130
130
|
else if (parsed.pathname === "/api/v1/status") {
|
|
@@ -281,7 +281,11 @@ export class ViewerServer {
|
|
|
281
281
|
const file = path.join(legacyAssetRoot, "static", name);
|
|
282
282
|
try {
|
|
283
283
|
const stat = statSync(file);
|
|
284
|
-
const type = name.endsWith(".css")
|
|
284
|
+
const type = name.endsWith(".css")
|
|
285
|
+
? "text/css; charset=utf-8"
|
|
286
|
+
: name.endsWith(".svg")
|
|
287
|
+
? "image/svg+xml; charset=utf-8"
|
|
288
|
+
: "application/javascript; charset=utf-8";
|
|
285
289
|
response.writeHead(200, { "Content-Type": type, "Content-Length": stat.size });
|
|
286
290
|
createReadStream(file).pipe(response);
|
|
287
291
|
}
|
|
@@ -53,7 +53,11 @@ export function buildInvocation(
|
|
|
53
53
|
const platform = options.platform ?? process.platform;
|
|
54
54
|
const pathForCwd = platform === "win32" ? path.win32 : path.posix;
|
|
55
55
|
const extensionPath = fileURLToPath(extensionFile);
|
|
56
|
-
const
|
|
56
|
+
const extensionDir = path.dirname(extensionPath);
|
|
57
|
+
const packageRoot = path.basename(path.dirname(extensionDir)) === "dist"
|
|
58
|
+
? path.resolve(extensionDir, "..", "..")
|
|
59
|
+
: path.resolve(extensionDir, "..");
|
|
60
|
+
const launcher = path.join(packageRoot, "bin", "mdview.js");
|
|
57
61
|
const resolvedPaths = input.paths.map((supplied) => {
|
|
58
62
|
const normalized = supplied.startsWith("@") ? supplied.slice(1) : supplied;
|
|
59
63
|
if (!normalized) throw new Error("show_markdown path cannot be empty");
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="left-main" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#152138" />
|
|
5
|
+
<stop offset=".38" stop-color="#1b2945" />
|
|
6
|
+
<stop offset=".68" stop-color="#181f3c" />
|
|
7
|
+
<stop offset="1" stop-color="#152138" />
|
|
8
|
+
</linearGradient>
|
|
9
|
+
<linearGradient id="left-accent" x1="0" y1="0" x2="0" y2="1">
|
|
10
|
+
<stop offset="0" stop-color="#295b7e" />
|
|
11
|
+
<stop offset=".44" stop-color="#4c4482" />
|
|
12
|
+
<stop offset="1" stop-color="#295b7e" />
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<linearGradient id="right-main" x1="0" y1="0" x2="0" y2="1">
|
|
15
|
+
<stop offset="0" stop-color="#152138" />
|
|
16
|
+
<stop offset=".32" stop-color="#181f3c" />
|
|
17
|
+
<stop offset=".72" stop-color="#1b2945" />
|
|
18
|
+
<stop offset="1" stop-color="#152138" />
|
|
19
|
+
</linearGradient>
|
|
20
|
+
<linearGradient id="right-accent" x1="0" y1="0" x2="0" y2="1">
|
|
21
|
+
<stop offset="0" stop-color="#4c4482" />
|
|
22
|
+
<stop offset=".52" stop-color="#295b7e" />
|
|
23
|
+
<stop offset="1" stop-color="#473e7e" />
|
|
24
|
+
</linearGradient>
|
|
25
|
+
<linearGradient id="purple-wash" x1="0" y1="0" x2="0" y2="1">
|
|
26
|
+
<stop offset="0" stop-color="#0a0d12" stop-opacity="0" />
|
|
27
|
+
<stop offset=".34" stop-color="#5b4c96" stop-opacity=".028" />
|
|
28
|
+
<stop offset=".66" stop-color="#0a0d12" stop-opacity="0" />
|
|
29
|
+
<stop offset="1" stop-color="#473e7e" stop-opacity=".022" />
|
|
30
|
+
</linearGradient>
|
|
31
|
+
</defs>
|
|
32
|
+
|
|
33
|
+
<rect width="100" height="100" fill="url(#purple-wash)" />
|
|
34
|
+
|
|
35
|
+
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
36
|
+
<path d="M 4 -6 C 1 18 -1 42 1 64 C 3 80 0 93 1 106" stroke="url(#left-main)" stroke-width="28" stroke-opacity=".20" />
|
|
37
|
+
<path d="M 4 -6 C 1 18 -1 42 1 64 C 3 80 0 93 1 106" stroke="url(#left-main)" stroke-width="18" stroke-opacity=".34" />
|
|
38
|
+
<path d="M 4 -6 C 1 18 -1 42 1 64 C 3 80 0 93 1 106" stroke="url(#left-accent)" stroke-width="9" stroke-opacity=".13" />
|
|
39
|
+
<path d="M 2 -4 C 0 24 0 48 2 72 C 3 84 1 96 2 104" stroke="#152138" stroke-width="5" stroke-opacity=".22" />
|
|
40
|
+
|
|
41
|
+
<path d="M 96 -6 C 99 20 101 45 99 67 C 97 82 100 95 99 106" stroke="url(#right-main)" stroke-width="28" stroke-opacity=".20" />
|
|
42
|
+
<path d="M 96 -6 C 99 20 101 45 99 67 C 97 82 100 95 99 106" stroke="url(#right-main)" stroke-width="18" stroke-opacity=".34" />
|
|
43
|
+
<path d="M 96 -6 C 99 20 101 45 99 67 C 97 82 100 95 99 106" stroke="url(#right-accent)" stroke-width="9" stroke-opacity=".13" />
|
|
44
|
+
<path d="M 98 -4 C 100 23 100 50 98 74 C 97 86 99 97 98 104" stroke="#152138" stroke-width="5" stroke-opacity=".22" />
|
|
45
|
+
</g>
|
|
46
|
+
</svg>
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
export async function
|
|
1
|
+
export async function unloadAndReload({ remove, reload }) {
|
|
2
2
|
await remove();
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
return reload();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function selectNextActiveFile(files, removedId, activeId) {
|
|
7
|
+
if (removedId !== activeId) return activeId;
|
|
8
|
+
const index = files.findIndex((file) => file.id === removedId);
|
|
9
|
+
if (index === -1) return activeId;
|
|
10
|
+
return files[index + 1]?.id ?? files[index - 1]?.id ?? null;
|
|
6
11
|
}
|
|
@@ -10,13 +10,26 @@
|
|
|
10
10
|
--line: #263243;
|
|
11
11
|
--code: #090c11;
|
|
12
12
|
--danger: #ff8d9b;
|
|
13
|
+
--linear-side-aura-background:
|
|
14
|
+
linear-gradient(90deg, rgba(21, 33, 56, .82) -12%, rgba(21, 33, 56, .42) 2%, rgba(76, 68, 130, .08) 10%, transparent 16%),
|
|
15
|
+
linear-gradient(90deg, rgba(41, 91, 126, .26) -8%, rgba(41, 91, 126, .10) 6%, transparent 14%),
|
|
16
|
+
linear-gradient(90deg, transparent 0%, transparent 84%, rgba(76, 68, 130, .08) 90%, rgba(21, 33, 56, .42) 99%, rgba(21, 33, 56, .82) 112%),
|
|
17
|
+
linear-gradient(90deg, transparent 0%, transparent 86%, rgba(41, 91, 126, .10) 94%, rgba(41, 91, 126, .26) 108%),
|
|
18
|
+
var(--bg);
|
|
19
|
+
--svg-side-aura-background: url("/static/side-aura.svg"), var(--bg);
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
* { box-sizing: border-box; }
|
|
16
|
-
html {
|
|
23
|
+
html {
|
|
24
|
+
scroll-behavior: smooth;
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
}
|
|
17
27
|
body {
|
|
28
|
+
min-height: 100vh;
|
|
18
29
|
margin: 0;
|
|
19
|
-
background:
|
|
30
|
+
background: var(--svg-side-aura-background);
|
|
31
|
+
background-repeat: no-repeat;
|
|
32
|
+
background-size: 100% 100%;
|
|
20
33
|
color: var(--text);
|
|
21
34
|
font: 16px/1.72 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
22
35
|
}
|
|
@@ -82,10 +95,22 @@ button:disabled { cursor: default; opacity: .45; }
|
|
|
82
95
|
}
|
|
83
96
|
.icon-button { padding: 2px 8px; font-size: 18px; }
|
|
84
97
|
.danger { color: var(--danger); }
|
|
98
|
+
.file-row {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: stretch;
|
|
101
|
+
gap: 4px;
|
|
102
|
+
margin: 4px 0;
|
|
103
|
+
border: 1px solid transparent;
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
}
|
|
106
|
+
.file-row.active {
|
|
107
|
+
border-color: rgba(124, 196, 255, .3);
|
|
108
|
+
background: rgba(124, 196, 255, .09);
|
|
109
|
+
}
|
|
85
110
|
.file-button {
|
|
86
111
|
display: block;
|
|
87
|
-
width:
|
|
88
|
-
|
|
112
|
+
min-width: 0;
|
|
113
|
+
flex: 1;
|
|
89
114
|
padding: 9px 10px;
|
|
90
115
|
overflow: hidden;
|
|
91
116
|
border-color: transparent;
|
|
@@ -95,10 +120,20 @@ button:disabled { cursor: default; opacity: .45; }
|
|
|
95
120
|
white-space: nowrap;
|
|
96
121
|
}
|
|
97
122
|
.file-button small { display: block; color: var(--muted); }
|
|
98
|
-
.file-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
.file-row.active .file-button { color: #fff; }
|
|
124
|
+
.file-actions {
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
gap: 2px;
|
|
128
|
+
padding-right: 4px;
|
|
129
|
+
}
|
|
130
|
+
.file-action {
|
|
131
|
+
min-width: 28px;
|
|
132
|
+
padding: 3px 6px;
|
|
133
|
+
border-color: transparent;
|
|
134
|
+
background: transparent;
|
|
135
|
+
font-size: 16px;
|
|
136
|
+
line-height: 1;
|
|
102
137
|
}
|
|
103
138
|
main { min-width: 0; padding-top: 4px; }
|
|
104
139
|
.document-toolbar {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { selectNextActiveFile, unloadAndReload } from "/static/viewer-actions.js";
|
|
2
2
|
|
|
3
3
|
const csrf = document.querySelector('meta[name="csrf-token"]').content;
|
|
4
4
|
const clientId = crypto.randomUUID();
|
|
@@ -16,9 +16,6 @@ const elements = {
|
|
|
16
16
|
status: document.getElementById("connection-status"),
|
|
17
17
|
sidebar: document.getElementById("sidebar"),
|
|
18
18
|
toggle: document.getElementById("sidebar-toggle"),
|
|
19
|
-
refresh: document.getElementById("refresh-file"),
|
|
20
|
-
unload: document.getElementById("unload-file"),
|
|
21
|
-
close: document.getElementById("close-viewer"),
|
|
22
19
|
};
|
|
23
20
|
|
|
24
21
|
async function api(path, options = {}) {
|
|
@@ -53,9 +50,12 @@ function notify(message) {
|
|
|
53
50
|
function renderFileList() {
|
|
54
51
|
elements.fileList.replaceChildren();
|
|
55
52
|
for (const file of state.files) {
|
|
53
|
+
const row = document.createElement("div");
|
|
54
|
+
row.className = `file-row${file.id === state.activeId ? " active" : ""}`;
|
|
55
|
+
|
|
56
56
|
const button = document.createElement("button");
|
|
57
57
|
button.type = "button";
|
|
58
|
-
button.className =
|
|
58
|
+
button.className = "file-button";
|
|
59
59
|
button.title = file.path;
|
|
60
60
|
button.append(document.createTextNode(file.displayName));
|
|
61
61
|
if (file.parentLabel) {
|
|
@@ -64,7 +64,35 @@ function renderFileList() {
|
|
|
64
64
|
button.append(parent);
|
|
65
65
|
}
|
|
66
66
|
button.addEventListener("click", () => selectFile(file.id, true));
|
|
67
|
-
|
|
67
|
+
|
|
68
|
+
const actions = document.createElement("div");
|
|
69
|
+
actions.className = "file-actions";
|
|
70
|
+
|
|
71
|
+
const refresh = document.createElement("button");
|
|
72
|
+
refresh.type = "button";
|
|
73
|
+
refresh.className = "file-action";
|
|
74
|
+
refresh.title = "Обновить документ";
|
|
75
|
+
refresh.setAttribute("aria-label", `Обновить ${file.displayName}`);
|
|
76
|
+
refresh.textContent = "⟳";
|
|
77
|
+
refresh.addEventListener("click", (event) => {
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
selectFile(file.id, true).catch(showError);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const unload = document.createElement("button");
|
|
83
|
+
unload.type = "button";
|
|
84
|
+
unload.className = "file-action danger";
|
|
85
|
+
unload.title = "Выгрузить документ";
|
|
86
|
+
unload.setAttribute("aria-label", `Выгрузить ${file.displayName}`);
|
|
87
|
+
unload.textContent = "×";
|
|
88
|
+
unload.addEventListener("click", (event) => {
|
|
89
|
+
event.stopPropagation();
|
|
90
|
+
unloadFile(file.id).catch(showError);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
actions.append(refresh, unload);
|
|
94
|
+
row.append(button, actions);
|
|
95
|
+
elements.fileList.append(row);
|
|
68
96
|
}
|
|
69
97
|
}
|
|
70
98
|
|
|
@@ -143,30 +171,31 @@ async function loadFiles({ announce = false } = {}) {
|
|
|
143
171
|
}
|
|
144
172
|
renderFileList();
|
|
145
173
|
if (state.activeId) await selectFile(state.activeId, state.pinned);
|
|
174
|
+
else showEmptyState();
|
|
146
175
|
if (announce && state.files.some((file) => !previous.has(file.id))) notify("Добавлены новые документы");
|
|
147
176
|
return state.files.length;
|
|
148
177
|
}
|
|
149
178
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
179
|
+
function showEmptyState() {
|
|
180
|
+
elements.title.textContent = "Markdown Viewer";
|
|
181
|
+
elements.source.textContent = "";
|
|
182
|
+
state.revision = null;
|
|
183
|
+
elements.document.innerHTML = '<p class="empty-state">Добавьте Markdown-файл командой <code>mdview файл.md</code>.</p>';
|
|
184
|
+
renderToc([]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function unloadFile(fileId) {
|
|
188
|
+
const nextActiveId = selectNextActiveFile(state.files, fileId, state.activeId);
|
|
189
|
+
await unloadAndReload({
|
|
154
190
|
remove: () => api(`/api/v1/files/${encodeURIComponent(fileId)}`, { method: "DELETE" }),
|
|
155
191
|
reload: async () => {
|
|
156
|
-
state.activeId =
|
|
192
|
+
state.activeId = nextActiveId;
|
|
157
193
|
state.pinned = false;
|
|
158
194
|
return loadFiles();
|
|
159
195
|
},
|
|
160
|
-
close: closeViewer,
|
|
161
196
|
});
|
|
162
197
|
}
|
|
163
198
|
|
|
164
|
-
async function closeViewer() {
|
|
165
|
-
await api("/api/v1/shutdown", { method: "POST", body: {} });
|
|
166
|
-
elements.status.textContent = "Viewer закрыт";
|
|
167
|
-
window.close();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
199
|
function connectEvents() {
|
|
171
200
|
const events = new EventSource(`/api/v1/events?clientId=${encodeURIComponent(clientId)}`);
|
|
172
201
|
events.addEventListener("open", () => { elements.status.textContent = "Подключено"; });
|
|
@@ -175,7 +204,6 @@ function connectEvents() {
|
|
|
175
204
|
events.addEventListener("server_closing", () => {
|
|
176
205
|
elements.status.textContent = "Viewer закрывается";
|
|
177
206
|
events.close();
|
|
178
|
-
window.close();
|
|
179
207
|
});
|
|
180
208
|
events.addEventListener("error", () => { elements.status.textContent = "Переподключение…"; });
|
|
181
209
|
}
|
|
@@ -185,9 +213,6 @@ function updateProgress() {
|
|
|
185
213
|
elements.progress.style.width = `${maximum > 0 ? (scrollY / maximum) * 100 : 0}%`;
|
|
186
214
|
}
|
|
187
215
|
|
|
188
|
-
elements.refresh.addEventListener("click", () => state.activeId && selectFile(state.activeId, state.pinned));
|
|
189
|
-
elements.unload.addEventListener("click", () => unloadActive().catch(showError));
|
|
190
|
-
elements.close.addEventListener("click", () => closeViewer().catch(showError));
|
|
191
216
|
elements.toggle.addEventListener("click", () => {
|
|
192
217
|
const open = elements.sidebar.classList.toggle("open");
|
|
193
218
|
elements.toggle.setAttribute("aria-expanded", String(open));
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
<div class="brand">pi · markdown viewer</div>
|
|
19
19
|
<div class="panel-heading">
|
|
20
20
|
<span>Файлы</span>
|
|
21
|
-
<button id="close-viewer" class="icon-button danger" type="button" title="Закрыть viewer">×</button>
|
|
22
21
|
</div>
|
|
23
22
|
<nav id="file-list" aria-label="Открытые Markdown-файлы"></nav>
|
|
24
23
|
</aside>
|
|
@@ -28,10 +27,6 @@
|
|
|
28
27
|
<h1 id="document-title">Markdown Viewer</h1>
|
|
29
28
|
<div id="source-path" class="source-path"></div>
|
|
30
29
|
</div>
|
|
31
|
-
<div class="toolbar-actions">
|
|
32
|
-
<button id="refresh-file" type="button">Обновить</button>
|
|
33
|
-
<button id="unload-file" type="button">Выгрузить</button>
|
|
34
|
-
</div>
|
|
35
30
|
</div>
|
|
36
31
|
<div id="notice" class="notice" role="status" hidden></div>
|
|
37
32
|
<div id="error" class="error" role="alert" hidden></div>
|