@heycys/gallery 0.2.0 → 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 (144) hide show
  1. package/THIRD-PARTY-NOTICES.md +1 -1
  2. package/browser/editor/components/icon/page.mjs +6 -4
  3. package/browser/editor/components/icons.mjs +2 -2
  4. package/browser/editor/page/annotate/index.mjs +3 -2
  5. package/browser/editor/page/board/index.mjs +2 -1
  6. package/browser/editor/page/board/settings.mjs +2 -1
  7. package/browser/editor/page/clipboard/import/read.mjs +20 -2
  8. package/browser/editor/page/core.mjs +3 -2
  9. package/browser/editor/page/custom-page.mjs +4 -3
  10. package/browser/editor/page/editor.mjs +9 -5
  11. package/browser/editor/page/emoji-picker/support.mjs +3 -2
  12. package/browser/editor/page/links/index.mjs +1 -12
  13. package/browser/editor/page/links/pages.mjs +3 -2
  14. package/browser/editor/page/outline/folds.mjs +3 -0
  15. package/browser/editor/page/post.mjs +2 -1
  16. package/browser/editor/page/present-links.mjs +4 -3
  17. package/browser/editor/page/present-shell.mjs +3 -58
  18. package/browser/editor/page/session.mjs +3 -37
  19. package/browser/editor/page/storage.mjs +5 -5
  20. package/browser/shared/component-catalog.mjs +2 -2
  21. package/browser/shared/document-copy.mjs +43 -0
  22. package/browser/shared/drafts.mjs +32 -50
  23. package/browser/shared/emoji/icon-value.mjs +4 -3
  24. package/browser/shared/extension-manifest.mjs +90 -0
  25. package/browser/shared/extensions.mjs +93 -7
  26. package/browser/shared/http.mjs +1 -1
  27. package/browser/shared/i18n/en.mjs +55 -14
  28. package/browser/shared/media.mjs +2 -1
  29. package/browser/shared/outline-shape.mjs +6 -0
  30. package/browser/shared/outline.mjs +95 -10
  31. package/browser/shared/page-address.mjs +8 -15
  32. package/browser/shared/paths.mjs +2 -1
  33. package/browser/shared/preferences.mjs +3 -1
  34. package/browser/shared/preview-server.mjs +51 -63
  35. package/browser/shared/service-location.mjs +18 -0
  36. package/browser/shared/session.mjs +3 -6
  37. package/browser/shared/workspace-address.mjs +19 -0
  38. package/browser/shared/workspace-columns.mjs +12 -9
  39. package/browser/shared/workspace-registry.mjs +6 -40
  40. package/browser/shared/workspace.mjs +3 -6
  41. package/browser/shared/writing-preferences.mjs +6 -4
  42. package/dist/{chunk-W7665ZEC.mjs → chunk-236JJXWY.mjs} +2 -2
  43. package/dist/{chunk-EAIFITAV.mjs → chunk-2KAVXMKG.mjs} +1 -1
  44. package/dist/chunk-2T4PZQEP.mjs +2 -0
  45. package/dist/{chunk-HXG7EOPJ.mjs → chunk-3SKOU6OZ.mjs} +1 -1
  46. package/dist/{chunk-AR5D57ZD.mjs → chunk-3XZOA7AH.mjs} +2 -2
  47. package/dist/{chunk-UYVCQK3H.mjs → chunk-4B2TB5K2.mjs} +1 -1
  48. package/dist/{chunk-3MBRL2PM.mjs → chunk-4KUO7R2M.mjs} +8 -8
  49. package/dist/chunk-4SNV4HA5.mjs +2 -0
  50. package/dist/chunk-53XDTHTG.mjs +4 -0
  51. package/dist/chunk-5XV3YYLG.mjs +2 -0
  52. package/dist/{chunk-ZDLPUOAG.mjs → chunk-65NTKF4N.mjs} +1 -1
  53. package/dist/chunk-6GV3CBB2.mjs +2 -0
  54. package/dist/{chunk-WQDQWCX5.mjs → chunk-6L6V7YR2.mjs} +1 -1
  55. package/dist/{chunk-ITOH5EGO.mjs → chunk-6NLXUKAY.mjs} +1 -1
  56. package/dist/chunk-6Q7ASAXJ.mjs +3 -0
  57. package/dist/{chunk-7OG26PGF.mjs → chunk-6QWQXHAV.mjs} +1 -1
  58. package/dist/chunk-6XD6U3QA.mjs +5 -0
  59. package/dist/{chunk-YNAWBAYN.mjs → chunk-7EFHNQ5B.mjs} +1 -1
  60. package/dist/{chunk-OFHXGWOX.mjs → chunk-7ISBBDJR.mjs} +1 -1
  61. package/dist/{chunk-QE5RG76S.mjs → chunk-7UZUQQ7Q.mjs} +29 -24
  62. package/dist/chunk-BLNQUR5R.mjs +168 -0
  63. package/dist/chunk-BMX3VUHY.mjs +2 -0
  64. package/dist/chunk-BN7M3IN7.mjs +8 -0
  65. package/dist/{chunk-K5XATEOO.mjs → chunk-BQNTTFPL.mjs} +1 -1
  66. package/dist/chunk-C257H6IA.mjs +2 -0
  67. package/dist/chunk-CHQU37CA.mjs +2 -0
  68. package/dist/{chunk-CMA72MAG.mjs → chunk-E4IHAIJ7.mjs} +1 -1
  69. package/dist/{chunk-VS7JIGJW.mjs → chunk-E7656LR7.mjs} +1 -1
  70. package/dist/{chunk-VFX2KNHP.mjs → chunk-ECZBLQKK.mjs} +1 -1
  71. package/dist/{chunk-MN7BZL4R.mjs → chunk-ERE2BXYV.mjs} +1 -1
  72. package/dist/chunk-FFV733MA.mjs +2 -0
  73. package/dist/{chunk-AHN3NFKJ.mjs → chunk-GJJRBGMY.mjs} +1 -1
  74. package/dist/{chunk-BZ5RAF7L.mjs → chunk-GKOBB6DH.mjs} +1 -1
  75. package/dist/chunk-GMJD7ID3.mjs +2 -0
  76. package/dist/{chunk-OEXWRJME.mjs → chunk-IA2SIEJQ.mjs} +1 -1
  77. package/dist/chunk-JPG2LBQM.mjs +11 -0
  78. package/dist/{chunk-POTA2FV4.mjs → chunk-JXXHAXYH.mjs} +1 -1
  79. package/dist/{chunk-FDMAXMQA.mjs → chunk-K3JKPFL5.mjs} +1 -1
  80. package/dist/{chunk-J5PUIV3Y.mjs → chunk-KLQP46Y3.mjs} +1 -1
  81. package/dist/chunk-LMUZQK5O.mjs +2 -0
  82. package/dist/{chunk-JLTZF4LA.mjs → chunk-M4NRDYI5.mjs} +1 -1
  83. package/dist/{chunk-HPOI4ZX7.mjs → chunk-MKIBJTJZ.mjs} +1 -1
  84. package/dist/{chunk-5K44HLTZ.mjs → chunk-MNWJ4EZY.mjs} +1 -1
  85. package/dist/{chunk-FETSREUN.mjs → chunk-NHP66IWR.mjs} +1 -1
  86. package/dist/{chunk-PGDE24N5.mjs → chunk-OEIJLMPI.mjs} +1 -1
  87. package/dist/{chunk-YROX2O22.mjs → chunk-PDZR3L3C.mjs} +1 -1
  88. package/dist/{chunk-6N3PX4XX.mjs → chunk-PLB7L22J.mjs} +1 -1
  89. package/dist/{chunk-LM4XFQHZ.mjs → chunk-Q4MS6OFC.mjs} +1 -1
  90. package/dist/{chunk-7JDX4E65.mjs → chunk-Q7P3Q5YX.mjs} +1 -1
  91. package/dist/{chunk-ZHEHPMBK.mjs → chunk-QNECWMGR.mjs} +1 -1
  92. package/dist/{chunk-YI43RG4P.mjs → chunk-QP7BOHE4.mjs} +1 -1
  93. package/dist/chunk-QVGOPHOF.mjs +2 -0
  94. package/dist/{chunk-DAZL73VK.mjs → chunk-RRKJMHNX.mjs} +2 -2
  95. package/dist/chunk-S3SX3COT.mjs +20 -0
  96. package/dist/{chunk-HDWTZ763.mjs → chunk-TKWEX4FD.mjs} +2 -2
  97. package/dist/{chunk-YLPSMDIS.mjs → chunk-ULXSPX5H.mjs} +2 -2
  98. package/dist/chunk-USIHBCSW.mjs +15 -0
  99. package/dist/{chunk-TR7TWGHB.mjs → chunk-VPMRP2TL.mjs} +1 -1
  100. package/dist/{chunk-ZQS6H473.mjs → chunk-VRPVVMRB.mjs} +1 -1
  101. package/dist/{chunk-F6HVDZR4.mjs → chunk-VZ2RQ3AM.mjs} +1 -1
  102. package/dist/{chunk-R2ZDLJYT.mjs → chunk-W5NPH5UA.mjs} +1 -1
  103. package/dist/chunk-WFGPMCTZ.mjs +4 -0
  104. package/dist/chunk-XZCFPKGO.mjs +2 -0
  105. package/dist/{chunk-4OKWZY5E.mjs → chunk-YN37ZDLW.mjs} +1 -1
  106. package/dist/chunk-ZFGMJVJC.mjs +2 -0
  107. package/dist/cli.mjs +1 -1
  108. package/dist/server.mjs +1 -1
  109. package/dist/worker.mjs +1 -1
  110. package/kit/report.css +3 -3
  111. package/kit/runtime.js +12 -2
  112. package/kit/state.js +2 -0
  113. package/kit/ui.js +5 -1
  114. package/package.json +1 -1
  115. package/release.json +7 -7
  116. package/skill/SKILL.md +13 -12
  117. package/skill/references/building-blocks.md +1 -1
  118. package/skill/references/extensions.md +48 -8
  119. package/web/assets/index-FLb9jCfl.css +1 -0
  120. package/web/assets/index-czjrXVjf.js +15 -0
  121. package/web/index.html +2 -2
  122. package/dist/chunk-5XA2ZBAE.mjs +0 -8
  123. package/dist/chunk-67N2QJYT.mjs +0 -2
  124. package/dist/chunk-6KBYRA2C.mjs +0 -2
  125. package/dist/chunk-DJISHPUL.mjs +0 -3
  126. package/dist/chunk-EKNHM3P4.mjs +0 -2
  127. package/dist/chunk-EP25HDAJ.mjs +0 -2
  128. package/dist/chunk-GHOYKMQ3.mjs +0 -4
  129. package/dist/chunk-IV64FFI4.mjs +0 -2
  130. package/dist/chunk-J43Z6LSS.mjs +0 -2
  131. package/dist/chunk-JA7G53PC.mjs +0 -2
  132. package/dist/chunk-JZT7I2OJ.mjs +0 -2
  133. package/dist/chunk-L2SUIS5S.mjs +0 -2
  134. package/dist/chunk-L7UP52JO.mjs +0 -2
  135. package/dist/chunk-MTYYHM2G.mjs +0 -15
  136. package/dist/chunk-MXUBKDRO.mjs +0 -15
  137. package/dist/chunk-NAMN3RAF.mjs +0 -2
  138. package/dist/chunk-ORID3AE7.mjs +0 -166
  139. package/dist/chunk-SWUTTZCN.mjs +0 -2
  140. package/dist/chunk-U7NZI6N4.mjs +0 -4
  141. package/dist/chunk-UJQJKNGK.mjs +0 -2
  142. package/dist/chunk-ZWWC4KLF.mjs +0 -20
  143. package/web/assets/index-BM_K-Auu.css +0 -1
  144. package/web/assets/index-DRslOBiv.js +0 -15
@@ -1,6 +1,6 @@
1
1
  # Third-party notices
2
2
 
3
- @heycys/gallery 0.2.0 carries the following code and data of others, bundled into dist/ and web/, built into kit/, or as files in vendor/ and browser/.
3
+ @heycys/gallery 0.2.2 carries the following code and data of others, bundled into dist/ and web/, built into kit/, or as files in vendor/ and browser/.
4
4
 
5
5
  ## @asamuzakjp/css-color 5.1.11
6
6
 
@@ -43,9 +43,9 @@ const STYLE = `
43
43
  [data-ge-editing] main ${STEP}[data-ge-number-hover]:not(:has(> .${ICON}))::before { box-shadow: inset 0 0 0 20px light-dark(rgb(55 53 47 / 8%), rgb(255 255 255 / 8%)); }
44
44
  .ge-add-icon { position: fixed; left: 0; top: 0; z-index: ${Z.affordance}; display: flex; align-items: center; gap: 6px; height: 28px; padding: 0 8px; border-radius: 6px;
45
45
  color: var(--ge-tx3); font: 14px/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
46
- white-space: nowrap; cursor: pointer; user-select: none; opacity: 0; transition: opacity .1s ease-in, background .1s ease-in; }
46
+ white-space: nowrap; cursor: pointer; user-select: none; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .1s ease-in, background .1s ease-in; }
47
47
  .ge-add-icon svg { display: block; flex: none; width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
48
- .ge-add-icon[data-shown] { opacity: 1; }
48
+ .ge-add-icon[data-shown] { opacity: 1; visibility: visible; pointer-events: auto; }
49
49
  .ge-add-icon:hover { background: light-dark(rgb(55 53 47 / 6%), rgb(255 255 255 / 6%)); }
50
50
  `;
51
51
 
@@ -187,21 +187,23 @@ export function create({ struct, say, isEditing, openMediaPicker, assetUrl, impo
187
187
  const add = element("div", { class: "ge-add-icon", role: "button", tabindex: "0", html: t("{0}添加图标", FACE_ICON) });
188
188
  add.addEventListener("mousedown", (event) => event.preventDefault());
189
189
  add.addEventListener("click", async () => {
190
+ if (!shown || !isEditing() || iconOf()) return;
190
191
  await loadEmojiData();
192
+ if (!shown || !isEditing() || iconOf()) return;
191
193
  setIcon(randomEmoji(), t("已添加页面图标"));
192
194
  });
193
195
  let shown = false;
194
196
  function showAdd() {
195
197
  const title = titleOf();
196
198
  const header = title?.parentElement;
197
- if (!title || !header) return hideAdd();
199
+ if (!isEditing() || !title || !header || iconOf()) return hideAdd();
198
200
  if (!add.isConnected) mount(add);
199
201
  const first = header.firstElementChild.getBoundingClientRect();
200
202
  const range = document.createRange();
201
203
  range.selectNodeContents(title);
202
204
  const text = range.getClientRects()[0] ?? title.getBoundingClientRect();
203
205
  add.style.transform = `translate(${Math.round(text.left - 8)}px, ${Math.round(first.top - 28 - 6)}px)`;
204
- if (!shown) requestAnimationFrame(() => add.setAttribute("data-shown", ""));
206
+ if (!shown) requestAnimationFrame(() => { if (shown) add.setAttribute("data-shown", ""); });
205
207
  shown = true;
206
208
  }
207
209
  function hideAdd() {
@@ -18,8 +18,8 @@ const LEVELS = {
18
18
  export const FORMULA_ICON = svg('<path d="M1.6 11.6 5.5 16.25 9.5 3.75h8.75"/><path d="m10.6 7.9 6.6 8.35M17.2 7.9l-6.6 8.35"/>');
19
19
 
20
20
  /** A link (two rings of a chain): the "/" menu's 链接, 链接视图, an empty link's chip, one drawing. */
21
- /** A page (as the workbench's tree draws one with no icon: Lucide's file-text, FinHarness's 文档, here in 20 units): the "/" menu's 页面, a page block left empty, a page with no icon in a search's rows. */
22
- export const PAGE_ICON = svg('<path d="M5 18.33a1.67 1.67 0 0 1-1.67-1.66V3.33a1.67 1.67 0 0 1 1.67-1.66h6.67a2 2 0 0 1 1.42.58l2.99 2.99a2 2 0 0 1 .59 1.43v10a1.67 1.67 0 0 1-1.67 1.66z"/><path d="M11.67 1.67v4.16c0 .46.37.84.83.84h4.17"/><path d="M8.33 7.5H6.67M13.33 10.83H6.67M13.33 14.17H6.67"/>');
21
+ /** A page (the workbench's plain Lucide file outline, here in 20 units): the "/" menu's 页面, a page block left empty, a page with no icon in a search's rows. */
22
+ export const PAGE_ICON = svg('<path d="M5 18.33a1.67 1.67 0 0 1-1.67-1.66V3.33a1.67 1.67 0 0 1 1.67-1.66h6.67a2 2 0 0 1 1.42.58l2.99 2.99a2 2 0 0 1 .59 1.43v10a1.67 1.67 0 0 1-1.67 1.66z"/><path d="M11.67 1.67v4.16c0 .46.37.84.83.84h4.17"/>');
23
23
 
24
24
  /** A section (分区): blocks gathered between brackets, the block menu's 组成新分区 and the section's line in 设置 › 组件列表. */
25
25
  export const SECTION_ICON = svg('<path d="M6 3.75h-.75a2 2 0 0 0-2 2v8.5a2 2 0 0 0 2 2H6M14 3.75h.75a2 2 0 0 1 2 2v8.5a2 2 0 0 1-2 2H14"/><path d="M7.25 7.75h5.5M7.25 12.25h5.5"/>');
@@ -1,3 +1,4 @@
1
+ import { documentKey, workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { CARD, RAIL, cardTops, markerLeft, markerTop, newAnnotationId, sameWords, squash, stackItems, tidy } from "../../../shared/annotations.mjs";
2
3
  import { t } from "../../../shared/i18n/index.mjs";
3
4
  import { SHARES_HEIGHT, standsAsPicture } from "../../components/registry.mjs";
@@ -106,7 +107,7 @@ const NOTICE_ROOM = 70;
106
107
  const IN_ROW = [...SHARES_HEIGHT].map((kind) => `[data-ge-unit-kind="${kind}"] > *`).join();
107
108
 
108
109
  export function createAnnotate({ slug, token, isEditing, picked, unpick = () => {}, wholePage = () => [], crossTexts, idFor, changed = () => {}, removed = () => {}, say }) {
109
- const stashKey = `gallery-edit:${slug}:notes`; // what could not be saved (offline), sent by the next page that reads them
110
+ const stashKey = `gallery-edit:${documentKey(slug)}:notes`; // what could not be saved (offline), sent by the next page that reads them
110
111
  let reading = null; // the first read from the server, under way or done (taken back when it failed)
111
112
  let counted = false; // that read is in and what it brought found: the page can say how many it has
112
113
  let items = new Map(); // id -> { id, created, updated, text, anchor } in page order, as the server last said, with the page's own changes
@@ -189,7 +190,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
189
190
  chain = run.catch(() => {});
190
191
  return run;
191
192
  };
192
- const fetchAll = () => fetch(`${ROUTE}?slug=${encodeURIComponent(slug)}`, { headers: { "X-Gallery-Token": token }, cache: "no-store" })
193
+ const fetchAll = () => fetch(workspaceAddress(`${ROUTE}?slug=${encodeURIComponent(slug)}`), { headers: { "X-Gallery-Token": token }, cache: "no-store" })
193
194
  .then((response) => response.json()).catch(() => ({ ok: false, reason: "offline" }));
194
195
  const wire = (item) => ({ id: item.id, created: item.created, updated: item.updated, text: item.text, anchor: item.anchor });
195
196
 
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { GAP, GRID, cellAt, fitHeight, gridFor, moved, pushDown, sizeFor, spanOf } from "../../../shared/board-places.mjs";
2
3
  import { cellsOf } from "../../../shared/card-size.mjs";
3
4
  import { language, t } from "../../../shared/i18n/index.mjs";
@@ -793,7 +794,7 @@ export function startBoard({ map, reload, hosted = false }) {
793
794
  menu.close();
794
795
  let listed = [];
795
796
  try {
796
- listed = (await (await fetch(`/__gallery/pages/${slug}/library?language=${language}`)).json()).blocks ?? [];
797
+ listed = (await (await fetch(workspaceAddress(`/__gallery/pages/${slug}/library?language=${language}`))).json()).blocks ?? [];
797
798
  } catch {
798
799
  say(t("连不上本地服务"), "error");
799
800
  return false;
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { Z } from "../../../shared/layers.mjs";
2
3
  import { language, t } from "../../../shared/i18n/index.mjs";
3
4
  import { createMenu } from "../menu.mjs";
@@ -59,7 +60,7 @@ export const formOpen = () => shown !== null;
59
60
  /** The workspace's documents, by title: [{ slug, title }] (none where the outline cannot be read). */
60
61
  async function documents() {
61
62
  try {
62
- const answer = await (await fetch("/__gallery/workspace/outline")).json();
63
+ const answer = await (await fetch(workspaceAddress("/__gallery/workspace/outline"))).json();
63
64
  return Object.values(answer.reports ?? {}).filter((report) => !report.instructions && report.kind !== "link").map(({ slug, title }) => ({ slug, title: title || slug }))
64
65
  .sort((a, b) => a.title.localeCompare(b.title, "zh-CN"));
65
66
  } catch {
@@ -14,7 +14,8 @@ import { addressOf } from "../../links/address.mjs";
14
14
  // formatting" (html.mjs containsRichFormatting; WordPress's always): spans, divs and paragraphs
15
15
  // alone paste as their text.
16
16
  // - Office's HTML (its namespaces) that looks like Word's goes through the Word filter (office.mjs).
17
- // - A whole $$ formula $$ → a formula block; several lines from VS Code → code in its language.
17
+ // - A whole $$ formula $$ → a formula block; IDE Markdown → document blocks, other IDE code →
18
+ // code in its language. An unspecified IDE language can be Markdown by its parsed structure.
18
19
  // - HTML: several lines of text, or a table → blocks (html.mjs htmlToBlocks); else one rich text.
19
20
  // - No HTML: several lines → Markdown (markdown.mjs); one line → inline Markdown (bold, links,
20
21
  // addresses, formulas) when it has any, else plain text. A whole line that is a display formula
@@ -89,6 +90,20 @@ function sameAddress(one, other) {
89
90
  }
90
91
  }
91
92
 
93
+ // VS Code and Cursor copy syntax-colored HTML beside the source text, even for Markdown. Use
94
+ // the same Markdown reader as an ordinary text paste, not the colored HTML. A Markdown language
95
+ // hint is enough; for an unspecified language, require some structure or inline formatting the
96
+ // reader actually recognizes. Unrecognized lines remain text; the whole selection need not be
97
+ // valid Markdown. A declared programming language wins over lookalike comments and operators.
98
+ function editorMarkdown({ text, language }) {
99
+ if (language !== "Markdown" && language !== "Plain Text") return null;
100
+ const specs = fromMarkdown(text);
101
+ if (language === "Markdown") return specs;
102
+ const marked = (blocks) => blocks.some((block) => block.type !== "text"
103
+ || block.title.some((piece) => piece[1]?.length) || marked(block.children));
104
+ return !addressOf(text) && marked(specs) ? specs : null;
105
+ }
106
+
92
107
  /**
93
108
  * Another program's content as { kind: "blocks", specs } | { kind: "rich", title } | { kind: "plain",
94
109
  * text } (notion-lite readClipboard, after the editor's own formats).
@@ -98,11 +113,14 @@ export function readExternal(data, options = {}) {
98
113
  const yuque = readYuque(data, options);
99
114
  if (yuque) return yuque;
100
115
  const external = externalContent(data, options);
101
- const { text, html } = external;
116
+ const { text } = external;
102
117
  const convert = { colors: external.colors };
103
118
  const equation = EQUATION.exec(text)?.[1];
104
119
  if (equation?.trim()) return { kind: "blocks", specs: [{ type: "equation", title: [[equation]], props: {}, children: [] }] };
120
+ const markdown = editorMarkdown(external);
121
+ if (markdown?.length && (multiline(text) || markdown.some((block) => block.type !== "text"))) return { kind: "blocks", specs: markdown };
105
122
  if (multiline(text) && external.language) return { kind: "blocks", specs: [{ type: "code", title: [[text]], props: { language: external.language }, children: [] }] };
123
+ const html = markdown ? "" : external.html;
106
124
  if (html) {
107
125
  if (multiline(text) || external.isTable) {
108
126
  const specs = mathifySpecs(htmlToBlocks(parseClipboardHTML(html, options), convert));
@@ -1,3 +1,4 @@
1
+ import { localPath } from "../../shared/workspace-address.mjs";
1
2
  import { escapeAttribute, escapeText, escapeTyped, unescapeText } from "../../shared/escape.mjs";
2
3
  import { formatRange, isInkClass, isUnderlineClass, markValue } from "./marks.mjs";
3
4
  import { ZWSP, inlineItems, spaceAround } from "../../shared/inline-spacing.mjs";
@@ -294,13 +295,13 @@ export function newMention(doc, { href, title, icon = null, drawn = null }, asse
294
295
  export function mentionOf(chip) {
295
296
  const img = chip.querySelector(`img.${MENTION_ICON}`);
296
297
  const src = img?.getAttribute("data-ge-src") ?? img?.getAttribute("src") ?? "";
297
- const served = /^\/__gallery\/edit\/asset\/([^/]+)\/(assets\/[^/?#]+)$/.exec(src);
298
+ const served = /^\/__gallery\/edit\/asset\/([^/]+)\/(assets\/[^/?#]+)$/.exec(localPath(src));
298
299
  return {
299
300
  href: chip.getAttribute("href") ?? "",
300
301
  title: chip.textContent.replace(/\s+/g, " ").trim(),
301
302
  icon: served ? served[2] : /^assets\/[^/]+$/.test(src) ? src : null,
302
303
  slug: served ? decodeURIComponent(served[1]) : null,
303
- drawn: src.startsWith("/__gallery/edit/icon?") ? src : null,
304
+ drawn: localPath(src).startsWith("/__gallery/edit/icon?") ? src : null,
304
305
  };
305
306
  }
306
307
 
@@ -1,3 +1,4 @@
1
+ import { localPath } from "../../shared/workspace-address.mjs";
1
2
  import { startBoard } from "./board/index.mjs";
2
3
  import { relayTo } from "./notices.mjs";
3
4
  import { connectWorkbench } from "./session.mjs";
@@ -21,7 +22,7 @@ import { connectWorkbench } from "./session.mjs";
21
22
  // document or another page, or a block's context.workspace.open: doc-open-request) and key. Opened on its own (not in a frame of the workbench), it has no
22
23
  // session: its board edits all the same, and the live script reloads it.
23
24
 
24
- const slug = decodeURIComponent(location.pathname.split("/")[2] ?? "");
25
+ const slug = decodeURIComponent(localPath(location.pathname).split("/")[2] ?? "");
25
26
  const IDLE = { editing: false, pending: false, saving: false, offline: false, conflicts: 0, unfit: 0, changed: false };
26
27
  // Nothing stands over a custom page as a modal.
27
28
  const NO_MODALS = { open: () => [], listen() {}, closeTop: () => false };
@@ -94,8 +95,8 @@ if (session) {
94
95
  window.open(url.href, "_blank", "noopener");
95
96
  return;
96
97
  }
97
- const custom = /^\/pages\/([a-z0-9-]+)\/?$/.exec(url.pathname)?.[1];
98
- const page = /^\/generated\/([a-z0-9-]+)\.html$/.exec(url.pathname)?.[1] ?? /^\/([a-z0-9-]+)\/(?:doc|report)\.html$/.exec(url.pathname)?.[1];
98
+ const custom = /^\/pages\/([a-z0-9-]+)\/?$/.exec(localPath(url.pathname))?.[1];
99
+ const page = /^\/generated\/([a-z0-9-]+)\.html$/.exec(localPath(url.pathname))?.[1] ?? /^\/([a-z0-9-]+)\/(?:doc|report)\.html$/.exec(localPath(url.pathname))?.[1];
99
100
  if (custom) {
100
101
  event.preventDefault();
101
102
  session.open({ hash: `@${custom}` });
@@ -1,3 +1,5 @@
1
+ import { documentKey, workspaceStoragePrefix } from "../../shared/workspace-address.mjs";
2
+ import { workspaceAddress } from "../../shared/workspace-address.mjs";
1
3
  // First: what the browser keeps under the names before 2026-10-28 carried to the new ones (the
2
4
  // editor's gallery-edit:… keys, src/shared/renamed-storage.mjs), before any module below reads them.
3
5
  import "../../shared/renamed-storage.mjs";
@@ -61,7 +63,7 @@ import { decoded, fullAddress, isFileLink } from "../../shared/page-address.mjs"
61
63
  import { goBackTo, placeIn, unmoved } from "../../shared/reading-place.mjs";
62
64
  import { heldList, holdPages } from "./links/pages.mjs";
63
65
  import { hideBlocks } from "./offered.mjs";
64
- import { local, storageOf } from "./storage.mjs";
66
+ import { storageOf } from "./storage.mjs";
65
67
  import { createTodoWrites } from "./todo-writes.mjs";
66
68
  import { language, t } from "../../shared/i18n/index.mjs";
67
69
 
@@ -131,7 +133,9 @@ let conflicts = null; // what the file refused, for the reader to decide (confli
131
133
  let unsaveable = { blank: 0, unplaced: 0 }; // blocks the page cannot save as they are
132
134
  let notice = null; // { text, time }: what a commit just said, to say again if the page reloads by itself
133
135
 
134
- const session = storageOf(() => sessionStorage);
136
+ const storageScope = { prefix: workspaceStoragePrefix() };
137
+ const local = storageOf(() => localStorage, storageScope);
138
+ const session = storageOf(() => sessionStorage, storageScope);
135
139
 
136
140
  // This tab, kept over reloads (sessionStorage). Each tab keeps what waits to be saved (the crash
137
141
  // buffer, autosave.mjs) under its own id, so two tabs of a report never overwrite each other's; the
@@ -140,7 +144,7 @@ const session = storageOf(() => sessionStorage);
140
144
  const newTab = () => Math.random().toString(36).slice(2, 10);
141
145
  let TAB = session.get(keys.tab) ?? newTab();
142
146
  session.set(keys.tab, TAB);
143
- const channel = (() => { try { return new BroadcastChannel(`gallery-edit:${MAP.slug}`); } catch { return null; } })();
147
+ const channel = (() => { try { return new BroadcastChannel(`gallery-edit:${documentKey(MAP.slug)}`); } catch { return null; } })();
144
148
  channel?.addEventListener("message", (event) => {
145
149
  if (event.data?.type === "who") channel.postMessage({ type: "here", tab: TAB });
146
150
  });
@@ -1411,7 +1415,7 @@ async function goTo({ slug = null, block = null, hash = null }, href) {
1411
1415
  if (block) jumpTo(block);
1412
1416
  } else if (SOLO) window.open(href, "_blank", "noopener");
1413
1417
  else if (window.parent !== window) workbench?.open(hash === null ? { target: slug, block } : { hash });
1414
- else if (hash === null || (await leadsToPage(hash))) COMMANDS["prepare-close"]().then(() => location.assign(hash === null ? `/generated/${slug}.html${block ? `#${encodeURIComponent(block)}` : ""}` : href));
1418
+ else if (hash === null || (await leadsToPage(hash))) COMMANDS["prepare-close"]().then(() => location.assign(hash === null ? workspaceAddress( `/generated/${slug}.html${block ? `#${encodeURIComponent(block)}` : ""}` ) : href));
1415
1419
  }
1416
1420
 
1417
1421
  function connectParts() {
@@ -1500,7 +1504,7 @@ function connect() {
1500
1504
  event.preventDefault();
1501
1505
  if (page) {
1502
1506
  if (window.parent !== window && !SOLO) workbench?.open({ hash: `@${page}` });
1503
- else COMMANDS["prepare-close"]().then(() => location.assign(`/pages/${page}`));
1507
+ else COMMANDS["prepare-close"]().then(() => location.assign(workspaceAddress(`/pages/${page}`)));
1504
1508
  } else goTo({ slug, block }, `/generated/${slug}.html${block ? `#${encodeURIComponent(block)}` : ""}`);
1505
1509
  });
1506
1510
  relayTo(workbench ? workbench.notice : null, workbench ? workbench.noticeRoom : null); // the workbench shows the page's notices, above the page's bottom panels
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { Z } from "../../../shared/layers.mjs";
2
3
  import { escapeMarkup } from "../../../shared/escape.mjs";
3
4
  import { icon } from "./icons.mjs";
@@ -198,7 +199,7 @@ export function connectPicker({ slug, token, notify }) {
198
199
  server = { slug, token };
199
200
  say = notify;
200
201
  setEmojiLibraryStorage({
201
- loadEmojiLibrary: () => fetch(`/__gallery/edit/emoji-library`).then((response) => response.json()).then((answer) => answer.list ?? []).catch(() => []),
202
- addEmoji: async ({ name, url }) => (await post("emoji-library", { name, url })).entry,
202
+ loadEmojiLibrary: () => fetch(workspaceAddress(`/__gallery/edit/emoji-library`)).then((response) => response.json()).then((answer) => (answer.list ?? []).map(entry => ({ ...entry, url: workspaceAddress(entry.url) }))).catch(() => []),
203
+ addEmoji: async ({ name, url }) => { const entry = (await post("emoji-library", { name, url })).entry; return { ...entry, url: workspaceAddress(entry.url) }; },
203
204
  });
204
205
  }
@@ -752,17 +752,6 @@ function opens(event) {
752
752
  // workspace's page of one folder name, opened as any web address. A tab
753
753
  // of its own opens with the click, the list read after, for only what the click itself opens does the
754
754
  // browser leave behind the page.
755
- async function followElsewhere(address, tab) {
756
- const opened = tab ? window.open("", "_blank") : null;
757
- const list = await readPages(use.own());
758
- const page = list && placeIn(list, address);
759
- const here = page && fullAddress(location.origin, page, list.links);
760
- if (opened) {
761
- opened.opener = null;
762
- opened.location.replace(here ?? address);
763
- } else if (page) use.go(page, here);
764
- else openTab(address);
765
- }
766
755
 
767
756
  // Go where `href`, a link of this page that is no anchor, leads, read as its file reads it, so that
768
757
  // doc.html beside it is this document: a document's address there, with `tab` in a tab of its own, a
@@ -771,7 +760,7 @@ async function followElsewhere(address, tab) {
771
760
  function follow(href, tab) {
772
761
  const address = fromFile(href, use.slug, location.origin);
773
762
  if (!address) return;
774
- if (elsewhere(address, location.origin)) return void followElsewhere(address, tab);
763
+ if (elsewhere(address, location.origin)) return void openTab(address);
775
764
  const place = placeOf(address);
776
765
  const file = referencedFile(href);
777
766
  if (place && !tab) use.go(place, address);
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { iconUrl } from "../../../shared/emoji/icon-value.mjs";
2
3
  import { escapeAttribute } from "../../../shared/escape.mjs";
3
4
  import { addressOn, decoded, hashAddress } from "../../../shared/page-address.mjs";
@@ -41,7 +42,7 @@ let shown = () => null; // this page, as readPages takes it
41
42
  * null when the list cannot be read (`webs`: the links of the sidebar's web links, which are no pages). `own`: this page, { slug, title } as it shows it.
42
43
  */
43
44
  export async function readPages(own = null) {
44
- const listed = await fetch(OUTLINE).then((answer) => answer.json()).catch(() => null);
45
+ const listed = await fetch(workspaceAddress(OUTLINE)).then((answer) => answer.json()).catch(() => null);
45
46
  if (!listed?.reports) return null;
46
47
  const titleOf = (slug) => (slug === own?.slug && own.title ? own.title : listed.reports[slug]?.title);
47
48
  const pages = [];
@@ -80,7 +81,7 @@ export function heldList() {
80
81
 
81
82
  /** Whether document `slug` is in the workspace's trash (not when the trash cannot be read). */
82
83
  export async function trashed(slug) {
83
- const listed = await fetch(TRASH).then((answer) => answer.json()).catch(() => null);
84
+ const listed = await fetch(workspaceAddress(TRASH)).then((answer) => answer.json()).catch(() => null);
84
85
  return inTrash(listed?.entries ?? [], slug);
85
86
  }
86
87
 
@@ -3,6 +3,7 @@
3
3
  // heading's block id, a tab's area and tab), the reports folded last kept and the oldest let go. The
4
4
  // headings folded on the page (fold/) are kept the same way under a key of their own (`key`).
5
5
 
6
+ import { documentKey } from "../../../shared/workspace-address.mjs";
6
7
  import { KEYS } from "../../../shared/renamed-storage.mjs";
7
8
 
8
9
  export const FOLDS_KEY = KEYS.outlineFolds;
@@ -18,6 +19,7 @@ function read(storage, key) {
18
19
 
19
20
  /** The keys folded in report `slug` (`storage`: { get, set }, page/storage.mjs), kept under `key`. */
20
21
  export function foldsOf(storage, slug, key = FOLDS_KEY) {
22
+ slug = documentKey(slug);
21
23
  const keys = read(storage, key).folds[slug];
22
24
  return new Set(Array.isArray(keys) ? keys.filter((key) => typeof key === "string") : []);
23
25
  }
@@ -27,6 +29,7 @@ export function foldsOf(storage, slug, key = FOLDS_KEY) {
27
29
  * folded last; none folded forgets the report. At most KEEP reports, the oldest going first.
28
30
  */
29
31
  export function keepFolds(storage, slug, folded, present = null, key = FOLDS_KEY) {
32
+ slug = documentKey(slug);
30
33
  const value = read(storage, key);
31
34
  const keys = [...folded].filter((each) => !present || present.has(each));
32
35
  delete value.folds[slug]; // put back last: the report folded last
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../shared/workspace-address.mjs";
1
2
  import { t, told } from "../../shared/i18n/index.mjs";
2
3
  // The page's one way to talk to the preview server: a POST of JSON with the report's token
3
4
  // (postJson: the request, a network error rejecting as fetch does; postAnswer: its answer read, a
@@ -5,7 +6,7 @@ import { t, told } from "../../shared/i18n/index.mjs";
5
6
  // says (failure) and how an upload carries a file (blobBase64).
6
7
 
7
8
  /** POST `body` as JSON to `route` with `token` (the map's); resolves with the Response. */
8
- export const postJson = (route, token, body, { keepalive = false } = {}) => fetch(route, {
9
+ export const postJson = (route, token, body, { keepalive = false } = {}) => fetch(workspaceAddress(route), {
9
10
  method: "POST",
10
11
  headers: { "Content-Type": "application/json", "X-Gallery-Token": token },
11
12
  body: JSON.stringify(body),
@@ -1,8 +1,9 @@
1
+ import { workspaceAddress } from "../../shared/workspace-address.mjs";
1
2
  import { t } from "../../shared/i18n/index.mjs";
2
3
  import { addressOn, elsewhere, fromFile, isPageAddress, mayBePage, pageAddress, workbenchHash } from "../../shared/page-address.mjs";
3
4
  import { inTrash, linkedPages, pageLinks } from "../../shared/page-links.mjs";
4
5
  import { entryOfKey } from "../../shared/extension-sidebar.mjs";
5
- import { SHELL, tell } from "./present-shell.mjs";
6
+ import { tell } from "./present-shell.mjs";
6
7
 
7
8
  // The links of a page shown in 演示视图 (?present: src/server/routes.mjs), which has no editor and whose
8
9
  // frame the presentation holds (src/workbench/app/features/present/). A link to a document or a block
@@ -21,11 +22,11 @@ import { SHELL, tell } from "./present-shell.mjs";
21
22
 
22
23
  const own = pageAddress(location.href)?.slug ?? null;
23
24
  const openTab = (address) => window.open(address, "_blank", "noopener");
24
- const read = (route) => fetch(`/__gallery/workspace/${route}`).then((answer) => answer.json()).catch(() => null);
25
+ const read = (route) => fetch(workspaceAddress(`/__gallery/workspace/${route}`)).then((answer) => answer.json()).catch(() => null);
25
26
 
26
27
  function go({ slug, block }) {
27
28
  const fragment = block ? `#${encodeURIComponent(block)}` : "";
28
- if (slug !== own) location.assign(`/generated/${slug}.html?present${SHELL ? `&shell=${encodeURIComponent(SHELL)}` : ""}${fragment}`); // a draft's presentation keeps the workbench around it (present-shell.mjs)
29
+ if (slug !== own) location.assign(workspaceAddress(`/generated/${slug}.html?present${fragment}`)); // a draft's presentation keeps the workbench around it (present-shell.mjs)
29
30
  else if (block) dispatchEvent(new CustomEvent("doc-jump-request", { detail: { hash: fragment } })); // the kit's jump, a step of the history as a link #<block id> is
30
31
  }
31
32
 
@@ -1,63 +1,8 @@
1
1
  import { pageAddress } from "../../shared/page-address.mjs";
2
- import { KEEPS_KEYS, stepSection } from "../../shared/present-step.mjs";
3
- import { PROTOCOL, PROTOCOLS, VERSION } from "../../shared/session.mjs";
2
+ import { PROTOCOL, VERSION } from "../../shared/session.mjs";
4
3
 
5
- // What a page shown in 演示视图 (?present: src/server/routes.mjs, no editor) says to the presentation around it and
6
- // hears from it. The presentation of a document of this workspace is a frame of the same origin, which it reaches into (its
7
- // scroll, its keys, its theme); that of a draft of another workspace (docs/workbench.md "Drafts of other workspaces") is a frame
8
- // of that workspace's server, named by the page's address (?shell=<the workbench's origin>) as the editor's page names it
9
- // (session.mjs): this page then asks its own server whether that is a Gallery server of this computer (GET
10
- // /__gallery/workspace/shell) and talks to it alone once it is, holding what it says until then; one that is not hears nothing.
11
- // It tells where the page is scrolled (`present-view`: { x, y }, with `ready` once the page can be spoken to) and the keys pressed
12
- // in it that are the presentation's (`present-key`: { key, ctrlKey, metaKey, altKey, kept (a control holds the key), prevented }),
13
- // and does what it is asked: `theme` (the workbench's, light or dark), `go-section` (上一节 / 下一节) and `scroll` ({ left, top }: the wheel). Every message is the session
14
- // protocol's envelope (src/shared/session.mjs).
15
-
16
- /** The origin of a workbench of another origin showing this page (?shell=<origin>), or null: this one's. */
17
- export const SHELL = (() => {
18
- const named = new URLSearchParams(location.search).get("shell");
19
- return named && named !== location.origin ? named : null;
20
- })();
21
-
22
- const vouched = (origin) => fetch(`/__gallery/workspace/shell?origin=${encodeURIComponent(origin)}`, { cache: "no-store" })
23
- .then((answer) => answer.json()).then((answer) => answer?.shell === true, () => false);
24
-
25
- let parentOrigin = SHELL ? null : location.origin;
26
- let held = SHELL ? [] : null;
27
-
28
- /** Tell the presentation around the page (`window.parent`) something; held until its origin is vouched for, dropped when it is not. */
4
+ // Presentations and their documents share one origin; links may notify their parent.
29
5
  export function tell(type, fields = {}) {
30
6
  if (window.parent === window) return;
31
- if (held) return void held.push([type, fields]);
32
- if (!parentOrigin) return;
33
- window.parent.postMessage({ protocol: PROTOCOL, v: VERSION, type, slug: pageAddress(location.href)?.slug ?? "", ...fields }, parentOrigin);
34
- }
35
-
36
- function bridge() {
37
- let frame = 0;
38
- const view = (ready = false) => tell("present-view", { x: scrollX, y: scrollY, ...(ready ? { ready: true } : {}) });
39
- addEventListener("scroll", () => { frame ||= requestAnimationFrame(() => { frame = 0; view(); }); }, { passive: true });
40
- addEventListener("keydown", (event) => {
41
- if (event.isComposing) return;
42
- tell("present-key", { key: event.key, ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, kept: Boolean(event.target?.closest?.(KEEPS_KEYS)), prevented: event.defaultPrevented });
43
- });
44
- addEventListener("message", (event) => {
45
- const message = event.data;
46
- if (event.source !== window.parent || event.origin !== parentOrigin || !PROTOCOLS.includes(message?.protocol) || message.v !== VERSION || message.type !== "command") return;
47
- if (message.name === "theme") dispatchEvent(new CustomEvent("doc-theme-request", { detail: { theme: message.theme === "dark" ? "dark" : "light" } })); // the kit's ui.js
48
- else if (message.name === "go-section") stepSection(window, message.direction > 0 ? 1 : -1);
49
- else if (message.name === "scroll") scrollBy({ left: Number(message.left) || 0, top: Number(message.top) || 0 }); // the wheel over the presentation's ink
50
- });
51
- view(true);
52
- }
53
-
54
- if (SHELL && window.parent !== window) {
55
- vouched(SHELL).then((ok) => {
56
- const waiting = held;
57
- held = null;
58
- if (!ok) return;
59
- parentOrigin = SHELL;
60
- for (const [type, fields] of waiting) tell(type, fields);
61
- bridge();
62
- });
7
+ window.parent.postMessage({ protocol: PROTOCOL, v: VERSION, type, slug: pageAddress(location.href)?.slug ?? "", ...fields }, location.origin);
63
8
  }
@@ -5,15 +5,7 @@ import { lookOf, lookOfElement } from "../../shared/document-look.mjs";
5
5
  // tells the workbench its state, never the report's content. Nothing happens when the page is not
6
6
  // embedded. The protocol is src/shared/session.mjs; how a workbench uses it, docs/workbench.md.
7
7
  //
8
- // A workbench of another workspace shows a draft of this one from its own origin (docs/workbench.md
9
- // "Drafts of other workspaces"), naming that origin in the frame's address (?shell=<origin>): the page
10
- // then asks its own server whether that is a Gallery server of this computer (GET
11
- // /__gallery/workspace/shell: on the registry, running, its token and process as it announces them),
12
- // and talks to it alone once it is, holding what it says until then; one that is not hears nothing and
13
- // is heard by nothing, so that no other page of this computer drives a document by framing it. Going,
14
- // such a page says `bye` (the workbench of another origin cannot see it go).
15
- //
16
- // A message counts only from the parent window, of this origin (or the shell's, vouched for) and protocol. A command names the
8
+ // A message counts only from the parent window, of this exact origin and protocol. A command names the
17
9
  // report it is meant for: one for another report (the iframe went to another page meanwhile) is
18
10
  // refused. Every command gets exactly one reply, once it is done. Besides the editor's commands the
19
11
  // session itself answers five: `theme` (show the report light or dark, or as the reader chose, without
@@ -27,16 +19,6 @@ import { lookOf, lookOfElement } from "../../shared/document-look.mjs";
27
19
 
28
20
  /** A page opened in a frame on its own (?solo), not as the workbench's report: no session, its own notices. */
29
21
  export const SOLO = new URLSearchParams(location.search).has("solo");
30
- /** The origin of a workbench of another workspace showing this page (?shell=<origin>), or null: this one's. */
31
- const SHELL = (() => {
32
- const named = new URLSearchParams(location.search).get("shell");
33
- return named && named !== location.origin ? named : null;
34
- })();
35
-
36
- // Whether this page's server vouches for the workbench at `origin` (GET /__gallery/workspace/shell).
37
- const vouched = (origin) => fetch(`/__gallery/workspace/shell?origin=${encodeURIComponent(origin)}`, { cache: "no-store" })
38
- .then((answer) => answer.json()).then((answer) => answer?.shell === true, () => false);
39
-
40
22
  /**
41
23
  * slug: the report's; snapshot(): { title, state } (state: STATE_FLAGS of src/shared/session.mjs);
42
24
  * commands: name -> async (message) => result ({ ok, … }); modals: what stands open over the page
@@ -56,27 +38,11 @@ const vouched = (origin) => fetch(`/__gallery/workspace/shell?origin=${encodeURI
56
38
  export function connectWorkbench({ slug, snapshot, commands, modals }) {
57
39
  if (window.parent === window || SOLO) return null;
58
40
  centreCards(modals);
59
- // The workbench's origin: this one's, or the shell's once this page's server vouched for it (null till
60
- // then: what the page says is held, and is dropped when it does not).
61
- let parentOrigin = SHELL ? null : location.origin;
62
- let held = SHELL ? [] : null;
63
- if (SHELL) {
64
- vouched(SHELL).then((ok) => {
65
- const waiting = held;
66
- held = null;
67
- if (!ok) return;
68
- parentOrigin = SHELL;
69
- for (const [type, fields, protocols] of waiting) send(type, fields, protocols);
70
- // A workbench of another origin sees no pagehide of this page: it is told.
71
- addEventListener("pagehide", () => send("bye", {}), { once: true });
72
- });
73
- }
41
+ const parentOrigin = location.origin;
74
42
  // The id the workbench speaks (src/shared/session.mjs, OLD_PROTOCOL): null until its first command,
75
43
  // and meanwhile every message goes under both, so that a workbench of either version hears it.
76
44
  let spoken = null;
77
45
  const send = (type, fields, protocols = spoken ? [spoken] : [PROTOCOL, OLD_PROTOCOL]) => {
78
- if (held) return void held.push([type, fields, protocols]);
79
- if (!parentOrigin) return;
80
46
  for (const protocol of protocols) window.parent.postMessage({ protocol, v: VERSION, type, slug, ...fields }, parentOrigin);
81
47
  };
82
48
  let last = null;
@@ -141,7 +107,7 @@ export function connectWorkbench({ slug, snapshot, commands, modals }) {
141
107
 
142
108
  addEventListener("message", async (event) => {
143
109
  const message = event.data;
144
- if (event.source !== window.parent || !parentOrigin || event.origin !== parentOrigin) return;
110
+ if (event.source !== window.parent || event.origin !== parentOrigin) return;
145
111
  if (!PROTOCOLS.includes(message?.protocol) || message.v !== VERSION || message.type !== "command") return;
146
112
  spoken = message.protocol;
147
113
  const run = Object.hasOwn(all, message.name) ? all[message.name] : null;
@@ -4,11 +4,11 @@
4
4
  // and the page works without (a list used lately is then kept for the page's life only).
5
5
 
6
6
  /** A storage area (`area()`: localStorage or sessionStorage) whose calls never throw. */
7
- export const storageOf = (area) => ({
8
- get(key) { try { return area().getItem(key); } catch { return null; } },
9
- set(key, value) { try { area().setItem(key, value); return true; } catch { return false; } },
10
- remove(key) { try { area().removeItem(key); } catch {} },
11
- keys() { try { return Object.keys(area()); } catch { return []; } },
7
+ export const storageOf = (area, { prefix = "" } = {}) => ({
8
+ get(key) { try { return area().getItem(prefix + key); } catch { return null; } },
9
+ set(key, value) { try { area().setItem(prefix + key, value); return true; } catch { return false; } },
10
+ remove(key) { try { area().removeItem(prefix + key); } catch {} },
11
+ keys() { try { return Object.keys(area()).filter(key => key.startsWith(prefix)).map(key => key.slice(prefix.length)); } catch { return []; } },
12
12
  });
13
13
 
14
14
  /** localStorage, safe. */
@@ -11,7 +11,7 @@ import { attr, walk } from "./html-tree.mjs";
11
11
  import { HOST, NET, netHostsOf } from "./net-policy.mjs";
12
12
  import { inLanguage, refusal, said, saying, told } from "./i18n/index.mjs";
13
13
  import { DEFAULT_PACK, OWN_SCOPE, PACK_FILE, PACK_ID, readPackManifest } from "./pack-manifest.mjs";
14
- import { DEVICE_NAMES, PACKAGE_MANIFEST, WORKSPACE_MARK, findWorkspace, layoutOf, markerOf, packageFolders } from "./workspace.mjs";
14
+ import { DEVICE_NAMES, PACKAGE_MANIFEST, WORKSPACE_MARK, defaultWorkspace, layoutOf, markerOf, packageFolders } from "./workspace.mjs";
15
15
 
16
16
  // 组件包, custom blocks (自定义块) as the UI and the Agent's documents call them: what the workspace's
17
17
  // component packages say of themselves to Gallery, read from
@@ -466,7 +466,7 @@ function readPackage(id, folder) {
466
466
  }
467
467
  }
468
468
 
469
- const workspaceRoot = () => findWorkspace().root;
469
+ const workspaceRoot = () => defaultWorkspace();
470
470
 
471
471
  // A file edited by hand holds what it holds: of its lists, what can be an id is read, once each, and
472
472
  // the rest is passed over, one that names nothing there doing nothing. An id may be of a package that
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { parse } from "parse5";
4
+ import { attr, walk } from "./html-tree.mjs";
5
+ import { escapeAttribute } from "./escape.mjs";
6
+ import { newNames } from "./new-names.mjs";
7
+ import { COMPONENTS_FOLDER, DOCUMENT_FOLDERS, SHARED_FOLDERS, documentFiles, packageFolders, readLink } from "./workspace.mjs";
8
+
9
+ /** Files belonging to a document, excluding packages/pages sharing a legacy reserved folder. */
10
+ export async function documentNames(root, slug) {
11
+ const entries = await fs.readdir(path.join(root, slug), { withFileTypes: true });
12
+ if (!SHARED_FOLDERS.includes(slug)) return entries.map(({ name }) => name);
13
+ const scopes = slug === COMPONENTS_FOLDER ? new Set(packageFolders(root).map(({ id }) => id.split("/")[0])) : new Set();
14
+ return entries.filter(entry => entry.isFile() || (DOCUMENT_FOLDERS.includes(entry.name) && !scopes.has(entry.name))).map(({ name }) => name);
15
+ }
16
+
17
+ /** Same copy primitive for duplicate and cross-workspace copy: local assets and metadata stay intact. */
18
+ export async function copyEntryFiles(root, slug, destination, copy, { names = new Map() } = {}) {
19
+ const source = path.join(root, slug);
20
+ const document = await fs.access(documentFiles(source).html).then(() => true, () => false);
21
+ const link = document ? null : await readLink(source);
22
+ const own = new Set(["", ...(await documentNames(root, slug))]);
23
+ await fs.cp(source, destination, { recursive: true, errorOnExist: true, force: false, filter: from => own.has(path.relative(source, from).split(path.sep)[0]) });
24
+ if (link) return [];
25
+ const file = documentFiles(destination).html;
26
+ const html = await fs.readFile(file, "utf8");
27
+ const text = newNames(html);
28
+ const edits = new Map(), packages = new Set();
29
+ walk(parse(text.replace(/^\uFEFF/, " "), { sourceCodeLocationInfo: true }), node => {
30
+ const component = attr(node, "data-doc-component");
31
+ if (component) packages.add(component);
32
+ for (const { name, value } of node.attrs ?? []) {
33
+ const at = node.sourceCodeLocation?.attrs?.[name];
34
+ if (!at) continue;
35
+ const relative = /^(\.\.\/)([a-z0-9]+(?:-[a-z0-9]+)*)(\/.*)$/.exec(value);
36
+ const next = name === "data-doc-id" && value === slug ? copy : ["href", "src", "poster"].includes(name) && relative && names.has(relative[2]) ? `../${names.get(relative[2])}${relative[3]}` : value;
37
+ if (next !== value) edits.set(at.startOffset, { to: at.endOffset, text: `${name}="${escapeAttribute(next)}"` });
38
+ }
39
+ });
40
+ const next = [...edits].sort(([a], [b]) => b - a).reduce((text, [from, edit]) => text.slice(0, from) + edit.text + text.slice(edit.to), text);
41
+ await fs.writeFile(file, next);
42
+ return [...packages];
43
+ }