@heycys/gallery 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/THIRD-PARTY-NOTICES.md +1 -1
  2. package/browser/editor/page/annotate/index.mjs +3 -2
  3. package/browser/editor/page/board/index.mjs +2 -1
  4. package/browser/editor/page/board/settings.mjs +2 -1
  5. package/browser/editor/page/core.mjs +3 -2
  6. package/browser/editor/page/custom-page.mjs +4 -3
  7. package/browser/editor/page/editor.mjs +9 -5
  8. package/browser/editor/page/emoji-picker/support.mjs +3 -2
  9. package/browser/editor/page/links/index.mjs +1 -12
  10. package/browser/editor/page/links/pages.mjs +3 -2
  11. package/browser/editor/page/outline/folds.mjs +3 -0
  12. package/browser/editor/page/post.mjs +2 -1
  13. package/browser/editor/page/present-links.mjs +4 -3
  14. package/browser/editor/page/present-shell.mjs +3 -58
  15. package/browser/editor/page/session.mjs +3 -37
  16. package/browser/editor/page/storage.mjs +5 -5
  17. package/browser/shared/component-catalog.mjs +2 -2
  18. package/browser/shared/drafts.mjs +26 -51
  19. package/browser/shared/emoji/icon-value.mjs +2 -1
  20. package/browser/shared/extension-manifest.mjs +90 -0
  21. package/browser/shared/extensions.mjs +26 -16
  22. package/browser/shared/http.mjs +1 -1
  23. package/browser/shared/i18n/en.mjs +46 -15
  24. package/browser/shared/media.mjs +2 -1
  25. package/browser/shared/outline.mjs +11 -51
  26. package/browser/shared/page-address.mjs +8 -15
  27. package/browser/shared/paths.mjs +2 -1
  28. package/browser/shared/preferences.mjs +3 -1
  29. package/browser/shared/preview-server.mjs +51 -63
  30. package/browser/shared/service-location.mjs +18 -0
  31. package/browser/shared/session.mjs +3 -6
  32. package/browser/shared/workspace-address.mjs +19 -0
  33. package/browser/shared/workspace-columns.mjs +1 -2
  34. package/browser/shared/workspace-registry.mjs +6 -40
  35. package/browser/shared/workspace.mjs +3 -6
  36. package/browser/shared/writing-preferences.mjs +6 -4
  37. package/dist/{chunk-BYSF5UMM.mjs → chunk-236JJXWY.mjs} +2 -2
  38. package/dist/{chunk-3A5I2GJK.mjs → chunk-2KAVXMKG.mjs} +1 -1
  39. package/dist/chunk-2T4PZQEP.mjs +2 -0
  40. package/dist/{chunk-SAHS56QM.mjs → chunk-3SKOU6OZ.mjs} +1 -1
  41. package/dist/{chunk-S2Y7VI4H.mjs → chunk-3XZOA7AH.mjs} +2 -2
  42. package/dist/{chunk-MKTX5PGV.mjs → chunk-43B75J5T.mjs} +1 -1
  43. package/dist/{chunk-VKSRM7EY.mjs → chunk-4B2TB5K2.mjs} +1 -1
  44. package/dist/{chunk-LV24RY2E.mjs → chunk-4K5KC6V2.mjs} +1 -1
  45. package/dist/{chunk-SOD23RGL.mjs → chunk-4KUO7R2M.mjs} +8 -8
  46. package/dist/chunk-4SNV4HA5.mjs +2 -0
  47. package/dist/chunk-53XDTHTG.mjs +4 -0
  48. package/dist/chunk-5XV3YYLG.mjs +2 -0
  49. package/dist/{chunk-VNUWREXU.mjs → chunk-65NTKF4N.mjs} +1 -1
  50. package/dist/chunk-6GV3CBB2.mjs +2 -0
  51. package/dist/{chunk-DH6GMDNN.mjs → chunk-6L6V7YR2.mjs} +1 -1
  52. package/dist/{chunk-TOE6DPLH.mjs → chunk-6NLXUKAY.mjs} +1 -1
  53. package/dist/chunk-6Q7ASAXJ.mjs +3 -0
  54. package/dist/{chunk-ZCP7BTOZ.mjs → chunk-6QBHU4DF.mjs} +1 -1
  55. package/dist/{chunk-QXVNUASA.mjs → chunk-6QWQXHAV.mjs} +1 -1
  56. package/dist/chunk-6XD6U3QA.mjs +5 -0
  57. package/dist/{chunk-36FIRJIF.mjs → chunk-742D2WWJ.mjs} +2 -2
  58. package/dist/{chunk-7M7UPWU6.mjs → chunk-7EFHNQ5B.mjs} +1 -1
  59. package/dist/{chunk-JI6T3A4Y.mjs → chunk-7ISBBDJR.mjs} +1 -1
  60. package/dist/{chunk-2GKGEVMR.mjs → chunk-7UZUQQ7Q.mjs} +25 -24
  61. package/dist/{chunk-VPVOAJ7X.mjs → chunk-AGDILNWB.mjs} +1 -1
  62. package/dist/{chunk-5IIH335D.mjs → chunk-ALZ6ZENZ.mjs} +1 -1
  63. package/dist/{chunk-G4YP7GVC.mjs → chunk-BFJGHSQN.mjs} +1 -1
  64. package/dist/chunk-BLNQUR5R.mjs +168 -0
  65. package/dist/chunk-BMX3VUHY.mjs +2 -0
  66. package/dist/chunk-BN7M3IN7.mjs +8 -0
  67. package/dist/{chunk-AMDGVDNV.mjs → chunk-BQNTTFPL.mjs} +1 -1
  68. package/dist/chunk-C257H6IA.mjs +2 -0
  69. package/dist/{chunk-74RPP2M2.mjs → chunk-C564NU2H.mjs} +1 -1
  70. package/dist/chunk-CHQU37CA.mjs +2 -0
  71. package/dist/{chunk-WCWYNFDF.mjs → chunk-E4IHAIJ7.mjs} +1 -1
  72. package/dist/{chunk-7XDYK3UC.mjs → chunk-E7656LR7.mjs} +1 -1
  73. package/dist/{chunk-KFZBYDIS.mjs → chunk-ECZBLQKK.mjs} +1 -1
  74. package/dist/{chunk-PAFSFVYB.mjs → chunk-ERE2BXYV.mjs} +1 -1
  75. package/dist/{chunk-ZEY2RLBT.mjs → chunk-ERLAIBOA.mjs} +2 -2
  76. package/dist/chunk-FFV733MA.mjs +2 -0
  77. package/dist/{chunk-CZ5KJSGK.mjs → chunk-GJJRBGMY.mjs} +1 -1
  78. package/dist/{chunk-VA3KAQZD.mjs → chunk-GKOBB6DH.mjs} +1 -1
  79. package/dist/chunk-GMJD7ID3.mjs +2 -0
  80. package/dist/{chunk-QZ35UAB4.mjs → chunk-GRAXKKG4.mjs} +1 -1
  81. package/dist/{chunk-2RKIHJI2.mjs → chunk-HYMEEXKN.mjs} +1 -1
  82. package/dist/{chunk-DKR7LFIY.mjs → chunk-IA2SIEJQ.mjs} +1 -1
  83. package/dist/{chunk-QK3VISCA.mjs → chunk-IA4PSIMD.mjs} +1 -1
  84. package/dist/chunk-JPG2LBQM.mjs +11 -0
  85. package/dist/{chunk-VGTMLQWZ.mjs → chunk-JXXHAXYH.mjs} +1 -1
  86. package/dist/{chunk-5MEXZ2UF.mjs → chunk-K3JKPFL5.mjs} +1 -1
  87. package/dist/{chunk-LE44MD3O.mjs → chunk-KLQP46Y3.mjs} +1 -1
  88. package/dist/{chunk-NAGMRVW4.mjs → chunk-LKX55H76.mjs} +1 -1
  89. package/dist/chunk-LMUZQK5O.mjs +2 -0
  90. package/dist/{chunk-CG3UHDKH.mjs → chunk-M4NRDYI5.mjs} +1 -1
  91. package/dist/{chunk-HM4E43VM.mjs → chunk-MAFIGVXY.mjs} +1 -1
  92. package/dist/{chunk-4DNHHJSL.mjs → chunk-MKIBJTJZ.mjs} +1 -1
  93. package/dist/{chunk-L7W624FD.mjs → chunk-MNWJ4EZY.mjs} +1 -1
  94. package/dist/{chunk-FXQBADMN.mjs → chunk-NHP66IWR.mjs} +1 -1
  95. package/dist/{chunk-YBB5LWZK.mjs → chunk-OEIJLMPI.mjs} +1 -1
  96. package/dist/{chunk-NWRZXELO.mjs → chunk-PAREWQGB.mjs} +1 -1
  97. package/dist/{chunk-7N2LPG33.mjs → chunk-PDZR3L3C.mjs} +1 -1
  98. package/dist/{chunk-YFJU3O7G.mjs → chunk-PLB7L22J.mjs} +1 -1
  99. package/dist/{chunk-4AMTKWDO.mjs → chunk-PUOQ65NC.mjs} +1 -1
  100. package/dist/chunk-Q4MS6OFC.mjs +2 -0
  101. package/dist/{chunk-3OFXMN6N.mjs → chunk-Q7P3Q5YX.mjs} +1 -1
  102. package/dist/{chunk-6MPKCH3P.mjs → chunk-QNECWMGR.mjs} +1 -1
  103. package/dist/{chunk-HBAUKKQH.mjs → chunk-QP7BOHE4.mjs} +1 -1
  104. package/dist/chunk-QVGOPHOF.mjs +2 -0
  105. package/dist/{chunk-NFABH3QW.mjs → chunk-RRKJMHNX.mjs} +2 -2
  106. package/dist/chunk-S3SX3COT.mjs +20 -0
  107. package/dist/{chunk-ZO7SFB2E.mjs → chunk-SQ6Y2ZMA.mjs} +2 -2
  108. package/dist/{chunk-VG5JEIUQ.mjs → chunk-TKWEX4FD.mjs} +2 -2
  109. package/dist/{chunk-CCSGFCLZ.mjs → chunk-U3SSPL4K.mjs} +1 -1
  110. package/dist/{chunk-DHB7UZ7F.mjs → chunk-U5XL7D6A.mjs} +1 -1
  111. package/dist/{chunk-CPS4SRPF.mjs → chunk-ULXSPX5H.mjs} +2 -2
  112. package/dist/{chunk-JXLSHVJ2.mjs → chunk-UMHUSOPJ.mjs} +1 -1
  113. package/dist/chunk-USIHBCSW.mjs +15 -0
  114. package/dist/{chunk-GWOYMENX.mjs → chunk-V6XZ4VIR.mjs} +1 -1
  115. package/dist/{chunk-AFGUR7SY.mjs → chunk-VPMRP2TL.mjs} +1 -1
  116. package/dist/{chunk-WTEGUPFG.mjs → chunk-VRPVVMRB.mjs} +1 -1
  117. package/dist/chunk-VZ2RQ3AM.mjs +2 -0
  118. package/dist/{chunk-XKOHV4H6.mjs → chunk-W5NPH5UA.mjs} +1 -1
  119. package/dist/chunk-WFGPMCTZ.mjs +4 -0
  120. package/dist/chunk-XZCFPKGO.mjs +2 -0
  121. package/dist/{chunk-QJ3TFJLQ.mjs → chunk-Y34NR5JL.mjs} +1 -1
  122. package/dist/{chunk-V5JRSGZF.mjs → chunk-YN37ZDLW.mjs} +1 -1
  123. package/dist/chunk-ZFGMJVJC.mjs +2 -0
  124. package/dist/{chunk-KH24LWAT.mjs → chunk-ZTHICSSF.mjs} +1 -1
  125. package/dist/cli.mjs +1 -1
  126. package/dist/server.mjs +1 -1
  127. package/dist/worker.mjs +1 -1
  128. package/kit/math.js +1 -1
  129. package/kit/runtime.js +12 -2
  130. package/kit/state.js +2 -0
  131. package/kit/ui.js +5 -1
  132. package/package.json +1 -1
  133. package/release.json +7 -7
  134. package/skill/SKILL.md +13 -13
  135. package/skill/references/building-blocks.md +1 -1
  136. package/skill/references/extensions.md +45 -9
  137. package/web/assets/{index-xP0uw3a0.css → index-FLb9jCfl.css} +1 -1
  138. package/web/assets/index-czjrXVjf.js +15 -0
  139. package/web/index.html +2 -2
  140. package/dist/chunk-46ACXOU5.mjs +0 -2
  141. package/dist/chunk-5DPABPBI.mjs +0 -2
  142. package/dist/chunk-5L26JUYA.mjs +0 -2
  143. package/dist/chunk-AEMVZYZQ.mjs +0 -166
  144. package/dist/chunk-ASAXY2DJ.mjs +0 -2
  145. package/dist/chunk-DAMVDXR3.mjs +0 -2
  146. package/dist/chunk-DLFZJJEE.mjs +0 -2
  147. package/dist/chunk-EKNHM3P4.mjs +0 -2
  148. package/dist/chunk-FCK6NXON.mjs +0 -3
  149. package/dist/chunk-FX42XU6J.mjs +0 -2
  150. package/dist/chunk-HATA2W7X.mjs +0 -2
  151. package/dist/chunk-JJNRIFND.mjs +0 -8
  152. package/dist/chunk-JL6R4FG4.mjs +0 -4
  153. package/dist/chunk-KUILYI7H.mjs +0 -4
  154. package/dist/chunk-NIW7F7LU.mjs +0 -4
  155. package/dist/chunk-OU3S466R.mjs +0 -20
  156. package/dist/chunk-QCEATQC4.mjs +0 -2
  157. package/dist/chunk-TDWJKOLN.mjs +0 -2
  158. package/dist/chunk-UGO5N3EI.mjs +0 -15
  159. package/dist/chunk-XMKHCUI6.mjs +0 -15
  160. package/dist/chunk-XQRUG4G3.mjs +0 -2
  161. package/dist/chunk-YPPJVDQI.mjs +0 -2
  162. package/dist/chunk-ZANMKAWS.mjs +0 -2
  163. package/web/assets/index-BHC5ZOMh.js +0 -15
  164. /package/dist/{chunk-N3DSBH5S.mjs → chunk-A55HN5OK.mjs} +0 -0
  165. /package/dist/{chunk-KNZJT4ET.mjs → chunk-QAIHRH3I.mjs} +0 -0
  166. /package/dist/{chunk-ZW2A6GBW.mjs → chunk-QR6A54KJ.mjs} +0 -0
  167. /package/dist/{chunk-74UW7QSL.mjs → chunk-UH4SIXTR.mjs} +0 -0
  168. /package/dist/{chunk-EKGYYOSP.mjs → chunk-W7XHE4CT.mjs} +0 -0
  169. /package/dist/{chunk-5MMN3N5J.mjs → chunk-YEY3RSFQ.mjs} +0 -0
@@ -0,0 +1,90 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { defaultWorkspace, layoutOf } from "./workspace.mjs";
4
+ import { extensionFolders } from "./extensions.mjs";
5
+ import { isExtensionId } from "./outline-shape.mjs";
6
+ import { sha256, writeAtomic } from "./files.mjs";
7
+ import { withOutlineLock } from "./outline.mjs";
8
+
9
+ export const extensionDirectory = id => {
10
+ if (!isExtensionId(id)) throw new Error("扩展 ID 不正确");
11
+ return path.join(layoutOf(defaultWorkspace()).extensions, id);
12
+ };
13
+
14
+ export function extensionPath(value) {
15
+ if (typeof value !== "string" || !value || value.includes("\\") || value.includes(":")) throw new Error("扩展文件必须使用相对路径");
16
+ const parts = value.split("/");
17
+ if (parts.some(part => !part || part === "." || part === ".." || /[\x00-\x1f]/.test(part))) throw new Error("扩展文件路径不正确");
18
+ return parts;
19
+ }
20
+
21
+ // Resolve every segment, including the extension directory, before serving or running a file.
22
+ export async function extensionFile(folder, name) {
23
+ const parts = extensionPath(name);
24
+ if ((await fs.lstat(path.dirname(folder))).isSymbolicLink()) throw new Error("扩展文件不能是符号链接");
25
+ let file = folder;
26
+ for (const part of [null, ...parts]) {
27
+ if (part !== null) file = path.join(file, part);
28
+ const stat = await fs.lstat(file);
29
+ if (stat.isSymbolicLink()) throw new Error("扩展文件不能是符号链接");
30
+ }
31
+ const [root, real] = await Promise.all([fs.realpath(folder), fs.realpath(file)]);
32
+ const relative = path.relative(root, real);
33
+ if (relative.startsWith("..") || path.isAbsolute(relative) || !(await fs.stat(real)).isFile()) throw new Error("扩展文件不在扩展目录内");
34
+ return real;
35
+ }
36
+
37
+ export async function readManifest(folder) {
38
+ const file = await extensionFile(folder, "extension.json");
39
+ if ((await fs.stat(file)).size > 65536) throw new Error("extension.json 超过 64KB");
40
+ const text = await fs.readFile(file, "utf8");
41
+ if (Buffer.byteLength(text) > 65536) throw new Error("extension.json 超过 64KB");
42
+ const value = JSON.parse(text);
43
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("extension.json 必须是对象");
44
+ if (value.id !== undefined && !isExtensionId(value.id)) throw new Error("扩展 ID 不正确");
45
+ if (value.workbench !== undefined && (!value.workbench || typeof value.workbench !== "object" || Array.isArray(value.workbench))) throw new Error("workbench 必须是对象");
46
+ for (const name of [value.workbench?.script, value.workbench?.style, value.agent].filter(value => value !== undefined)) await extensionFile(folder, name);
47
+ if (value.server !== undefined) {
48
+ const command = value.server?.command;
49
+ if (!Array.isArray(command) || !command.length || command.length > 32 || command.some(word => typeof word !== "string" || !word || word.includes("\0"))) throw new Error("server.command 必须是非空的参数数组");
50
+ }
51
+ return { ...value, revision: sha256(text) };
52
+ }
53
+
54
+ const configFile = () => path.join(defaultWorkspace(), "gallery.json");
55
+ const configuration = async () => JSON.parse(await fs.readFile(configFile(), "utf8"));
56
+
57
+ export async function listCapabilities() {
58
+ const config = await configuration();
59
+ return Promise.all((await extensionFolders(defaultWorkspace())).map(async ({ id, folder }) => {
60
+ try {
61
+ const manifest = await readManifest(folder);
62
+ const grant = config.extensions?.[id];
63
+ return { id, folder, manifest, enabled: grant?.enabled === true && grant.revision === manifest.revision,
64
+ confirmed: grant?.revision === manifest.revision, agent: manifest.agent ? path.join(folder, manifest.agent) : null };
65
+ } catch (error) {
66
+ return { id, folder, enabled: false, confirmed: false, manifest: null, error: error.code === "ENOENT" ? null : error.message };
67
+ }
68
+ }));
69
+ }
70
+
71
+ export async function setExtensionEnabled({ id, enabled, revision, confirmed = false }) {
72
+ const folder = extensionDirectory(id);
73
+ if (typeof enabled !== "boolean") throw new Error("扩展开关的值不正确");
74
+ return withOutlineLock(defaultWorkspace(), async () => {
75
+ const manifest = await readManifest(folder);
76
+ if (manifest.revision !== revision) throw new Error("扩展刚刚更新,请重新打开设置");
77
+ const config = await configuration();
78
+ if (enabled && config.extensions?.[id]?.revision !== revision && !confirmed) throw new Error("请先确认扩展的脚本和后台权限");
79
+ config.extensions = { ...config.extensions, [id]: { enabled, revision } };
80
+ await writeAtomic(configFile(), JSON.stringify(config, null, 2) + "\n");
81
+ });
82
+ }
83
+
84
+ export async function forgetExtensionGrant(id) {
85
+ const config = await configuration();
86
+ if (config.extensions?.[id]) {
87
+ delete config.extensions[id];
88
+ await writeAtomic(configFile(), JSON.stringify(config, null, 2) + "\n");
89
+ }
90
+ }
@@ -5,7 +5,7 @@ import { said } from "./i18n/index.mjs";
5
5
  import { isExtensionId } from "./outline-shape.mjs";
6
6
  import { defaultWorkspace, DOCUMENT_FOLDERS, layoutOf } from "./workspace.mjs";
7
7
  import { sameDirectory } from "./preview-server.mjs";
8
- import { writeAtomic } from "./files.mjs";
8
+ import { sha256, writeAtomic } from "./files.mjs";
9
9
 
10
10
  // The extensions visible in a workspace (docs/workbench.md "Extension columns"): public columns from
11
11
  // the default workspace plus this project's own, a project sidebar.json taking precedence by id. Each a folder
@@ -32,18 +32,12 @@ export async function extensionFolders(root) {
32
32
  /** Public columns live in the default workspace; a project's own sidebar.json overrides the same id.
33
33
  * Empty local folders do not hide a public column. Keep one source for listing, outline and writes.
34
34
  */
35
- export async function visibleExtensionFolders(root) {
36
- const home = defaultWorkspace();
37
- const roots = sameDirectory(home, root) ? [root] : [home, root];
38
- const found = new Map();
39
- for (const owner of roots) {
40
- for (const entry of await extensionFolders(owner)) {
41
- const file = path.join(entry.folder, SIDEBAR_FILE);
42
- const present = await fs.lstat(file).then(() => true, () => false);
43
- if (present || !found.has(entry.id)) found.set(entry.id, { ...entry, root: owner, file, present });
44
- }
45
- }
46
- return [...found.values()].sort((a, b) => a.id < b.id ? -1 : 1);
35
+ export async function visibleExtensionFolders() {
36
+ const root = defaultWorkspace();
37
+ return Promise.all((await extensionFolders(root)).map(async entry => {
38
+ const file = path.join(entry.folder, SIDEBAR_FILE);
39
+ return { ...entry, root, file, present: await fs.lstat(file).then(() => true, () => false) };
40
+ }));
47
41
  }
48
42
 
49
43
  /** Resolve an id, never a client-supplied directory, to the file this workspace actually shows. */
@@ -81,6 +75,7 @@ export async function readExtension(root, id) {
81
75
  return { ...empty, error: said("读不出这个文件:{0}", error.code ?? "error") };
82
76
  }
83
77
  const read = readSidebarText(text);
78
+ empty.revision = sha256(text);
84
79
  if (read.error) {
85
80
  // What can still be said of a file that does not read: its name, when it has one.
86
81
  let title = id;
@@ -103,7 +98,7 @@ export async function listExtensions(root, { all = false } = {}) {
103
98
  return all ? read : read.filter((extension) => !extension.missing);
104
99
  }
105
100
 
106
- /** Edit only a column's title or the order of its root entries. Called under the workspace lock.
101
+ /** Edit a column's title/root order, remove a root subtree or delete its sidebar.json. Called under the workspace lock.
107
102
  * Read the latest source, keep unknown fields and whole subtrees, and refuse stale UI changes.
108
103
  * Atomic replacement checks the original text again so an external writer is not silently overwritten.
109
104
  */
@@ -113,12 +108,17 @@ export async function editExtension(root, id, change) {
113
108
  const stale = () => refused(409, "stale", said("扩展目录刚在别处改过,请重试"));
114
109
  if (!isExtensionId(id) || !change || typeof change !== "object") return bad();
115
110
  const rename = Object.hasOwn(change, "title");
116
- if (rename ? (typeof change.title !== "string" || typeof change.baseTitle !== "string" || change.move !== undefined)
117
- : (!change.move || !["before", "after"].includes(change.move.side) || typeof change.move.entry !== "string" || typeof change.move.ref !== "string" || !Array.isArray(change.baseOrder))) return bad();
111
+ const remove = Object.hasOwn(change, "remove");
112
+ if ([rename, remove, Object.hasOwn(change, "move")].filter(Boolean).length !== 1) return bad();
113
+ if (remove ? (change.remove !== "column" && (typeof change.remove?.entry !== "string" || !change.remove.entry)
114
+ || typeof change.baseFile !== "string" || typeof change.baseRevision !== "string")
115
+ : rename ? (typeof change.title !== "string" || typeof change.baseTitle !== "string")
116
+ : (!change.move || !["before", "after"].includes(change.move.side) || typeof change.move.entry !== "string" || typeof change.move.ref !== "string" || !Array.isArray(change.baseOrder))) return bad();
118
117
  const name = rename ? change.title.replace(/\s+/g, " ").trim() : null;
119
118
  if (rename && (!name || name.length > SIDEBAR_LIMITS.column)) return bad();
120
119
  const folder = path.join(layoutOf(root).extensions, id);
121
120
  const file = path.join(folder, SIDEBAR_FILE);
121
+ if (remove && change.baseFile !== file) return stale(); // An override must not redirect a confirmed deletion.
122
122
  // Never turn an extension path supplied by the client or a symlink into an arbitrary-file write.
123
123
  for (const [target, directory] of [[layoutOf(root).extensions, true], [folder, true], [file, false]]) {
124
124
  const stat = await fs.lstat(target).catch(() => null);
@@ -127,6 +127,12 @@ export async function editExtension(root, id, change) {
127
127
  if (!directory && stat.size > SIDEBAR_LIMITS.bytes) return refused(409, "extension", said("文件超过 1MB"));
128
128
  }
129
129
  const source = await fs.readFile(file, "utf8");
130
+ if (remove && sha256(source) !== change.baseRevision) return stale();
131
+ if (remove && change.remove === "column") {
132
+ if ((await fs.readFile(file, "utf8")) !== source) return stale();
133
+ await fs.unlink(file); // Only the validated sidebar file; the extension's other files stay.
134
+ return { ok: true, written: true };
135
+ }
130
136
  const read = readSidebarText(source);
131
137
  if (read.error) return refused(409, "extension", read.error);
132
138
  const value = JSON.parse(source.replace(/^\uFEFF/, ""));
@@ -134,6 +140,10 @@ export async function editExtension(root, id, change) {
134
140
  if (read.sidebar.title !== change.baseTitle) return stale();
135
141
  if (read.sidebar.title === name) return { ok: true, written: false };
136
142
  value.title = name;
143
+ } else if (remove) {
144
+ const index = value.entries.findIndex(entry => entry.id === change.remove.entry);
145
+ if (index < 0) return bad(); // Roots only; a root's entire subtree goes with it.
146
+ value.entries.splice(index, 1);
137
147
  } else {
138
148
  const order = value.entries.map(entry => entry.id);
139
149
  if (JSON.stringify(order) !== JSON.stringify(change.baseOrder)) return stale();
@@ -8,7 +8,7 @@
8
8
  // could lay an editable page under a decoy, the reader's clicks and keys then editing the files (the security
9
9
  // review of 0.2.0), nor localhost, which may lead to another program on [::1] (no workbench is shown there:
10
10
  // src/server/routes.mjs toLoopback).
11
- export const FRAMED_BY = { "Content-Security-Policy": "frame-ancestors 'self' http://127.0.0.1:*" };
11
+ export const FRAMED_BY = { "Content-Security-Policy": "frame-ancestors 'self'" };
12
12
 
13
13
  // A JSON answer.
14
14
  export function sendJson(response, status, body) {
@@ -1,6 +1,46 @@
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.",
6
+ "启用": "Enable",
7
+ "扩展设置保存失败": "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
+ "后台程序": "Backend process",
14
+ "侧栏目录": "Sidebar directory",
15
+ "扩展列表读取失败": "Could not load extensions",
16
+ "扩展 {0} 加载失败": "Could not load extension {0}",
17
+ "扩展正在更新,请稍后再试": "The extension is being updated. Try again shortly.",
18
+ "扩展后台暂未就绪,请查看扩展日志": "The extension backend is not ready. Check its log.",
19
+ "扩展后台请求超时": "The extension backend request timed out",
20
+ "扩展操作失败": "The extension operation failed",
21
+ "扩展目录不能是符号链接": "The extensions directory cannot be a symbolic link",
22
+ "扩展超过 100MB 或 10000 个文件": "The extension exceeds 100MB or 10000 files",
23
+ "安装源不能是符号链接": "The installation source cannot be a symbolic link",
24
+ "扩展只能包含普通文件和文件夹": "Extensions may contain only regular files and directories",
25
+ "扩展 ZIP 超过 100MB": "The extension ZIP exceeds 100MB",
26
+ "ZIP 中没有 extension.json": "The ZIP has no extension.json",
27
+ "ZIP 的文件必须在同一个扩展文件夹内": "All ZIP files must belong to one extension directory",
28
+ "ZIP 中存在重名文件": "The ZIP contains duplicate file names",
29
+ "请选择扩展文件夹或 ZIP": "Choose an extension directory or ZIP",
30
+ "已安装的扩展不能是符号链接": "An installed extension cannot be a symbolic link",
31
+ "扩展不能是符号链接": "An extension cannot be a symbolic link",
32
+ "扩展 ID 不正确": "Invalid extension ID",
33
+ "扩展文件必须使用相对路径": "Extension files must use relative paths",
34
+ "扩展文件路径不正确": "Invalid extension file path",
35
+ "扩展文件不能是符号链接": "Extension files cannot be symbolic links",
36
+ "扩展文件不在扩展目录内": "The file is not inside the extension directory",
37
+ "extension.json 超过 64KB": "extension.json exceeds 64KB",
38
+ "extension.json 必须是对象": "extension.json must be an object",
39
+ "workbench 必须是对象": "workbench must be an object",
40
+ "server.command 必须是非空的参数数组": "server.command must be a nonempty array of arguments",
41
+ "扩展开关的值不正确": "Invalid extension enabled value",
42
+ "扩展刚刚更新,请重新打开设置": "The extension was updated. Reopen settings.",
43
+ "请先确认扩展的脚本和后台权限": "Confirm the extension’s script and backend permissions first",
4
44
  "扩展目录刚在别处改过,请重试": "The extension directory changed elsewhere. Please try again.",
5
45
  "找不到这个扩展": "This extension could not be found",
6
46
  "扩展路径不是普通文件或文件夹": "The extension path is not a regular file or directory",
@@ -158,6 +198,12 @@ export default {
158
198
  "永久删除「{0}」?": "Delete “{0}” forever?",
159
199
  "永久删除回收区中的全部 {0} 项?": "Permanently delete all {0} items in the trash?",
160
200
  "删除全部": "Delete all",
201
+ "删除分栏": "Delete column",
202
+ "删除页面": "Delete page",
203
+ "删除分栏「{0}」?": "Delete column “{0}”?",
204
+ "删除页面「{0}」?": "Delete page “{0}”?",
205
+ "将从 sidebar.json 删除这个条目及其全部子项,共用此配置的工作区会同步更新。不会删除网站上的原文档,此操作无法撤销。": "Remove this entry and all its children from sidebar.json in every workspace sharing this configuration. The original web documents stay. This cannot be undone.",
206
+ "将删除此分栏的 sidebar.json,共用此配置的工作区会同步移除分栏。其他文件和网站上的原文档不受影响,此操作无法撤销。": "Delete this column's sidebar.json in every workspace sharing this configuration. Other files and the original web documents stay. This cannot be undone.",
161
207
  "这份文档": "this document",
162
208
  "删除后无法恢复。": "This can't be undone.",
163
209
  "正在导出…": "Exporting…",
@@ -1404,15 +1450,12 @@ export default {
1404
1450
  "网址太长了(最多 2048 个字符)": "The address is too long (2048 characters at most)",
1405
1451
  "名称不能是空的,也不能超过 200 个字": "The name can't be empty or longer than 200 characters",
1406
1452
  "图标只能是表情或图标页签里的图标": "The icon must be an emoji or an icon of the Icons tab",
1407
- "正在启动它所在工作区的服务…": "Starting the server of its workspace…",
1408
- "没能打开这份底稿:{0}": "Couldn't open this draft: {0}",
1409
1453
  "正在读取别的工作区…": "Reading the other workspaces…",
1410
1454
  "别处的底稿 · {0}": "Draft from elsewhere · {0}",
1411
1455
  "别处的底稿": "Draft from elsewhere",
1412
1456
  "改的是那篇底稿的标题": "Changes the title of that draft",
1413
1457
  "只改它在这里的显示位置,文件不动": "Changes only where it shows here: no file moves",
1414
1458
  "在文件夹中显示": "Show in folder",
1415
- "在它的工作区里打开": "Open in its workspace",
1416
1459
  "「{0}」已移至它所在工作区的回收区": "“{0}” moved to the trash of its workspace",
1417
1460
  "副本在它所在的工作区里": "The copy is made in its workspace",
1418
1461
  "导入的页面在它所在的工作区里": "The imported pages are made in its workspace",
@@ -1422,7 +1465,6 @@ export default {
1422
1465
  "找不到底稿:本机登记的两个工作区编号相同": "Draft not found: two registered workspaces share one id",
1423
1466
  "找不到底稿:{0}": "Draft not found: {0}",
1424
1467
  "找不到底稿": "Draft not found",
1425
- "这份文档在它自己的工作区里,不在这里的侧边栏中": "This document is in its own workspace, not in this sidebar",
1426
1468
  "找不到它所在的工作区": "Its workspace was not found",
1427
1469
  "没有打开文件夹:{0}": "The folder was not opened: {0}",
1428
1470
  "已从文档树移除「{0}」,底稿没有删除": "Removed “{0}” from the sidebar; the draft was not deleted",
@@ -1436,17 +1478,7 @@ export default {
1436
1478
  "会把 {0} 登记成一个工作区(在里面放 gallery.json 和 .gallery/)": "{0} will be registered as a workspace (a gallery.json and a .gallery/ put in it)",
1437
1479
  "没能把它的工作区记进本机的登记表": "Its workspace could not be added to this machine's registry",
1438
1480
  "本机登记的 {0} 个工作区编号相同({1}),先用 gallery workspaces --forget 去掉其中一个": "{0} registered workspaces share one id ({1}): take one off with gallery workspaces --forget first",
1439
- "这是当前的工作区": "This is the current workspace",
1440
- "这个工作区不在本机的登记表里": "This workspace is not on this machine's registry",
1441
- "本机登记的工作区里有两个用的是同一个编号,先用 gallery workspaces --forget 去掉其中一个": "Two registered workspaces share this id: take one off with gallery workspaces --forget first",
1442
- "找不到它所在的工作区:它不在本机的登记表里": "Its workspace was not found: it is not on this machine's registry",
1443
- "找不到它所在的工作区:{0} 不在了": "Its workspace was not found: {0} is gone",
1444
1481
  "找不到这个文件夹": "Folder not found",
1445
- "它所在工作区的服务没有在运行": "The server of its workspace is not running",
1446
- "它所在工作区的服务没有回答": "The server of its workspace did not answer",
1447
- "没能启动它所在工作区的服务:{0}": "The server of its workspace did not start: {0}",
1448
- "这个命令不能转给别的工作区": "This command can't be passed on to another workspace",
1449
- "它所在工作区的服务没有给出结果": "The server of its workspace gave no answer",
1450
1482
  // Extension columns (src/shared/extension-sidebar.mjs, extensions.mjs, workbench/app/features/sidebar/SidebarExtension.tsx)
1451
1483
  "这个条目已被扩展移除": "This entry was removed by its extension",
1452
1484
  "条目读不出来:{0}": "Entries can't be read: {0}",
@@ -1532,5 +1564,4 @@ export default {
1532
1564
  "将「{0}」及其子页面复制到「{1}」,原项保留。": "Copy “{0}” and its child pages to “{1}”, keeping the originals.",
1533
1565
  "已复制 {0} 项,原项保留": "Copied {0} items; originals kept",
1534
1566
  "跨工作区请复制,不能只移动目录位置": "Copy items to another workspace instead of moving their tree positions",
1535
- "自定义块 {0} 缺失或版本冲突,未复制": "Custom block {0} is missing or has a version conflict; nothing was copied",
1536
1567
  };
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "./workspace-address.mjs";
1
2
  // The media of a report, one place for what every system asks about it: the files its media
2
3
  // blocks, its page icon and any other reference of its markup keep in the report's folder
3
4
  // (assets/<file>), where the preview serves them, and what the editor may upload. Shared by the
@@ -127,7 +128,7 @@ export function referencedFile(value) {
127
128
  export const MEDIA_ROUTES = { asset: imageFile, file: assetFile, embed: embedFile };
128
129
 
129
130
  /** The address the preview serves a file of a report's folder at (`value`: assets/<file>). */
130
- export const mediaUrl = (route, slug, value) => `/__gallery/edit/${route}/${slug}/${value}`;
131
+ export const mediaUrl = (route, slug, value) => workspaceAddress(`/__gallery/edit/${route}/${slug}/${value}`);
131
132
 
132
133
  // ---------- what may be uploaded ----------
133
134
 
@@ -9,14 +9,8 @@ import { forgetSnapshot, shownWorkspaces, snapshotOf } from "./workspace-columns
9
9
  import { defaultWorkspace, layoutOf, listDocumentSlugs, listLinkSlugs, pageSlug } from "./workspace.mjs";
10
10
  import { readRegistry, workspaceId, workspaceKey } from "./workspace-registry.mjs";
11
11
 
12
- /** Registered workspaces share the default workspace's existing tree. Unregistered/temporary
13
- * workspaces remain standalone; no registry discovery or copying of document files is needed. */
14
- export function outlineRoot(root) {
15
- const home = defaultWorkspace();
16
- return workspaceKey(root) === workspaceKey(home) || readRegistry().workspaces.some(entry => workspaceKey(entry.root) === workspaceKey(root)) ? home : root;
17
- }
18
-
19
- export const sharedOutline = root => workspaceKey(outlineRoot(root)) === workspaceKey(defaultWorkspace());
12
+ /** All document folders share the default workspace’s outline. */
13
+ export const outlineRoot = () => defaultWorkspace();
20
14
 
21
15
  /** Moving a reference across owners must use the confirmed copy command. */
22
16
  export function ownsOutline(root, value) {
@@ -49,7 +43,7 @@ function ownedTree(tree, self) {
49
43
  // Registered workspaces project one shared tree, partitioned by file owner. Their former local files
50
44
  // only seed new entries; the default workspace stores subsequent order/hierarchy edits. The format
51
45
  // below is also the standalone/legacy reader. Its picked/hidden/left rules are not applied to shared
52
- // trees, which list every registered document and link. Project-only extensions keep local positions.
46
+ // trees, which list every registered document and link. Extensions are read from home alone.
53
47
  // The workspace's outline: the sidebar's columns and which entry sits under which, in what order
54
48
  // (<workspace>/.gallery/outline.json, docs/workbench.md "Workspace commands"; its shape: ./outline-shape.mjs).
55
49
  // The file, version 2:
@@ -179,7 +173,7 @@ async function localOutlineAndFile(root, { others: withOthers = true, shared = f
179
173
 
180
174
  /** Translate the same tree between the default workspace's ids and a project's local ids.
181
175
  * No second ordering is kept: only the current workspace's column is displayed first. */
182
- function projectTree(value, from, to, { first = false } = {}) {
176
+ function projectTree(value, from, to) {
183
177
  if (!value || workspaceKey(from) === workspaceKey(to)) return value;
184
178
  const fromId = workspaceId(from), toId = workspaceId(to);
185
179
  const own = value.groups.find(group => group.workspace === toId);
@@ -195,34 +189,20 @@ function projectTree(value, from, to, { first = false } = {}) {
195
189
  }
196
190
  return group;
197
191
  });
198
- if (first) groups = [...groups.filter(group => group.id === DOCS), ...groups.filter(group => group.id !== DOCS)];
199
192
  return { ...value, groups, children: Object.fromEntries(Object.entries(value.children).map(([parent, list]) => [key(parent), list.map(entry)])) };
200
193
  }
201
194
 
202
195
  export async function outlineAndFile(root, { others: withOthers = true } = {}) {
203
196
  const home = outlineRoot(root);
204
- if (workspaceKey(home) === workspaceKey(root)) return localOutlineAndFile(root, { others: withOthers, shared: sharedOutline(root) });
197
+ if (workspaceKey(home) === workspaceKey(root)) return localOutlineAndFile(root, { others: withOthers, shared: true });
205
198
  // A command may just have created/trashed a local item before placing it in the shared tree.
206
199
  forgetSnapshot(root);
207
200
  const shared = await localOutlineAndFile(home, { shared: true });
208
- let outline = projectTree(shared.outline, home, root, { first: true });
209
- // A project's extensions remain private; public ones still come from the default workspace.
210
- outline.groups = withExtensions(outline.groups, await extensionColumnIds(root));
211
- const publicIds = new Set(await extensionColumnIds(home));
212
- const positions = (await readJson(layoutOf(root).outline))?.extensionPositions;
213
- for (const position of Array.isArray(positions) ? [...positions].reverse() : []) {
214
- const group = outline.groups.find(group => group.id === position.id && group.extension && !publicIds.has(group.extension));
215
- if (!group) continue;
216
- outline.groups = outline.groups.filter(item => item !== group);
217
- const at = outline.groups.findIndex(item => item.id === position.before);
218
- outline.groups.splice(at < 0 ? outline.groups.length : at, 0, group);
219
- }
220
- outline.children = Object.fromEntries(Object.entries(outline.children).filter(([key]) => !isTop(key) || outline.groups.some(group => topOf(group.id) === key)));
221
- for (const group of outline.groups) outline.children[topOf(group.id)] ??= [];
201
+ let outline = projectTree(shared.outline, home, root);
222
202
  const ownLinks = await listLinkSlugs(root);
223
203
  const others = await otherWorkspaces(root);
224
204
  outline = { ...outline, links: ownLinks, drafts: Object.values(outline.children).flat().filter(isForeign), shown: others.map(({ id, root: other, isDefault }) => ({ id, root: other, isDefault })) };
225
- return { outline, stored: projectTree(shared.stored, home, root, { first: true }), others };
205
+ return { outline, stored: projectTree(shared.stored, home, root), others };
226
206
  }
227
207
 
228
208
  // The own column's name before it was 默认文档 or the workspace's (文档, Documents in English), which a
@@ -476,30 +456,10 @@ export function fileOf(value) {
476
456
  */
477
457
  export async function writeOutline(root, value) {
478
458
  const home = outlineRoot(root);
479
- if (sharedOutline(root) && !ownsOutline(root, value)) throw Object.assign(new Error("跨工作区请复制,不能只移动目录位置"), { code: "CROSS_WORKSPACE" });
480
- if (sharedOutline(root)) value = { ...value, groups: (value.groups ?? [{ id: DOCS }]).map(group => group.id === DOCS || group.workspace || group.extension ? group : { ...group, owner: group.owner ?? workspaceId(root) }) };
459
+ if (!ownsOutline(root, value)) throw Object.assign(new Error("跨工作区请复制,不能只移动目录位置"), { code: "CROSS_WORKSPACE" });
460
+ value = { ...value, groups: (value.groups ?? [{ id: DOCS }]).map(group => group.id === DOCS || group.workspace || group.extension ? group : { ...group, owner: group.owner ?? workspaceId(root) }) };
481
461
  if (workspaceKey(home) !== workspaceKey(root)) {
482
- const current = (await localOutlineAndFile(home)).outline;
483
- const translated = projectTree(value, root, home);
484
- // The current-column-first projection is presentation only. Keep the shared column order;
485
- // deliberate moves of other columns use the relative order submitted by the client.
486
- const ownId = workspaceId(root);
487
- const at = current.groups.findIndex(group => group.workspace === ownId);
488
- const own = translated.groups.find(group => group.workspace === ownId);
489
- translated.groups = translated.groups.filter(group => group !== own);
490
- if (own) translated.groups.splice(Math.max(0, Math.min(at, translated.groups.length)), 0, own);
491
- const publicIds = new Set(await extensionColumnIds(home));
492
- const privateGroup = group => group.extension && !publicIds.has(group.extension);
493
- const positions = value.groups.filter(privateGroup).map(group => ({ id: group.id, before: value.groups.slice(value.groups.indexOf(group) + 1).find(group => !privateGroup(group))?.id ?? null }));
494
- const localFile = layoutOf(root).outline;
495
- const local = await readJson(localFile) ?? {};
496
- if ((positions.length || local.extensionPositions?.length) && JSON.stringify(positions) !== JSON.stringify(local.extensionPositions)) {
497
- await fs.mkdir(path.dirname(localFile), { recursive: true });
498
- await writeAtomic(localFile, `${JSON.stringify({ ...local, extensionPositions: positions }, null, 2)}\n`);
499
- }
500
- translated.groups = translated.groups.filter(group => !group.extension || publicIds.has(group.extension));
501
- for (const group of current.groups) if (group.extension && !translated.groups.some(item => item.id === group.id)) translated.groups.push(group);
502
- value = translated;
462
+ value = projectTree(value, root, home);
503
463
  root = home;
504
464
  }
505
465
  const file = layoutOf(root).outline;
@@ -589,7 +549,7 @@ export function moveDocument(root, slug, where) {
589
549
  return withOutlineLock(root, async () => {
590
550
  const { outline, stored } = await outlineAndFile(root);
591
551
  const result = moveIn(outline.children, slug, where);
592
- if (result.ok && sharedOutline(root) && !ownsOutline(root, outline)) return { ok: false, reason: "cross-workspace" };
552
+ if (result.ok && !ownsOutline(root, outline)) return { ok: false, reason: "cross-workspace" };
593
553
  const written = result.ok && !writtenAs(outline, stored);
594
554
  if (written) await writeOutline(root, outline);
595
555
  return { ...result, ...(result.ok ? { written } : {}) };
@@ -1,4 +1,5 @@
1
- import { earlierLink, pageLink, pageOfLink } from "./page-links.mjs";
1
+ import { localPath } from "./workspace-address.mjs";
2
+ import { pageLink, pageOfLink } from "./page-links.mjs";
2
3
 
3
4
  // A document's address, as a link, a paste or the address bar may write it (docs/workbench.md, "A
4
5
  // document's addresses"), read in one place: { slug, block }, block null for the page itself. Pure: the
@@ -16,11 +17,8 @@ import { earlierLink, pageLink, pageOfLink } from "./page-links.mjs";
16
17
  // one form that needs `links` (pageLinks: a Map of slug to link), for only the list can say which
17
18
  // document an id is or whether a name is a slug at all; given, `links` also turns away every slug it
18
19
  // does not hold. Without it such an address is none here, and workbenchHash hands it to the workbench,
19
- // which has the list. pageAddress reads no port, as a link outlives a server; but this computer runs
20
- // other programs too, and other workspaces, whose folder of one name has one page id, so an address
21
- // on another port (elsewhere) is a document's only if the list holds it, by its folder name or its
22
- // link of now (addressOn), or for a folder named doc- and eight characters, the link it had before
23
- // those eight were its id (shared/page-links.mjs earlierId).
20
+ // which has the list. pageAddress parses a shape; addressOn additionally requires the current
21
+ // service’s port. An address of another port is always external, even when its slug matches.
24
22
 
25
23
  const SLUG = "[a-z0-9]+(?:-[a-z0-9]+)*"; // shared/workspace.mjs slugPattern, which needs Node
26
24
  const FILE = "(?:doc|report)\\.html";
@@ -71,7 +69,7 @@ export function pageAddress(text, links = null) {
71
69
  if (inFile) return known(inFile[1], decoded(inFile[2] ?? ""));
72
70
  const url = ownAddress(address);
73
71
  if (!url) return null;
74
- const path = PAGE.exec(url.pathname) ?? OWN_FILE.exec(url.pathname);
72
+ const path = PAGE.exec(localPath(url.pathname)) ?? OWN_FILE.exec(localPath(url.pathname));
75
73
  if (path) return known(path[1], decoded(url.hash.slice(1)));
76
74
  return url.pathname === "/" && url.hash.length > 1 ? hashAddress(decoded(url.hash.slice(1)), links) : null;
77
75
  }
@@ -87,19 +85,14 @@ export function workbenchHash(text) {
87
85
 
88
86
  /**
89
87
  * The document and block `text` names of those in `links` (a Map, or null with no list) as read on the
90
- * server of `origin`, as pageAddress reads it; but a workbench's address on another port says what it
91
- * is by its words, the folder name or the link the document has now, for the same id there may be
92
- * another workspace's page. With no `origin`, as a file is read, every port is another.
88
+ * server of `origin`, as pageAddress reads it. Another port is always external; with no origin
89
+ * supplied, no running service’s port is inferred from matching titles or folder names.
93
90
  * @param {string} text
94
91
  * @param {Map<string, string> | null} links
95
92
  * @param {string} [origin]
96
93
  */
97
94
  export function addressOn(text, links, origin = "http://127.0.0.1") {
98
- const place = pageAddress(text, links);
99
- const name = place && elsewhere(text, origin) ? workbenchHash(text)?.split("/")[0] : undefined;
100
- if (name === undefined || links.has(name)) return place;
101
- const link = links.get(place.slug);
102
- return link === name || earlierLink(place.slug, link) === name ? place : null;
95
+ return elsewhere(text, origin) ? null : pageAddress(text, links);
103
96
  }
104
97
 
105
98
  /** Whether `text` is an address of a document of this workspace, as far as can be told with no list. */
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { serviceLocation } from "./service-location.mjs";
4
5
  import { findWorkspace, layoutOf } from "./workspace.mjs";
5
6
 
6
7
  // Where everything is on disk, for every system, in two kinds: the workspace's (the user's documents
@@ -42,7 +43,7 @@ export const qaDirectory = workspace.qa; // gallery qa's pictures of what it fla
42
43
  export const backupsDirectory = workspace.backups; // documents as they were before a migration (add-block-ids.mjs)
43
44
  export const editStateDirectory = workspace.state; // the editor's token, snapshots, recent versions…
44
45
  // What a server a command started says while it runs (it runs on after that command).
45
- export const previewLogFile = workspace.serverLog;
46
+ export const previewLogFile = serviceLocation().serverLog;
46
47
 
47
48
  // Gallery's own files, which a command never writes (a global install may be read-only): the shared
48
49
  // kit and the starter, what the preview serves, what the checks and the import read. In the release
@@ -1,3 +1,4 @@
1
+ import { documentKey } from "./workspace-address.mjs";
1
2
  // The reader's preferences of Gallery's UI (the workbench's 设置): switches (and a width) kept in localStorage,
2
3
  // which the workbench and every report page share (one origin), so the page editor follows a change
3
4
  // at once (the storage event reaches every other page of the origin). Each has its default (off, but for
@@ -88,7 +89,7 @@ function openSlugs() {
88
89
 
89
90
  /** Whether report `slug` has its outline on (off until it is switched on there). */
90
91
  export function outlineOpen(slug) {
91
- return typeof slug === "string" && slug !== "" && openSlugs().includes(slug);
92
+ return typeof slug === "string" && slug !== "" && openSlugs().includes(documentKey(slug));
92
93
  }
93
94
 
94
95
  /**
@@ -96,6 +97,7 @@ export function outlineOpen(slug) {
96
97
  * reports, the one switched on longest ago let go); off, it is taken out.
97
98
  */
98
99
  export function setOutlineOpen(slug, on) {
100
+ slug = documentKey(slug);
99
101
  if (typeof slug !== "string" || slug === "") return;
100
102
  try {
101
103
  const slugs = openSlugs().filter((one) => one !== slug);