@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
@@ -1,17 +1,47 @@
1
1
  // English for Gallery's UI, by the Chinese text the source writes (index.mjs t()), in the order the
2
2
  // sources use them. tests/unit/i18n.mjs holds every string to an entry here and every entry to a string.
3
3
  export default {
4
- "启用扩展「{0}」?": "Enable extension “{0}”?",
5
- "此扩展可以在工作台运行脚本;后台程序拥有 Gallery 的本地文件和网络访问权限。仅启用你信任的扩展。": "This extension can run workbench scripts. Its backend has Gallery’s local file and network permissions. Only enable extensions you trust.",
4
+ "配置错误": "Configuration error",
5
+ "界面错误": "UI error",
6
+ "后台异常": "Backend error",
7
+ "还没有安装扩展": "No extensions installed",
8
+ "把扩展的文件夹或 zip 交给 Agent,或点下面的「安装扩展」。": "Give an extension folder or ZIP to an Agent, or click Install extension below.",
9
+ "安装扩展": "Install extension",
10
+ "安装": "Install",
11
+ "这个扩展关着:它的按钮和后台程序都不运行。": "This extension is disabled. Its buttons and backend are not running.",
12
+ "尚未启动": "Not started",
13
+ "正在启动": "Starting",
14
+ "运行中": "Running",
15
+ "正在重试": "Retrying",
16
+ "文件夹": "Folder",
17
+ "新偏好": "New preference",
18
+ "删除偏好「{0}」?": "Delete preference “{0}”?",
19
+ "对应的 Markdown 文件会被删除,无法恢复。": "Its Markdown file will be permanently deleted.",
20
+ "Agent 写作前会查看启用的偏好,并按本次任务选择适用的规则。": "Before writing, the Agent checks enabled preferences and selects the rules relevant to this task.",
21
+ "未保存": "Unsaved",
22
+ "新建偏好": "New preference",
23
+ "启用偏好:{0}": "Enable preference: {0}",
24
+ "全部偏好已停用,Agent 将按本次要求写作。": "All preferences are disabled. The Agent will follow your current request.",
25
+ "找不到写作偏好": "Writing preference not found",
26
+ "扩展错误记录无效": "Invalid extension error record",
27
+ "偏好文件已被修改,请重新打开后再试": "The preference file changed. Reopen it before trying again.",
28
+ "偏好名称不能包含路径或特殊字符": "Preference names cannot contain paths or special characters",
29
+ "写作偏好路径无效": "Invalid preference path",
30
+ "写作偏好不能是链接或特殊文件": "Preferences cannot be links or special files",
31
+ "偏好文件夹必须是普通文件夹": "The preferences folder must be an ordinary directory",
32
+ "偏好列表已变更,请重试": "The preference list changed. Try again.",
33
+ "偏好文件夹不能是链接": "The preferences folder cannot be a link",
34
+ "启用状态无效": "Invalid enabled state",
35
+ "未知的偏好操作": "Unknown preference action",
36
+ "拓展列表": "Extensions",
37
+ "开机自启动": "Start at login",
38
+ "登录系统后在后台启动 Gallery,不打开浏览器窗口": "Start Gallery in the background at login without opening a browser",
39
+ "开机自启动设置读取失败": "Could not read startup settings",
40
+ "开机自启动需要 Windows 或 macOS 上正式安装的 Gallery。": "Startup requires an installed Gallery package on Windows or macOS.",
41
+ "开机自启动设置失败:{0}": "Could not change startup settings: {0}",
6
42
  "启用": "Enable",
7
43
  "扩展设置保存失败": "Could not save extension settings",
8
- "扩展设置已保存。重新打开 Gallery 后生效。": "Extension settings saved. Reopen Gallery to apply them.",
9
- "来自默认工作区,所有文档共用。": "From the default workspace, shared by all documents.",
10
- "还没有安装扩展。": "No extensions installed yet.",
11
- "工作台脚本": "Workbench script",
12
- "工作台样式": "Workbench style",
13
44
  "后台程序": "Backend process",
14
- "侧栏目录": "Sidebar directory",
15
45
  "扩展列表读取失败": "Could not load extensions",
16
46
  "扩展 {0} 加载失败": "Could not load extension {0}",
17
47
  "扩展正在更新,请稍后再试": "The extension is being updated. Try again shortly.",
@@ -39,8 +69,6 @@ export default {
39
69
  "workbench 必须是对象": "workbench must be an object",
40
70
  "server.command 必须是非空的参数数组": "server.command must be a nonempty array of arguments",
41
71
  "扩展开关的值不正确": "Invalid extension enabled value",
42
- "扩展刚刚更新,请重新打开设置": "The extension was updated. Reopen settings.",
43
- "请先确认扩展的脚本和后台权限": "Confirm the extension’s script and backend permissions first",
44
72
  "扩展目录刚在别处改过,请重试": "The extension directory changed elsewhere. Please try again.",
45
73
  "找不到这个扩展": "This extension could not be found",
46
74
  "扩展路径不是普通文件或文件夹": "The extension path is not a regular file or directory",
@@ -90,6 +118,8 @@ export default {
90
118
  "文档": "Documents",
91
119
  "重命名": "Rename",
92
120
  "移动到": "Move to",
121
+ "历史版本": "Version history",
122
+ "这份文档以前的样子,可以恢复到其中一版": "This document as it was before: restore any of its versions",
93
123
  "导入": "Import",
94
124
  "移至回收区": "Move to trash",
95
125
  "已移动到「{0}」": "Moved to “{0}”",
@@ -101,6 +131,7 @@ export default {
101
131
  "永久删除": "Delete forever",
102
132
  "回收区是空的": "Trash is empty",
103
133
  "偏好": "Preferences",
134
+ "功能": "Features",
104
135
  "显示编辑框": "Show editing frame",
105
136
  "点进一段文字时,在它周围显示蓝色虚线框": "A dashed blue frame around the text you click into",
106
137
  "减少通知": "Fewer notifications",
@@ -122,7 +153,6 @@ export default {
122
153
  "拖动调整宽度": "Drag to resize",
123
154
  "点击收起侧边栏": "Click to close sidebar",
124
155
  "Agent 写报告时照着写的规则,每次写报告前都会先读": "The rules Agents write reports by, read before every report",
125
- "Agent 写报告时照着写的规则:写什么、怎么写、怎样忠于材料。每次写报告前它都会先读这里。": "The rules Agents write reports by: what to write, how, and how to stay true to the materials. They read this before writing any report.",
126
156
  "写作偏好里有一处这里显示不了:{0}。Agent 照样读得到;在这里保存后,它会被去掉。": "One part of the writing preferences can't be shown here: {0}. Agents still read it; once you save here, it will be removed.",
127
157
  "写作偏好里有 {0} 处这里显示不了,例如:{1}。Agent 照样读得到;在这里保存后,它们会被去掉。": "{0} parts of the writing preferences can't be shown here, such as: {1}. Agents still read them; once you save here, they will be removed.",
128
158
  "写作偏好没有保存:{0}": "The writing preferences weren't saved: {0}",
@@ -177,6 +207,7 @@ export default {
177
207
  "浅色": "Light",
178
208
  "深色": "Dark",
179
209
  "找不到这份文档": "Document not found",
210
+ "这个文档名已经有了": "A document or page already has that name",
180
211
  "令牌失效": "The token has expired",
181
212
  "请求来源不对": "Request from the wrong origin",
182
213
  "请求地址不对": "Request to the wrong address",
@@ -282,7 +313,7 @@ export default {
282
313
  "新增分栏": "Add column",
283
314
  "已调整栏宽:{0} · 双击竖线平分 · Ctrl+Z 撤销": "Column widths: {0} · double-click the line to even them · Ctrl+Z to undo",
284
315
  "已平分栏宽 · Ctrl+Z 撤销": "Columns evened · Ctrl+Z to undo",
285
- "已取消调整。": "Resize cancelled.",
316
+ "已取消调整": "Resize cancelled",
286
317
  "复制我的文字": "Copy my text",
287
318
  "复制这一块": "Copy this block",
288
319
  "AI 的写入把你之前保存的这段修改改回去了。": "The AI's write undid an edit you had saved here.",
@@ -318,7 +349,7 @@ export default {
318
349
  "收起": "Collapse",
319
350
  "你的版本": "Your version",
320
351
  "文件里的(AI 的)": "In the file (the AI's)",
321
- "已复制。": "Copied.",
352
+ "已复制": "Copied",
322
353
  "没有可复制的文字。": "Nothing to copy.",
323
354
  "没能完成,请再试一次。": "That didn't work; try again.",
324
355
  "你的": "Yours",
@@ -346,8 +377,8 @@ export default {
346
377
  "最后面": "bottom",
347
378
  "已移动{0} · Ctrl+Z 撤销": "Moved {0} · Ctrl+Z to undo",
348
379
  "正在移动{0} · 拖到要放的位置后松开 · 停在选项卡上切换,停在折叠块标题上展开 · Esc 取消": "Moving {0} · release where it should go · rest on a tab to switch to it, on a toggle's title to open it · Esc to cancel",
349
- "已取消移动。": "Move cancelled.",
350
- "位置没有变化。": "Nothing moved.",
380
+ "已取消移动": "Move cancelled",
381
+ "位置没有变化": "Nothing moved",
351
382
  "{0} · 拖动栏间的竖线调宽度 · Ctrl+Z 撤销": "{0} · drag the line between columns to resize · Ctrl+Z to undo",
352
383
  "已移动这一栏": "Column moved",
353
384
  "已把{0}放进新的一栏": "Put {0} in a new column",
@@ -385,7 +416,41 @@ export default {
385
416
  "没能确认:{0}": "Could not keep: {0}",
386
417
  "Agent 刚刚又改了文档,再看一眼": "The Agent just changed the document again: have another look",
387
418
  "撤回之后文档又改过了,没法撤销": "The document changed after the revert, so it cannot be undone",
419
+ "恢复之后文档又改过了,没法撤销": "The document changed after the restore, so it cannot be undone",
388
420
  "撤销已经过期": "It is too late to undo",
421
+ "找不到这个版本": "Version not found",
422
+ "这一版的内容": "This version's content",
423
+ "没能读出这一版": "Could not read this version",
424
+ "还有 {0} 块": "{0} more blocks",
425
+ "版本": "Versions",
426
+ "还没有存下的版本": "No versions kept yet",
427
+ "你": "You",
428
+ "未知": "Unknown",
429
+ "最早存下的样子": "The earliest version kept",
430
+ "Agent 在自用页面上改的": "Edited on the Agent's own page",
431
+ "页面上的修改": "Edited on the page",
432
+ "撤回了 Agent 的修改": "Reverted the Agent's changes",
433
+ "撤销了撤回或恢复": "Undid a revert or a restore",
434
+ "恢复了更早的版本": "Restored an earlier version",
435
+ "导出时补上了编号": "Block ids added on export",
436
+ "改了标题": "Renamed",
437
+ "改写了文件": "Rewrote the file",
438
+ "加了 {0} 块": "{0} blocks added",
439
+ "改了 {0} 块": "{0} changed",
440
+ "删了 {0} 块": "{0} deleted",
441
+ "块没有变": "No block changed",
442
+ "加了": "Added",
443
+ "改了": "Changed",
444
+ "删了": "Deleted",
445
+ "「{0}」": "“{0}”",
446
+ "这就是现在的样子": "This is the document as it is now",
447
+ "恢复到这一版?": "Restore this version?",
448
+ "文档会变回{0}的样子。现在的样子会先存成一个版本,之后还能恢复回来。": "The document goes back to how it was {0}. As it is now, it is kept as a version first, so you can restore that later.",
449
+ "已恢复到{0}的版本": "Restored the version from {0}",
450
+ "还有修改没保存或有冲突待处理,处理完再恢复": "Some edits are not saved yet or wait for you: deal with them, then restore",
451
+ "还有修改没保存或有冲突待处理": "Some edits are not saved yet or wait for you",
452
+ "没能恢复:{0}": "Could not restore: {0}",
453
+ "已撤销": "Undone",
389
454
  "预览服务器出错": "The preview server failed",
390
455
  "{0} 处修改和文件的最新版本冲突,没有保存:在左下方选择怎么处理。": "{0} edits conflict with the file's latest version and weren't saved: choose what to do at the bottom left.",
391
456
  "自定义块刚刚变了,{0} 处修改没有保存:在左下方选择怎么处理。": "A custom block just changed and {0} edits weren't saved: choose what to do at the bottom left.",
@@ -525,7 +590,6 @@ export default {
525
590
  "其余修改已保存;红框处的内容还没有保存。": "Other changes are saved; what's marked in red isn't saved yet.",
526
591
  "其余修改已保存;红框处的内容文档存不下,没有保存。": "Other changes are saved; what's marked in red can't be stored in the document and wasn't saved.",
527
592
  "其余修改已保存;还没写完的块(红框或空标题)没有保存。": "Other changes are saved; unfinished blocks (in red, or with an empty title) aren't.",
528
- "已保存。": "Saved.",
529
593
  "有上次没保存的修改,但现在连不上预览服务器;连上后刷新页面即可放回。": "There are unsaved changes from last time, but the preview server can't be reached; reload once it's back to put them in.",
530
594
  "有上次没保存的修改还没能放回(文件正在被修改,或预览服务器出错);稍后刷新页面会再试。": "Unsaved changes from last time couldn't be put back yet (the file is being changed, or the preview server failed); reloading later tries again.",
531
595
  "文件里的这一段已被修改,你的版本如下:": "This paragraph changed in the file; your version:",
@@ -784,7 +848,7 @@ export default {
784
848
  "显示为": "Show as",
785
849
  "选择视图": "Choose a view",
786
850
  "还没有链接地址。": "No link address yet.",
787
- "已复制链接。": "Link copied.",
851
+ "已复制链接": "Link copied",
788
852
  "这个块已经不在了。": "This block is no longer here.",
789
853
  "复制块链接": "Copy link to block",
790
854
  "这一块还没有保存,保存后才能复制它的链接。": "This block isn't saved yet: copy its link once it is.",
@@ -792,7 +856,7 @@ export default {
792
856
  "没有找到这个页面。": "Page not found.",
793
857
  "没能复制:浏览器不允许写剪贴板。": "Not copied: the browser doesn't allow writing to the clipboard.",
794
858
  "不支持这种链接地址。": "This kind of link address isn't supported.",
795
- "链接已移除。": "Link removed.",
859
+ "链接已移除": "Link removed",
796
860
  "链接:{0}": "Link: {0}",
797
861
  "链接视图": "Link",
798
862
  "标题视图": "Title",
@@ -1262,6 +1326,7 @@ export default {
1262
1326
  "复制批注": "Copy annotations",
1263
1327
  "删除批注": "Delete annotation",
1264
1328
  "全部删除": "Delete all",
1329
+ "删除已处理": "Delete done",
1265
1330
  "{0} 条": "{0}",
1266
1331
  "还没有批注。选中文字或点块右边的按钮来写": "None yet. Select text or use a block's button",
1267
1332
  "写下建议…": "Write a suggestion…",
@@ -1329,12 +1394,10 @@ export default {
1329
1394
  "写作偏好没有字体与布局": "Writing preferences have no font and layout",
1330
1395
  "字体与布局不对": "The font and layout given are not right",
1331
1396
  "这份文档的标题和说明读不出来": "This document's title and description could not be read",
1332
- "应用到全部文档": "Apply to all documents",
1333
- "这份文档的字体与布局用到全部文档,新建的文档也一样": "Use this document's font and layout in every document, new ones too",
1334
- "这份文档的字体与布局用到它所在工作区的全部文档,新建的文档也一样": "Use this document's font and layout in every document of its workspace, new ones too",
1335
- "已应用到全部文档": "Applied to all documents",
1336
- "已应用到全部文档,替换了 {0} 篇的单独设置": "Applied to all documents; own settings replaced in {0}",
1337
- "没有应用到全部文档:{0}": "Not applied to all documents: {0}",
1397
+ "已设置为全局默认": "Global default saved",
1398
+ "全局默认设置失败:{0}": "Could not set the global default: {0}",
1399
+ "所有工作区共用此默认字体与布局,保留文档的单独设置": "Use this default appearance across all workspaces, keeping individual document settings",
1400
+ "设置为全局默认": "Set as global default",
1338
1401
  // 自定义页(侧栏「新建」「搜索」下面那几行、搜索、回收区)
1339
1402
  "自定义页": "Custom page",
1340
1403
  "更多页面": "More pages",
@@ -1526,6 +1589,8 @@ export default {
1526
1589
  "这就是当前的工作区": "This is the current workspace",
1527
1590
  "侧边栏最多 {0} 栏,放不下这个工作区了": "The sidebar holds {0} columns at most: there is no room for this workspace",
1528
1591
  "默认文档": "Default documents",
1592
+ "沙盒": "Sandbox",
1593
+ "沙盒只能使用自己的工作区。": "The sandbox can only use its own workspace.",
1529
1594
  "这不是 Gallery 的底稿或工作区,可以用「导入」把文件复制进来": "This is no Gallery draft or workspace: Import copies a file in",
1530
1595
  "这份底稿不在它工作区的第一层(在回收区里,或是写作偏好),不能添加": "This draft is not at the top of its workspace (it is in the trash, or the writing preferences): it can't be added",
1531
1596
  "这个文件夹的名字不是 Gallery 文档的写法(小写字母、数字和连字符),不能添加": "This folder's name is not a Gallery document's (lowercase letters, digits and hyphens): it can't be added",
@@ -1564,4 +1629,38 @@ export default {
1564
1629
  "将「{0}」及其子页面复制到「{1}」,原项保留。": "Copy “{0}” and its child pages to “{1}”, keeping the originals.",
1565
1630
  "已复制 {0} 项,原项保留": "Copied {0} items; originals kept",
1566
1631
  "跨工作区请复制,不能只移动目录位置": "Copy items to another workspace instead of moving their tree positions",
1632
+ "无法打开文件夹": "Could not open the folder",
1633
+ "已启用「{0}」": "Enabled “{0}”",
1634
+ "已停用「{0}」": "Disabled “{0}”",
1635
+ "刷新": "Refresh",
1636
+ "移除扩展「{0}」?": "Remove extension “{0}”?",
1637
+ "它的文件夹会被删除,包括它在侧栏里的栏目。这一步不能撤销。": "Its folder, including its sidebar column, will be deleted. This cannot be undone.",
1638
+ "已移除「{0}」": "Removed “{0}”",
1639
+ "移除扩展…": "Remove extension…",
1640
+ "在侧栏中显示": "Show in sidebar",
1641
+ "给 Gallery 加按钮、侧栏栏目和后台功能,所有工作区共用。": "Add buttons, sidebar columns and backend features shared by all workspaces.",
1642
+ "界面": "Interface",
1643
+ "Agent 说明": "Agent instructions",
1644
+ "extension.json 读不出来:{0}": "Cannot read extension.json: {0}",
1645
+ "后台程序退出了(退出码 {0})": "The backend exited (code {0})",
1646
+ "查看日志": "View log",
1647
+ "刷新页面后生效": "Refresh the page to apply",
1648
+ "启用扩展:{0}": "Enable extension: {0}",
1649
+ "修好 extension.json 之后才能启用": "Fix extension.json before enabling",
1650
+ "侧栏栏目": "Sidebar columns",
1651
+ "{0} 个条目": "{0} entries",
1652
+ "已安装并启用「{0}」": "Installed and enabled “{0}”",
1653
+ "粘贴扩展的文件夹或 zip 的路径": "Paste the extension folder or zip path",
1654
+ "ZIP 数据无效或超过 100MB": "Invalid ZIP data or ZIP exceeds 100 MB",
1655
+ "扩展文件夹不正确": "Invalid extension folder",
1656
+ "请选择扩展路径或 ZIP": "Choose an extension path or ZIP",
1657
+ "Gallery 服务暂时无法响应,请稍后重试扩展操作": "The Gallery service cannot respond right now. Retry the extension operation later.",
1658
+ "enable 必须是布尔值": "enable must be a boolean",
1659
+ "扩展名称、版本和描述必须是短文本": "The extension name, version and description must be short text",
1660
+ "icon 不正确": "Invalid icon",
1661
+ "icon 必须是表情或扩展里的图片": "The icon must be an emoji or an image inside the extension",
1662
+ "server.timeout 必须是 1000 到 600000 毫秒": "server.timeout must be between 1000 and 600000 milliseconds",
1663
+ "找不到扩展": "Extension not found",
1664
+ "扩展引用的文件不存在": "A file referenced by the extension is missing",
1665
+ "找不到 extension.json": "extension.json was not found",
1567
1666
  };
@@ -0,0 +1,109 @@
1
+ import { DOCS, isTop, newGroupId, topOf } from "./outline-shape.mjs";
2
+
3
+ // The sidebar's column and entry changes as plain functions of an outline ({ groups, children }, as
4
+ // shared/outline.mjs reads it), the same ones the workbench makes (src/workbench/app/workspace/meta.ts:
5
+ // addColumn, renameColumn, removeColumn, straysOf, detach, leaveOut), for gallery column and gallery
6
+ // workspaces --hide, which write the outline they make under its lock or through the running server
7
+ // (src/cli/tree.mjs editOutline). Each returns a new outline, or null when it changes nothing or cannot;
8
+ // the argument is never changed.
9
+
10
+ const tidy = (value) => (typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "");
11
+ const listOf = (outline, key) => outline.children[key] ?? [];
12
+ const parentOf = (outline, slug) => Object.entries(outline.children).find(([, list]) => list.includes(slug))?.[0] ?? null;
13
+
14
+ function withList(outline, key, list) {
15
+ const children = { ...outline.children };
16
+ if (list.length || isTop(key)) children[key] = list;
17
+ else delete children[key];
18
+ return { ...outline, children };
19
+ }
20
+
21
+ function insert(outline, slug, parent, index) {
22
+ const list = listOf(outline, parent).filter((each) => each !== slug);
23
+ list.splice(index === undefined ? list.length : Math.max(0, Math.min(index, list.length)), 0, slug);
24
+ return withList(outline, parent, list);
25
+ }
26
+
27
+ /** A new column `column` ({ id, title?, owner? }) at `index` (the end when none); null for an id there is already. */
28
+ export function addColumn(outline, column, index) {
29
+ if (outline.groups.some((group) => group.id === column.id)) return null;
30
+ const groups = [...outline.groups];
31
+ groups.splice(index === undefined ? groups.length : Math.max(0, Math.min(index, groups.length)), 0, column);
32
+ return { ...outline, groups, children: { ...outline.children, [topOf(column.id)]: [] } };
33
+ }
34
+
35
+ /** A new column's id, none of the outline's. */
36
+ export const newColumnId = (outline) => newGroupId(outline.groups);
37
+
38
+ /** Column `id` named `title` (null: back to the name it goes by); null for no such column, an extension's, an empty name or the same. */
39
+ export function renameColumn(outline, id, title) {
40
+ const name = title === null ? null : tidy(title).slice(0, 100);
41
+ const group = outline.groups.find((each) => each.id === id);
42
+ if (!group || group.extension || name === "" || (name ?? undefined) === group.title) return null;
43
+ const groups = outline.groups.map((each) => {
44
+ if (each.id !== id) return each;
45
+ const { title: _old, ...rest } = each;
46
+ void _old;
47
+ return name ? { ...rest, title: name } : rest;
48
+ });
49
+ return { ...outline, groups };
50
+ }
51
+
52
+ /**
53
+ * The entries in the column `id` of a workspace that are not that workspace's drafts (documents dragged in, web
54
+ * links, another workspace's drafts), with each one's place: [{ slug, parent, index }], in the sidebar's order.
55
+ */
56
+ export function straysOf(outline, id) {
57
+ const group = outline.groups.find((each) => each.id === id);
58
+ if (!group?.workspace) return [];
59
+ const own = `~${group.workspace}/`;
60
+ const found = [];
61
+ const walk = (key) => listOf(outline, key).forEach((slug, index) => {
62
+ if (slug.startsWith(own)) walk(slug);
63
+ else found.push({ slug, parent: key, index });
64
+ });
65
+ walk(topOf(id));
66
+ return found;
67
+ }
68
+
69
+ /**
70
+ * Column `id` removed from the sidebar as 从侧边栏移除 removes it. A workspace's column is hidden (`hidden`: the workspace,
71
+ * its files and the registry stay), what was dragged into it going last to the own column; one the user made (or an
72
+ * older Gallery's 链接) is deleted, its entries with those under them going last to the column of its owner's workspace
73
+ * (the own one), no file deleted. null for the own column, an extension's, none and one hidden already.
74
+ */
75
+ export function removeColumn(outline, id) {
76
+ if (id === DOCS || !outline.groups.some((group) => group.id === id && !group.extension && !group.hidden)) return null;
77
+ if (outline.groups.some((group) => group.id === id && group.workspace)) {
78
+ let next = { ...outline, groups: outline.groups.map((group) => (group.id === id ? { ...group, hidden: true } : group)) };
79
+ for (const { slug, parent } of straysOf(outline, id)) next = insert(withList(next, parent, listOf(next, parent).filter((each) => each !== slug)), slug, "");
80
+ return next;
81
+ }
82
+ const top = topOf(id);
83
+ const owner = outline.groups.find((group) => group.id === id)?.owner;
84
+ const destination = owner ? topOf(outline.groups.find((group) => group.workspace === owner)?.id ?? DOCS) : "";
85
+ const children = { ...outline.children, [destination]: [...listOf(outline, destination), ...listOf(outline, top)] };
86
+ delete children[top];
87
+ return { ...outline, children, groups: outline.groups.filter((group) => group.id !== id) };
88
+ }
89
+
90
+ /**
91
+ * Draft `entry` ("~<ws id>/<slug>") taken out of the tree as 从文档树移除 takes it (the document is not touched): the entries
92
+ * under it go up to where it was; when its workspace's column is a whole one (not `picked`), its slug is put among that
93
+ * column's `left`, so that the column does not bring it back. null for an entry the outline does not hold.
94
+ */
95
+ export function removeDraft(outline, entry) {
96
+ const parent = parentOf(outline, entry);
97
+ if (parent === null) return null;
98
+ const siblings = listOf(outline, parent);
99
+ const index = siblings.indexOf(entry);
100
+ const next = withList(withList(outline, parent, [...siblings.slice(0, index), ...listOf(outline, entry), ...siblings.slice(index + 1)]), entry, []);
101
+ const parts = /^~(ws-[0-9a-f]{6})\/(.+)$/.exec(entry);
102
+ const group = parts ? next.groups.find((each) => each.workspace === parts[1]) : undefined;
103
+ if (!parts || !group || group.picked) return next;
104
+ const left = (group.left ?? []).filter((slug) => slug !== parts[2]);
105
+ left.push(parts[2]);
106
+ const { left: _old, ...rest } = group;
107
+ void _old;
108
+ return { ...next, groups: next.groups.map((each) => (each === group ? { ...rest, left } : each)) };
109
+ }
@@ -105,6 +105,12 @@ export async function withOutlineLock(root, task, { local = false } = {}) {
105
105
  await fs.writeFile(lock, mine, { flag: "wx" });
106
106
  break;
107
107
  } catch (error) {
108
+ // Windows can briefly keep a just-unlinked lock delete-pending. Wait for the
109
+ // handle to close; do not remove a live writer's lock or treat it as stale.
110
+ if (process.platform === 'win32' && ['EPERM', 'EBUSY'].includes(error.code) && Date.now() <= until) {
111
+ await new Promise(resolve => setTimeout(resolve, 20));
112
+ continue;
113
+ }
108
114
  if (error.code !== "EEXIST") throw error;
109
115
  const age = await fs.stat(lock).then(({ mtimeMs }) => Date.now() - mtimeMs, () => 0);
110
116
  if (age > LOCK_STALE) await fs.rm(lock, { force: true });
@@ -476,13 +476,15 @@ export async function createPage(slug, title, { root, icon = null, lang, blank =
476
476
  }
477
477
 
478
478
  /**
479
- * Custom page `slug`'s title written into its page.json (the workbench's 重命名), every other field as
480
- * it was; page.html is not touched (its <title> and header are the Agent's). Resolves to its meta, or
481
- * null when the page is not there; throws for a title that is empty or longer than 200 characters.
479
+ * Custom page `slug`'s title and/or icon written into its page.json (the workbench's 重命名 writes the title,
480
+ * gallery page set both), every other field as it was; page.html is not touched (its <title> and header are
481
+ * the Agent's). `icon` a value (the caller holds it to what an icon may be), or null to take it away; left
482
+ * out, it stays. Resolves to its meta, or null when the page is not there; throws (code "title") for a title
483
+ * that is empty or longer than 200 characters.
482
484
  */
483
- export async function renamePage(slug, title, root) {
484
- const name = typeof title === "string" ? title.replace(/\s+/g, " ").trim() : "";
485
- if (!name || name.length > 200) throw Object.assign(new Error("A page's title is 1 to 200 characters."), { code: "title" });
485
+ export async function setPageMeta(slug, { title, icon } = {}, root) {
486
+ const name = title === undefined ? undefined : typeof title === "string" ? title.replace(/\s+/g, " ").trim() : "";
487
+ if (name !== undefined && (!name || name.length > 200)) throw Object.assign(new Error("A page's title is 1 to 200 characters."), { code: "title" });
486
488
  if (!pageExists(slug, root)) return null;
487
489
  const { json } = pageFiles(slug, root);
488
490
  for (let tries = 0; tries < 3; tries++) {
@@ -492,9 +494,19 @@ export async function renamePage(slug, title, root) {
492
494
  value = text === null ? {} : JSON.parse(text.replace(/^/, ""));
493
495
  } catch {}
494
496
  if (!value || typeof value !== "object" || Array.isArray(value)) value = {};
495
- const next = `${JSON.stringify({ ...value, title: name }, null, 2)}\n`;
496
- const written = text === null ? (await fs.writeFile(json, next, { flag: "wx" }).then(() => true, () => false)) : await writeAtomic(json, next, { expect: text });
497
+ const next = { ...value, ...(name !== undefined ? { title: name } : {}) };
498
+ if (icon === null) delete next.icon;
499
+ else if (icon !== undefined) next.icon = icon;
500
+ const body = `${JSON.stringify(next, null, 2)}\n`;
501
+ const written = text === null ? (await fs.writeFile(json, body, { flag: "wx" }).then(() => true, () => false)) : await writeAtomic(json, body, { expect: text });
497
502
  if (written) break;
498
503
  }
499
504
  return readPageMeta(slug, root);
500
505
  }
506
+
507
+ /**
508
+ * Custom page `slug`'s title written into its page.json (the workbench's 重命名), every other field as
509
+ * it was; page.html is not touched (its <title> and header are the Agent's). Resolves to its meta, or
510
+ * null when the page is not there; throws for a title that is empty or longer than 200 characters.
511
+ */
512
+ export const renamePage = (slug, title, root) => setPageMeta(slug, { title: typeof title === "string" ? title : "" }, root);
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { serviceLocation } from "./service-location.mjs";
5
+ import { userDirectory } from "./user-directory.mjs";
5
6
  import { findWorkspace, layoutOf } from "./workspace.mjs";
6
7
 
7
8
  // Where everything is on disk, for every system, in two kinds: the workspace's (the user's documents
@@ -58,8 +59,9 @@ export const starterDirectory = inPackage(["starter"], "src", "render", "starter
58
59
  * pack is installed, and gallery block add copies a block of (shared/block-packs.mjs, shared/shipped-blocks.mjs).
59
60
  */
60
61
  export const packsDirectory = inPackage(["packs"], "src", "packs");
61
- export const vendorDirectory = inPackage(["vendor"], "public", "vendor");
62
- export const kitPublicDirectory = inPackage(["kit"], "public", "kit");
62
+ const sandboxAssets = !release && process.env.GALLERY_SANDBOX ? path.join(userDirectory("cache"), "public") : null;
63
+ export const vendorDirectory = sandboxAssets ? path.join(sandboxAssets, "vendor") : inPackage(["vendor"], "public", "vendor");
64
+ export const kitPublicDirectory = sandboxAssets ? path.join(sandboxAssets, "kit") : inPackage(["kit"], "public", "kit");
63
65
  /** The editor's browser modules, at their path under src/ (the preview serves them so: src/server/routes.mjs). */
64
66
  export const browserDirectory = inPackage(["browser"], "src");
65
67
  /** The emoji and icon data (the editor fetches it; the render reads it for a page's icon). */
@@ -70,6 +72,7 @@ export const workbenchDirectory = release ? path.join(packageRoot, "web") : null
70
72
  export const sdkDirectory = path.join(packageRoot, "sdk");
71
73
  /** Gallery's default 写作偏好 (shared/writing-preferences.mjs). */
72
74
  export const defaultPreferencesFile = inPackage(["assets", "writing-preferences.md"], "src", "shared", "writing-preferences.md");
75
+ export const previewDirectory = inPackage(["assets", "preview"], "src", "preview");
73
76
  /** TypeScript (src/render/typescript.mjs): the release's own minified copy, beside the libs it reads; null: the typescript package. */
74
77
  export const typescriptFile = release ? path.join(packageRoot, "vendor", "typescript", "typescript.js") : null;
75
78
  /** Where the type check finds d3's types (the type library d3, and the d3-* it names). */
@@ -0,0 +1,137 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { defaultWorkspace, ensureWorkspace, markerOf, pageSlug, documentDirectory, documentFiles, DEVICE_NAMES } from './workspace.mjs';
4
+ import { defaultPreferencesFile } from './writing-preferences.mjs';
5
+ import { sha256, writeAtomic } from './files.mjs';
6
+ import { withOutlineLock } from './outline.mjs';
7
+
8
+ export const GENERAL_PREFERENCE = 'preferences/项目报告写作.md';
9
+ const configFile = () => path.join(defaultWorkspace(), 'gallery.json');
10
+ async function configuration() {
11
+ const text = await fs.readFile(configFile(), 'utf8').catch(error => { if (error.code === 'ENOENT') return '{}'; throw error; });
12
+ const { reads, value } = markerOf(text);
13
+ if (!reads || !value || typeof value !== 'object' || Array.isArray(value)) throw new Error('gallery.json 读不出来:改好这个文件后再试');
14
+ return value;
15
+ }
16
+ export function preferenceName(name) {
17
+ if (typeof name !== 'string' || !name.trim() || name !== name.trim() || name.length > 80 || /[<>:"/\\|?*\x00-\x1f]/.test(name) || /[. ]$/.test(name) || name.startsWith('.') || DEVICE_NAMES.has(name.split('.')[0].toLowerCase())) throw new Error('偏好名称不能包含路径或特殊字符');
18
+ return name;
19
+ }
20
+ export async function preferenceFile(id) {
21
+ if (typeof id !== 'string' || !id.startsWith('preferences/') || !id.endsWith('.md') || 'preferences/' + preferenceName(id.slice(12, -3)) + '.md' !== id) throw new Error('写作偏好路径无效');
22
+ const root = defaultWorkspace(), file = path.join(root, id);
23
+ for (const candidate of [path.join(root, 'preferences'), file]) {
24
+ const stat = await fs.lstat(candidate);
25
+ if (stat.isSymbolicLink() || (candidate === file && (!stat.isFile() || stat.nlink > 1))) throw new Error('写作偏好不能是链接或特殊文件');
26
+ }
27
+ return file;
28
+ }
29
+ export const titledPreference = (text, name) => /^\uFEFF?\s*#\s+[^\r\n]*(?:\r?\n|$)/.test(text) ? text.replace(/^\uFEFF?\s*#\s+[^\r\n]*(?:\r?\n|$)/, () => '# ' + name + '\n') : '# ' + name + '\n\n' + text;
30
+ export const listPreferenceDocuments = () => withOutlineLock(defaultWorkspace(), readPreferenceDocuments);
31
+ async function initialize(config) {
32
+ if (config.preferences?.initialized) return false;
33
+ const root = defaultWorkspace(), legacy = path.join(root, 'preferences.md');
34
+ const stat = await fs.lstat(legacy).catch(error => { if (error.code !== 'ENOENT') throw error; return null; });
35
+ if (stat && (!stat.isFile() || stat.isSymbolicLink() || stat.nlink > 1)) throw new Error('写作偏好不能是链接或特殊文件');
36
+ const directory = path.join(root, 'preferences');
37
+ await fs.mkdir(directory, { recursive: true });
38
+ if ((await fs.lstat(directory)).isSymbolicLink()) throw new Error('偏好文件夹不能是链接');
39
+ const text = titledPreference(await fs.readFile(stat ? legacy : defaultPreferencesFile, 'utf8'), '项目报告写作');
40
+ const file = path.join(root, GENERAL_PREFERENCE);
41
+ try { await fs.writeFile(file, text, { flag: 'wx' }); }
42
+ catch (error) {
43
+ if (error.code !== 'EEXIST') throw error;
44
+ // A previous migration may have stopped after writing the destination. Never overwrite a different document.
45
+ if (stat && await fs.readFile(await preferenceFile(GENERAL_PREFERENCE), 'utf8') !== text) throw new Error('偏好文件已被修改,请重新打开后再试');
46
+ }
47
+ const slug = await pageSlug(root, 'preferences.md');
48
+ if (slug) {
49
+ const json = documentFiles(documentDirectory(slug, root)).json;
50
+ const metadata = JSON.parse(await fs.readFile(json, 'utf8').catch(error => { if (error.code !== 'ENOENT') throw error; return '{}'; }));
51
+ await writeAtomic(json, JSON.stringify({ ...metadata, instructions: true, preference: GENERAL_PREFERENCE, title: '项目报告写作' }, null, 2) + '\n');
52
+ }
53
+ const mapped = values => Array.isArray(values) ? values.map(id => id === 'preferences.md' ? GENERAL_PREFERENCE : id) : [];
54
+ const order = mapped(config.preferences?.order);
55
+ config.preferences = { ...config.preferences, initialized: true, order: order.length ? order : [GENERAL_PREFERENCE], off: mapped(config.preferences?.off) };
56
+ await writeAtomic(configFile(), JSON.stringify(config, null, 2) + '\n');
57
+ if (stat) await fs.unlink(legacy);
58
+ return !stat;
59
+ }
60
+ // Readers share the writer's lock so a rename cannot temporarily hide a tab and discard its draft.
61
+ async function readPreferenceDocuments() {
62
+ const config = await configuration();
63
+ const made = await initialize(config);
64
+ const directory = path.join(defaultWorkspace(), 'preferences');
65
+ const stat = await fs.lstat(directory).catch(error => { if (error.code !== 'ENOENT') throw error; return null; });
66
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) throw new Error('偏好文件夹必须是普通文件夹');
67
+ const entries = stat ? await fs.readdir(directory, { withFileTypes: true }) : [];
68
+ const ids = entries.filter(entry => entry.isFile() && !entry.isSymbolicLink() && entry.name.endsWith('.md') && !entry.name.startsWith('.')).map(entry => 'preferences/' + entry.name).sort();
69
+ const order = Array.isArray(config.preferences?.order) ? config.preferences.order : [];
70
+ const off = new Set(Array.isArray(config.preferences?.off) ? config.preferences.off : []);
71
+ return Promise.all([...new Set([...order.filter(id => ids.includes(id)), ...ids])].map(async id => {
72
+ let file, text;
73
+ try { file = await preferenceFile(id); text = await fs.readFile(file, 'utf8'); }
74
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
75
+ return { id, file, name: path.basename(id, '.md'), enabled: !off.has(id), revision: sha256(text), slug: await pageSlug(defaultWorkspace(), id), made: id === GENERAL_PREFERENCE && made };
76
+ })).then(items => items.filter(Boolean));
77
+ }
78
+
79
+ /** All writers use the existing workspace lock; the rest of gallery.json survives unchanged. */
80
+ export async function changePreferenceDocuments(change) {
81
+ await ensureWorkspace(defaultWorkspace());
82
+ return withOutlineLock(defaultWorkspace(), async () => {
83
+ const list = await readPreferenceDocuments();
84
+ const item = list.find(item => item.id === change.id);
85
+ const config = await configuration();
86
+ let order = list.map(item => item.id), off = list.filter(item => !item.enabled).map(item => item.id), selected = change.id;
87
+ if (change.action === 'order') {
88
+ if (!Array.isArray(change.order) || new Set(change.order).size !== list.length || change.order.length !== list.length || change.order.some(id => !order.includes(id))) throw new Error('偏好列表已变更,请重试');
89
+ order = change.order;
90
+ } else if (change.action === 'create' || change.action === 'duplicate') {
91
+ if (change.action === 'duplicate' && !item) throw new Error('找不到写作偏好');
92
+ const name = preferenceName(change.name), id = 'preferences/' + name + '.md';
93
+ const directory = path.join(defaultWorkspace(), 'preferences');
94
+ await fs.mkdir(directory, { recursive: true });
95
+ if ((await fs.lstat(directory)).isSymbolicLink()) throw new Error('偏好文件夹不能是链接');
96
+ const text = change.action === 'duplicate' ? await fs.readFile(await preferenceFile(item.id), 'utf8') : '';
97
+ await fs.writeFile(path.join(defaultWorkspace(), id), titledPreference(text, name), { flag: 'wx' });
98
+ const at = item ? order.indexOf(item.id) + 1 : order.length;
99
+ order.splice(at, 0, id); selected = id;
100
+ } else {
101
+ if (!item) throw new Error('找不到写作偏好');
102
+ const file = await preferenceFile(item.id);
103
+ if (change.action === 'enabled') {
104
+ if (typeof change.enabled !== 'boolean') throw new Error('启用状态无效');
105
+ off = off.filter(id => id !== item.id); if (!change.enabled) off.push(item.id);
106
+ } else if (change.action === 'rename' || change.action === 'delete') {
107
+ if (change.revision !== item.revision) throw new Error('偏好文件已被修改,请重新打开后再试');
108
+ if (change.action === 'rename') {
109
+ const name = preferenceName(change.name), id = 'preferences/' + name + '.md';
110
+ if (id === item.id) return { items: list, selected: id };
111
+ const text = titledPreference(await fs.readFile(file, 'utf8'), name);
112
+ await fs.writeFile(path.join(defaultWorkspace(), id), text, { flag: 'wx' });
113
+ if (item.slug) {
114
+ const json = documentFiles(documentDirectory(item.slug, defaultWorkspace())).json;
115
+ const metadata = JSON.parse(await fs.readFile(json, 'utf8'));
116
+ // Keep the editor's slug (and mounted iframe/draft) stable across a rename.
117
+ await writeAtomic(json, JSON.stringify({ ...metadata, preference: id, title: name, ...(metadata.madeSame ? { madeSame: { ...metadata.madeSame, file: sha256(text).slice(0, 16) } } : {}) }, null, 2) + '\n');
118
+ }
119
+ await fs.unlink(file);
120
+ order = order.map(value => value === item.id ? id : value); off = off.map(value => value === item.id ? id : value); selected = id;
121
+ } else {
122
+ await fs.unlink(file);
123
+ order = order.filter(id => id !== item.id); off = off.filter(id => id !== item.id); selected = order[0];
124
+ // Invalidate its association; the editor draft is internal and may still be mounted elsewhere.
125
+ if (item.slug) {
126
+ const json = documentFiles(documentDirectory(item.slug, defaultWorkspace())).json;
127
+ const metadata = JSON.parse(await fs.readFile(json, 'utf8'));
128
+ await writeAtomic(json, JSON.stringify({ ...metadata, preference: null }, null, 2) + '\n');
129
+ }
130
+ }
131
+ } else throw new Error('未知的偏好操作');
132
+ }
133
+ config.preferences = { ...config.preferences, order, off };
134
+ await writeAtomic(configFile(), JSON.stringify(config, null, 2) + '\n');
135
+ return { items: await readPreferenceDocuments(), selected };
136
+ });
137
+ }