@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.
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/browser/editor/components/icon/page.mjs +6 -4
- package/browser/editor/components/icons.mjs +2 -2
- package/browser/editor/page/annotate/index.mjs +3 -2
- package/browser/editor/page/board/index.mjs +2 -1
- package/browser/editor/page/board/settings.mjs +2 -1
- package/browser/editor/page/clipboard/import/read.mjs +20 -2
- package/browser/editor/page/core.mjs +3 -2
- package/browser/editor/page/custom-page.mjs +4 -3
- package/browser/editor/page/editor.mjs +9 -5
- package/browser/editor/page/emoji-picker/support.mjs +3 -2
- package/browser/editor/page/links/index.mjs +1 -12
- package/browser/editor/page/links/pages.mjs +3 -2
- package/browser/editor/page/outline/folds.mjs +3 -0
- package/browser/editor/page/post.mjs +2 -1
- package/browser/editor/page/present-links.mjs +4 -3
- package/browser/editor/page/present-shell.mjs +3 -58
- package/browser/editor/page/session.mjs +3 -37
- package/browser/editor/page/storage.mjs +5 -5
- package/browser/shared/component-catalog.mjs +2 -2
- package/browser/shared/document-copy.mjs +43 -0
- package/browser/shared/drafts.mjs +32 -50
- package/browser/shared/emoji/icon-value.mjs +4 -3
- package/browser/shared/extension-manifest.mjs +90 -0
- package/browser/shared/extensions.mjs +93 -7
- package/browser/shared/http.mjs +1 -1
- package/browser/shared/i18n/en.mjs +55 -14
- package/browser/shared/media.mjs +2 -1
- package/browser/shared/outline-shape.mjs +6 -0
- package/browser/shared/outline.mjs +95 -10
- package/browser/shared/page-address.mjs +8 -15
- package/browser/shared/paths.mjs +2 -1
- package/browser/shared/preferences.mjs +3 -1
- package/browser/shared/preview-server.mjs +51 -63
- package/browser/shared/service-location.mjs +18 -0
- package/browser/shared/session.mjs +3 -6
- package/browser/shared/workspace-address.mjs +19 -0
- package/browser/shared/workspace-columns.mjs +12 -9
- package/browser/shared/workspace-registry.mjs +6 -40
- package/browser/shared/workspace.mjs +3 -6
- package/browser/shared/writing-preferences.mjs +6 -4
- package/dist/{chunk-W7665ZEC.mjs → chunk-236JJXWY.mjs} +2 -2
- package/dist/{chunk-EAIFITAV.mjs → chunk-2KAVXMKG.mjs} +1 -1
- package/dist/chunk-2T4PZQEP.mjs +2 -0
- package/dist/{chunk-HXG7EOPJ.mjs → chunk-3SKOU6OZ.mjs} +1 -1
- package/dist/{chunk-AR5D57ZD.mjs → chunk-3XZOA7AH.mjs} +2 -2
- package/dist/{chunk-UYVCQK3H.mjs → chunk-4B2TB5K2.mjs} +1 -1
- package/dist/{chunk-3MBRL2PM.mjs → chunk-4KUO7R2M.mjs} +8 -8
- package/dist/chunk-4SNV4HA5.mjs +2 -0
- package/dist/chunk-53XDTHTG.mjs +4 -0
- package/dist/chunk-5XV3YYLG.mjs +2 -0
- package/dist/{chunk-ZDLPUOAG.mjs → chunk-65NTKF4N.mjs} +1 -1
- package/dist/chunk-6GV3CBB2.mjs +2 -0
- package/dist/{chunk-WQDQWCX5.mjs → chunk-6L6V7YR2.mjs} +1 -1
- package/dist/{chunk-ITOH5EGO.mjs → chunk-6NLXUKAY.mjs} +1 -1
- package/dist/chunk-6Q7ASAXJ.mjs +3 -0
- package/dist/{chunk-7OG26PGF.mjs → chunk-6QWQXHAV.mjs} +1 -1
- package/dist/chunk-6XD6U3QA.mjs +5 -0
- package/dist/{chunk-YNAWBAYN.mjs → chunk-7EFHNQ5B.mjs} +1 -1
- package/dist/{chunk-OFHXGWOX.mjs → chunk-7ISBBDJR.mjs} +1 -1
- package/dist/{chunk-QE5RG76S.mjs → chunk-7UZUQQ7Q.mjs} +29 -24
- package/dist/chunk-BLNQUR5R.mjs +168 -0
- package/dist/chunk-BMX3VUHY.mjs +2 -0
- package/dist/chunk-BN7M3IN7.mjs +8 -0
- package/dist/{chunk-K5XATEOO.mjs → chunk-BQNTTFPL.mjs} +1 -1
- package/dist/chunk-C257H6IA.mjs +2 -0
- package/dist/chunk-CHQU37CA.mjs +2 -0
- package/dist/{chunk-CMA72MAG.mjs → chunk-E4IHAIJ7.mjs} +1 -1
- package/dist/{chunk-VS7JIGJW.mjs → chunk-E7656LR7.mjs} +1 -1
- package/dist/{chunk-VFX2KNHP.mjs → chunk-ECZBLQKK.mjs} +1 -1
- package/dist/{chunk-MN7BZL4R.mjs → chunk-ERE2BXYV.mjs} +1 -1
- package/dist/chunk-FFV733MA.mjs +2 -0
- package/dist/{chunk-AHN3NFKJ.mjs → chunk-GJJRBGMY.mjs} +1 -1
- package/dist/{chunk-BZ5RAF7L.mjs → chunk-GKOBB6DH.mjs} +1 -1
- package/dist/chunk-GMJD7ID3.mjs +2 -0
- package/dist/{chunk-OEXWRJME.mjs → chunk-IA2SIEJQ.mjs} +1 -1
- package/dist/chunk-JPG2LBQM.mjs +11 -0
- package/dist/{chunk-POTA2FV4.mjs → chunk-JXXHAXYH.mjs} +1 -1
- package/dist/{chunk-FDMAXMQA.mjs → chunk-K3JKPFL5.mjs} +1 -1
- package/dist/{chunk-J5PUIV3Y.mjs → chunk-KLQP46Y3.mjs} +1 -1
- package/dist/chunk-LMUZQK5O.mjs +2 -0
- package/dist/{chunk-JLTZF4LA.mjs → chunk-M4NRDYI5.mjs} +1 -1
- package/dist/{chunk-HPOI4ZX7.mjs → chunk-MKIBJTJZ.mjs} +1 -1
- package/dist/{chunk-5K44HLTZ.mjs → chunk-MNWJ4EZY.mjs} +1 -1
- package/dist/{chunk-FETSREUN.mjs → chunk-NHP66IWR.mjs} +1 -1
- package/dist/{chunk-PGDE24N5.mjs → chunk-OEIJLMPI.mjs} +1 -1
- package/dist/{chunk-YROX2O22.mjs → chunk-PDZR3L3C.mjs} +1 -1
- package/dist/{chunk-6N3PX4XX.mjs → chunk-PLB7L22J.mjs} +1 -1
- package/dist/{chunk-LM4XFQHZ.mjs → chunk-Q4MS6OFC.mjs} +1 -1
- package/dist/{chunk-7JDX4E65.mjs → chunk-Q7P3Q5YX.mjs} +1 -1
- package/dist/{chunk-ZHEHPMBK.mjs → chunk-QNECWMGR.mjs} +1 -1
- package/dist/{chunk-YI43RG4P.mjs → chunk-QP7BOHE4.mjs} +1 -1
- package/dist/chunk-QVGOPHOF.mjs +2 -0
- package/dist/{chunk-DAZL73VK.mjs → chunk-RRKJMHNX.mjs} +2 -2
- package/dist/chunk-S3SX3COT.mjs +20 -0
- package/dist/{chunk-HDWTZ763.mjs → chunk-TKWEX4FD.mjs} +2 -2
- package/dist/{chunk-YLPSMDIS.mjs → chunk-ULXSPX5H.mjs} +2 -2
- package/dist/chunk-USIHBCSW.mjs +15 -0
- package/dist/{chunk-TR7TWGHB.mjs → chunk-VPMRP2TL.mjs} +1 -1
- package/dist/{chunk-ZQS6H473.mjs → chunk-VRPVVMRB.mjs} +1 -1
- package/dist/{chunk-F6HVDZR4.mjs → chunk-VZ2RQ3AM.mjs} +1 -1
- package/dist/{chunk-R2ZDLJYT.mjs → chunk-W5NPH5UA.mjs} +1 -1
- package/dist/chunk-WFGPMCTZ.mjs +4 -0
- package/dist/chunk-XZCFPKGO.mjs +2 -0
- package/dist/{chunk-4OKWZY5E.mjs → chunk-YN37ZDLW.mjs} +1 -1
- package/dist/chunk-ZFGMJVJC.mjs +2 -0
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/report.css +3 -3
- package/kit/runtime.js +12 -2
- package/kit/state.js +2 -0
- package/kit/ui.js +5 -1
- package/package.json +1 -1
- package/release.json +7 -7
- package/skill/SKILL.md +13 -12
- package/skill/references/building-blocks.md +1 -1
- package/skill/references/extensions.md +48 -8
- package/web/assets/index-FLb9jCfl.css +1 -0
- package/web/assets/index-czjrXVjf.js +15 -0
- package/web/index.html +2 -2
- package/dist/chunk-5XA2ZBAE.mjs +0 -8
- package/dist/chunk-67N2QJYT.mjs +0 -2
- package/dist/chunk-6KBYRA2C.mjs +0 -2
- package/dist/chunk-DJISHPUL.mjs +0 -3
- package/dist/chunk-EKNHM3P4.mjs +0 -2
- package/dist/chunk-EP25HDAJ.mjs +0 -2
- package/dist/chunk-GHOYKMQ3.mjs +0 -4
- package/dist/chunk-IV64FFI4.mjs +0 -2
- package/dist/chunk-J43Z6LSS.mjs +0 -2
- package/dist/chunk-JA7G53PC.mjs +0 -2
- package/dist/chunk-JZT7I2OJ.mjs +0 -2
- package/dist/chunk-L2SUIS5S.mjs +0 -2
- package/dist/chunk-L7UP52JO.mjs +0 -2
- package/dist/chunk-MTYYHM2G.mjs +0 -15
- package/dist/chunk-MXUBKDRO.mjs +0 -15
- package/dist/chunk-NAMN3RAF.mjs +0 -2
- package/dist/chunk-ORID3AE7.mjs +0 -166
- package/dist/chunk-SWUTTZCN.mjs +0 -2
- package/dist/chunk-U7NZI6N4.mjs +0 -4
- package/dist/chunk-UJQJKNGK.mjs +0 -2
- package/dist/chunk-ZWWC4KLF.mjs +0 -20
- package/web/assets/index-BM_K-Auu.css +0 -1
- package/web/assets/index-DRslOBiv.js +0 -15
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Third-party notices
|
|
2
2
|
|
|
3
|
-
@heycys/gallery 0.2.
|
|
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 (
|
|
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"
|
|
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)}
|
|
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;
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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${
|
|
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 {
|
|
3
|
-
import { PROTOCOL, PROTOCOLS, VERSION } from "../../shared/session.mjs";
|
|
2
|
+
import { PROTOCOL, VERSION } from "../../shared/session.mjs";
|
|
4
3
|
|
|
5
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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,
|
|
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 = () =>
|
|
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
|
+
}
|