gutterpress 0.9.0-alpha.1 → 0.9.0-alpha.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.
Files changed (30) hide show
  1. package/dist/api/index.js +1 -1
  2. package/dist/{audit-k16v3n98.js → audit-nhn2pjz3.js} +3 -3
  3. package/dist/{build-8tqr4gbb.js → build-san7fv2z.js} +3 -3
  4. package/dist/{cli-rkqxbnkq.js → cli-e5zhb0xs.js} +1 -1
  5. package/dist/{cli-yp0d2hp8.js → cli-k4bd06sd.js} +377 -78
  6. package/dist/{cli-v2dze8ja.js → cli-najycadg.js} +1 -1
  7. package/dist/cli.js +15 -15
  8. package/dist/{doctor-57czxy3q.js → doctor-hrk0kxxz.js} +2 -2
  9. package/dist/{index-m3pa4wyf.js → index-bynn850m.js} +379 -80
  10. package/dist/index.js +1 -1
  11. package/dist/lib/markdown/assemble.d.ts +4 -6
  12. package/dist/lib/markdown/chapter-id.d.ts +1 -1
  13. package/dist/lib/markdown/index.d.ts +1 -1
  14. package/dist/{lint-4sq0dvf6.js → lint-96j9hrj4.js} +3 -3
  15. package/dist/{new-f2nmyr5b.js → new-8p38wavc.js} +3 -3
  16. package/dist/{pagedjs-interface-80g3xa8p.js → pagedjs-interface-qxvzgwd7.js} +92 -33
  17. package/dist/{plugin-xskq3dtx.js → plugin-ees6nhkc.js} +3 -3
  18. package/dist/{preflight-epcffags.js → preflight-1q6c2edh.js} +3 -3
  19. package/dist/preview/file-watcher.d.ts +39 -17
  20. package/dist/preview/http-server.d.ts +5 -2
  21. package/dist/preview/server-context.d.ts +2 -0
  22. package/dist/preview-shell-6dqexx1m.js +581 -0
  23. package/dist/{preview-xf5dktz2.js → preview-y5a2zen1.js} +3 -3
  24. package/dist/{publish-97vdwq31.js → publish-rm9yb3wh.js} +3 -3
  25. package/dist/render.js +10 -19
  26. package/dist/{repair-cz4z4hkg.js → repair-zgq7q2g6.js} +3 -3
  27. package/dist/server.d.ts +2 -0
  28. package/dist/{validate-swzejnd8.js → validate-nr0xa6sa.js} +3 -3
  29. package/package.json +1 -1
  30. package/dist/preview-shell-rghtfmc8.js +0 -155
@@ -1164,17 +1164,6 @@ async function assembleBookHtml(opts) {
1164
1164
  throw new Error("No markdown files to render");
1165
1165
  }
1166
1166
  const md = createMarkdownRenderer(opts.plugins);
1167
- let sourceChapter = null;
1168
- if (opts.wrapChapters) {
1169
- const renderToken = md.renderer.renderToken.bind(md.renderer);
1170
- md.renderer.renderToken = (tokens, idx, options) => {
1171
- const token = tokens[idx];
1172
- if (sourceChapter !== null && token.level === 0 && token.map !== null && token.type.endsWith("_open")) {
1173
- token.attrSet("data-chapter-src", sourceChapter);
1174
- }
1175
- return renderToken(tokens, idx, options);
1176
- };
1177
- }
1178
1167
  let bodyContent = "";
1179
1168
  const imageRefs = new Set;
1180
1169
  for (const file of files) {
@@ -1187,20 +1176,22 @@ async function assembleBookHtml(opts) {
1187
1176
  throw new Error(`Failed to read file ${file}: ${errorMsg}`);
1188
1177
  }
1189
1178
  const env = {};
1190
- sourceChapter = opts.wrapChapters ? chapterId : null;
1191
- let rendered;
1192
- try {
1193
- rendered = md.render(content, env);
1194
- } finally {
1195
- sourceChapter = null;
1196
- }
1179
+ const rendered = md.render(content, env);
1197
1180
  if (env.layoutWarnings && env.layoutWarnings.length > 0) {
1198
1181
  opts.onChapterWarnings?.(chapterId, env.layoutWarnings);
1199
1182
  }
1200
1183
  for (const ref of env.imageRefs ?? [])
1201
1184
  imageRefs.add(ref);
1202
- bodyContent += rendered + `
1185
+ if (opts.wrapChapters) {
1186
+ const safe = chapterId.replace(/&/g, "&").replace(/"/g, """);
1187
+ bodyContent += `<div class="gutterpress-chapter" data-chapter-src="${safe}">
1188
+ ${rendered}
1189
+ </div>
1190
+ `;
1191
+ } else {
1192
+ bodyContent += rendered + `
1203
1193
  `;
1194
+ }
1204
1195
  }
1205
1196
  if (opts.onImageRefs) {
1206
1197
  for (const ref of collectHtmlImageRefs(bodyContent))
@@ -3387,7 +3378,7 @@ import git from "isomorphic-git";
3387
3378
  // package.json
3388
3379
  var package_default = {
3389
3380
  name: "gutterpress",
3390
- version: "0.9.0-alpha.1",
3381
+ version: "0.9.0-alpha.2",
3391
3382
  description: "Markdown-to-PDF converter for professional print layout using Paged.js and Ghostscript.",
3392
3383
  author: "itlackey",
3393
3384
  license: "MPL-2.0",
@@ -3651,13 +3642,13 @@ var favicon_default = "./favicon-wkbm9cjn.ico";
3651
3642
  var manifest_schema_default = "./manifest.schema-z61rzw44.json";
3652
3643
 
3653
3644
  // src/assets/preview/scripts/pagedjs-interface.js
3654
- var pagedjs_interface_default = "./pagedjs-interface-80g3xa8p.js";
3645
+ var pagedjs_interface_default = "./pagedjs-interface-qxvzgwd7.js";
3655
3646
 
3656
3647
  // src/assets/preview/scripts/pagedjs-bridge.js
3657
3648
  var pagedjs_bridge_default = "./pagedjs-bridge-vn4hk9fx.js";
3658
3649
 
3659
3650
  // src/assets/preview/scripts/preview-shell.js
3660
- var preview_shell_default = "./preview-shell-rghtfmc8.js";
3651
+ var preview_shell_default = "./preview-shell-6dqexx1m.js";
3661
3652
 
3662
3653
  // src/assets/vendor/paged.polyfill.js
3663
3654
  var paged_polyfill_default = "./paged.polyfill-n95pbxfn.js";
@@ -7668,12 +7659,17 @@ async function renderPreviewBook(inputPath, config, opts) {
7668
7659
  }
7669
7660
  });
7670
7661
  }
7671
- function injectPreviewScripts(html) {
7662
+ function injectPreviewScripts(html, pageIsolateChapters) {
7672
7663
  const iface = `<script src="/preview/scripts/pagedjs-interface.js"></script>
7673
7664
  ` + `<script src="/preview/scripts/pagedjs-bridge.js"></script>
7674
7665
  `;
7675
- return html.replace(pagedjsPolyfillTagRegex(), iface + BREAK_INSIDE_HANDLER + `
7666
+ let output = html.replace(pagedjsPolyfillTagRegex(), iface + BREAK_INSIDE_HANDLER + `
7676
7667
  <script src="/vendor/paged.polyfill.js"></script>`);
7668
+ if (pageIsolateChapters && /<\/head>/i.test(output)) {
7669
+ output = output.replace(/<\/head>/i, `<style>.gutterpress-chapter{break-before:page}</style>
7670
+ </head>`);
7671
+ }
7672
+ return output;
7677
7673
  }
7678
7674
  async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
7679
7675
  if (!inputPath) {
@@ -7682,9 +7678,10 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
7682
7678
  return;
7683
7679
  }
7684
7680
  const nextAssets = new Map;
7681
+ const incremental = incrementalPreviewEnabled();
7685
7682
  const html = await renderPreviewBook(inputPath, config, {
7686
7683
  files: config.source?.files ?? null,
7687
- wrapChapters: true,
7684
+ wrapChapters: incremental,
7688
7685
  onCssAssets: (copies) => {
7689
7686
  for (const copy of copies)
7690
7687
  nextAssets.set(copy.to, copy.from);
@@ -7693,7 +7690,39 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
7693
7690
  cssAssets.clear();
7694
7691
  for (const [to, from] of nextAssets)
7695
7692
  cssAssets.set(to, from);
7696
- await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html), "utf-8");
7693
+ await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, incremental), "utf-8");
7694
+ }
7695
+ async function renderChapterPreviewHtml(inputPath, file, config) {
7696
+ const html = await renderPreviewBook(inputPath, config, {
7697
+ files: [canonicalChapterId(file)],
7698
+ wrapChapters: true
7699
+ });
7700
+ return injectPreviewScripts(html, true);
7701
+ }
7702
+ function describeChanges(changes, inputResolved) {
7703
+ const files = [];
7704
+ for (const [changedPath, event] of changes) {
7705
+ const relative2 = path10.relative(inputResolved, path10.resolve(changedPath));
7706
+ if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative2))
7707
+ continue;
7708
+ files.push({
7709
+ relativePath: canonicalChapterId(relative2),
7710
+ ext: path10.extname(changedPath).toLowerCase(),
7711
+ event
7712
+ });
7713
+ }
7714
+ return files;
7715
+ }
7716
+ function decideBroadcast(files, changeCount, incremental) {
7717
+ const only = files.length === 1 ? files[0] : null;
7718
+ if (incremental && changeCount === 1 && only?.ext === ".md" && only.event !== "unlink" && only.event !== "unlinkDir") {
7719
+ return {
7720
+ kind: "chapter-splice",
7721
+ chapterId: canonicalChapterId(only.relativePath),
7722
+ relativePath: only.relativePath
7723
+ };
7724
+ }
7725
+ return { kind: "full-reload" };
7697
7726
  }
7698
7727
  function isDotPathUnderRoot(candidatePath, root) {
7699
7728
  const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -7812,21 +7841,43 @@ function createFileWatcher(state) {
7812
7841
  let closed = false;
7813
7842
  const pendingChanges = new Map;
7814
7843
  const suppressInitialExternalAdds = new Set;
7844
+ const settledWrites = new Map;
7845
+ let immediatePending = false;
7815
7846
  function scheduleRebuild() {
7816
7847
  if (closed)
7817
7848
  return;
7818
7849
  if (state.rebuildTimer)
7819
7850
  clearTimeout(state.rebuildTimer);
7820
- state.rebuildTimer = setTimeout(runRebuild, DEBOUNCE.FILE_WATCH);
7821
- }
7822
- function recordChange(event, filePath2) {
7851
+ const timer = setTimeout(() => {
7852
+ if (state.rebuildTimer === timer)
7853
+ state.rebuildTimer = null;
7854
+ runRebuild();
7855
+ }, DEBOUNCE.FILE_WATCH);
7856
+ state.rebuildTimer = timer;
7857
+ }
7858
+ async function recordChange(event, filePath2) {
7823
7859
  if (closed)
7824
7860
  return;
7825
- debug(`File ${event}: ${filePath2}`);
7826
- pendingChanges.set(path10.resolve(filePath2), event);
7861
+ const target = path10.resolve(filePath2);
7862
+ const settled = settledWrites.get(target);
7863
+ if (settled && event !== "unlink" && Date.now() <= settled.expiresAt) {
7864
+ const diskContent = await fsp4.readFile(target, "utf8").catch(() => null);
7865
+ if (closed)
7866
+ return;
7867
+ if (settledWrites.get(target) === settled && diskContent === settled.content) {
7868
+ debug(`Suppressed settled-write watcher echo: ${target}`);
7869
+ return;
7870
+ }
7871
+ }
7872
+ if (settledWrites.get(target) === settled)
7873
+ settledWrites.delete(target);
7874
+ debug(`File ${event}: ${target}`);
7875
+ pendingChanges.set(target, event);
7827
7876
  scheduleRebuild();
7828
7877
  }
7829
- watcher.on("all", recordChange);
7878
+ watcher.on("all", (event, filePath2) => {
7879
+ recordChange(event, filePath2);
7880
+ });
7830
7881
  externalWatcher.on("all", (event, filePath2) => {
7831
7882
  const target = path10.resolve(filePath2);
7832
7883
  if (!desiredExternals.has(target))
@@ -7835,6 +7886,43 @@ function createFileWatcher(state) {
7835
7886
  return;
7836
7887
  recordChange(event, target);
7837
7888
  });
7889
+ function isWatchedSource(target) {
7890
+ const rel = path10.relative(inputResolved, target);
7891
+ const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel);
7892
+ if (inProject)
7893
+ return !isIgnoredWatchPath(target, inputResolved);
7894
+ return desiredExternals.has(target);
7895
+ }
7896
+ function acceptSettledWrite(target, writtenContent) {
7897
+ const settled = { content: writtenContent, expiresAt: Date.now() + 2000 };
7898
+ settledWrites.set(target, settled);
7899
+ const expiry = setTimeout(() => {
7900
+ if (settledWrites.get(target) === settled)
7901
+ settledWrites.delete(target);
7902
+ }, 2000);
7903
+ expiry.unref?.();
7904
+ pendingChanges.set(target, "change");
7905
+ immediatePending = true;
7906
+ if (state.rebuildTimer) {
7907
+ clearTimeout(state.rebuildTimer);
7908
+ state.rebuildTimer = null;
7909
+ }
7910
+ if (!state.isRebuilding)
7911
+ runRebuild();
7912
+ }
7913
+ state.notifySettledWrite = (filePath2, writtenContent) => {
7914
+ if (closed)
7915
+ return;
7916
+ const target = path10.resolve(filePath2);
7917
+ if (isWatchedSource(target)) {
7918
+ acceptSettledWrite(target, writtenContent);
7919
+ return;
7920
+ }
7921
+ syncQueue.then(() => {
7922
+ if (!closed && isWatchedSource(target))
7923
+ acceptSettledWrite(target, writtenContent);
7924
+ });
7925
+ };
7838
7926
  let watchedExternalRoots = new Set;
7839
7927
  let syncQueue = Promise.resolve();
7840
7928
  function syncExternalWatches() {
@@ -7886,10 +7974,15 @@ function createFileWatcher(state) {
7886
7974
  async function runRebuild() {
7887
7975
  if (closed || state.isRebuilding)
7888
7976
  return;
7977
+ if (state.rebuildTimer) {
7978
+ clearTimeout(state.rebuildTimer);
7979
+ state.rebuildTimer = null;
7980
+ }
7889
7981
  const changes = [...pendingChanges.entries()];
7890
7982
  pendingChanges.clear();
7891
7983
  if (changes.length === 0)
7892
7984
  return;
7985
+ immediatePending = false;
7893
7986
  state.isRebuilding = true;
7894
7987
  let resolveInFlight;
7895
7988
  const inFlight = new Promise((resolve10) => {
@@ -7909,8 +8002,14 @@ function createFileWatcher(state) {
7909
8002
  await generateAndWriteHtml(inputResolved, state.tempDir, updatedConfig, state.cssAssets);
7910
8003
  if (closed)
7911
8004
  return;
7912
- state.previewServer?.broadcastReload();
7913
- info(changes.length > 1 ? `Preview updated (${changes.length} files changed — full reload)` : "Preview updated");
8005
+ const decision = decideBroadcast(describeChanges(changes, inputResolved), changes.length, incrementalPreviewEnabled());
8006
+ if (decision.kind === "chapter-splice") {
8007
+ state.previewServer?.broadcastContentUpdate(decision.chapterId);
8008
+ info(`Chapter updated: ${decision.relativePath}`);
8009
+ } else {
8010
+ state.previewServer?.broadcastReload();
8011
+ info(changes.length > 1 ? `Preview updated (${changes.length} files changed — full reload)` : "Preview updated");
8012
+ }
7914
8013
  } catch (err) {
7915
8014
  error("Failed to regenerate preview:", err);
7916
8015
  } finally {
@@ -7918,13 +8017,19 @@ function createFileWatcher(state) {
7918
8017
  resolveInFlight();
7919
8018
  if (state.rebuildPromise === inFlight)
7920
8019
  state.rebuildPromise = null;
7921
- if (!closed && pendingChanges.size > 0)
7922
- scheduleRebuild();
8020
+ if (!closed && pendingChanges.size > 0) {
8021
+ if (immediatePending)
8022
+ runRebuild();
8023
+ else
8024
+ scheduleRebuild();
8025
+ }
7923
8026
  }
7924
8027
  }
7925
8028
  const closeBookWatcher = watcher.close.bind(watcher);
7926
8029
  watcher.close = async () => {
7927
8030
  closed = true;
8031
+ state.notifySettledWrite = null;
8032
+ settledWrites.clear();
7928
8033
  await syncQueue.catch(() => {});
7929
8034
  await state.rebuildPromise?.catch(() => {});
7930
8035
  await externalWatcher.close();
@@ -8077,6 +8182,7 @@ function createServerState(inputPath, tempDir, config, options) {
8077
8182
  rebuildTimer: null,
8078
8183
  isRebuilding: false,
8079
8184
  rebuildPromise: null,
8185
+ notifySettledWrite: null,
8080
8186
  previewServer: null,
8081
8187
  isShuttingDown: false,
8082
8188
  tempDir,
@@ -8089,6 +8195,7 @@ function createServerState(inputPath, tempDir, config, options) {
8089
8195
  // src/preview/http-server.ts
8090
8196
  import http2 from "node:http";
8091
8197
  import net from "node:net";
8198
+ import { randomUUID } from "node:crypto";
8092
8199
  import { readFile as readFile21, stat as stat4 } from "node:fs/promises";
8093
8200
  import path12 from "path";
8094
8201
  import { WebSocket, WebSocketServer } from "ws";
@@ -8123,7 +8230,8 @@ function openPath(filePath2) {
8123
8230
 
8124
8231
  // src/preview/http-server.ts
8125
8232
  var HMR_PATH = "/__gutterpress-hmr";
8126
- var HMR_CLIENT_SNIPPET = `
8233
+ function hmrClientSnippet(initialRevision, instanceId) {
8234
+ return `
8127
8235
  <script>
8128
8236
  (function () {
8129
8237
  // When loaded by the preview SHELL (iframe double-buffer), the shell loads us
@@ -8133,6 +8241,10 @@ var HMR_CLIENT_SNIPPET = `
8133
8241
  // for scroll-anchor and reload.
8134
8242
  if (/[?&]gutterpressshell=1/.test(location.search)) return;
8135
8243
  var ANCHOR_KEY = 'gutterpress-scroll-anchor';
8244
+ var appliedRevision = ${initialRevision};
8245
+ var appliedInstance = ${JSON.stringify(instanceId)};
8246
+ var readyToAcknowledge = false;
8247
+ var reloadRequested = false;
8136
8248
 
8137
8249
  // Find the element nearest the top of the viewport that carries a source
8138
8250
  // line (markdown-it-source-map emits data-source-line on block elements).
@@ -8183,7 +8295,13 @@ var HMR_CLIENT_SNIPPET = `
8183
8295
  }
8184
8296
  }
8185
8297
  if (el) {
8186
- window.scrollBy(0, el.getBoundingClientRect().top - a.offset);
8298
+ window.scrollBy({
8299
+ top: el.getBoundingClientRect().top - a.offset,
8300
+ behavior: 'instant'
8301
+ });
8302
+ if (window.previewAPI && typeof window.previewAPI.refresh === 'function') {
8303
+ window.previewAPI.refresh();
8304
+ }
8187
8305
  return;
8188
8306
  }
8189
8307
  if (tries++ < 240) setTimeout(attempt, 25); // wait for pagination
@@ -8191,44 +8309,100 @@ var HMR_CLIENT_SNIPPET = `
8191
8309
  }
8192
8310
  var restored = false;
8193
8311
  function restoreOnce() { if (restored) return; restored = true; restoreAnchor(); }
8312
+ function finishInitialRender() {
8313
+ restoreOnce();
8314
+ readyToAcknowledge = true;
8315
+ acknowledge();
8316
+ }
8194
8317
  // CRITICAL ordering: when the Paged.js polyfill is present it re-paginates on
8195
- // load and its PagedConfig.after calls scrollTo(0,0) THEN fires
8318
+ // load and its PagedConfig.after scrolls to the document start THEN fires
8196
8319
  // 'renderingComplete'. So in engine mode we MUST wait for that event —
8197
- // restoring earlier would be wiped by the scrollTo(0,0). In static mode (no
8320
+ // restoring earlier would be wiped by that initial scroll. In static mode (no
8198
8321
  // engine) the content is final immediately, so restore right after load.
8199
- var hasEngine = !!document.querySelector('script[src*="paged.polyfill"], script[src*="pagedjs"]');
8200
- window.addEventListener('renderingComplete', restoreOnce);
8322
+ var hasEngine = !!document.querySelector('script[src*="paged.polyfill"]');
8323
+ window.addEventListener('renderingComplete', finishInitialRender);
8201
8324
  if (!hasEngine) {
8202
8325
  if (document.readyState === 'loading') {
8203
- document.addEventListener('DOMContentLoaded', function () { setTimeout(restoreOnce, 50); });
8326
+ document.addEventListener('DOMContentLoaded', function () { setTimeout(finishInitialRender, 50); });
8204
8327
  } else {
8205
- setTimeout(restoreOnce, 50);
8328
+ setTimeout(finishInitialRender, 50);
8206
8329
  }
8207
8330
  }
8208
- setTimeout(restoreOnce, 10000); // safety net if renderingComplete never fires
8331
+ var ws = null;
8332
+ var reconnectTimer = null;
8333
+ var reconnectDelay = 250;
8334
+ var stopped = false;
8209
8335
 
8210
- var ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '${HMR_PATH}');
8211
- ws.onmessage = function (e) {
8212
- var msg;
8213
- try { msg = JSON.parse(e.data); } catch (_) { return; }
8214
- if (msg.type === 'full-reload') {
8336
+ function acknowledge() {
8337
+ if (!readyToAcknowledge || !ws || ws.readyState !== 1) return;
8338
+ try {
8339
+ ws.send(JSON.stringify({
8340
+ type: 'reload-applied',
8341
+ instance: appliedInstance,
8342
+ revision: appliedRevision
8343
+ }));
8344
+ } catch (_) {}
8345
+ }
8346
+
8347
+ function connect() {
8348
+ if (stopped) return;
8349
+ ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '${HMR_PATH}');
8350
+ ws.onopen = function () {
8351
+ reconnectDelay = 250;
8352
+ acknowledge();
8353
+ };
8354
+ ws.onmessage = function (e) {
8355
+ var msg;
8356
+ try { msg = JSON.parse(e.data); } catch (_) { return; }
8357
+ if (
8358
+ msg.type !== 'reload-state' &&
8359
+ msg.type !== 'full-reload' &&
8360
+ msg.type !== 'content-update'
8361
+ ) return;
8362
+ var instance = typeof msg.instance === 'string' ? msg.instance : null;
8363
+ var revision = Number(msg.revision);
8364
+ if (!instance || !Number.isSafeInteger(revision) || revision < 0) return;
8365
+ if (instance === appliedInstance && revision <= appliedRevision) {
8366
+ acknowledge();
8367
+ return;
8368
+ }
8369
+ if (reloadRequested) return;
8370
+ reloadRequested = true;
8215
8371
  try { var a = captureAnchor(); if (a) sessionStorage.setItem(ANCHOR_KEY, JSON.stringify(a)); } catch (_) {}
8216
8372
  location.reload();
8217
- return;
8218
- }
8219
- };
8373
+ };
8374
+ ws.onclose = function () {
8375
+ ws = null;
8376
+ if (stopped || reconnectTimer !== null) return;
8377
+ reconnectTimer = setTimeout(function () {
8378
+ reconnectTimer = null;
8379
+ connect();
8380
+ }, reconnectDelay);
8381
+ reconnectDelay = Math.min(reconnectDelay * 2, 5000);
8382
+ };
8383
+ ws.onerror = function () { try { ws.close(); } catch (_) {} };
8384
+ }
8385
+
8386
+ window.addEventListener('beforeunload', function () {
8387
+ stopped = true;
8388
+ if (reconnectTimer !== null) clearTimeout(reconnectTimer);
8389
+ });
8390
+ connect();
8220
8391
  })();
8221
8392
  </script>
8222
8393
  `;
8223
- var SHELL_HTML = `<!doctype html>
8394
+ }
8395
+ function shellHtml(initialRevision, instanceId) {
8396
+ return `<!doctype html>
8224
8397
  <html lang="en"><head><meta charset="utf-8"><title>gutterpress preview</title>
8225
8398
  <style>html,body{margin:0;height:100%;background:#fff;overflow:hidden}
8226
8399
  iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}</style>
8227
8400
  </head><body>
8228
8401
  <iframe id="gutterpress-active" src="/book.html?gutterpressshell=1" title="preview"></iframe>
8229
- <script>window.__GUTTERPRESS_HMR=${JSON.stringify(HMR_PATH)};</script>
8402
+ <script>window.__GUTTERPRESS_HMR=${JSON.stringify(HMR_PATH)};window.__GUTTERPRESS_INSTANCE=${JSON.stringify(instanceId)};window.__GUTTERPRESS_REVISION=${initialRevision};</script>
8230
8403
  <script src="/preview/scripts/preview-shell.js"></script>
8231
8404
  </body></html>`;
8405
+ }
8232
8406
  function probePort(port, host) {
8233
8407
  return new Promise((resolve10) => {
8234
8408
  const srv = net.createServer();
@@ -8270,13 +8444,14 @@ async function findAvailablePort(startPort, host = "127.0.0.1") {
8270
8444
  }
8271
8445
  throw new BuildError(`Could not find an available port on ${host} from ${firstPort} to ${port - 1}; all are already in use. ` + "Stop another preview server or pass --port with an available port.", undefined, { cause: lastBusyError });
8272
8446
  }
8273
- function injectHmrClient(html) {
8447
+ function injectHmrClient(html, revision, instanceId) {
8448
+ const snippet = hmrClientSnippet(revision, instanceId);
8274
8449
  const closingBody = html.lastIndexOf("</body>");
8275
8450
  if (closingBody === -1)
8276
- return html + HMR_CLIENT_SNIPPET;
8277
- return html.slice(0, closingBody) + HMR_CLIENT_SNIPPET + html.slice(closingBody);
8451
+ return html + snippet;
8452
+ return html.slice(0, closingBody) + snippet + html.slice(closingBody);
8278
8453
  }
8279
- async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders = {}) {
8454
+ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders = {}, hmrRevision = 0, hmrInstance = "") {
8280
8455
  let filePath2 = absPath;
8281
8456
  try {
8282
8457
  const s = await stat4(filePath2);
@@ -8297,7 +8472,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8297
8472
  }
8298
8473
  const isHtml = filePath2.endsWith(".html") || filePath2.endsWith(".htm");
8299
8474
  if (isHtml) {
8300
- const withHmr = injectHmrClient(data.toString("utf-8"));
8475
+ const withHmr = injectHmrClient(data.toString("utf-8"), hmrRevision, hmrInstance);
8301
8476
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
8302
8477
  res.end(withHmr);
8303
8478
  return;
@@ -8316,11 +8491,88 @@ function matchesEmbedded(urlPathname) {
8316
8491
  return EMBEDDED_PREFIXES.some((p) => urlPathname.startsWith(p));
8317
8492
  }
8318
8493
  async function createPreviewServer(state, port) {
8319
- const clients = new Set;
8494
+ const ACK_RETRY_MS = 2000;
8495
+ const MAX_ACK_RETRIES = 95;
8496
+ const instanceId = randomUUID();
8497
+ let reloadRevision = 0;
8498
+ const clients = new Map;
8499
+ function sendRevision(ws, type, file) {
8500
+ if (ws.readyState !== WebSocket.OPEN)
8501
+ return;
8502
+ try {
8503
+ ws.send(JSON.stringify({
8504
+ type,
8505
+ instance: instanceId,
8506
+ revision: reloadRevision,
8507
+ ...file ? { file } : {}
8508
+ }));
8509
+ } catch {}
8510
+ }
8511
+ function waitForAcknowledgement(ws) {
8512
+ const client = clients.get(ws);
8513
+ if (!client)
8514
+ return;
8515
+ if (client.acknowledgedRevision >= reloadRevision) {
8516
+ if (client.retryTimer !== null)
8517
+ clearTimeout(client.retryTimer);
8518
+ client.retryTimer = null;
8519
+ client.retryAttempts = 0;
8520
+ return;
8521
+ }
8522
+ if (client.retryTimer !== null)
8523
+ return;
8524
+ client.retryTimer = setTimeout(() => {
8525
+ const current = clients.get(ws);
8526
+ if (!current)
8527
+ return;
8528
+ current.retryTimer = null;
8529
+ if (current.retryAttempts >= MAX_ACK_RETRIES) {
8530
+ ws.terminate();
8531
+ return;
8532
+ }
8533
+ current.retryAttempts++;
8534
+ sendRevision(ws, "reload-state");
8535
+ waitForAcknowledgement(ws);
8536
+ }, ACK_RETRY_MS);
8537
+ client.retryTimer.unref?.();
8538
+ }
8539
+ function removeClient(ws) {
8540
+ const client = clients.get(ws);
8541
+ if (client && client.retryTimer !== null)
8542
+ clearTimeout(client.retryTimer);
8543
+ clients.delete(ws);
8544
+ }
8320
8545
  const wss = new WebSocketServer({ noServer: true });
8321
8546
  wss.on("connection", (ws) => {
8322
- clients.add(ws);
8323
- ws.on("close", () => clients.delete(ws));
8547
+ clients.set(ws, { acknowledgedRevision: -1, retryAttempts: 0, retryTimer: null });
8548
+ ws.on("message", (raw) => {
8549
+ let message;
8550
+ try {
8551
+ message = JSON.parse(raw.toString());
8552
+ } catch {
8553
+ return;
8554
+ }
8555
+ if (!message || typeof message !== "object")
8556
+ return;
8557
+ const payload = message;
8558
+ if (payload.type !== "reload-applied")
8559
+ return;
8560
+ const revision = Number(payload.revision);
8561
+ if (payload.instance !== instanceId || !Number.isSafeInteger(revision) || revision < 0 || revision > reloadRevision)
8562
+ return;
8563
+ const client = clients.get(ws);
8564
+ if (!client)
8565
+ return;
8566
+ client.acknowledgedRevision = Math.max(client.acknowledgedRevision, revision);
8567
+ waitForAcknowledgement(ws);
8568
+ });
8569
+ ws.on("close", () => removeClient(ws));
8570
+ ws.on("error", () => {
8571
+ removeClient(ws);
8572
+ ws.terminate();
8573
+ });
8574
+ sendRevision(ws, "reload-state");
8575
+ waitForAcknowledgement(ws);
8324
8576
  });
8325
8577
  const server = http2.createServer(async (req, res) => {
8326
8578
  const url = new URL(req.url, "http://127.0.0.1");
@@ -8343,6 +8595,34 @@ async function createPreviewServer(state, port) {
8343
8595
  res.end("Not Found");
8344
8596
  return;
8345
8597
  }
8598
+ if (url.pathname === "/__chapter" && req.method === "GET") {
8599
+ const rawFile = url.searchParams.get("file");
8600
+ const file = rawFile ? canonicalChapterId(rawFile) : "";
8601
+ const revision = Number(url.searchParams.get("revision"));
8602
+ const configuredFiles = state.config.source?.files;
8603
+ if (!file || !state.currentInputPath || !incrementalPreviewEnabled() || !Number.isSafeInteger(revision) || revision !== reloadRevision || path12.extname(file).toLowerCase() !== ".md" || hasDotSegment(file) || !resolveWithinRoot(file, state.currentInputPath) || Array.isArray(configuredFiles) && configuredFiles.length > 0 && !configuredFiles.some((configured) => canonicalChapterId(configured) === file)) {
8604
+ res.writeHead(400);
8605
+ res.end("Bad Request");
8606
+ return;
8607
+ }
8608
+ try {
8609
+ const html = await renderChapterPreviewHtml(state.currentInputPath, file, state.config);
8610
+ if (revision !== reloadRevision) {
8611
+ res.writeHead(409);
8612
+ res.end("Superseded");
8613
+ return;
8614
+ }
8615
+ res.writeHead(200, {
8616
+ "Content-Type": "text/html; charset=utf-8",
8617
+ "Cache-Control": "no-store"
8618
+ });
8619
+ res.end(html);
8620
+ } catch (error2) {
8621
+ res.writeHead(500);
8622
+ res.end(error2 instanceof Error ? error2.message : String(error2));
8623
+ }
8624
+ return;
8625
+ }
8346
8626
  if (matchesEmbedded(url.pathname)) {
8347
8627
  if (req.headers["if-none-match"] === EMBEDDED_ETAG) {
8348
8628
  res.writeHead(304, { ETag: EMBEDDED_ETAG, "Cache-Control": EMBEDDED_CACHE_CONTROL });
@@ -8361,7 +8641,7 @@ async function createPreviewServer(state, port) {
8361
8641
  }
8362
8642
  if (url.pathname === "/" && incrementalPreviewEnabled()) {
8363
8643
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
8364
- res.end(SHELL_HTML);
8644
+ res.end(shellHtml(reloadRevision, instanceId));
8365
8645
  return;
8366
8646
  }
8367
8647
  const pathname = url.pathname === "/" ? "/book.html" : url.pathname;
@@ -8388,7 +8668,7 @@ async function createPreviewServer(state, port) {
8388
8668
  res.end("Not Found");
8389
8669
  return;
8390
8670
  }
8391
- await serveStatic(absPath, res).catch((err) => {
8671
+ await serveStatic(absPath, res, "no-store", {}, reloadRevision, instanceId).catch((err) => {
8392
8672
  if (!res.headersSent) {
8393
8673
  res.writeHead(500);
8394
8674
  res.end(`Internal Server Error: ${err.message}`);
@@ -8415,14 +8695,21 @@ async function createPreviewServer(state, port) {
8415
8695
  openPath(serverUrl).catch(() => {});
8416
8696
  }
8417
8697
  let stopped = false;
8418
- const broadcast = (payload) => {
8698
+ const broadcastUpdate = (type, file) => {
8419
8699
  if (stopped)
8420
8700
  return;
8421
- const message = JSON.stringify(payload);
8422
- for (const ws of clients) {
8423
- if (ws.readyState === WebSocket.OPEN)
8424
- ws.send(message);
8701
+ reloadRevision++;
8702
+ for (const [ws] of clients) {
8703
+ sendRevision(ws, type, file);
8704
+ waitForAcknowledgement(ws);
8705
+ }
8706
+ };
8707
+ const updateInFlight = () => {
8708
+ for (const client of clients.values()) {
8709
+ if (client.acknowledgedRevision < reloadRevision)
8710
+ return true;
8425
8711
  }
8712
+ return false;
8426
8713
  };
8427
8714
  return {
8428
8715
  port: boundPort,
@@ -8430,8 +8717,11 @@ async function createPreviewServer(state, port) {
8430
8717
  if (stopped)
8431
8718
  return;
8432
8719
  stopped = true;
8433
- for (const ws of clients)
8720
+ for (const [ws, client] of clients) {
8721
+ if (client.retryTimer !== null)
8722
+ clearTimeout(client.retryTimer);
8434
8723
  ws.terminate();
8724
+ }
8435
8725
  clients.clear();
8436
8726
  wss.close();
8437
8727
  await new Promise((resolve10) => {
@@ -8442,7 +8732,13 @@ async function createPreviewServer(state, port) {
8442
8732
  });
8443
8733
  },
8444
8734
  broadcastReload() {
8445
- broadcast({ type: "full-reload" });
8735
+ broadcastUpdate("full-reload");
8736
+ },
8737
+ broadcastContentUpdate(file) {
8738
+ if (updateInFlight())
8739
+ broadcastUpdate("full-reload");
8740
+ else
8741
+ broadcastUpdate("content-update", canonicalChapterId(file));
8446
8742
  }
8447
8743
  };
8448
8744
  }
@@ -8495,7 +8791,10 @@ async function startPreviewServer(options) {
8495
8791
  }
8496
8792
  await shutdownServer(state);
8497
8793
  },
8498
- restart: restartPreview2
8794
+ restart: restartPreview2,
8795
+ notifySettledWrite: (filePath2, writtenContent) => {
8796
+ state.notifySettledWrite?.(filePath2, writtenContent);
8797
+ }
8499
8798
  };
8500
8799
  }
8501
8800
  // src/lib/diagnostics.ts
@@ -8762,7 +9061,7 @@ import path15 from "node:path";
8762
9061
 
8763
9062
  // src/lib/manifest-doc.ts
8764
9063
  import { existsSync as existsSync12 } from "node:fs";
8765
- import { randomUUID } from "node:crypto";
9064
+ import { randomUUID as randomUUID2 } from "node:crypto";
8766
9065
  import { mkdir as mkdir7, open, readFile as readFile23, rename as rename3, rm as rm2 } from "node:fs/promises";
8767
9066
  import path14 from "node:path";
8768
9067
  import { parseDocument, isSeq, YAMLSeq } from "yaml";
@@ -8784,7 +9083,7 @@ async function loadManifestDoc(projectDir) {
8784
9083
  async function writeManifestDoc(file, doc) {
8785
9084
  const dir = path14.dirname(file);
8786
9085
  await mkdir7(dir, { recursive: true });
8787
- const temporary = path14.join(dir, `.${path14.basename(file)}.tmp-${process.pid}-${randomUUID()}`);
9086
+ const temporary = path14.join(dir, `.${path14.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
8788
9087
  try {
8789
9088
  const handle = await open(temporary, "wx", 438);
8790
9089
  try {
@@ -9439,13 +9738,13 @@ async function deleteSnippet(projectDir, fileName) {
9439
9738
  await rm3(resolveSnippetPath(projectDir, fileName), { force: true });
9440
9739
  }
9441
9740
  // src/lib/plugin-manager.ts
9442
- import { randomUUID as randomUUID3 } from "node:crypto";
9741
+ import { randomUUID as randomUUID4 } from "node:crypto";
9443
9742
  import { cp as cp4, lstat as lstat3, mkdir as mkdir12, rename as rename5, rm as rm5, stat as stat7 } from "node:fs/promises";
9444
9743
  import path19 from "node:path";
9445
9744
  import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
9446
9745
 
9447
9746
  // src/lib/npm-plugin-installer.ts
9448
- import { createHash as createHash4, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
9747
+ import { createHash as createHash4, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
9449
9748
  import { createReadStream as createReadStream2 } from "node:fs";
9450
9749
  import {
9451
9750
  lstat as lstat2,
@@ -10121,7 +10420,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10121
10420
  }
10122
10421
  try {
10123
10422
  await lstat2(finalRoot);
10124
- backupRoot = `${finalRoot}.backup-${randomUUID2()}`;
10423
+ backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
10125
10424
  await rename4(finalRoot, backupRoot);
10126
10425
  } catch (error2) {
10127
10426
  if (error2.code !== "ENOENT")
@@ -10400,8 +10699,8 @@ async function addLocalPluginUnlocked(projectDir, sourcePath) {
10400
10699
  const destDir = path19.join(projectDir, PLUGINS_DIR);
10401
10700
  await mkdir12(destDir, { recursive: true });
10402
10701
  const dest = path19.join(destDir, base);
10403
- const staging = path19.join(destDir, `.import-${randomUUID3()}`);
10404
- const backup = path19.join(destDir, `.backup-${randomUUID3()}`);
10702
+ const staging = path19.join(destDir, `.import-${randomUUID4()}`);
10703
+ const backup = path19.join(destDir, `.backup-${randomUUID4()}`);
10405
10704
  let hasBackup = false;
10406
10705
  try {
10407
10706
  if (info2.isDirectory()) {