@heycys/gallery 0.2.2 → 0.2.4

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 (222) hide show
  1. package/README.md +1 -1
  2. package/THIRD-PARTY-NOTICES.md +1 -1
  3. package/assets/preview/runtime.js +156 -0
  4. package/assets/preview/shell.html +107 -0
  5. package/assets/writing-preferences.md +1 -1
  6. package/browser/editor/components/divider/index.mjs +1 -1
  7. package/browser/editor/page/annotate/card.mjs +6 -0
  8. package/browser/editor/page/annotate/index.mjs +22 -2
  9. package/browser/editor/page/columns/index.mjs +1 -1
  10. package/browser/editor/page/conflicts.mjs +1 -1
  11. package/browser/editor/page/drag/index.mjs +1 -1
  12. package/browser/editor/page/editor.mjs +43 -5
  13. package/browser/editor/page/focus.mjs +104 -0
  14. package/browser/editor/page/handle/index.mjs +14 -2
  15. package/browser/editor/page/links/index.mjs +2 -2
  16. package/browser/editor/page/quiet.mjs +3 -3
  17. package/browser/editor/page/resize/index.mjs +1 -1
  18. package/browser/editor/page/session.mjs +4 -1
  19. package/browser/editor/page/ui.mjs +1 -1
  20. package/browser/shared/annotations.mjs +22 -3
  21. package/browser/shared/block-packs.mjs +13 -4
  22. package/browser/shared/document-look.mjs +14 -0
  23. package/browser/shared/drafts.mjs +1 -1
  24. package/browser/shared/extension-manifest.mjs +40 -12
  25. package/browser/shared/favorite-merge.mjs +17 -0
  26. package/browser/shared/favorites.mjs +77 -0
  27. package/browser/shared/i18n/en.mjs +123 -24
  28. package/browser/shared/outline-ops.mjs +109 -0
  29. package/browser/shared/outline.mjs +6 -0
  30. package/browser/shared/pages.mjs +20 -8
  31. package/browser/shared/paths.mjs +5 -2
  32. package/browser/shared/preference-documents.mjs +137 -0
  33. package/browser/shared/presence.mjs +94 -0
  34. package/browser/shared/preview-server.mjs +8 -5
  35. package/browser/shared/qa-levels.mjs +9 -1
  36. package/browser/shared/session.mjs +4 -0
  37. package/browser/shared/shipped-blocks.mjs +22 -4
  38. package/browser/shared/startup.mjs +101 -0
  39. package/browser/shared/workspace-address.mjs +3 -0
  40. package/browser/shared/workspace-columns.mjs +1 -1
  41. package/browser/shared/workspace-look.mjs +21 -8
  42. package/browser/shared/workspace-registry.mjs +6 -3
  43. package/browser/shared/workspace.mjs +37 -10
  44. package/browser/shared/writing-preferences.mjs +2 -42
  45. package/dist/chunk-25LYSMAE.mjs +2 -0
  46. package/dist/chunk-2I6ZVC5O.mjs +2 -0
  47. package/dist/{chunk-M4NRDYI5.mjs → chunk-2MYTOOP6.mjs} +1 -1
  48. package/dist/chunk-2VNENLER.mjs +2 -0
  49. package/dist/chunk-3FERXZF5.mjs +4 -0
  50. package/dist/chunk-3M6SBBGU.mjs +2 -0
  51. package/dist/chunk-3VCAAF34.mjs +2 -0
  52. package/dist/{chunk-JXXHAXYH.mjs → chunk-3ZTRLGSA.mjs} +1 -1
  53. package/dist/chunk-4FRA5AUE.mjs +2 -0
  54. package/dist/chunk-4LR2QEC2.mjs +4 -0
  55. package/dist/chunk-56SVJENP.mjs +966 -0
  56. package/dist/chunk-5CQISL2G.mjs +4 -0
  57. package/dist/{chunk-RRKJMHNX.mjs → chunk-5I5ZDDWC.mjs} +5 -5
  58. package/dist/chunk-5IHDLEB4.mjs +10 -0
  59. package/dist/chunk-5LRDMV7O.mjs +2 -0
  60. package/dist/chunk-5MVUYRKG.mjs +5 -0
  61. package/dist/chunk-5TLLIZT7.mjs +2 -0
  62. package/dist/chunk-6ILJWFYW.mjs +4 -0
  63. package/dist/chunk-6N25CJ6Y.mjs +3 -0
  64. package/dist/{chunk-SQ6Y2ZMA.mjs → chunk-7DTINW6U.mjs} +2 -2
  65. package/dist/chunk-7KWAGKIK.mjs +2 -0
  66. package/dist/chunk-7WNUOAFK.mjs +2 -0
  67. package/dist/chunk-7XCRW5RC.mjs +2 -0
  68. package/dist/{chunk-ERE2BXYV.mjs → chunk-7XSDTILF.mjs} +1 -1
  69. package/dist/chunk-ABKYFOVR.mjs +2 -0
  70. package/dist/chunk-AMUF7PUN.mjs +2 -0
  71. package/dist/{chunk-PLB7L22J.mjs → chunk-B2EIMERT.mjs} +2 -2
  72. package/dist/{chunk-QNECWMGR.mjs → chunk-B45IAAU6.mjs} +1 -1
  73. package/dist/chunk-B6RAGF4R.mjs +2 -0
  74. package/dist/chunk-BSJKFE7I.mjs +4 -0
  75. package/dist/chunk-C72EEVLK.mjs +2 -0
  76. package/dist/chunk-CRAUKZ23.mjs +4 -0
  77. package/dist/chunk-F4CT35KL.mjs +2 -0
  78. package/dist/chunk-F63BNJLG.mjs +2 -0
  79. package/dist/chunk-FBKUPS44.mjs +2 -0
  80. package/dist/chunk-FL52ZL5Z.mjs +15 -0
  81. package/dist/chunk-FOZCU4VW.mjs +2 -0
  82. package/dist/chunk-G5HV7BC3.mjs +2 -0
  83. package/dist/{chunk-ERLAIBOA.mjs → chunk-GB5SOYQJ.mjs} +2 -2
  84. package/dist/chunk-GN3SG3IF.mjs +2 -0
  85. package/dist/{chunk-6QWQXHAV.mjs → chunk-HFTBVEZK.mjs} +1 -1
  86. package/dist/chunk-IOW5HLRK.mjs +2 -0
  87. package/dist/chunk-IQ3GJNTI.mjs +6 -0
  88. package/dist/chunk-K6SPFBUJ.mjs +2 -0
  89. package/dist/chunk-K7Y7SPTS.mjs +2 -0
  90. package/dist/chunk-L2IJCEUH.mjs +2 -0
  91. package/dist/{chunk-GPIWQ4X4.mjs → chunk-L3VT6LM4.mjs} +1 -1
  92. package/dist/chunk-L4WLQYFC.mjs +5 -0
  93. package/dist/{chunk-FFV733MA.mjs → chunk-LAMCAGK6.mjs} +1 -1
  94. package/dist/chunk-LHGKPMTI.mjs +2 -0
  95. package/dist/chunk-LJZIYVNA.mjs +3 -0
  96. package/dist/chunk-LSVCTLS2.mjs +2 -0
  97. package/dist/chunk-LZY74XTP.mjs +2 -0
  98. package/dist/chunk-MC4LRSVH.mjs +2 -0
  99. package/dist/chunk-MCEXN4G5.mjs +2 -0
  100. package/dist/chunk-MTLAOCFF.mjs +181 -0
  101. package/dist/chunk-MZ66LN3B.mjs +2 -0
  102. package/dist/chunk-N5QWJRRI.mjs +2 -0
  103. package/dist/chunk-NF4MMOUB.mjs +2 -0
  104. package/dist/chunk-NHJYAJ73.mjs +3 -0
  105. package/dist/chunk-NRSD7SVY.mjs +2 -0
  106. package/dist/chunk-NT23I4XW.mjs +2 -0
  107. package/dist/chunk-NUALABIS.mjs +3 -0
  108. package/dist/chunk-NW6LAU22.mjs +2 -0
  109. package/dist/chunk-NWWHDGKF.mjs +5 -0
  110. package/dist/chunk-OFRCVMSJ.mjs +2 -0
  111. package/dist/chunk-OKVQQ3CX.mjs +4 -0
  112. package/dist/chunk-OM24O4KR.mjs +8 -0
  113. package/dist/{chunk-4B2TB5K2.mjs → chunk-PBMOC7KA.mjs} +2 -2
  114. package/dist/chunk-PVTCP43W.mjs +2 -0
  115. package/dist/chunk-Q62HZAJS.mjs +2 -0
  116. package/dist/chunk-QBJWIR3Q.mjs +2 -0
  117. package/dist/chunk-QDSEZX7Z.mjs +6 -0
  118. package/dist/chunk-QR4V4A2Z.mjs +15 -0
  119. package/dist/{chunk-QVGOPHOF.mjs → chunk-QRLVVZ3R.mjs} +1 -1
  120. package/dist/chunk-QW3QG67J.mjs +2 -0
  121. package/dist/chunk-R2T2EVWY.mjs +8 -0
  122. package/dist/{chunk-GJJRBGMY.mjs → chunk-R5RERE6G.mjs} +1 -1
  123. package/dist/chunk-RFHEWT3Y.mjs +5 -0
  124. package/dist/chunk-RS2Z2JQP.mjs +2 -0
  125. package/dist/chunk-S6YEC4GE.mjs +16 -0
  126. package/dist/chunk-SHZLMGGW.mjs +2 -0
  127. package/dist/chunk-TALJQM2O.mjs +2 -0
  128. package/dist/chunk-TLNJWEFY.mjs +22 -0
  129. package/dist/{chunk-6Q7ASAXJ.mjs → chunk-TQZU7D7C.mjs} +1 -1
  130. package/dist/chunk-UTKNWYBA.mjs +2 -0
  131. package/dist/chunk-V3TM3TEN.mjs +2 -0
  132. package/dist/chunk-V3XROSP5.mjs +7 -0
  133. package/dist/{chunk-6QBHU4DF.mjs → chunk-V5JTW2LH.mjs} +1 -1
  134. package/dist/chunk-VAXLSISW.mjs +4 -0
  135. package/dist/chunk-VBHI3RG6.mjs +2 -0
  136. package/dist/{chunk-6L6V7YR2.mjs → chunk-VKZJRGDR.mjs} +1 -1
  137. package/dist/chunk-VZXZM7NS.mjs +2 -0
  138. package/dist/chunk-WDS6HEPE.mjs +2 -0
  139. package/dist/chunk-WHGANNEY.mjs +13 -0
  140. package/dist/chunk-WMJWC4QY.mjs +56 -0
  141. package/dist/{chunk-VZ2RQ3AM.mjs → chunk-WWONJJQS.mjs} +1 -1
  142. package/dist/chunk-X4WNLFIK.mjs +2 -0
  143. package/dist/chunk-XNNCQROX.mjs +11 -0
  144. package/dist/chunk-XRYECF23.mjs +170 -0
  145. package/dist/{chunk-K3JKPFL5.mjs → chunk-Z4NEXXFK.mjs} +1 -1
  146. package/dist/{chunk-236JJXWY.mjs → chunk-ZFVF2YPV.mjs} +2 -2
  147. package/dist/chunk-ZM2IX3R2.mjs +2 -0
  148. package/dist/chunk-ZNGGDUZI.mjs +2 -0
  149. package/dist/chunk-ZOEJQOKT.mjs +2 -0
  150. package/dist/cli.mjs +1 -1
  151. package/dist/server.mjs +1 -1
  152. package/dist/update-fetch.mjs +1 -1
  153. package/dist/worker.mjs +1 -1
  154. package/kit/runtime.js +1 -0
  155. package/package.json +1 -1
  156. package/release.json +4 -4
  157. package/skill/SKILL.md +54 -148
  158. package/skill/references/building-blocks.md +4 -4
  159. package/skill/references/charts-and-figures.md +73 -0
  160. package/skill/references/extensions.md +47 -4
  161. package/skill/references/pages-and-data.md +22 -3
  162. package/skill/references/preference-files.md +7 -0
  163. package/skill/references/previews.md +17 -0
  164. package/skill/references/writing-html.md +21 -82
  165. package/web/assets/index-B4uUluLM.js +15 -0
  166. package/web/assets/index-C3vHT8sH.css +1 -0
  167. package/web/index.html +2 -2
  168. package/dist/chunk-2KAVXMKG.mjs +0 -4
  169. package/dist/chunk-2T4PZQEP.mjs +0 -2
  170. package/dist/chunk-3SKOU6OZ.mjs +0 -10
  171. package/dist/chunk-3XZOA7AH.mjs +0 -170
  172. package/dist/chunk-4KUO7R2M.mjs +0 -16
  173. package/dist/chunk-4OZWXZE5.mjs +0 -5
  174. package/dist/chunk-4SNV4HA5.mjs +0 -2
  175. package/dist/chunk-53XDTHTG.mjs +0 -4
  176. package/dist/chunk-5XV3YYLG.mjs +0 -2
  177. package/dist/chunk-65NTKF4N.mjs +0 -6
  178. package/dist/chunk-6GV3CBB2.mjs +0 -2
  179. package/dist/chunk-6NLXUKAY.mjs +0 -4
  180. package/dist/chunk-6XD6U3QA.mjs +0 -5
  181. package/dist/chunk-7EFHNQ5B.mjs +0 -3
  182. package/dist/chunk-7ISBBDJR.mjs +0 -2
  183. package/dist/chunk-7M2BQQGZ.mjs +0 -2
  184. package/dist/chunk-7UZUQQ7Q.mjs +0 -554
  185. package/dist/chunk-ALZ6ZENZ.mjs +0 -3
  186. package/dist/chunk-BLNQUR5R.mjs +0 -168
  187. package/dist/chunk-BMX3VUHY.mjs +0 -2
  188. package/dist/chunk-BN7M3IN7.mjs +0 -8
  189. package/dist/chunk-C257H6IA.mjs +0 -2
  190. package/dist/chunk-CHQU37CA.mjs +0 -2
  191. package/dist/chunk-DR73Y2UJ.mjs +0 -3
  192. package/dist/chunk-E4IHAIJ7.mjs +0 -2
  193. package/dist/chunk-EURLTOTZ.mjs +0 -4
  194. package/dist/chunk-F4ZA6JRZ.mjs +0 -3
  195. package/dist/chunk-GKOBB6DH.mjs +0 -2
  196. package/dist/chunk-GMJD7ID3.mjs +0 -2
  197. package/dist/chunk-IA2SIEJQ.mjs +0 -2
  198. package/dist/chunk-JEUK3ZO7.mjs +0 -2
  199. package/dist/chunk-JPG2LBQM.mjs +0 -11
  200. package/dist/chunk-KLQP46Y3.mjs +0 -4
  201. package/dist/chunk-LMUZQK5O.mjs +0 -2
  202. package/dist/chunk-MKIBJTJZ.mjs +0 -6
  203. package/dist/chunk-MNWJ4EZY.mjs +0 -2
  204. package/dist/chunk-NHP66IWR.mjs +0 -5
  205. package/dist/chunk-OEIJLMPI.mjs +0 -2
  206. package/dist/chunk-PDZR3L3C.mjs +0 -2
  207. package/dist/chunk-PUOQ65NC.mjs +0 -2
  208. package/dist/chunk-Q4MS6OFC.mjs +0 -2
  209. package/dist/chunk-Q7P3Q5YX.mjs +0 -13
  210. package/dist/chunk-QP7BOHE4.mjs +0 -2
  211. package/dist/chunk-S3SX3COT.mjs +0 -20
  212. package/dist/chunk-TANZRINB.mjs +0 -2
  213. package/dist/chunk-UGWMMEJ5.mjs +0 -15
  214. package/dist/chunk-ULXSPX5H.mjs +0 -5
  215. package/dist/chunk-USIHBCSW.mjs +0 -15
  216. package/dist/chunk-VPMRP2TL.mjs +0 -2
  217. package/dist/chunk-WFGPMCTZ.mjs +0 -4
  218. package/dist/chunk-XZCFPKGO.mjs +0 -2
  219. package/dist/chunk-ZFGMJVJC.mjs +0 -2
  220. package/web/assets/index-FLb9jCfl.css +0 -1
  221. package/web/assets/index-czjrXVjf.js +0 -15
  222. /package/dist/{chunk-PAREWQGB.mjs → chunk-NOGQX56R.mjs} +0 -0
@@ -0,0 +1,94 @@
1
+ // What the browser tells the server about what the reader has open (gallery status, notes/plans/agent-parity-2026-11-11.md
2
+ // part one, 4): each workbench tab (a window of the workbench, one document or custom page open in it) reports itself to
3
+ // its server now and then (POST /__gallery/workspace/presence: src/workbench/app/session/presence.ts), the server keeps
4
+ // the reports in memory only (src/workbench/presence.mjs), and gallery status asks for them. Pure: the shape of a report, what
5
+ // it is cut to and how a report is read as the command prints it, shared by the browser, the server and the command.
6
+ //
7
+ // A report: { tab, entry, kind, title, visible, focused, touched, flags, notes, caret, selection } or { tab, gone: true }.
8
+ // tab the workbench tab's own name (made when it loads, kept until it closes)
9
+ // entry what is open, as the workbench names it: a document's slug ("weekly"), a draft of another workspace
10
+ // ("~ws-abc123/weekly"), a custom page ("@home", or "@~ws-abc123/home" of another workspace)
11
+ // kind "document" or "page"
12
+ // title the title the reader sees (the page's, so one just typed counts)
13
+ // visible the tab is the one shown in its window; focused the window has the focus
14
+ // touched the reader did something in it (a heartbeat is not): what "the last one used" goes by
15
+ // flags the page's save state (shared/session.mjs STATE_FLAGS: editing, pending, saving, offline, conflicts, unfit, changed)
16
+ // notes how many 批注 the page has
17
+ // caret { block (a data-block-id or null: a block the file has no id for yet), text (its words) } or null
18
+ // selection { blocks: [data-block-id…], text (the selected words, or the blocks' words), kind: "text" | "blocks" } or null
19
+ // What a word of it is cut to is below: the words the reader selected are the most private thing a report holds, so
20
+ // they are cut short and never leave the computer (the server answers only to this computer, and only with the token).
21
+
22
+ /** How long a tab's report counts after its last one (ms): the tab sends one every 20 s while it is open, one more as it closes. */
23
+ export const ALIVE_FOR = 90_000;
24
+ /** The most of a selection (and of a block's words) a report carries, in characters. */
25
+ export const WORDS_MOST = 200;
26
+ /** The most blocks a selection names. */
27
+ export const BLOCKS_MOST = 20;
28
+
29
+ const TAB = /^[a-z0-9]{4,40}$/;
30
+ const ENTRY = /^@?(?:~ws-[0-9a-f]{6}\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/;
31
+ const BLOCK = /^[a-z0-9]{8}$/;
32
+ // Not KINDS: the dev server's Vite config bundles the server's modules into one scope, and a second top-level KINDS
33
+ // renames qa-levels.mjs's, whose functions' text a page runs (layoutNotesScript): the page then names one it never had.
34
+ const PRESENCE_KINDS = new Set(["document", "page"]);
35
+
36
+ const bool = (value) => value === true;
37
+ const count = (value) => (Number.isInteger(value) && value > 0 ? Math.min(value, 9999) : 0);
38
+
39
+ /** `text` as a line of words: runs of white space as one space, the ends trimmed, cut to `most` ({ text, more: whether it was cut }). */
40
+ export function cutWords(text, most = WORDS_MOST) {
41
+ const words = typeof text === "string" ? text.replace(/[​]/g, "").replace(/\s+/g, " ").trim() : "";
42
+ const chars = [...words];
43
+ return chars.length > most ? { text: chars.slice(0, most).join(""), more: true } : { text: words, more: false };
44
+ }
45
+
46
+ const blockOf = (value) => (typeof value === "string" && BLOCK.test(value) ? value : null);
47
+
48
+ /**
49
+ * A report as the server keeps it, or null when it is none: `{ tab, gone: true }` for a tab that closed; else the fields
50
+ * above, each cut to what it may be (a block's id an id, a selection's words 200 characters, 20 blocks at most).
51
+ */
52
+ export function cleanReport(body) {
53
+ if (!body || typeof body !== "object" || Array.isArray(body) || typeof body.tab !== "string" || !TAB.test(body.tab)) return null;
54
+ if (body.gone === true) return { tab: body.tab, gone: true };
55
+ if (typeof body.entry !== "string" || body.entry.length > 160 || !ENTRY.test(body.entry) || !PRESENCE_KINDS.has(body.kind)) return null;
56
+ if ((body.kind === "page") !== body.entry.startsWith("@")) return null;
57
+ const flags = body.flags && typeof body.flags === "object" ? body.flags : {};
58
+ let caret = null;
59
+ if (body.caret && typeof body.caret === "object") {
60
+ const words = cutWords(body.caret.text);
61
+ caret = { block: blockOf(body.caret.block), text: words.text };
62
+ }
63
+ let selection = null;
64
+ if (body.selection && typeof body.selection === "object") {
65
+ const words = cutWords(body.selection.text);
66
+ const blocks = Array.isArray(body.selection.blocks) ? body.selection.blocks.map(blockOf).filter(Boolean).slice(0, BLOCKS_MOST) : [];
67
+ selection = { kind: body.selection.kind === "blocks" ? "blocks" : "text", blocks, text: words.text, truncated: words.more, ...(Number.isInteger(body.selection.length) ? { length: Math.min(body.selection.length, 1e7) } : {}) };
68
+ }
69
+ return {
70
+ tab: body.tab,
71
+ entry: body.entry,
72
+ kind: body.kind,
73
+ title: cutWords(body.title, 200).text,
74
+ visible: bool(body.visible),
75
+ focused: bool(body.focused),
76
+ touched: bool(body.touched),
77
+ flags: { editing: bool(flags.editing), pending: bool(flags.pending), saving: bool(flags.saving), offline: bool(flags.offline), conflicts: count(flags.conflicts), unfit: count(flags.unfit), changed: bool(flags.changed) },
78
+ notes: count(body.notes),
79
+ caret,
80
+ selection,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Where saving stands for a report's flags, the one word gallery status says: "conflict" (the reader has something to decide
86
+ * on the page: a conflict, or content the document cannot hold), "offline" (commits do not get through), "pending" (changes wait
87
+ * to be saved, or are on their way), else "saved".
88
+ */
89
+ export function saveOf(flags) {
90
+ if ((flags?.conflicts ?? 0) > 0) return "conflict";
91
+ if (flags?.offline) return "offline";
92
+ if (flags?.pending || flags?.saving) return "pending";
93
+ return "saved";
94
+ }
@@ -121,6 +121,7 @@ export async function answerOf(found, timeout = 10_000) {
121
121
  const response = await fetch(`http://127.0.0.1:${found.port}/__gallery/workspace/info`, { redirect: "error", signal: AbortSignal.timeout(timeout) });
122
122
  const info = response.ok ? await response.json() : null;
123
123
  const same = info?.token === found.token && info.server?.pid === found.pid && typeof info.server.home === "string" && sameDirectory(info.server.home, found.home) && sameDirectory(info.server.data, serviceLocation().data);
124
+ if (process.env.GALLERY_SANDBOX && info?.server?.sandbox !== process.env.GALLERY_SANDBOX) return "no";
124
125
  return same ? "yes" : "no";
125
126
  } catch (error) {
126
127
  return error?.name === "TimeoutError" ? "busy" : "no";
@@ -174,7 +175,7 @@ async function goneWithin(pid, ms) {
174
175
  * when its process still runs after `wait` ms, that process killed. Resolves to { ok: true, killed }
175
176
  * or { ok: false, reason: "dev" (npm run dev's: stopped where it runs) | "running" (it would not stop), message }.
176
177
  */
177
- export async function stopServer(found, { wait = 10_000 } = {}) {
178
+ export async function stopServer(found, { wait = 10_000, force = !process.env.GALLERY_SANDBOX } = {}) {
178
179
  const host = `127.0.0.1:${found.port}`;
179
180
  let status = 0;
180
181
  try {
@@ -189,10 +190,12 @@ export async function stopServer(found, { wait = 10_000 } = {}) {
189
190
  } catch {} // it closed as it answered, or did not answer: its process says
190
191
  if (status === 409) return { ok: false, reason: "dev", message: `The shared Gallery server is npm run dev's (port ${found.port}, pid ${found.pid}): stop it where it runs.` };
191
192
  if (await goneWithin(found.pid, wait)) return { ok: true, killed: false };
192
- try {
193
- process.kill(found.pid);
194
- } catch {}
195
- if (await goneWithin(found.pid, 3000)) return { ok: true, killed: true };
193
+ if (force) {
194
+ try {
195
+ process.kill(found.pid);
196
+ } catch {}
197
+ if (await goneWithin(found.pid, 3000)) return { ok: true, killed: true };
198
+ }
196
199
  return { ok: false, reason: "running", message: `The server on port ${found.port} (pid ${found.pid}) did not stop: end that process yourself.` };
197
200
  }
198
201
 
@@ -89,5 +89,13 @@ export function noteInWords(note, { english = false } = {}) {
89
89
  return text;
90
90
  }
91
91
 
92
+ // The dev server's Vite config bundles the server's modules into one scope, renaming a constant another module also
93
+ // has at its top (KINDS$1: src/editor/history.mjs and src/render/images.mjs have a KINDS too), and the functions' text
94
+ // below then names the renamed one: the script declares each constant under every name that text has for it.
92
95
  /** levelOf, layoutNotesOf and noteInWords as a script's text, for a page (the preview's note) to read notes as qa does and say them. */
93
- export const layoutNotesScript = () => `const WIDE = ${WIDE}; const KINDS = ${JSON.stringify(KINDS)}; const NARROW_INFO = ${JSON.stringify(NARROW_INFO)}; ${levelOf}; ${layoutNotesOf}; ${noteInWords};`;
96
+ // Each constant is declared under every name the functions' text has for it (KINDS, or KINDS$1 where a bundle renamed it).
97
+ export const layoutNotesScript = () => {
98
+ const body = `${levelOf}; ${layoutNotesOf}; ${noteInWords};`;
99
+ const declare = (name, value) => [...new Set(body.match(new RegExp(`\\b${name}(?:\\$\\d+)?\\b`, "g")) ?? [name])].map((each) => `const ${each} = ${value};`).join(" ");
100
+ return `${declare("WIDE", WIDE)} ${declare("KINDS", JSON.stringify(KINDS))} ${declare("NARROW_INFO", JSON.stringify(NARROW_INFO))} ${body}`;
101
+ };
@@ -35,6 +35,10 @@ import { documentKey } from "./workspace-address.mjs";
35
35
  // consent a block's card asks to be let online (its 允许访问: the kit's net.js):
36
36
  // { block (its id), hosts (those of its needs waiting) }: the workbench
37
37
  // opens its consent dialog, which writes what the reader says
38
+ // focus where the reader is on the page, when it changed (editor/page/focus.mjs): { caret, selection },
39
+ // caret: { block (a data-block-id, or null), text } or null; selection: { kind ("text" or
40
+ // "blocks"), blocks: [data-block-id…], text } or null, each cut to 200 characters. The workbench
41
+ // reports it, with the tab's other facts, to its server: gallery status (shared/presence.mjs)
38
42
  // annotations-removed
39
43
  // a 批注 deleted from its card: { items }: the workbench says so, as
40
44
  // it does for 全部删除, and its 撤销 puts them back
@@ -127,6 +127,14 @@ export function writeRecord(root, record) {
127
127
  /** Where a block of the workspace at `root` is. */
128
128
  const folderIn = (root, id) => path.join(layoutOf(root).components, ...id.split("/"));
129
129
 
130
+ function versionIn(folder) {
131
+ try {
132
+ return JSON.parse(fs.readFileSync(path.join(folder, PACKAGE_MANIFEST), "utf8")).version ?? null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
130
138
  /**
131
139
  * What the workspace at `root` holds of the shipped `block`: { added (a folder of its id is there),
132
140
  * version (what its manifest says, or null), changed (its files are not what was added: changed since,
@@ -136,10 +144,7 @@ export function shippedState(root, block) {
136
144
  const folder = folderIn(root, block.id);
137
145
  const now = fingerprintOf(folder);
138
146
  if (now === null) return { added: false, version: null, changed: false, behind: false };
139
- let version = null;
140
- try {
141
- version = JSON.parse(fs.readFileSync(path.join(folder, PACKAGE_MANIFEST), "utf8")).version ?? null;
142
- } catch {}
147
+ const version = versionIn(folder);
143
148
  const kept = recordOf(root)[block.id];
144
149
  const shipped = fingerprintOf(block.folder);
145
150
  const changed = now !== shipped && now !== kept?.fingerprint;
@@ -189,6 +194,8 @@ export function versionAfter(a, b) {
189
194
  * changed since it was added (or no add put it there), when Gallery's comes after the copy's; else null.
190
195
  */
191
196
  export function shippedNewer(root, block) {
197
+ // A content comparison matters only when Gallery actually ships a newer version.
198
+ if (!versionAfter(block.version, versionIn(folderIn(root, block.id)))) return null;
192
199
  const state = shippedState(root, block);
193
200
  return state.changed && versionAfter(block.version, state.version) ? block.version : null;
194
201
  }
@@ -235,6 +242,17 @@ export function followShipped(root, { ids = null, directory = packsDirectory, lo
235
242
  return done;
236
243
  }
237
244
 
245
+ /**
246
+ * The blocks of `blocks` (shippedBlocks') that the workspace at `root` holds as Gallery ships them now,
247
+ * an install of their pack having just found or made them so (installOfficialPacks): looked at, so that
248
+ * followShipped, which would find nothing to do for them, does not read each one's files again (a
249
+ * server's start read every block twice over for it, some 150 ms).
250
+ */
251
+ export function followedAlready(root, blocks) {
252
+ const key = path.resolve(root);
253
+ for (const block of blocks) looked.set(`${key}${path.resolve(block.folder)}`, block.version);
254
+ }
255
+
238
256
  const following = new Set(); // the workspaces whose server runs in this process (followOnRead), by their resolved root
239
257
  const keyOf = (root) => (process.platform === "win32" ? path.resolve(root).toLowerCase() : path.resolve(root));
240
258
 
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { packageRoot, release } from "./paths.mjs";
7
+ import { serviceLocation } from "./service-location.mjs";
8
+ import { defaultWorkspace } from "./workspace.mjs";
9
+ import { writeAtomic } from "./files.mjs";
10
+
11
+ const execute = promisify(execFile);
12
+ const RUN = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
13
+ const NAME = "HeycysGallery";
14
+ const LABEL = "com.heycys.gallery";
15
+ const xml = value => String(value).replace(/[&<>"']/g, char => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[char]);
16
+ const quoted = value => '"' + String(value).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1') + '"';
17
+ const read = file => fs.readFile(file, "utf8").catch(error => { if (error.code === "ENOENT") return null; throw error; });
18
+
19
+ // One user-level login item, pointing to the installed CLI. The CLI reuses serverFor's
20
+ // single-service lock, saved port and hidden process creation; it never opens a browser.
21
+ // No postinstall hook, administrator access or keep-alive loop: gallery stop stays stopped.
22
+ export function createStartup({ platform = process.platform, home = os.homedir(), data = serviceLocation().data,
23
+ workspace = defaultWorkspace(), node = process.execPath, cli = path.join(packageRoot, "bin", "gallery.mjs"),
24
+ available = release && /[\\/]node_modules[\\/]@heycys[\\/]gallery$/.test(packageRoot) && !/(?:^|[\\/])_npx[\\/]/.test(packageRoot) && process.env.GALLERY_NO_AUTOSTART !== "1",
25
+ run = (program, args) => execute(program, args, { windowsHide: true, timeout: 10000 }) } = {}) {
26
+ const supported = platform === "win32" || platform === "darwin";
27
+ const settings = path.join(data, "startup.json");
28
+ const script = path.join(data, "startup.js");
29
+ const plist = path.join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
30
+ let queue = Promise.resolve();
31
+ const serial = task => { const next = queue.then(task); queue = next.catch(() => {}); return next; };
32
+ const config = async () => {
33
+ const text = await read(settings);
34
+ const value = text === null ? {} : JSON.parse(text);
35
+ if (!value || typeof value !== "object" || Array.isArray(value) || (value.enabled !== undefined && typeof value.enabled !== "boolean")) throw new Error("Invalid Gallery startup settings");
36
+ return { text, value };
37
+ };
38
+ async function registration() {
39
+ if (platform === "darwin") return read(plist);
40
+ if (platform !== "win32") return null;
41
+ try {
42
+ const { stdout } = await run("reg.exe", ["query", RUN, "/v", NAME]);
43
+ return /REG_SZ\s+(.+?)\s*$/m.exec(stdout)?.[1] ?? null;
44
+ } catch (error) { if (error.code === 1) return null; throw error; }
45
+ }
46
+ const command = [path.join(process.env.SystemRoot || "C:\\Windows", "System32", "wscript.exe"), "//B", "//Nologo", script].map(quoted).join(" ");
47
+ const environment = { GALLERY_DATA_DIR: data, GALLERY_DEFAULT_WORKSPACE: workspace, GALLERY_WORKSPACE: workspace, GALLERY_NO_BROWSER: "1", GALLERY_NO_UPDATE_CHECK: "1" };
48
+ const windowsScript = `// Gallery login launcher. Window style 0: no console or browser.\nvar shell = new ActiveXObject("WScript.Shell");\nvar env = shell.Environment("Process");\n${Object.entries(environment).map(([key, value]) => `env(${JSON.stringify(key)}) = ${JSON.stringify(value)};`).join("\n")}\nshell.Run(${JSON.stringify([node, cli, "startup", "run"].map(quoted).join(" "))}, 0, false);\n`;
49
+ const macPlist = `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>
50
+ <key>Label</key><string>${LABEL}</string>
51
+ <key>ProgramArguments</key><array>${[node, cli, "startup", "run"].map(value => `<string>${xml(value)}</string>`).join("")}</array>
52
+ <key>RunAtLoad</key><true/>
53
+ <key>AbandonProcessGroup</key><true/>
54
+ <key>EnvironmentVariables</key><dict>${Object.entries(environment).map(([key, value]) => `<key>${xml(key)}</key><string>${xml(value)}</string>`).join("")}</dict>
55
+ <key>StandardOutPath</key><string>${xml(path.join(data, "logs", "startup.log"))}</string>
56
+ <key>StandardErrorPath</key><string>${xml(path.join(data, "logs", "startup.log"))}</string>
57
+ </dict></plist>\n`;
58
+ const desired = platform === "win32" ? command : macPlist;
59
+ async function apply(enabled) {
60
+ const current = await registration();
61
+ if (enabled) {
62
+ await fs.mkdir(path.join(data, "logs"), { recursive: true });
63
+ if (platform === "win32") {
64
+ // WSH reads ANSI by default. ASCII Unicode escapes preserve Chinese user paths
65
+ // independently of the machine's active code page.
66
+ const encoded = windowsScript.replace(/[^\x00-\x7f]/g, character => "\\u" + character.charCodeAt(0).toString(16).padStart(4, "0"));
67
+ if (await read(script) !== encoded) await writeAtomic(script, encoded);
68
+ if (current !== desired) await run("reg.exe", ["add", RUN, "/v", NAME, "/t", "REG_SZ", "/d", command, "/f"]);
69
+ } else if (current !== desired) {
70
+ await fs.mkdir(path.dirname(plist), { recursive: true });
71
+ await writeAtomic(plist, macPlist);
72
+ }
73
+ } else if (current !== null) {
74
+ if (platform === "win32") await run("reg.exe", ["delete", RUN, "/v", NAME, "/f"]);
75
+ else await fs.unlink(plist);
76
+ }
77
+ }
78
+ async function status() {
79
+ const { value } = await config();
80
+ const installed = supported && available && await registration() !== null;
81
+ return { supported, available: supported && available, enabled: Boolean(installed) && value.enabled !== false, requested: value.enabled !== false };
82
+ }
83
+ return {
84
+ status,
85
+ sync: () => serial(async () => { if (supported && available) await apply((await config()).value.enabled !== false); return status(); }),
86
+ set: enabled => serial(async () => {
87
+ if (typeof enabled !== "boolean") throw new Error("enabled must be a boolean");
88
+ if (!supported || !available) throw new Error("Autostart requires an installed Gallery package on Windows or macOS");
89
+ const { text, value } = await config();
90
+ await fs.mkdir(data, { recursive: true });
91
+ // Persist the user's choice first. A failed OS operation remains retryable, and a
92
+ // disabled choice can never be silently turned back on by the next package update.
93
+ if (!await writeAtomic(settings, JSON.stringify({ ...value, enabled }, null, 2) + "\n", { expect: text })) throw new Error("Startup settings changed; try again");
94
+ await apply(enabled);
95
+ return status();
96
+ }),
97
+ };
98
+ }
99
+
100
+ let manager;
101
+ export const startup = () => manager ??= createStartup();
@@ -11,6 +11,9 @@ export const localPath = (pathname) => pathname.replace(PROJECT, "") || "/";
11
11
  export const currentProjectBase = () => PROJECT.exec(globalThis.location?.pathname ?? "")?.[1] ?? "";
12
12
  export function workspaceAddress(address, base = currentProjectBase()) {
13
13
  if (!base || typeof address !== "string" || projectOfPath(address)) return address;
14
+ // Editor modules, emoji data and the global custom-block catalog are shared by every
15
+ // document. Keep their canonical URLs to reuse the cache without project redirects.
16
+ if (/^\/__gallery\/(?:edit\/(?:v\/[a-f0-9]{16}\/)?src\/|components\/)/.test(address)) return address;
14
17
  return /^\/(?:__gallery\/|generated\/|pages\/)/.test(address) ? base + address : address;
15
18
  }
16
19
 
@@ -115,7 +115,7 @@ async function read(root, readOwn) {
115
115
  return [slug, { ...link, kind: "link", title: link?.title ?? slug, description: "", path: path.join(root, slug, "link.json") }];
116
116
  }
117
117
  const files = documentFiles(path.join(root, slug));
118
- const metadata = await fs.readFile(files.json, "utf8").then((text) => JSON.parse(text.replace(/^/, "")), () => null);
118
+ const metadata = await fs.readFile(files.json, "utf8").then((text) => JSON.parse(text.replace(/^/, ""))).catch(() => null);
119
119
  const value = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {};
120
120
  return [slug, { title: oneLine(value.title) || slug, description: oneLine(value.description), icon: shownIcon(value.icon), path: files.html }];
121
121
  })));
@@ -1,18 +1,15 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { userDirectory } from "./user-directory.mjs";
3
5
  import { effectiveLook, lookOf, withLookField } from "./document-look.mjs";
4
6
  import { writeAtomic } from "./files.mjs";
5
7
  import { refusal, said } from "./i18n/index.mjs";
6
8
  import { WORKSPACE_MARK, documentDirectory, documentFiles, layoutOf, markerOf } from "./workspace.mjs";
7
9
 
8
- // The workspace's default look (字体与布局's 应用到全部文档, src/shared/document-look.mjs): `look` in its
9
- // gallery.json, beside what the file says of the components, each choice that is not the default
10
- // kept and the field gone when none is. A document shows its own choices (doc.json's `look`) over it,
11
- // choice by choice, and a document with none of its own, a new one among them, shows it whole. A
12
- // workspace without it, an older one's, has every choice at its default. The 写作偏好 page (a doc.json
13
- // marked "instructions": true) has no look, the workspace's or its own. Here for the server alone: the
14
- // render, the workbench's service and the preview's watcher read it from disk; a page and the
15
- // workbench's app are told the look a document shows.
10
+ // One user-level appearance default; legacy workspace defaults are a fallback until
11
+ // a global choice is made. Document overrides win field by field. Instructions pages
12
+ // are exempt. Rendering, report metadata and export all share this resolver.
16
13
 
17
14
  // gallery.json as it reads now: what it holds when it is ours (a workspace without one holds nothing),
18
15
  // else why it cannot be taken (its components' messages: src/shared/component-catalog.mjs).
@@ -34,10 +31,26 @@ const readText = (file) => {
34
31
 
35
32
  /** The workspace's default look at `root` (lookOf's form), {} for none, for no gallery.json and for one that does not read or is not ours. */
36
33
  export function workspaceLook(root) {
34
+ const global = readText(globalLookFile());
35
+ if (global != null) {
36
+ try { const value = JSON.parse(global); if (value && typeof value.look === "object" && !Array.isArray(value.look)) return lookOf(value.look); } catch {}
37
+ }
37
38
  const text = readText(layoutOf(root).marker);
38
39
  return text === undefined ? {} : lookOf(settingsOf(text).value?.look);
39
40
  }
40
41
 
42
+ // One default for all storage directories. An explicit empty look overrides legacy
43
+ // gallery.json defaults too; until the user chooses a global default, keep those intact.
44
+ export const globalLookFile = () => path.join(userDirectory("data"), "appearance.json");
45
+ export async function writeGlobalLook(look) {
46
+ const file = globalLookFile();
47
+ const text = await fs.readFile(file, "utf8").catch(error => { if (error.code === "ENOENT") return null; throw error; });
48
+ const next = JSON.stringify({ look: lookOf(look) }, null, 2) + "\n";
49
+ if (text === next) return;
50
+ await fs.mkdir(path.dirname(file), { recursive: true });
51
+ if (!await writeAtomic(file, next, { expect: text })) throw new Error("Global appearance changed; try again");
52
+ }
53
+
41
54
  /**
42
55
  * What document `slug`'s doc.json says of its look at `root`: { own: its `look` as written (null for
43
56
  * none, for a doc.json that does not read and for none at all), instructions: whether it is the 写作偏好
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { writeAtomic } from "./files.mjs";
6
6
  import { userDirectory } from "./user-directory.mjs";
7
- import { layoutOf, markerOf } from "./workspace.mjs";
7
+ import { defaultWorkspace, layoutOf, markerOf } from "./workspace.mjs";
8
8
 
9
9
  // The registry of the workspaces used on this machine (notes/plans/workspaces-links-plan-2026-11-08.md
10
10
  // §2.3): one file in the user's data folder (../shared/user-directory.mjs "data": %LOCALAPPDATA%\gallery
@@ -33,6 +33,7 @@ export const registryFile = (options = {}) => path.join(userDirectory("data", op
33
33
 
34
34
  /** A workspace's path as one key, however it is spelt: absolute, and lower case on Windows. */
35
35
  export const workspaceKey = (root, platform = process.platform) => (platform === "win32" ? path.resolve(root).toLowerCase() : path.resolve(root));
36
+ const allowed = root => !process.env.GALLERY_SANDBOX || workspaceKey(root) === workspaceKey(defaultWorkspace());
36
37
 
37
38
  /** The workspace's short id: ws- and the first six hex digits of the SHA-256 of its key. */
38
39
  export const workspaceId = (root, platform = process.platform) => `ws-${createHash("sha256").update(workspaceKey(root, platform)).digest("hex").slice(0, 6)}`;
@@ -67,7 +68,9 @@ export function registryOf(text, platform = process.platform) {
67
68
  /** The registry as read now (registryOf of its file; none there: an empty one). */
68
69
  export function readRegistry({ file = registryFile() } = {}) {
69
70
  try {
70
- return registryOf(readFileSync(file, "utf8"));
71
+ const registry = registryOf(readFileSync(file, "utf8"));
72
+ if (process.env.GALLERY_SANDBOX) registry.workspaces = registry.workspaces.filter(({ root }) => allowed(root));
73
+ return registry;
71
74
  } catch {
72
75
  return { version: 1, workspaces: [] };
73
76
  }
@@ -145,7 +148,7 @@ export function isWorkspace(root) {
145
148
  */
146
149
  export async function registerWorkspace(root, { file = registryFile(), now = Date.now(), force = false } = {}) {
147
150
  const resolved = path.resolve(root);
148
- if (!isWorkspace(resolved)) return "none";
151
+ if (!allowed(resolved) || !isWorkspace(resolved)) return "none";
149
152
  const key = workspaceKey(resolved);
150
153
  const recent = (entry) => {
151
154
  const age = now - Date.parse(entry.seen);
@@ -76,7 +76,7 @@ export const PACKAGE_MANIFEST = "component.json";
76
76
  /** The folders a document keeps beside its files: what is its own of a folder it shares with the packages or the custom pages. */
77
77
  export const DOCUMENT_FOLDERS = ["assets", "materials"];
78
78
  /** The folders at the root that hold no document but may share their name with one made before them. */
79
- export const SHARED_FOLDERS = [COMPONENTS_FOLDER, PAGES_FOLDER, EXTENSIONS_FOLDER];
79
+ export const SHARED_FOLDERS = [COMPONENTS_FOLDER, PAGES_FOLDER, EXTENSIONS_FOLDER, 'preferences'];
80
80
  /** Whether a new document (or custom page) may not be called `name`, whatever the workspace holds. */
81
81
  export const keptName = (name) => RESERVED.has(name) || SHARED_FOLDERS.includes(name);
82
82
  /** Whether `name` may be a document's slug. */
@@ -167,6 +167,24 @@ function registryDocuments() {
167
167
  return utf8 ? valueOf(utf8.toString("utf8")) : null;
168
168
  }
169
169
 
170
+ // The Personal value as registryDocuments reads it, once a process: every defaultWorkspace() asks for it
171
+ // where GALLERY_DEFAULT_WORKSPACE names no folder (a server `gallery open` started: some 300 times as a
172
+ // document's page loads, and a command a dozen), and each reg.exe blocked for some 30 ms.
173
+ let personal;
174
+ const registryOnce = () => (personal === undefined ? (personal = registryDocuments()) : personal);
175
+ // The user's own home as the system knows it, once a process too.
176
+ let ownHome;
177
+ const realHomeOnce = () => {
178
+ if (ownHome === undefined) {
179
+ try {
180
+ ownHome = os.userInfo().homedir;
181
+ } catch {
182
+ ownHome = null;
183
+ }
184
+ }
185
+ return ownHome;
186
+ };
187
+
170
188
  const sameFolder = (a, b, platform) => {
171
189
  const [one, two] = [path.resolve(a), path.resolve(b)];
172
190
  return platform === "win32" ? one.toLowerCase() === two.toLowerCase() : one === two;
@@ -174,7 +192,7 @@ const sameFolder = (a, b, platform) => {
174
192
 
175
193
  /**
176
194
  * The user's Documents folder, the default workspace's parent: on Windows the one Explorer shows as
177
- * 文档 (SHELL_FOLDERS' Personal, its %VARIABLES% expanded), read for the user's own home alone (a test
195
+ * 文档 (SHELL_FOLDERS' Personal, read once a process, its %VARIABLES% expanded), read for the user's own home alone (a test
178
196
  * or a sandbox naming another HOME/USERPROFILE gets <that home>/Documents, never the user's folder);
179
197
  * on Linux the XDG documents folder (~/.config/user-dirs.dirs' XDG_DOCUMENTS_DIR: ~/文档 on a Chinese
180
198
  * desktop); a folder either names that is not there, or not absolute, is passed over; else, and on
@@ -182,8 +200,8 @@ const sameFolder = (a, b, platform) => {
182
200
  */
183
201
  export function documentsFolder({
184
202
  home = os.homedir(), env = process.env, platform = process.platform,
185
- realHome = (() => { try { return os.userInfo().homedir; } catch { return null; } })(),
186
- readRegistry = registryDocuments, readText = (file) => readFileSync(file, "utf8"), exists = existsSync,
203
+ realHome = realHomeOnce(),
204
+ readRegistry = registryOnce, readText = (file) => readFileSync(file, "utf8"), exists = existsSync,
187
205
  } = {}) {
188
206
  const fallback = path.join(home, "Documents");
189
207
  const usable = (folder) => folder && path.isAbsolute(folder) && exists(folder) ? path.normalize(folder) : null;
@@ -194,10 +212,15 @@ export function documentsFolder({
194
212
  value = readRegistry();
195
213
  } catch {}
196
214
  if (!value) return fallback;
197
- const variables = Object.fromEntries(Object.entries(env).map(([name, setting]) => [name.toUpperCase(), setting]));
198
- variables.USERPROFILE ??= home;
215
+ // A variable by its name in any case, USERPROFILE the home where none is set (the environment looked
216
+ // through only for a name not written in capitals there: Windows' own is read by any case).
217
+ const variable = (name) => {
218
+ const wanted = name.toUpperCase();
219
+ const setting = env[wanted] ?? Object.entries(env).findLast(([key]) => key.toUpperCase() === wanted)?.[1];
220
+ return setting ?? (wanted === "USERPROFILE" ? home : undefined);
221
+ };
199
222
  let unknown = false;
200
- const expanded = value.replace(/%([^%]+)%/g, (whole, name) => variables[name.toUpperCase()] ?? ((unknown = true), whole));
223
+ const expanded = value.replace(/%([^%]+)%/g, (whole, name) => variable(name) ?? ((unknown = true), whole));
201
224
  return (!unknown && usable(expanded)) || fallback;
202
225
  }
203
226
  if (platform === "linux") {
@@ -462,9 +485,13 @@ export async function readLink(folder) {
462
485
  return text === null ? null : readLinkText(text);
463
486
  }
464
487
 
465
- /** The 写作偏好 page's slug (the first document under .gallery/pages/), or null when there is none yet. */
466
- export async function pageSlug(root) {
467
- return (await documentsIn(layoutOf(root).preferencesPage))[0]?.slug ?? null;
488
+ /** The editor page associated with one preference file; legacy unmarked pages belong to the original. */
489
+ export async function pageSlug(root, preference = null) {
490
+ for (const item of await documentsIn(layoutOf(root).preferencesPage)) {
491
+ const meta = await readJson(documentFiles(item.folder).json);
492
+ if (meta?.preference !== null && (preference === null || meta?.preference === preference || (preference === 'preferences.md' && meta?.preference === undefined))) return item.slug;
493
+ }
494
+ return null;
468
495
  }
469
496
 
470
497
  /** The id of an entry of the trash, a folder of .gallery/trash/. */
@@ -1,46 +1,6 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { defaultWorkspace } from "./workspace.mjs";
4
- import { defaultPreferencesFile } from "./paths.mjs";
1
+ export { defaultPreferencesFile } from "./paths.mjs";
5
2
 
6
- // 写作偏好: the user's rules for writing a report, which an Agent reads before it writes (SKILL.md),
7
- // at <workspace>/preferences.md (shared/workspace.mjs). Gallery ships its default (writing-preferences.md,
8
- // beside this module; the release's assets/writing-preferences.md: paths.mjs defaultPreferencesFile): what
9
- // to write, in what order, the opening summary, titles, fidelity to the materials, evidence). The first
10
- // time the preferences are read and there is no preferences.md in the workspace (by gallery new, gallery
11
- // import, gallery list, or the workbench's 写作偏好 in 设置), it is made from that default; from then on it is the user's, edited in the workbench (its page, src/shared/reports.mjs
12
- // isInstructions, written as this file when the user saves it: src/transfer/service.mjs), and
13
- // never written over by the default again: a file that exists stays as it is, an empty one too.
14
-
15
- /** Gallery's default writing preferences (Markdown). */
16
- export { defaultPreferencesFile };
17
-
18
- /** Where the user's writing preferences are, for the documents of workspace `reportsDirectory`. */
19
- export const preferencesFileOf = () => path.join(defaultWorkspace(), "preferences.md");
20
-
21
- /**
22
- * The user's writing preferences: { file, text, made }, the file made from Gallery's default
23
- * first when there is none (`made`), never over one that exists (another reader making it at the
24
- * same moment: its file is read).
25
- */
26
- export async function readWritingPreferences() {
27
- const reportsDirectory = defaultWorkspace();
28
- const file = preferencesFileOf();
29
- const existing = await fs.readFile(file, "utf8").catch((error) => {
30
- if (error.code === "ENOENT") return null;
31
- throw error;
32
- });
33
- if (existing !== null) return { file, text: existing, made: false };
34
- const text = await fs.readFile(defaultPreferencesFile, "utf8");
35
- await fs.mkdir(reportsDirectory, { recursive: true });
36
- try {
37
- await fs.writeFile(file, text, { flag: "wx" });
38
- } catch (error) {
39
- if (error.code !== "EEXIST") throw error;
40
- return { file, text: await fs.readFile(file, "utf8"), made: false };
41
- }
42
- return { file, text, made: true };
43
- }
3
+ // Storage and migration live in preference-documents.mjs.
44
4
 
45
5
  /** Whether preferences (their Markdown) say anything: a letter or a digit under the title (its first line, a # heading). */
46
6
  export const saysAnything = (text) => /[\p{L}\p{N}]/u.test(text.replace(/^\s*# [^\n]*/, ""));
@@ -0,0 +1,2 @@
1
+ import { createRequire as __galleryRequire } from "node:module"; import { fileURLToPath as __galleryPath } from "node:url"; const require = __galleryRequire(import.meta.url); const __filename = __galleryPath(import.meta.url); const __dirname = __galleryPath(new URL(".", import.meta.url));
2
+ import{h as a,i as b}from"./chunk-TLNJWEFY.mjs";import"./chunk-ZOEJQOKT.mjs";import"./chunk-IA4PSIMD.mjs";import"./chunk-6N25CJ6Y.mjs";import"./chunk-F4CT35KL.mjs";import"./chunk-6ILJWFYW.mjs";import"./chunk-VRPVVMRB.mjs";import"./chunk-UMHUSOPJ.mjs";import"./chunk-W5NPH5UA.mjs";import"./chunk-ECZBLQKK.mjs";import"./chunk-E7656LR7.mjs";import"./chunk-TKWEX4FD.mjs";import"./chunk-A55HN5OK.mjs";import"./chunk-5L7RYZCL.mjs";export{b as createEditService,a as loadToken};
@@ -0,0 +1,2 @@
1
+ import { createRequire as __galleryRequire } from "node:module"; import { fileURLToPath as __galleryPath } from "node:url"; const require = __galleryRequire(import.meta.url); const __filename = __galleryPath(import.meta.url); const __dirname = __galleryPath(new URL(".", import.meta.url));
2
+ import{a as d,e as c,h as x,n as v}from"./chunk-B6RAGF4R.mjs";import{a as N}from"./chunk-5TLLIZT7.mjs";import{b as m,f as $}from"./chunk-L4WLQYFC.mjs";import"./chunk-5L7RYZCL.mjs";import I from"node:path";import{draftOf as L,workspaceOf as b}from"../browser/shared/drafts.mjs";import{pageLinks as j,pageOfLink as A}from"../browser/shared/page-links.mjs";import{reportsDirectory as E}from"../browser/shared/paths.mjs";import{isWorkspace as F,isWorkspaceId as K,registerQuietly as Q,workspaceId as f,workspaceKey as O}from"../browser/shared/workspace-registry.mjs";import{documentDirectory as S,documentFiles as U}from"../browser/shared/workspace.mjs";var D=r=>[...r.documents.values()].filter(({url:e})=>e===void 0);async function q(r){if(K(r)){let t=b(r);return t.root?{root:t.root,id:r}:{refused:`${r} is ${t.refused==="ambiguous"?"the id of more than one registered workspace":"no registered workspace"}: gallery workspaces lists them, with their ids and paths.`}}let e=I.resolve(r);if(!F(e))return{refused:`${e} is not a Gallery workspace (it has no gallery.json of Gallery's): gallery init "${e}" makes one, gallery workspaces lists the registered ones.`};await Q(e);let i=b(f(e));return i.root&&O(i.root)===O(e)?{root:e,id:f(e)}:{refused:`${e} could not be registered (gallery workspaces lists the registered ones).`}}async function Y(r){let{positional:e,values:i}=$({values:["--to","--under"],argv:r});if(e.length!==1)throw new m("Name the document to copy, one.");if(!i.has("--to"))throw new m("Say where it is copied to: --to <workspace> (its path, or its ws- id from gallery workspaces).");let t=await d(),s=await x(e[0],"the document to copy",t);s.refused&&(console.error(s.refused),process.exit(1));let T=t.documents.get(s.slug),w=L(s.slug),W=f(E),g=w?.ws??W,C=w?.slug??s.slug,o=await q(i.get("--to"));o.refused&&(console.error(`${o.refused} Nothing copied.`),process.exit(1)),o.id===g&&(console.error(`${c(t,s.slug)} is in ${o.root} already: gallery duplicate <slug> copies it there, after itself.`),process.exit(1)),T.url!==void 0&&(console.error(`${c(t,s.slug)} is a web link, not a document: gallery link add makes the same link in the other workspace (gallery link add <url> "<name>" --workspace "${o.root}").`),process.exit(1));let u=await d(o.root),a="";if(i.has("--under")){let l=i.get("--under"),y=D(u).some(({slug:G})=>G===l)?l:A(j(D(u)),l.replace(/^.*#/,""));y||(console.error(`No document "${l}" in ${o.root} to put the copy under: gallery list --tree --workspace "${o.root}" shows its documents. Nothing copied.`),process.exit(1)),a=y}let n=await v("copy-from",{sourceWs:g,slug:C,parent:a,index:(u.children[a]??[]).length,keep:!0},{root:o.root});n?.ok||(console.error(`Not copied: ${n?.message??n?.reason??"no answer"}.`),process.exit(1));let h=await d(o.root),k=h.documents.get(n.slug),p=n.count-1;console.log(`Copied ${c(t,s.slug)}${p?` with the ${p} document${p===1?"":"s"} under it`:""} to ${o.root}: ${n.slug}${k?.title?` (${k.title})`:""}, last ${a?`under ${c(h,a)}`:"at its top level"}. The original stays where it was.`),console.log(`The copy's file: ${N(U(S(n.slug,o.root)).html)}`),console.log(`\u4E0B\u4E00\u6B65\uFF1Awork on the copy in that workspace: gallery open ${n.slug} --workspace "${o.root}" (every command takes --workspace).`)}export{Y as run};
@@ -1,2 +1,2 @@
1
1
  import { createRequire as __galleryRequire } from "node:module"; import { fileURLToPath as __galleryPath } from "node:url"; const require = __galleryRequire(import.meta.url); const __filename = __galleryPath(import.meta.url); const __dirname = __galleryPath(new URL(".", import.meta.url));
2
- import{e as H,f as K,g as Q}from"./chunk-S3SX3COT.mjs";import{d as D,f as E,g as T,h as F}from"./chunk-TKWEX4FD.mjs";import{existsSync as R}from"node:fs";var U=120;function W(n,I){let p=({packages:s,problems:c})=>new Map([...s,...c].map(([a,{stamp:t}])=>[a,t])),[l,f]=[p(n),p(I)];return[...new Set([...l.keys(),...f.keys()])].filter(s=>l.get(s)!==f.get(s)).sort()}function A({root:n,tell:I,delay:p=U,quiet:l=Q,hold:f=H,limit:s=K,now:c=Date.now,timers:a={set:(t,m)=>setTimeout(t,m),clear:t=>clearTimeout(t)}}){let t=null,m=0,u=null,r=new Map;T(n);let M=i=>{t&&m<=c()+i||(a.clear(t),m=c()+i,t=a.set(w,i),t.unref?.())};function w({settled:i=!1}={}){t=null;let d=T(n),o=E(n,d.settings),b=c();o.stamp!==u?.stamp&&(u={stamp:o.stamp,at:b});let y=b-u.at;if(y<l&&!i)return M(l-y);let g=new Map(o.packages),k=new Map(o.problems),h=1/0;for(let[e,L]of d.packages){if(!(!o.settings.disabled.includes(e)&&(k.has(e)||!g.has(e)&&R(L.folder)))){r.delete(e);continue}r.has(e)||r.set(e,u.at);let O=Math.min(f-y,s-(b-r.get(e)));if(O<=0){r.delete(e);continue}g.set(e,L),k.delete(e),h=Math.min(h,O)}let{settings:C,packs:S}=o,x=D({packages:g,problems:k,settings:C,packs:S});if(x!==d.stamp){let e={packages:g,problems:k,settings:C,packs:S,stamp:x};F(n,e),I(e,W(d,e))}h!==1/0&&M(h)}return{changed:()=>M(p),taken(){a.clear(t),w({settled:!0})},close(){a.clear(t),t=null}}}export{U as a,A as b};
2
+ import{e as H,f as K,g as Q}from"./chunk-TLNJWEFY.mjs";import{d as D,f as E,g as T,h as F}from"./chunk-TKWEX4FD.mjs";import{existsSync as R}from"node:fs";var U=120;function W(n,I){let p=({packages:s,problems:c})=>new Map([...s,...c].map(([a,{stamp:t}])=>[a,t])),[l,f]=[p(n),p(I)];return[...new Set([...l.keys(),...f.keys()])].filter(s=>l.get(s)!==f.get(s)).sort()}function A({root:n,tell:I,delay:p=U,quiet:l=Q,hold:f=H,limit:s=K,now:c=Date.now,timers:a={set:(t,m)=>setTimeout(t,m),clear:t=>clearTimeout(t)}}){let t=null,m=0,u=null,r=new Map;T(n);let M=i=>{t&&m<=c()+i||(a.clear(t),m=c()+i,t=a.set(w,i),t.unref?.())};function w({settled:i=!1}={}){t=null;let d=T(n),o=E(n,d.settings),b=c();o.stamp!==u?.stamp&&(u={stamp:o.stamp,at:b});let y=b-u.at;if(y<l&&!i)return M(l-y);let g=new Map(o.packages),k=new Map(o.problems),h=1/0;for(let[e,L]of d.packages){if(!(!o.settings.disabled.includes(e)&&(k.has(e)||!g.has(e)&&R(L.folder)))){r.delete(e);continue}r.has(e)||r.set(e,u.at);let O=Math.min(f-y,s-(b-r.get(e)));if(O<=0){r.delete(e);continue}g.set(e,L),k.delete(e),h=Math.min(h,O)}let{settings:C,packs:S}=o,x=D({packages:g,problems:k,settings:C,packs:S});if(x!==d.stamp){let e={packages:g,problems:k,settings:C,packs:S,stamp:x};F(n,e),I(e,W(d,e))}h!==1/0&&M(h)}return{changed:()=>M(p),taken(){a.clear(t),w({settled:!0})},close(){a.clear(t),t=null}}}export{U as a,A as b};
@@ -0,0 +1,2 @@
1
+ import { createRequire as __galleryRequire } from "node:module"; import { fileURLToPath as __galleryPath } from "node:url"; const require = __galleryRequire(import.meta.url); const __filename = __galleryPath(import.meta.url); const __dirname = __galleryPath(new URL(".", import.meta.url));
2
+ import{a as P,b as S}from"./chunk-HFTBVEZK.mjs";import{a as y,d as C,k as b,r as h,t as g}from"./chunk-BSJKFE7I.mjs";import{c as m,g as c,k,v as w}from"./chunk-TKWEX4FD.mjs";import{BLOCK_ICON as E}from"../browser/editor/components/icons.mjs";import{LISTED_BLOCKS as O}from"../browser/editor/components/registry.mjs";import{PACKAGE_ID as L}from"../browser/shared/components/installed.mjs";import{netHostsOf as I}from"../browser/shared/net-policy.mjs";import{inLanguage as A,refusal as l,said as o}from"../browser/shared/i18n/index.mjs";function T({root:s,taken:u}){let d=()=>{let e=c(s),i=new Map(C().map(t=>[t.id,t]));return{packages:w(e).map(t=>{let n=i.get(t.id);return{...t,newer:n?b(s,n):null,...n?{addAs:n.pack===y?n.name:n.id}:{}}}),packs:g(s,{settings:e.settings}),packageIcon:E,blocks:O.map(({id:t,name:n,icon:a,hideable:r})=>({id:t,name:{zh:A("zh",n),en:A("en",n)},icon:a,hidden:r&&e.settings.hidden.includes(t),hideable:r})),problem:k(e)}},B=({id:e,hidden:i,shown:t,disabled:n,unlisted:a,net:r})=>{if(![i,t,n,a,r].every(f=>f===void 0||typeof f=="boolean"))return o("\u5F00\u5173\u7684\u503C\u4E0D\u5BF9");let p=O.find(f=>f.id===e);return p?n!==void 0||t!==void 0||a!==void 0||r!==void 0?o("\u5185\u7F6E\u5757\u53EA\u80FD\u9690\u85CF\uFF0C\u4E0D\u80FD\u505C\u7528"):i&&!p.hideable?o("\u8FD9\u4E2A\u5757\u4E0D\u80FD\u9690\u85CF"):null:typeof e=="string"&&L.test(e)?r&&I(c(s).packages.get(e)?.needs).length===0?o("\u8FD9\u4E2A\u5757\u4E0D\u8054\u7F51"):null:o("\u6CA1\u6709\u8FD9\u4E2A\u7EC4\u4EF6")};return{read:d,write:async e=>{if(e?.pack!==void 0){let{pack:r,enabled:p}=e;if(typeof p!="boolean")throw l(o("\u5F00\u5173\u7684\u503C\u4E0D\u5BF9"));if(typeof r!="string"||!g(s,{settings:c(s).settings}).some(({id:f})=>f===r))throw l(o("\u6CA1\u6709\u8FD9\u4E2A\u5757\u5305"));return await m(s,{pack:r,enabled:p}),u(),d()}let i=B(e);if(i)throw l(i);let{net:t,...n}=e,a=t===void 0?void 0:t?I(c(s).packages.get(e.id)?.needs):[];return await m(s,{...n,...a?{net:a}:{}}),u(),d()},install:({path:e,zip:i}={})=>{let t=typeof i=="string"?S(Buffer.from(i,"base64")):typeof e=="string"&&e.trim()?P(e):null;if(!t)throw l(o("\u7C98\u8D34\u5757\u5305\u7684\u6587\u4EF6\u5939\u6216 zip \u7684\u8DEF\u5F84"),{code:"path"});let n=h(s,t);return u(),{...d(),installed:n}}}}export{T as a};
@@ -0,0 +1,4 @@
1
+ import { createRequire as __galleryRequire } from "node:module"; import { fileURLToPath as __galleryPath } from "node:url"; const require = __galleryRequire(import.meta.url); const __filename = __galleryPath(import.meta.url); const __dirname = __galleryPath(new URL(".", import.meta.url));
2
+ import"./chunk-5L7RYZCL.mjs";import t from"node:fs/promises";import l from"node:path";import{storedOf as g}from"../browser/shared/outline.mjs";import{entryPattern as k,isTop as h,topOf as j}from"../browser/shared/outline-shape.mjs";import{workspaceId as S}from"../browser/shared/workspace-registry.mjs";import{isDocumentSource as b,isSlugName as O,layoutOf as y,markerOf as F}from"../browser/shared/workspace.mjs";var N=new Set(["outline.json","blocks.json","hints.json","emoji-library","pages"]),L=new Set([".git","node_modules"]);async function _(o,w){let r=y(o),n=y(w),p=await t.lstat(r.marker).catch(()=>null);if(!p?.isFile()||p.isSymbolicLink())throw new Error("\u6E90\u5DE5\u4F5C\u533A\u9700\u8981\u666E\u901A\u6587\u4EF6 gallery.json");let c=F(await t.readFile(r.marker,"utf8"));if(!c.ours)throw new Error("\u6E90\u76EE\u5F55\u4E0D\u662F Gallery \u5DE5\u4F5C\u533A");delete c.value.extensions,await t.writeFile(n.marker,JSON.stringify(c.value,null,2)+`
3
+ `,{flag:"wx"});let f=0;async function a(i,e){let s=await t.lstat(i);if(s.isSymbolicLink()){f++;return}if(s.isFile())return t.copyFile(i,e);if(!s.isDirectory()){f++;return}await t.mkdir(e);for(let d of await t.readdir(i))L.has(d.toLowerCase())||await a(l.join(i,d),l.join(e,d))}for(let i of await t.readdir(o,{withFileTypes:!0})){let{name:e}=i,s=l.join(o,e);if(["preferences.md","preferences","components","pages","extensions"].includes(e))await a(s,l.join(w,e));else if(O(e)){if(i.isSymbolicLink()){f++;continue}if(!i.isDirectory())continue;(await t.readdir(s)).some(u=>b(u)||u==="link.json")&&await a(s,l.join(w,e))}}let m=await t.lstat(r.gallery).catch(i=>{if(i.code!=="ENOENT")throw i;return null});if(m?.isDirectory()&&!m.isSymbolicLink()){await t.mkdir(n.gallery);for(let i of await t.readdir(r.gallery))N.has(i)&&await a(l.join(r.gallery,i),l.join(n.gallery,i));await x(n.outline,o)}return f+Number(m?.isSymbolicLink()??!1)}async function x(o,w){let r=await t.readFile(o,"utf8").then(JSON.parse).catch(()=>null),n=g(r);if(!n)return;let p=S(w),c=n.groups.filter(i=>!i.workspace&&(!i.owner||i.owner===p)).map(({owner:i,...e})=>e),f=new Set(c.map(i=>j(i.id))),a=i=>typeof i=="string"&&!i.startsWith("~")&&k.test(i),m=Object.fromEntries(Object.entries(n.children).filter(([i])=>h(i)?f.has(i):a(i)).map(([i,e])=>[i,e.filter(a)]));await t.writeFile(o,JSON.stringify({version:2,groups:c,children:m,...r.head?{head:r.head}:{}},null,2)+`
4
+ `)}export{_ as copySandboxWorkspace};
@@ -0,0 +1,2 @@
1
+ import { createRequire as __galleryRequire } from "node:module"; import { fileURLToPath as __galleryPath } from "node:url"; const require = __galleryRequire(import.meta.url); const __filename = __galleryPath(import.meta.url); const __dirname = __galleryPath(new URL(".", import.meta.url));
2
+ import{c as a,d as b,e as c,f as d,g as e,h as f,i as g,j as h,k as i,l as j,m as k,n as l,o as m,p as n,q as o,r as p,s as q,t as r,u as s,v as t,w as u,x as v}from"./chunk-B2EIMERT.mjs";import"./chunk-HYMEEXKN.mjs";import"./chunk-A55HN5OK.mjs";import"./chunk-5L7RYZCL.mjs";export{d as ASSETS,o as afterKitTags,s as allowedScripts,t as allowedStyles,j as assetProblems,i as assetTags,a as d3SourcePath,q as earlyScriptFailures,b as katexFontsSourceDirectory,h as kitTags,u as loadKatexCss,v as loadVendorFile,c as mhchemSourcePath,l as pruneUnusedKitTags,f as publicFileOf,e as publicPathOf,p as tagFailures,g as tagOf,k as usedAssets,r as withKitCopies,m as withKitTags,n as withNeededKitTags};