@dimm-city/print-md 0.3.1 → 0.4.0

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.
@@ -0,0 +1,8 @@
1
+ title: "{{TITLE}}"
2
+ authors:
3
+ - "{{AUTHOR}}"
4
+ source:
5
+ files:
6
+ - chapter-01.md
7
+ output:
8
+ filename: "{{OUTPUT_PDF}}"
@@ -67,6 +67,13 @@
67
67
  window.addEventListener('renderingComplete', function (e) {
68
68
  post({ type: 'pmd:event', name: 'renderingComplete', detail: e.detail });
69
69
  });
70
+ // ADR 0005: source-position sync + click-to-source.
71
+ window.addEventListener('sourceLineChanged', function (e) {
72
+ post({ type: 'pmd:event', name: 'sourceLineChanged', detail: e.detail });
73
+ });
74
+ window.addEventListener('elementActivated', function (e) {
75
+ post({ type: 'pmd:event', name: 'elementActivated', detail: e.detail });
76
+ });
70
77
 
71
78
  // Announce readiness as soon as previewAPI is defined.
72
79
  function announceReady() {
@@ -0,0 +1,430 @@
1
+ // Interface adapter: exposes window.previewAPI for the parent toolbar.
2
+ // Paged.js paginates into .pagedjs_page elements. We use PagedConfig.after
3
+ // to know when rendering is done.
4
+
5
+ (function () {
6
+ 'use strict';
7
+
8
+ var pages = [];
9
+ var currentPage = 1;
10
+ var debugMode = false;
11
+ var currentViewMode = 'two-column';
12
+ var ignoreScrollUntil = 0;
13
+
14
+ function refreshPages() {
15
+ pages = Array.from(document.querySelectorAll('.pagedjs_page'));
16
+ return pages;
17
+ }
18
+
19
+ function clampPage(n) {
20
+ if (pages.length === 0) return 1;
21
+ var page = Number(n);
22
+ if (!Number.isFinite(page)) page = 1;
23
+ return Math.max(1, Math.min(Math.round(page), pages.length));
24
+ }
25
+
26
+ function detectVisiblePage() {
27
+ if (pages.length === 0) return 1;
28
+ // Use getBoundingClientRect (viewport-relative, post-zoom) rather than
29
+ // offsetTop. The viewer applies CSS `zoom` for fit-width; under `zoom`,
30
+ // offsetTop stays in PRE-zoom layout coords while window.scrollY is POST-zoom,
31
+ // so mixing them pinned the detected page to 1. getBoundingClientRect is
32
+ // consistent with the rendered viewport at any zoom.
33
+ var line = window.innerHeight / 3; // reference line in the upper third
34
+ var vh = window.innerHeight;
35
+ var last = 0;
36
+ for (var i = 0; i < pages.length; i++) {
37
+ var top = pages[i].getBoundingClientRect().top;
38
+ if (top <= line) last = i; // last page whose top is at/above the line
39
+ else if (top > vh) break; // well below the viewport — stop scanning
40
+ }
41
+ // In spread/two-column view a row holds two pages at the same top; report the
42
+ // FIRST page of that row (matches single view, where each row is one page).
43
+ var rowTop = pages[last].getBoundingClientRect().top;
44
+ while (last > 0 && Math.abs(pages[last - 1].getBoundingClientRect().top - rowTop) < 2) last--;
45
+ return last + 1;
46
+ }
47
+
48
+ function scrollToCurrentPage() {
49
+ if (pages.length === 0) return;
50
+ var page = clampPage(currentPage);
51
+ currentPage = page;
52
+ ignoreScrollUntil = Date.now() + 300;
53
+ pages[page - 1].scrollIntoView({ behavior: 'instant', block: 'start', inline: 'nearest' });
54
+ }
55
+
56
+ function pageStep(mode) {
57
+ return (mode || currentViewMode) === 'single' ? 1 : 2;
58
+ }
59
+
60
+ // ── Source-mapping helpers (ADR 0005) ──────────────────────────────────────
61
+ // Every block element carries data-source-line (markdown-it-source-map). These
62
+ // map rendered DOM <-> markdown source line and rendered DOM <-> paged.js page,
63
+ // which is the same-origin info the cross-iframe host cannot compute itself.
64
+ function lineOf(el) {
65
+ if (!el || !el.getAttribute) return null;
66
+ var n = parseInt(el.getAttribute('data-source-line'), 10);
67
+ return Number.isFinite(n) ? n : null;
68
+ }
69
+
70
+ function pageIndexOf(el) {
71
+ if (!el || !el.closest) return 0;
72
+ var pg = el.closest('.pagedjs_page');
73
+ if (!pg) return 0;
74
+ if (pages.length === 0) refreshPages();
75
+ var idx = pages.indexOf(pg);
76
+ return idx >= 0 ? idx + 1 : 0;
77
+ }
78
+
79
+ function sourcedBlocks() {
80
+ return Array.from(document.querySelectorAll('[data-source-line]'));
81
+ }
82
+
83
+ // data-source-line resets PER FILE, so a line number is only unambiguous when
84
+ // paired with its chapter (data-chapter-src — the source filename). These two
85
+ // helpers scope line lookups to a chapter so editor<->preview sync maps to the
86
+ // right file in a multi-chapter book.
87
+ function chapterOf(el) {
88
+ var c = el && el.closest ? el.closest('[data-chapter-src]') : null;
89
+ return c ? c.getAttribute('data-chapter-src') : null;
90
+ }
91
+
92
+ function blocksInChapter(chapter) {
93
+ if (!chapter) return sourcedBlocks();
94
+ try {
95
+ return Array.from(
96
+ document.querySelectorAll('[data-chapter-src="' + chapter + '"] [data-source-line]')
97
+ );
98
+ } catch (_e) {
99
+ return sourcedBlocks();
100
+ }
101
+ }
102
+
103
+ // The block straddling the top of the viewport (greatest top still at/above a
104
+ // reference line just below the viewport edge); falls back to the first block
105
+ // below the fold when scrolled to the very top.
106
+ function topVisibleSourceEl() {
107
+ var blocks = sourcedBlocks();
108
+ if (blocks.length === 0) return null;
109
+ var ref = 4;
110
+ var best = null, bestTop = -Infinity;
111
+ for (var i = 0; i < blocks.length; i++) {
112
+ var top = blocks[i].getBoundingClientRect().top;
113
+ if (top <= ref && top > bestTop) { bestTop = top; best = blocks[i]; }
114
+ }
115
+ if (!best) {
116
+ for (var j = 0; j < blocks.length; j++) {
117
+ if (blocks[j].getBoundingClientRect().top >= 0) { best = blocks[j]; break; }
118
+ }
119
+ }
120
+ return best || blocks[0];
121
+ }
122
+
123
+ // Resolve a scrollTo/highlight target ({line}|{id}|{selector}|{page} or a bare
124
+ // line number) to a DOM element.
125
+ function resolveTarget(target) {
126
+ if (target == null) return null;
127
+ if (typeof target === 'number') target = { line: target };
128
+ if (target.selector) { try { return document.querySelector(target.selector); } catch (_e) { return null; } }
129
+ if (target.id) return document.getElementById(target.id);
130
+ if (target.page != null) { refreshPages(); return pages[clampPage(target.page) - 1] || null; }
131
+ if (target.line != null) {
132
+ var line = Number(target.line);
133
+ var blocks = blocksInChapter(target.chapter);
134
+ var best = null, bestLine = -Infinity;
135
+ for (var i = 0; i < blocks.length; i++) {
136
+ var l = lineOf(blocks[i]);
137
+ if (l != null && l <= line && l > bestLine) { bestLine = l; best = blocks[i]; }
138
+ }
139
+ return best || blocks[0] || null;
140
+ }
141
+ return null;
142
+ }
143
+
144
+ var api = {
145
+ getTotalPages: function () { refreshPages(); return pages.length; },
146
+ getCurrentPage: function () { return currentPage; },
147
+ goToPage: function (n) {
148
+ refreshPages();
149
+ currentPage = clampPage(n);
150
+ scrollToCurrentPage();
151
+ return api.notifyPageChange();
152
+ },
153
+ getPageDimensions: function () {
154
+ refreshPages();
155
+ var page = pages[0] || null;
156
+ var pagesEl = document.querySelector('.pagedjs_pages');
157
+ if (!page) return null;
158
+ return {
159
+ width: currentViewMode === 'single' ? page.offsetWidth : (pagesEl ? pagesEl.scrollWidth : page.offsetWidth),
160
+ height: page.offsetHeight
161
+ };
162
+ },
163
+ firstPage: function () { return api.goToPage(1); },
164
+ prevPage: function (mode) { return api.goToPage(currentPage - pageStep(mode)); },
165
+ nextPage: function (mode) { return api.goToPage(currentPage + pageStep(mode)); },
166
+ lastPage: function () { refreshPages(); return api.goToPage(pages.length); },
167
+ setViewMode: function (mode) {
168
+ refreshPages();
169
+ currentViewMode = mode || 'two-column';
170
+ document.body.classList.remove('view-single', 'view-spread', 'view-two-column');
171
+ if (mode) document.body.classList.add('view-' + mode);
172
+ scrollToCurrentPage();
173
+ return api.notifyPageChange();
174
+ },
175
+ setZoom: function (z) {
176
+ document.documentElement.style.setProperty('--pmd-zoom', z);
177
+ },
178
+ toggleDebugMode: function () {
179
+ debugMode = !debugMode;
180
+ document.body.classList.toggle('debug', debugMode);
181
+ return debugMode;
182
+ },
183
+ notifyPageChange: function () {
184
+ var detail = { currentPage: api.getCurrentPage(), totalPages: pages.length };
185
+ window.dispatchEvent(new CustomEvent('pageChanged', { detail: detail }));
186
+ return detail;
187
+ },
188
+ notifyRenderingComplete: function () {
189
+ window.dispatchEvent(new CustomEvent('renderingComplete', {
190
+ detail: { totalPages: pages.length }
191
+ }));
192
+ },
193
+ // Re-read the page list and recompute the current page from the scroll
194
+ // position, then notify. The incremental preview shell calls this after it
195
+ // splices a chapter's pages into the live DOM (Paged.js does NOT re-run, so
196
+ // the cached page list and counters would otherwise go stale — freezing the
197
+ // toolbar's page number and breaking scroll sync).
198
+ refresh: function () {
199
+ refreshPages();
200
+ currentPage = detectVisiblePage();
201
+ return api.notifyPageChange();
202
+ },
203
+
204
+ // ── ADR 0005 generic primitives ─────────────────────────────────────────
205
+ // Bumped whenever a command/event is added so a hot-updated SPA can
206
+ // feature-detect against an older bundled lib.
207
+ getProtocolVersion: function () { return 2; },
208
+
209
+ // Heading tree with page + source line — powers chapter jump (UX-013), TOC,
210
+ // minimap, scrollspy. Page math needs same-origin paged.js access, so it
211
+ // lives here rather than being derived host-side.
212
+ getOutline: function () {
213
+ refreshPages();
214
+ var hs = Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6'));
215
+ var out = [];
216
+ for (var i = 0; i < hs.length; i++) {
217
+ var h = hs[i];
218
+ var text = (h.textContent || '').trim();
219
+ if (!text) continue;
220
+ out.push({
221
+ level: parseInt(h.tagName.charAt(1), 10),
222
+ text: text,
223
+ id: h.id || null,
224
+ sourceLine: lineOf(h),
225
+ chapter: chapterOf(h),
226
+ page: pageIndexOf(h),
227
+ index: i
228
+ });
229
+ }
230
+ return out;
231
+ },
232
+
233
+ // Single anchored-jump primitive: target {line}|{id}|{selector}|{page}.
234
+ // Returns the resolved {page, sourceLine}. Suppresses the scroll-driven
235
+ // pageChanged/sourceLineChanged echo so host-driven jumps don't loop back.
236
+ scrollTo: function (target, opts) {
237
+ opts = opts || {};
238
+ var el = resolveTarget(target);
239
+ if (!el) return null;
240
+ ignoreScrollUntil = Date.now() + 350;
241
+ el.scrollIntoView({
242
+ behavior: opts.smooth ? 'smooth' : 'instant',
243
+ block: opts.block || 'start',
244
+ inline: 'nearest'
245
+ });
246
+ var page = pageIndexOf(el);
247
+ if (page) currentPage = page;
248
+ return { page: page || currentPage, sourceLine: lineOf(el) };
249
+ },
250
+
251
+ // Source line + page of the block at the top of the viewport (host scroll
252
+ // position read for preview->editor sync).
253
+ getVisibleSource: function () {
254
+ var el = topVisibleSourceEl();
255
+ return el ? { sourceLine: lineOf(el), chapter: chapterOf(el), page: pageIndexOf(el) } : null;
256
+ },
257
+
258
+ // Generic, read-only DOM extraction. fields: 'text'|'id'|'sourceLine'|
259
+ // 'page'|'tag'|'rectTop'|{attr:'name'}. No eval, no innerHTML — this is the
260
+ // forward-compat hook that lets the SPA build find/figure-list/etc. with no
261
+ // further lib change.
262
+ queryDom: function (spec) {
263
+ spec = spec || {};
264
+ if (!spec.selector) return [];
265
+ var fields = spec.fields || ['text'];
266
+ var els;
267
+ try { els = Array.from(document.querySelectorAll(spec.selector)); }
268
+ catch (_e) { return []; }
269
+ if (spec.limit && els.length > spec.limit) els = els.slice(0, spec.limit);
270
+ return els.map(function (el) {
271
+ var row = {};
272
+ for (var i = 0; i < fields.length; i++) {
273
+ var f = fields[i];
274
+ if (f === 'text') row.text = (el.textContent || '').trim();
275
+ else if (f === 'id') row.id = el.id || null;
276
+ else if (f === 'sourceLine') row.sourceLine = lineOf(el);
277
+ else if (f === 'page') row.page = pageIndexOf(el);
278
+ else if (f === 'chapter') row.chapter = chapterOf(el);
279
+ else if (f === 'tag') row.tag = el.tagName.toLowerCase();
280
+ else if (f === 'rectTop') row.rectTop = el.getBoundingClientRect().top;
281
+ else if (f && typeof f === 'object' && f.attr) row['attr:' + f.attr] = el.getAttribute(f.attr);
282
+ }
283
+ return row;
284
+ });
285
+ },
286
+
287
+ // Toggle a marker class on matched elements (DOM write inside the frame).
288
+ // spec: {line?|id?|selector?, group?, scroll?, transient?, transientMs?}.
289
+ // Powers find-in-page, editor-cursor echo, annotations.
290
+ highlight: function (spec) {
291
+ spec = spec || {};
292
+ var group = spec.group || 'default';
293
+ var els = [];
294
+ if (spec.selector) { try { els = Array.from(document.querySelectorAll(spec.selector)); } catch (_e) {} }
295
+ else if (spec.id) { var byId = document.getElementById(spec.id); if (byId) els = [byId]; }
296
+ else if (spec.line != null) { var one = resolveTarget({ line: spec.line }); if (one) els = [one]; }
297
+ for (var i = 0; i < els.length; i++) {
298
+ els[i].classList.add('pmd-hl');
299
+ els[i].setAttribute('data-pmd-hl-group', group);
300
+ }
301
+ if (spec.scroll && els[0]) {
302
+ ignoreScrollUntil = Date.now() + 350;
303
+ els[0].scrollIntoView({ behavior: 'instant', block: 'center', inline: 'nearest' });
304
+ }
305
+ if (spec.transient && els.length) {
306
+ setTimeout(function () {
307
+ for (var j = 0; j < els.length; j++) {
308
+ if (els[j].getAttribute('data-pmd-hl-group') === group) {
309
+ els[j].classList.remove('pmd-hl');
310
+ els[j].removeAttribute('data-pmd-hl-group');
311
+ }
312
+ }
313
+ }, spec.transientMs || 1200);
314
+ }
315
+ return { count: els.length };
316
+ },
317
+
318
+ clearHighlights: function (group) {
319
+ var sel = group ? '.pmd-hl[data-pmd-hl-group="' + group + '"]' : '.pmd-hl';
320
+ var els = Array.from(document.querySelectorAll(sel));
321
+ for (var i = 0; i < els.length; i++) {
322
+ els[i].classList.remove('pmd-hl');
323
+ els[i].removeAttribute('data-pmd-hl-group');
324
+ }
325
+ return { cleared: els.length };
326
+ }
327
+ };
328
+
329
+ window.previewAPI = api;
330
+
331
+ // Default highlight style (preview-only; never part of the PDF build path).
332
+ // Guarded so a minimal/headless DOM (e.g. unit-test harness) can't crash here.
333
+ if (typeof document.createElement === 'function') {
334
+ try {
335
+ var hlStyle = document.createElement('style');
336
+ hlStyle.textContent =
337
+ '.pmd-hl{outline:2px solid var(--pmd-hl-color,#4ea1ff);outline-offset:2px;' +
338
+ 'background:var(--pmd-hl-bg,rgba(78,161,255,.14));' +
339
+ 'transition:outline-color .2s,background .2s;}';
340
+ (document.head || document.documentElement).appendChild(hlStyle);
341
+ } catch (_e) { /* non-fatal: highlight just renders unstyled */ }
342
+ }
343
+
344
+ // Click-to-source: emit elementActivated when the user clicks a source-mapped
345
+ // block. Never preventDefault (links/selection keep working); the host decides
346
+ // whether to act. (ADR 0005)
347
+ if (typeof document.addEventListener === 'function') {
348
+ document.addEventListener('click', function (e) {
349
+ var el = e.target && e.target.closest ? e.target.closest('[data-source-line]') : null;
350
+ if (!el) return;
351
+ window.dispatchEvent(new CustomEvent('elementActivated', {
352
+ detail: { sourceLine: lineOf(el), chapter: chapterOf(el), id: el.id || null, tag: el.tagName.toLowerCase() }
353
+ }));
354
+ }, true);
355
+ }
356
+
357
+ var observedPageCount = 0;
358
+ var pageObserverQueued = false;
359
+ function publishObservedPageCount() {
360
+ pageObserverQueued = false;
361
+ if (window.__PAGED_RENDERED__ === true) return;
362
+ var count = refreshPages().length;
363
+ if (count > observedPageCount) {
364
+ observedPageCount = count;
365
+ window.dispatchEvent(new CustomEvent('pageChanged', {
366
+ detail: { currentPage: count, totalPages: count }
367
+ }));
368
+ }
369
+ }
370
+ var pageObserver = new MutationObserver(function () {
371
+ if (window.__PAGED_RENDERED__ === true) return;
372
+ if (pageObserverQueued) return;
373
+ pageObserverQueued = true;
374
+ window.requestAnimationFrame(publishObservedPageCount);
375
+ });
376
+
377
+ function startPageObserver() {
378
+ var target = document.body || document.documentElement;
379
+ if (!target) return false;
380
+ pageObserver.observe(target, { childList: true, subtree: true });
381
+ return true;
382
+ }
383
+
384
+ if (!startPageObserver()) {
385
+ document.addEventListener('DOMContentLoaded', function onReady() {
386
+ document.removeEventListener('DOMContentLoaded', onReady);
387
+ startPageObserver();
388
+ });
389
+ }
390
+
391
+ // Scroll tracking
392
+ var scrollTimer = null;
393
+ var lastSourceLine = -1;
394
+ window.addEventListener('scroll', function () {
395
+ if (pages.length === 0) refreshPages();
396
+ if (pages.length === 0) return;
397
+ if (scrollTimer) clearTimeout(scrollTimer);
398
+ scrollTimer = setTimeout(function () {
399
+ if (Date.now() < ignoreScrollUntil) return;
400
+ var page = detectVisiblePage();
401
+ if (page !== currentPage) {
402
+ currentPage = page;
403
+ api.notifyPageChange();
404
+ }
405
+ // Emit finer-grained source position for editor sync (ADR 0005).
406
+ var topEl = topVisibleSourceEl();
407
+ var sl = topEl ? lineOf(topEl) : null;
408
+ if (sl != null && sl !== lastSourceLine) {
409
+ lastSourceLine = sl;
410
+ window.dispatchEvent(new CustomEvent('sourceLineChanged', {
411
+ detail: { sourceLine: sl, chapter: chapterOf(topEl), page: pageIndexOf(topEl) }
412
+ }));
413
+ }
414
+ }, 150);
415
+ });
416
+
417
+ // Paged.js calls this when rendering is complete
418
+ window.PagedConfig = window.PagedConfig || {};
419
+ window.PagedConfig.after = function (flow) {
420
+ refreshPages();
421
+ observedPageCount = pages.length;
422
+ pageObserver.disconnect();
423
+ currentPage = 1;
424
+ ignoreScrollUntil = Date.now() + 300;
425
+ window.scrollTo(0, 0);
426
+ console.log('Paged.js rendered ' + pages.length + ' pages');
427
+ api.notifyRenderingComplete();
428
+ setTimeout(api.notifyPageChange, 0);
429
+ };
430
+ })();
@@ -0,0 +1,164 @@
1
+ // print-md preview shell controller (shared by the CLI preview and the Electron
2
+ // viewer). Hosts book.html in an iframe and applies live edits without flicker:
3
+ // - css-update → hot-swap the stylesheet into the active book (no reload)
4
+ // - content-update → re-paginate ONLY the edited chapter and splice it in
5
+ // - full-reload → double-buffer: paginate a hidden iframe, then swap
6
+ // Scroll position is preserved via a chapter-scoped source-line anchor.
7
+ //
8
+ // TRANSPORT IS ABSTRACTED: change events arrive via connectChanges(), which uses
9
+ // a WebSocket by default but honors an injected window.__PMD_CHANGE_SOURCE
10
+ // (subscribe(cb) → unsubscribe) — so an Electron-native host can feed events over
11
+ // IPC instead of HTTP/WS with no change to this controller. The shell HTML sets
12
+ // window.__PMD_HMR to the WS path.
13
+ (function () {
14
+ 'use strict';
15
+ var active = document.getElementById('pmd-active');
16
+ var building = null;
17
+ if (!active) return;
18
+
19
+ // Transparent bridge relay: forward host-toolbar commands (parent → shell) to
20
+ // the active book iframe, and its replies/events back up. Lets the viewer drive
21
+ // the book through the shell with no extra code (thin pass-through).
22
+ window.addEventListener('message', function (e) {
23
+ try {
24
+ if (window.parent !== window && e.source === window.parent) {
25
+ if (active && active.contentWindow) active.contentWindow.postMessage(e.data, '*');
26
+ } else if (active && e.source === active.contentWindow && window.parent !== window) {
27
+ window.parent.postMessage(e.data, '*');
28
+ }
29
+ } catch (_) {}
30
+ });
31
+
32
+ function fdoc(f) { try { return f.contentDocument; } catch (_) { return null; } }
33
+ function fwin(f) { try { return f.contentWindow; } catch (_) { return null; } }
34
+
35
+ function hotCss(p) {
36
+ var d = fdoc(active); if (!d) return;
37
+ var id = 'pmd-hot-' + p.replace(/[^a-z0-9]/gi, '_');
38
+ var prev = d.getElementById(id); if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
39
+ var l = d.createElement('link'); l.rel = 'stylesheet'; l.id = id; l.href = p + '?t=' + Date.now();
40
+ (d.head || d.documentElement).appendChild(l);
41
+ }
42
+
43
+ function chapterOf(el) { var c = el.closest && el.closest('[data-chapter-src]'); return c ? c.getAttribute('data-chapter-src') : null; }
44
+
45
+ // data-source-line resets PER FILE, so the anchor is scoped to its chapter —
46
+ // otherwise the same line number matches an element in a different chapter and
47
+ // restore jumps the view.
48
+ function capture(f) {
49
+ var d = fdoc(f); if (!d) return null;
50
+ var els = d.querySelectorAll('[data-source-line]'), best = null, bestTop = -Infinity;
51
+ for (var i = 0; i < els.length; i++) { var r = els[i].getBoundingClientRect(); if (r.bottom < 0 || r.height === 0) continue; if (r.top <= 80 && r.top > bestTop) { bestTop = r.top; best = els[i]; } }
52
+ if (!best) { for (var j = 0; j < els.length; j++) { var rr = els[j].getBoundingClientRect(); if (rr.bottom > 0 && rr.height > 0) { best = els[j]; break; } } }
53
+ if (!best) return null;
54
+ return { chapter: chapterOf(best), line: best.getAttribute('data-source-line'), offset: best.getBoundingClientRect().top };
55
+ }
56
+
57
+ function restore(f, a) {
58
+ if (!a) return; var w = fwin(f), d = fdoc(f); if (!w || !d) return;
59
+ var scope = a.chapter ? '[data-chapter-src="' + a.chapter + '"] ' : '';
60
+ var el = d.querySelector(scope + '[data-source-line="' + a.line + '"]');
61
+ if (!el) { // exact line gone (edited) → nearest source line WITHIN the chapter
62
+ var els = d.querySelectorAll((a.chapter ? '[data-chapter-src="' + a.chapter + '"] ' : '') + '[data-source-line]');
63
+ var want = parseInt(a.line, 10), best = null, bestDiff = Infinity;
64
+ for (var i = 0; i < els.length; i++) { var ln = parseInt(els[i].getAttribute('data-source-line'), 10); var diff = Math.abs(ln - want); if (diff < bestDiff) { bestDiff = diff; best = els[i]; } }
65
+ el = best;
66
+ }
67
+ if (!el) return;
68
+ w.scrollBy(0, el.getBoundingClientRect().top - a.offset);
69
+ }
70
+
71
+ // Tag each rendered page with the chapter (data-chapter-src) it contains, so a
72
+ // single edited chapter's pages can be located and replaced.
73
+ function tagPages(f) {
74
+ var d = fdoc(f); if (!d) return;
75
+ var pages = d.querySelectorAll('.pagedjs_page');
76
+ for (var i = 0; i < pages.length; i++) {
77
+ if (pages[i].getAttribute('data-chapter-src')) continue;
78
+ var ch = pages[i].querySelector('.pmd-chapter[data-chapter-src]');
79
+ if (ch) pages[i].setAttribute('data-chapter-src', ch.getAttribute('data-chapter-src'));
80
+ }
81
+ }
82
+
83
+ // Run cb once the frame has paginated (renderingComplete) or, for static
84
+ // output, once pages exist.
85
+ function onReady(f, cb) {
86
+ var w = fwin(f), d = fdoc(f); if (!w || !d) { cb(); return; }
87
+ var has = !!d.querySelector('script[src*="paged.polyfill"], script[src*="pagedjs"]');
88
+ if (has) { var done = false; var g = function () { if (done) return; done = true; cb(); }; w.addEventListener('renderingComplete', g, { once: true }); setTimeout(g, 180000); }
89
+ else { var t = 0; (function p() { var dd = fdoc(f); if (dd && dd.querySelectorAll('.pagedjs_page').length > 0) { cb(); return; } if (t++ < 800) setTimeout(p, 25); else cb(); })(); }
90
+ }
91
+
92
+ // Full-document double-buffer: paginate a hidden iframe, then swap it in.
93
+ function swap() {
94
+ if (building && building.parentNode) building.parentNode.removeChild(building); building = null;
95
+ var anchor = capture(active);
96
+ var f = document.createElement('iframe');
97
+ f.style.visibility = 'hidden'; f.setAttribute('aria-hidden', 'true');
98
+ f.src = '/book.html?pmdshell=1&bust=' + Date.now(); building = f;
99
+ var finished = false;
100
+ function finish() {
101
+ if (finished || building !== f) return; finished = true;
102
+ restore(f, anchor);
103
+ f.style.visibility = 'visible'; f.removeAttribute('aria-hidden');
104
+ var old = active; active = f; building = null; tagPages(active);
105
+ requestAnimationFrame(function () { requestAnimationFrame(function () { if (old && old.parentNode) old.parentNode.removeChild(old); }); });
106
+ }
107
+ f.addEventListener('load', function () { onReady(f, finish); });
108
+ document.body.appendChild(f);
109
+ }
110
+
111
+ // INCREMENTAL: re-paginate ONLY the edited chapter in a hidden iframe, then
112
+ // replace that chapter's pages in the live view. Page numbers are a live CSS
113
+ // counter so they re-flow automatically; the toolbar interface is refreshed
114
+ // (Paged.js didn't re-run). Falls back to a full double-buffer swap on failure.
115
+ function spliceChapter(file) {
116
+ var anchor = capture(active);
117
+ tagPages(active);
118
+ var f = document.createElement('iframe'); f.style.visibility = 'hidden'; f.setAttribute('aria-hidden', 'true');
119
+ f.src = '/__chapter?file=' + encodeURIComponent(file) + '&t=' + Date.now();
120
+ f.addEventListener('load', function () {
121
+ onReady(f, function () {
122
+ try {
123
+ var ad = fdoc(active), sd = fdoc(f);
124
+ var container = ad.querySelector('.pagedjs_pages') || ad.body;
125
+ var oldPages = [].slice.call(ad.querySelectorAll('.pagedjs_page[data-chapter-src="' + file + '"]'));
126
+ var newPages = [].slice.call(sd.querySelectorAll('.pagedjs_page'));
127
+ if (!oldPages.length || !newPages.length) throw new Error('no pages ' + oldPages.length + '/' + newPages.length);
128
+ var at = oldPages[0];
129
+ for (var i = 0; i < newPages.length; i++) {
130
+ var imp = ad.importNode(newPages[i], true);
131
+ imp.setAttribute('data-chapter-src', file);
132
+ container.insertBefore(imp, at);
133
+ }
134
+ for (var j = 0; j < oldPages.length; j++) oldPages[j].parentNode.removeChild(oldPages[j]);
135
+ restore(active, anchor);
136
+ try { var api = fwin(active) && fwin(active).previewAPI; if (api && api.refresh) api.refresh(); } catch (_) {}
137
+ } catch (err) { if (window.console) console.warn('[pmd] incremental splice failed, full swap:', err); swap(); }
138
+ if (f.parentNode) f.parentNode.removeChild(f);
139
+ });
140
+ });
141
+ document.body.appendChild(f);
142
+ }
143
+
144
+ function tagInitial() { onReady(active, function () { tagPages(active); }); }
145
+ if (active.contentDocument && active.contentDocument.readyState === 'complete') tagInitial();
146
+ active.addEventListener('load', tagInitial);
147
+
148
+ // Transport: WS by default; honor an injected change source for Electron-native.
149
+ function connectChanges(onMsg) {
150
+ var src = window.__PMD_CHANGE_SOURCE;
151
+ if (src && typeof src.subscribe === 'function') return src.subscribe(onMsg);
152
+ var path = window.__PMD_HMR || '/__print-md-hmr';
153
+ var ws = new WebSocket(location.origin.replace(/^http/, 'ws') + path);
154
+ ws.onmessage = function (e) { var m; try { m = JSON.parse(e.data); } catch (_) { return; } onMsg(m); };
155
+ return function () { try { ws.close(); } catch (_) {} };
156
+ }
157
+
158
+ connectChanges(function (m) {
159
+ if (!m || !m.type) return;
160
+ if (m.type === 'css-update' && m.path) { hotCss(m.path); return; }
161
+ if (m.type === 'content-update' && m.file) { spliceChapter(m.file); return; }
162
+ if (m.type === 'full-reload') { swap(); return; }
163
+ });
164
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dimm-city/print-md",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Markdown-to-PDF converter for professional print layout using Paged.js and Ghostscript.",
5
5
  "author": "itlackey",
6
6
  "license": "MPL-2.0",