mdv-live 0.5.5 → 0.5.8
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 +102 -0
- package/README.md +154 -23
- package/bin/mdv.js +141 -81
- package/package.json +1 -1
- package/src/api/marpNote/guards.js +79 -0
- package/src/api/marpNote/handleGet.js +65 -0
- package/src/api/marpNote/handlePut.js +162 -0
- package/src/api/marpNote/readDeck.js +42 -0
- package/src/api/marpNote.js +40 -0
- package/src/api/pdf.js +65 -8
- package/src/concurrency/pathLock.js +39 -0
- package/src/rendering/index.js +9 -1
- package/src/rendering/markdown.js +4 -11
- package/src/rendering/marp.js +11 -32
- package/src/rendering/marpNoteWriter.js +156 -0
- package/src/rendering/marpitAdapter.js +139 -0
- package/src/server.js +29 -4
- package/src/static/app.js +369 -22
- package/src/static/index.html +24 -0
- package/src/static/lib/apiClient.js +73 -0
- package/src/static/lib/presenterChannel.js +33 -0
- package/src/static/lib/saveQueue.js +71 -0
- package/src/static/lib/tabRegistry.js +32 -0
- package/src/static/presenter.html +687 -0
- package/src/static/styles.css +34 -0
- package/src/styles/index.js +90 -0
- package/src/styles/report.example.css +201 -0
- package/src/styles/report.pdf-options.example.json +10 -0
- package/src/utils/atomicWrite.js +159 -0
- package/src/utils/errors.js +50 -0
- package/src/utils/etag.js +11 -0
- package/src/utils/lineMath.js +86 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP client used by the main MDV window.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes URL construction, common headers (If-Match), JSON parsing, and
|
|
5
|
+
* error normalization. Loaded as a classic `<script>` so the API is exposed
|
|
6
|
+
* on `window.MDVApi`.
|
|
7
|
+
*/
|
|
8
|
+
(function () {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
async function jsonOrEmpty(res) {
|
|
12
|
+
try { return await res.json(); } catch { return {}; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** GET /api/marp/decks/:path */
|
|
16
|
+
async function getDeck(path) {
|
|
17
|
+
const res = await fetch('/api/marp/decks/' + encodeURIComponent(path));
|
|
18
|
+
return { res, data: await jsonOrEmpty(res) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** PUT /api/marp/decks/:path/slides/:N/note (If-Match required) */
|
|
22
|
+
async function saveMarpNote(path, slideIndex, note, ifMatch) {
|
|
23
|
+
const url = '/api/marp/decks/' + encodeURIComponent(path)
|
|
24
|
+
+ '/slides/' + slideIndex + '/note';
|
|
25
|
+
const res = await fetch(url, {
|
|
26
|
+
method: 'PUT',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'If-Match': ifMatch
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ note })
|
|
32
|
+
});
|
|
33
|
+
return { res, data: await jsonOrEmpty(res) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Other MDV endpoints used throughout the app -------------------------
|
|
37
|
+
|
|
38
|
+
function fetchTree() { return fetch('/api/tree'); }
|
|
39
|
+
function expandTree(path) {
|
|
40
|
+
return fetch('/api/tree/expand?path=' + encodeURIComponent(path));
|
|
41
|
+
}
|
|
42
|
+
function fetchFile(path) {
|
|
43
|
+
return fetch('/api/file?path=' + encodeURIComponent(path));
|
|
44
|
+
}
|
|
45
|
+
function saveFile(path, content) {
|
|
46
|
+
return fetch('/api/file', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({ path, content })
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function fetchInfo() { return fetch('/api/info'); }
|
|
53
|
+
function exportPdf(payload) {
|
|
54
|
+
return fetch('/api/pdf/export', {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify(payload)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof globalThis !== 'undefined') {
|
|
62
|
+
globalThis.MDVApi = {
|
|
63
|
+
getDeck,
|
|
64
|
+
saveMarpNote,
|
|
65
|
+
fetchTree,
|
|
66
|
+
expandTree,
|
|
67
|
+
fetchFile,
|
|
68
|
+
saveFile,
|
|
69
|
+
fetchInfo,
|
|
70
|
+
exportPdf
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the BroadcastChannel name and message schemas
|
|
3
|
+
* used between the main MDV window and the Presenter window.
|
|
4
|
+
*
|
|
5
|
+
* Imported from a `<script>` tag (no module loader) so we expose globals on
|
|
6
|
+
* `window.MDVPresenterChannel`.
|
|
7
|
+
*
|
|
8
|
+
* Message types (discriminated by `type`):
|
|
9
|
+
*
|
|
10
|
+
* main → presenter
|
|
11
|
+
* { type: 'slides', path, html, css, etag, notes, notesMultiplicity, current }
|
|
12
|
+
* { type: 'slides', empty: true, reason } ← clear / no-deck
|
|
13
|
+
* { type: 'index', index }
|
|
14
|
+
* { type: 'note-saved', slideIndex, ok, etag?, normalizedNote?, code?, reason? }
|
|
15
|
+
*
|
|
16
|
+
* presenter → main
|
|
17
|
+
* { type: 'request-slides' }
|
|
18
|
+
* { type: 'goto', index }
|
|
19
|
+
* { type: 'edit-note', path, etag, slideIndex, note }
|
|
20
|
+
*/
|
|
21
|
+
(function () {
|
|
22
|
+
'use strict';
|
|
23
|
+
const CHANNEL_NAME = 'mdv-marp-presenter';
|
|
24
|
+
|
|
25
|
+
function create() {
|
|
26
|
+
if (typeof BroadcastChannel === 'undefined') return null;
|
|
27
|
+
return new BroadcastChannel(CHANNEL_NAME);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof globalThis !== 'undefined') {
|
|
31
|
+
globalThis.MDVPresenterChannel = { CHANNEL_NAME, create };
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-deck save queue with per-slide coalescing.
|
|
3
|
+
*
|
|
4
|
+
* - Saves to the same deck are processed strictly serially (the server has
|
|
5
|
+
* a per-path mutex, but client-side serialization keeps user-visible
|
|
6
|
+
* ordering intuitive).
|
|
7
|
+
* - New edits for the same slideIndex overwrite any pending value (coalesce).
|
|
8
|
+
* - Other slides' pending edits keep their place in insertion order.
|
|
9
|
+
* - `saveFn(path, slideIndex, note, etag)` is supplied by the caller.
|
|
10
|
+
*
|
|
11
|
+
* Loaded as a classic <script>; exposes window.MDVSaveQueue.
|
|
12
|
+
*/
|
|
13
|
+
(function () {
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
function createSaveQueue({ saveFn }) {
|
|
17
|
+
/** @type {Map<string, { pendingBySlide: Map<number, {note:string, etag:string|null}>, isDraining: boolean }>} */
|
|
18
|
+
const queue = new Map();
|
|
19
|
+
|
|
20
|
+
function enqueue(path, slideIndex, note, etag) {
|
|
21
|
+
let entry = queue.get(path);
|
|
22
|
+
if (!entry) {
|
|
23
|
+
entry = { pendingBySlide: new Map(), isDraining: false };
|
|
24
|
+
queue.set(path, entry);
|
|
25
|
+
}
|
|
26
|
+
entry.pendingBySlide.set(slideIndex, { note, etag });
|
|
27
|
+
if (!entry.isDraining) drain(path);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function drain(path) {
|
|
31
|
+
const entry = queue.get(path);
|
|
32
|
+
if (!entry || entry.isDraining) return;
|
|
33
|
+
entry.isDraining = true;
|
|
34
|
+
try {
|
|
35
|
+
while (entry.pendingBySlide.size > 0) {
|
|
36
|
+
const it = entry.pendingBySlide.entries().next();
|
|
37
|
+
if (it.done) break;
|
|
38
|
+
const [slideIndex, payload] = it.value;
|
|
39
|
+
entry.pendingBySlide.delete(slideIndex);
|
|
40
|
+
try {
|
|
41
|
+
await saveFn(path, slideIndex, payload.note, payload.etag);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// Caller logs; never let drain break.
|
|
44
|
+
console.error('saveQueue saveFn error', err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} finally {
|
|
48
|
+
entry.isDraining = false;
|
|
49
|
+
if (entry.pendingBySlide.size === 0) queue.delete(path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Drop all pending edits for a path (e.g. when its tab is closed). */
|
|
54
|
+
function dropPath(path) {
|
|
55
|
+
const entry = queue.get(path);
|
|
56
|
+
if (!entry) return;
|
|
57
|
+
entry.pendingBySlide.clear();
|
|
58
|
+
// If a drain is isDraining, it will exit cleanly when the map is empty.
|
|
59
|
+
if (!entry.isDraining) queue.delete(path);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Test/observation helper. */
|
|
63
|
+
function _size() { return queue.size; }
|
|
64
|
+
|
|
65
|
+
return { enqueue, drain, dropPath, _size };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof globalThis !== 'undefined') {
|
|
69
|
+
globalThis.MDVSaveQueue = { createSaveQueue };
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight tab life-cycle hub.
|
|
3
|
+
*
|
|
4
|
+
* Subscribers (e.g. PresenterView) register cleanup callbacks once at init
|
|
5
|
+
* time, and TabManager fires `notifyClosed(path)` when a tab is closed.
|
|
6
|
+
* Solves the memory leak the audit found in `lastSavedEtag` and the save
|
|
7
|
+
* queue when users open and close many decks.
|
|
8
|
+
*/
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const closeListeners = [];
|
|
13
|
+
const switchListeners = [];
|
|
14
|
+
|
|
15
|
+
function onTabClosed(fn) { if (typeof fn === 'function') closeListeners.push(fn); }
|
|
16
|
+
function onTabSwitched(fn) { if (typeof fn === 'function') switchListeners.push(fn); }
|
|
17
|
+
|
|
18
|
+
function notifyClosed(path) {
|
|
19
|
+
for (const fn of closeListeners) {
|
|
20
|
+
try { fn(path); } catch (err) { console.error('tabRegistry close listener error', err); }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function notifySwitched(activePath) {
|
|
24
|
+
for (const fn of switchListeners) {
|
|
25
|
+
try { fn(activePath); } catch (err) { console.error('tabRegistry switch listener error', err); }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof globalThis !== 'undefined') {
|
|
30
|
+
globalThis.MDVTabRegistry = { onTabClosed, onTabSwitched, notifyClosed, notifySwitched };
|
|
31
|
+
}
|
|
32
|
+
})();
|