@heycys/gallery 0.1.0 → 0.1.1
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 +51 -1
- package/browser/editor/components/callout/index.mjs +40 -15
- package/browser/editor/components/columns/page.mjs +41 -28
- package/browser/editor/components/details/index.mjs +2 -1
- package/browser/editor/components/dialog/index.mjs +2 -1
- package/browser/editor/components/dialog/page.mjs +0 -18
- package/browser/editor/components/divider/index.mjs +1 -1
- package/browser/editor/components/figure/page.mjs +10 -1
- package/browser/editor/components/file/page.mjs +2 -2
- package/browser/editor/components/flowchart/add.mjs +2 -2
- package/browser/editor/components/heading/index.mjs +2 -0
- package/browser/editor/components/heading/page.mjs +6 -0
- package/browser/editor/components/highlight/index.mjs +4 -20
- package/browser/editor/components/icons.mjs +5 -2
- package/browser/editor/components/image/page.mjs +2 -2
- package/browser/editor/components/installed/index.mjs +71 -25
- package/browser/editor/components/installed/page.mjs +122 -21
- package/browser/editor/components/kit.mjs +3 -0
- package/browser/editor/components/media/page.mjs +44 -14
- package/browser/editor/components/pages.mjs +2 -2
- package/browser/editor/components/registry.mjs +56 -30
- package/browser/editor/components/section/index.mjs +2 -1
- package/browser/editor/components/set-apart.mjs +24 -0
- package/browser/editor/components/table/index.mjs +5 -3
- package/browser/editor/components/table/page.mjs +29 -18
- package/browser/editor/page/annotate/anchors.mjs +4 -3
- package/browser/editor/page/annotate/index.mjs +61 -34
- package/browser/editor/page/autosave.mjs +50 -4
- package/browser/editor/page/beside.mjs +5 -5
- package/browser/editor/page/block-menu/colors.mjs +3 -1
- package/browser/editor/page/block-menu/index.mjs +8 -6
- package/browser/editor/page/blocks.mjs +16 -7
- package/browser/editor/page/caret.mjs +4 -1
- package/browser/editor/page/changes/bar.mjs +102 -0
- package/browser/editor/page/changes/ghost.mjs +206 -0
- package/browser/editor/page/changes/gone-words.mjs +94 -0
- package/browser/editor/page/changes/index.mjs +447 -0
- package/browser/editor/page/changes/words.mjs +27 -0
- package/browser/editor/page/clipboard/import/colors.mjs +3 -3
- package/browser/editor/page/clipboard/index.mjs +32 -13
- package/browser/editor/page/clipboard/own/blocks.mjs +14 -5
- package/browser/editor/page/clipboard/own/describe.mjs +16 -7
- package/browser/editor/page/clipboard/own/export.mjs +6 -6
- package/browser/editor/page/clipboard/own/selection.mjs +2 -2
- package/browser/editor/page/clipboard/own/writers.mjs +0 -11
- package/browser/editor/page/columns/index.mjs +1 -1
- package/browser/editor/page/commit.mjs +19 -4
- package/browser/editor/page/conflicts.mjs +6 -26
- package/browser/editor/page/convert.mjs +8 -7
- package/browser/editor/page/core.mjs +55 -17
- package/browser/editor/page/drag/index.mjs +40 -24
- package/browser/editor/page/drag/preview.mjs +9 -4
- package/browser/editor/page/editor.mjs +70 -15
- package/browser/editor/page/emoji-picker/emoji-tab.mjs +22 -13
- package/browser/editor/page/emoji-picker/grid.mjs +7 -5
- package/browser/editor/page/emoji-picker/icon-tab.mjs +4 -4
- package/browser/editor/page/emoji-picker/icons.mjs +0 -1
- package/browser/editor/page/emoji-picker/index.mjs +11 -9
- package/browser/editor/page/emoji-picker/styles.mjs +161 -2
- package/browser/editor/page/emoji-picker/support.mjs +6 -4
- package/browser/editor/page/emoji-picker/upload.mjs +12 -11
- package/browser/editor/page/fills.mjs +5 -3
- package/browser/editor/page/fold/index.mjs +348 -0
- package/browser/editor/page/fold/rule.mjs +125 -0
- package/browser/editor/page/folded.mjs +23 -0
- package/browser/editor/page/format-bar/bar.mjs +2 -1
- package/browser/editor/page/format-bar/index.mjs +1 -1
- package/browser/editor/page/geometry.mjs +8 -6
- package/browser/editor/page/handle/index.mjs +17 -6
- package/browser/editor/page/hints/index.mjs +1 -1
- package/browser/editor/page/insert/index.mjs +30 -17
- package/browser/editor/page/long-runs.mjs +75 -0
- package/browser/editor/page/look.mjs +15 -3
- package/browser/editor/page/margin.mjs +45 -24
- package/browser/editor/page/menu.mjs +69 -25
- package/browser/editor/page/merge.mjs +24 -0
- package/browser/editor/page/notices.mjs +1 -1
- package/browser/editor/page/offered.mjs +1 -1
- package/browser/editor/page/outline/folds.mjs +11 -10
- package/browser/editor/page/outline/index.mjs +131 -45
- package/browser/editor/page/outline/list.mjs +23 -11
- package/browser/editor/page/outline/read.mjs +1 -1
- package/browser/editor/page/outline/strip.mjs +7 -1
- package/browser/editor/page/panel.mjs +5 -4
- package/browser/editor/page/resize/index.mjs +1 -1
- package/browser/editor/page/selection/delete.mjs +8 -6
- package/browser/editor/page/selection/index.mjs +10 -3
- package/browser/editor/page/session.mjs +54 -3
- package/browser/editor/page/table/cell-bar.mjs +4 -1
- package/browser/editor/page/table/cells.mjs +1 -1
- package/browser/editor/page/table/icons.mjs +1 -1
- package/browser/editor/page/table/index.mjs +13 -8
- package/browser/editor/page/table/widths.mjs +9 -4
- package/browser/editor/page/text-select/index.mjs +18 -3
- package/browser/editor/page/tooltip.mjs +1 -1
- package/browser/editor/page/type-grid.mjs +4 -3
- package/browser/editor/page/typing/code-edges.mjs +63 -17
- package/browser/editor/page/typing/index.mjs +12 -6
- package/browser/editor/page/typing/leave.mjs +1 -1
- package/browser/shared/annotations.mjs +8 -10
- package/browser/shared/code-folds.mjs +5 -1
- package/browser/shared/colors.mjs +15 -13
- package/browser/shared/component-catalog.mjs +24 -10
- package/browser/shared/components/callout.mjs +4 -2
- package/browser/shared/components/index.mjs +61 -7
- package/browser/shared/components/installed.mjs +75 -5
- package/browser/shared/components/page.mjs +10 -1
- package/browser/shared/components/table.mjs +1 -1
- package/browser/shared/components/text.mjs +2 -2
- package/browser/shared/document-look.mjs +178 -0
- package/browser/shared/emoji/data/LICENSE-lucide.txt +43 -0
- package/browser/shared/emoji/data/README.md +4 -2
- package/browser/shared/emoji/data/categories.json +1 -1
- package/browser/shared/emoji/data/keywords-en.json +1 -1
- package/browser/shared/emoji/data/keywords-zh.json +1 -1
- package/browser/shared/emoji/data/names.json +1 -1
- package/browser/shared/emoji/data/positions.json +1 -1
- package/browser/shared/emoji/data/twemoji-index.json +1 -1
- package/browser/shared/emoji/data/twemoji-svg.bin +0 -0
- package/browser/shared/emoji/data.mjs +10 -12
- package/browser/shared/emoji/emoji.mjs +8 -7
- package/browser/shared/emoji/icon-value.mjs +4 -3
- package/browser/shared/html-tree.mjs +11 -5
- package/browser/shared/i18n/en.mjs +67 -8
- package/browser/shared/long-runs.mjs +39 -0
- package/browser/shared/new-names.mjs +5 -2
- package/browser/shared/outline.mjs +194 -0
- package/browser/shared/preferences.mjs +10 -4
- package/browser/shared/session.mjs +5 -4
- package/browser/shared/workspace-look.mjs +91 -0
- package/browser/shared/zoom.mjs +5 -5
- package/dist/chunk-3HIRPVLU.mjs +2 -0
- package/dist/chunk-4FS7Z7WR.mjs +2 -0
- package/dist/{chunk-LHTFLRB6.mjs → chunk-5MHMDLZD.mjs} +2 -2
- package/dist/{chunk-YXAUQPVK.mjs → chunk-5ZKJ3H4P.mjs} +3 -3
- package/dist/{chunk-ZBIM6ARY.mjs → chunk-64K7IPLO.mjs} +55 -16
- package/dist/{chunk-QYKCJ7FG.mjs → chunk-6QXGZM36.mjs} +1 -1
- package/dist/chunk-6RGJOMJP.mjs +16 -0
- package/dist/{chunk-EUX7IC5J.mjs → chunk-7DTINW6U.mjs} +2 -2
- package/dist/{chunk-6CYG3Z4V.mjs → chunk-AJ73XJF3.mjs} +1 -1
- package/dist/chunk-AUZGSZJH.mjs +13 -0
- package/dist/{chunk-7QQBR7PB.mjs → chunk-BFE45AKL.mjs} +1 -1
- package/dist/chunk-BGGDRNYQ.mjs +10 -0
- package/dist/{chunk-L4DVONOG.mjs → chunk-BXO2ZY5P.mjs} +1 -1
- package/dist/chunk-CEH4MRCC.mjs +2 -0
- package/dist/chunk-CY2FNNAB.mjs +16 -0
- package/dist/{chunk-LC7YNQFG.mjs → chunk-DFUDV4LX.mjs} +1 -1
- package/dist/chunk-DKXG4XDG.mjs +2 -0
- package/dist/chunk-E7LWZRXY.mjs +4 -0
- package/dist/chunk-EZQ34M3F.mjs +124 -0
- package/dist/chunk-FDLXYYG4.mjs +4 -0
- package/dist/chunk-FUKQNOQS.mjs +9 -0
- package/dist/{chunk-DB4WKGYF.mjs → chunk-GB5SOYQJ.mjs} +2 -2
- package/dist/{chunk-ACOPDCNB.mjs → chunk-IVDLMMBD.mjs} +1 -1
- package/dist/chunk-JPOSLGOC.mjs +6 -0
- package/dist/chunk-JVHHHGEA.mjs +3 -0
- package/dist/chunk-L3QKH4OK.mjs +26 -0
- package/dist/{chunk-B3VTYBYR.mjs → chunk-LCTHTZIL.mjs} +2 -2
- package/dist/chunk-LEF6DRCK.mjs +59 -0
- package/dist/chunk-LS4F7JUM.mjs +2 -0
- package/dist/chunk-NO5NJCHO.mjs +2 -0
- package/dist/{chunk-UNUEWH4S.mjs → chunk-NQPVAU63.mjs} +1 -1
- package/dist/chunk-O35FIXRB.mjs +2 -0
- package/dist/chunk-PL6GHLGD.mjs +15 -0
- package/dist/{chunk-G7TOXONE.mjs → chunk-QJIR2BLP.mjs} +1 -1
- package/dist/chunk-QUPLVPB2.mjs +7 -0
- package/dist/chunk-R7BNENLY.mjs +4 -0
- package/dist/chunk-S6O4UHLC.mjs +2 -0
- package/dist/{chunk-HMHK3EQ6.mjs → chunk-SGJWQKEI.mjs} +1 -1
- package/dist/chunk-SYGN7GLU.mjs +2 -0
- package/dist/chunk-TYK6VNAB.mjs +10 -0
- package/dist/{chunk-EVJ3BX3M.mjs → chunk-U6KNPJPR.mjs} +1 -1
- package/dist/{chunk-YZ5Z6H4W.mjs → chunk-UALRNVSE.mjs} +1 -1
- package/dist/chunk-UMHUSOPJ.mjs +2 -0
- package/dist/chunk-V3HXVQEM.mjs +2 -0
- package/dist/{chunk-UXUK4PMC.mjs → chunk-V5JTW2LH.mjs} +1 -1
- package/dist/chunk-V5VXUJKG.mjs +2 -0
- package/dist/chunk-VBXMADMP.mjs +2 -0
- package/dist/chunk-VNV55RUA.mjs +2 -0
- package/dist/chunk-VPD2PS35.mjs +2 -0
- package/dist/chunk-W3BGBJQW.mjs +2 -0
- package/dist/chunk-W4VKN3GF.mjs +7 -0
- package/dist/chunk-W5BXXPDJ.mjs +3 -0
- package/dist/{chunk-Z52HTUW7.mjs → chunk-XNROSH2T.mjs} +2 -2
- package/dist/chunk-XUNHRCOG.mjs +6 -0
- package/dist/{chunk-6U2SLSDN.mjs → chunk-Y6RLRPG3.mjs} +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/math.js +18 -2
- package/kit/report.css +248 -137
- package/kit/runtime.js +126 -51
- package/kit/tables.js +51 -1
- package/kit/ui.js +97 -0
- package/package.json +1 -1
- package/release.json +8 -8
- package/sdk/doc-kit.d.ts +9 -3
- package/skill/SKILL.md +22 -10
- package/skill/references/building-components.md +46 -6
- package/skill/references/example-report.html +1 -1
- package/skill/references/writing-html.md +4 -4
- package/skill/sdk/doc-kit.d.ts +9 -3
- package/web/assets/index-Bh74DfsV.css +1 -0
- package/web/assets/index-UW0RyFFt.js +13 -0
- package/web/index.html +2 -2
- package/browser/editor/components/callout/page.mjs +0 -13
- package/dist/chunk-2PBALTR3.mjs +0 -57
- package/dist/chunk-3LA3EMDE.mjs +0 -15
- package/dist/chunk-5FRMCDPY.mjs +0 -16
- package/dist/chunk-5LEK2CBS.mjs +0 -2
- package/dist/chunk-5OFN5AHC.mjs +0 -2
- package/dist/chunk-5SIYS5HN.mjs +0 -2
- package/dist/chunk-5TEK3RAD.mjs +0 -2
- package/dist/chunk-7GHT2UCA.mjs +0 -15
- package/dist/chunk-ATCJYAR4.mjs +0 -2
- package/dist/chunk-C7HWOBIF.mjs +0 -2
- package/dist/chunk-E7VYDG2X.mjs +0 -2
- package/dist/chunk-FQGQH4KM.mjs +0 -2
- package/dist/chunk-GPF4YWWB.mjs +0 -9
- package/dist/chunk-GYQV7Y4J.mjs +0 -120
- package/dist/chunk-HQQMJCQJ.mjs +0 -10
- package/dist/chunk-IERUOGC3.mjs +0 -6
- package/dist/chunk-ITNLYXXV.mjs +0 -3
- package/dist/chunk-NFIBI4IP.mjs +0 -26
- package/dist/chunk-O52JMOCZ.mjs +0 -2
- package/dist/chunk-QMQUXTU4.mjs +0 -12
- package/dist/chunk-QVWVFC5K.mjs +0 -6
- package/dist/chunk-QZY6Q64C.mjs +0 -2
- package/dist/chunk-TDQP3DKQ.mjs +0 -10
- package/dist/chunk-V4CPWKMZ.mjs +0 -2
- package/dist/chunk-VACRB2NE.mjs +0 -3
- package/dist/chunk-W63L7367.mjs +0 -7
- package/dist/chunk-W7CXPCRV.mjs +0 -6
- package/dist/chunk-XINNG4F2.mjs +0 -4
- package/dist/chunk-YL2DPWVS.mjs +0 -4
- package/web/assets/index-B6Ai3wsa.js +0 -13
- package/web/assets/index-SYPC3EZy.css +0 -1
|
Binary file
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
// (page icons in its tree); the server reads the pack itself for an export (src/render/emoji.mjs).
|
|
7
7
|
//
|
|
8
8
|
// What differs from notion-lite: the data is fetched (the page editor's modules are served as
|
|
9
|
-
// they are, unbundled); no sprite sheet
|
|
10
|
-
// the
|
|
11
|
-
//
|
|
9
|
+
// they are, unbundled); no sprite sheet; the custom emojis are the workspace's library (custom.mjs);
|
|
10
|
+
// settings and the frequently used ones are Gallery's own (localStorage). The Twemoji pack is shown on
|
|
11
|
+
// every system (since 2026-11, the user's word: Notion and notion-lite show the system's emoji font on
|
|
12
|
+
// Apple's, Gallery does not), the system's font only where the browser cannot unpack the pack or the
|
|
13
|
+
// pack lacks the emoji. The data holds no flag (Notion's Flags group taken out: data/README.md).
|
|
12
14
|
|
|
13
15
|
import { KEYS } from "../renamed-storage.mjs";
|
|
14
16
|
|
|
@@ -20,10 +22,6 @@ const FRECENCY_KEY = KEYS.emojiFrecency;
|
|
|
20
22
|
export const SKIN_TONE_HANDS = ["✋", "✋🏻", "✋🏼", "✋🏽", "✋🏾", "✋🏿"];
|
|
21
23
|
export const SKIN_TONE_VARIATIONS = SKIN_TONE_HANDS.length - 1;
|
|
22
24
|
|
|
23
|
-
// Notion shows Twemoji on Windows, Linux and Android, and the system's own emoji font on Apple's.
|
|
24
|
-
const agent = globalThis.navigator?.userAgent ?? "";
|
|
25
|
-
export const emojiStyle = /Android|Windows|Linux/i.test(agent) ? "twitter" : "raw";
|
|
26
|
-
|
|
27
25
|
const state = { positions: null, categories: null, keywords: {}, unique: {}, loaded: false, failed: false, svgIndex: null, svgPack: null };
|
|
28
26
|
let loading = null;
|
|
29
27
|
|
|
@@ -52,8 +50,8 @@ export function loadEmojiData() {
|
|
|
52
50
|
json("categories.json"),
|
|
53
51
|
json("keywords-en.json"),
|
|
54
52
|
json("keywords-zh.json"),
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
json("twemoji-index.json"),
|
|
54
|
+
loadSvgPack(),
|
|
57
55
|
]).then(([positions, categories, en, zh, svgIndex, svgPack]) => {
|
|
58
56
|
state.svgIndex = svgPack ? svgIndex : null;
|
|
59
57
|
state.svgPack = svgPack;
|
|
@@ -189,8 +187,8 @@ export function recordEmojiUse(emoji) {
|
|
|
189
187
|
frecency = map;
|
|
190
188
|
writeStorage(FRECENCY_KEY, JSON.stringify(map));
|
|
191
189
|
}
|
|
192
|
-
/** The recently used: by count, then by the last use, two rows of them. */
|
|
193
|
-
export function recentEmojis(perRow) {
|
|
194
|
-
return Object.keys(frecency).sort((a, b) => (frecency[b].f !== frecency[a].f ? frecency[b].f - frecency[a].f : frecency[b].u - frecency[a].u)).slice(0, 2 * perRow);
|
|
190
|
+
/** The recently used: by count, then by the last use, two rows of them; `keep`: those still known (a flag used before, taken out since, no longer shows). */
|
|
191
|
+
export function recentEmojis(perRow, keep = () => true) {
|
|
192
|
+
return Object.keys(frecency).filter(keep).sort((a, b) => (frecency[b].f !== frecency[a].f ? frecency[b].f - frecency[a].f : frecency[b].u - frecency[a].u)).slice(0, 2 * perRow);
|
|
195
193
|
}
|
|
196
194
|
export const frecencyBoost = (emoji) => (frecency[emoji] ? 100 * frecency[emoji].f : 0);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { escapeMarkup } from "../escape.mjs";
|
|
2
|
-
import { emojiDataFailed, emojiDataLoaded, emojiInfo,
|
|
2
|
+
import { emojiDataFailed, emojiDataLoaded, emojiInfo, emojiSvgUrl, hasEmojiSvg, svgUrlOf } from "./data.mjs";
|
|
3
3
|
|
|
4
|
-
// Showing an emoji (notion-lite's ui/emoji/emoji.js): on
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// over a thousand of them), its codepoints kept in data-svg until
|
|
4
|
+
// Showing an emoji (notion-lite's ui/emoji/emoji.js): on every system an <img> of its Twemoji vector
|
|
5
|
+
// image (data.mjs); the system's emoji font only where the data failed or the pack lacks it (a
|
|
6
|
+
// placeholder of its size while the data comes). `lazy`: the image's address is made once it
|
|
7
|
+
// scrolls near (the picker draws over a thousand of them), its codepoints kept in data-svg until
|
|
8
|
+
// then (lazySrc).
|
|
8
9
|
|
|
9
10
|
const BLANK_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
|
|
10
11
|
const px = (size) => (typeof size === "number" ? `${size}px` : size);
|
|
@@ -29,8 +30,8 @@ function rawEmojiHTML(emoji, { size } = {}) {
|
|
|
29
30
|
*/
|
|
30
31
|
export function emojiHTML(emoji, { size, lazy = false } = {}) {
|
|
31
32
|
const side = size ? px(size) : "100%";
|
|
32
|
-
if (
|
|
33
|
-
const info =
|
|
33
|
+
if (!emojiDataLoaded() && !emojiDataFailed()) return `<span class="ge-emoji-pending" style="display:inline-block;width:${side};height:${side}"></span>`;
|
|
34
|
+
const info = emojiInfo(emoji);
|
|
34
35
|
if (!info || !hasEmojiSvg(info)) return rawEmojiHTML(emoji, { size });
|
|
35
36
|
if (lazy) return `<img class="ge-emoji" alt="${esc(emoji)}" src="${BLANK_GIF}" data-svg="${info.codepoints}" draggable="false" style="width:${side};height:${side}">`;
|
|
36
37
|
return `<img class="ge-emoji" alt="${esc(emoji)}" src="${emojiSvgUrl(info)}" draggable="false" style="width:${side};height:${side}">`;
|
|
@@ -73,11 +73,12 @@ export const iconUrl = (value, slug = null, { tab = false } = {}) => `/__gallery
|
|
|
73
73
|
const [TAB_LIGHT, TAB_DARK] = ICON_COLOR_VALUES.gray;
|
|
74
74
|
/**
|
|
75
75
|
* The browser tab's image of a page with no icon (the workbench's and the preview page's): the page
|
|
76
|
-
* the workbench's tree shows for one (its "page" icon, components/icons.ts
|
|
77
|
-
*
|
|
76
|
+
* the workbench's tree shows for one (its "page" icon, components/icons.ts: Lucide's file-text,
|
|
77
|
+
* FinHarness's 文档), its box cropped to `1 1 22 22` so it fills the tab as an emoji does, in the tab
|
|
78
|
+
* glyphs' gray, light or dark as the system is. An image, never none: a tab given no image, or one that does not
|
|
78
79
|
* load, keeps the one it showed (Chrome), so a page with no icon opened after one with one showed that one's.
|
|
79
80
|
*/
|
|
80
|
-
export const DEFAULT_TAB_ICON = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="
|
|
81
|
+
export const DEFAULT_TAB_ICON = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><style>svg{color:${TAB_LIGHT}}@media (prefers-color-scheme:dark){svg{color:${TAB_DARK}}}</style><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`)}`;
|
|
81
82
|
|
|
82
83
|
/** The value of an icon's element as the file (or a page) has it, or null. */
|
|
83
84
|
export function iconValueOf(html) {
|
|
@@ -41,13 +41,19 @@ export const elementsOf = (node) => (node.childNodes ?? []).filter((child) => ch
|
|
|
41
41
|
* Every element under `root`, in document order, handed to `visit`. What a template holds is part of
|
|
42
42
|
* the page's markup (`inTemplates`, the default) or left out, as a browser's querySelectorAll does
|
|
43
43
|
* (what is inert there). A template in an <svg> or <math> holds no content: its children are the page's.
|
|
44
|
-
* `stop(element)`: what it says yes to is handed to `visit`, and what that holds is not
|
|
44
|
+
* `stop(element)`: what it says yes to is handed to `visit`, and what that holds is not, but for
|
|
45
|
+
* the elements `through(element)` gives of it, whose insides the walk goes on in (themselves not handed
|
|
46
|
+
* to `visit`): an instance of a component package and its slots (components/index.mjs BODY).
|
|
45
47
|
*/
|
|
46
|
-
export function walk(root, visit, { inTemplates = true, stop = null } = {}) {
|
|
48
|
+
export function walk(root, visit, { inTemplates = true, stop = null, through = null } = {}) {
|
|
49
|
+
const options = { inTemplates, stop, through };
|
|
47
50
|
for (const child of elementsOf(root)) {
|
|
48
51
|
visit(child);
|
|
49
|
-
if (stop?.(child))
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
if (stop?.(child)) {
|
|
53
|
+
for (const inner of through?.(child) ?? []) walk(inner, visit, options);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!(child.tagName === "template" && child.content)) walk(child, visit, options);
|
|
57
|
+
else if (inTemplates) walk(child.content, visit, options);
|
|
52
58
|
}
|
|
53
59
|
}
|
|
@@ -58,7 +58,6 @@ export default {
|
|
|
58
58
|
"恢复": "Restore",
|
|
59
59
|
"永久删除": "Delete forever",
|
|
60
60
|
"回收区是空的": "Trash is empty",
|
|
61
|
-
"刷新文档列表": "Refresh document list",
|
|
62
61
|
"偏好": "Preferences",
|
|
63
62
|
"显示编辑框": "Show editing frame",
|
|
64
63
|
"点进一段文字时,在它周围显示蓝色虚线框": "A dashed blue frame around the text you click into",
|
|
@@ -72,6 +71,7 @@ export default {
|
|
|
72
71
|
"侧边栏": "Sidebar",
|
|
73
72
|
"关闭侧边栏": "Close sidebar",
|
|
74
73
|
"搜索文档": "Search documents",
|
|
74
|
+
"新建": "New",
|
|
75
75
|
"设置": "Settings",
|
|
76
76
|
"正在连接本地服务": "Connecting to the local service",
|
|
77
77
|
"外观:{0}": "Appearance: {0}",
|
|
@@ -108,7 +108,9 @@ export default {
|
|
|
108
108
|
"目录": "Outline",
|
|
109
109
|
"在页面右侧显示标题目录,点击标题跳到那里": "Shows the page's headings at its right; click one to go there",
|
|
110
110
|
"拖动调整目录宽度": "Drag to resize the outline",
|
|
111
|
-
"
|
|
111
|
+
"收起目录": "Collapse outline",
|
|
112
|
+
"展开目录": "Expand outline",
|
|
113
|
+
"页面不够宽,变宽后展开": "Not wide enough: expands once there is room",
|
|
112
114
|
"所有内容": "Everything",
|
|
113
115
|
"文件和图片除外": "No files or images",
|
|
114
116
|
"导出格式": "Export format",
|
|
@@ -177,6 +179,7 @@ export default {
|
|
|
177
179
|
"操作没有成功(HTTP {0})": "That didn't work (HTTP {0})",
|
|
178
180
|
"整体块": "Whole block",
|
|
179
181
|
"{0} 块": "{0} blocks",
|
|
182
|
+
"{0}和折叠的 {1} 块": "{0} and the {1} blocks folded under it",
|
|
180
183
|
"至少要保留一个选项卡。": "Keep at least one tab.",
|
|
181
184
|
" 等": " and more",
|
|
182
185
|
"页面其他地方引用了这块({0},例如链接或详情卡片、无障碍关联),删掉会让它们失效,请让 AI 删除。": "Other parts of the page refer to this block ({0}: links, detail cards or accessibility references); deleting it would break them, so ask the AI to delete it.",
|
|
@@ -312,6 +315,25 @@ export default {
|
|
|
312
315
|
"打不开这个网页": "This page can't be opened",
|
|
313
316
|
"浏览器存储写不进去(空间满了,或被禁用):没保存的修改只在这个页面上,先不要关掉它。": "The browser's storage can't be written (full, or turned off): unsaved changes exist only on this page, so keep it open.",
|
|
314
317
|
"AI 的写入把你之前保存的 {0} 处修改改回去了:在左下方选择保留哪一边。": "The AI's write undid {0} of your saved edits: choose which side to keep at the bottom left.",
|
|
318
|
+
// The Agent's changes (editor/page/changes/, the 设置 switch 显示变更)
|
|
319
|
+
"显示变更": "Show changes",
|
|
320
|
+
"Agent 改过文档后,新增的标绿、删掉的标红,可以在页面底部确认或撤回": "After an Agent edits a document, what it added shows in green and what it deleted in red, to keep or revert at the bottom of the page",
|
|
321
|
+
"Agent 的修改": "The Agent's changes",
|
|
322
|
+
"Agent 已修改": "Agent made changes",
|
|
323
|
+
"Agent 新增 {0} 块、改动 {1} 块、删除 {2} 块": "The Agent added {0}, changed {1} and deleted {2} blocks",
|
|
324
|
+
"撤回": "Revert",
|
|
325
|
+
"确认": "Keep",
|
|
326
|
+
"撤回 Agent 的这些修改,你自己的修改会保留": "Revert the Agent's changes; yours stay",
|
|
327
|
+
"保留这些修改,不再标出": "Keep these changes and stop marking them",
|
|
328
|
+
"已撤回 Agent 的修改": "Reverted the Agent's changes",
|
|
329
|
+
"还有修改没保存或有冲突待处理,处理完再撤回": "Some edits are not saved yet or wait for you: deal with them, then revert",
|
|
330
|
+
"没能撤回:{0}": "Could not revert: {0}",
|
|
331
|
+
"没能撤销:{0}": "Could not undo: {0}",
|
|
332
|
+
"没能确认:{0}": "Could not keep: {0}",
|
|
333
|
+
"Agent 刚刚又改了文档,再看一眼": "The Agent just changed the document again: have another look",
|
|
334
|
+
"撤回之后文档又改过了,没法撤销": "The document changed after the revert, so it cannot be undone",
|
|
335
|
+
"撤销已经过期": "It is too late to undo",
|
|
336
|
+
"预览服务器出错": "The preview server failed",
|
|
315
337
|
"{0} 处修改和文件的最新版本冲突,没有保存:在左下方选择怎么处理。": "{0} edits conflict with the file's latest version and weren't saved: choose what to do at the bottom left.",
|
|
316
338
|
"组件包刚刚变了,{0} 处修改没有保存:在左下方选择怎么处理。": "A component package just changed and {0} edits weren't saved: choose what to do at the bottom left.",
|
|
317
339
|
"{0} 处内容文档存不下,没有保存:在左下方选择怎么处理。": "{0} items can't be stored in the document and weren't saved: choose what to do at the bottom left.",
|
|
@@ -332,10 +354,11 @@ export default {
|
|
|
332
354
|
"链接": "Link",
|
|
333
355
|
"这一块里没有可以直接修改的文字。": "This block has no text to edit directly.",
|
|
334
356
|
"这个组件没有运行:内容只能看,可以整块移动、复制和删除。": "This component is not running: what it holds can only be read, and the block moved, copied and deleted whole.",
|
|
357
|
+
"内容区": "content area",
|
|
335
358
|
"组件里的文字": "A component's text",
|
|
336
359
|
"组件@block": "Component",
|
|
337
360
|
"组件@group": "Components",
|
|
338
|
-
"
|
|
361
|
+
"组件列表": "Components",
|
|
339
362
|
"组件包": "Packages",
|
|
340
363
|
"内置块": "Built-in blocks",
|
|
341
364
|
"正常": "Normal",
|
|
@@ -381,7 +404,10 @@ export default {
|
|
|
381
404
|
"{0} 要写成 { zh, en } 两种语言的文字": "{0} is words in both languages: { zh, en }",
|
|
382
405
|
"{0} 要写成一张表": "{0} is a table, by name",
|
|
383
406
|
"{0} 写错了:{1}": "{0} is wrong: {1}",
|
|
384
|
-
"{0} 的 edit 只能是 text、data 或 none": "{0}: edit is text, data or none",
|
|
407
|
+
"{0} 的 edit 只能是 text、data、flow 或 none": "{0}: edit is text, data, flow or none",
|
|
408
|
+
"{0} 是插槽,名字只用小写字母、数字和连字符": "{0} is a slot: its name is lowercase letters, digits and hyphens",
|
|
409
|
+
"{0} 是插槽,由 data-doc-slot=\"{1}\" 标出,不写 at": "{0} is a slot, marked data-doc-slot=\"{1}\": it has no at",
|
|
410
|
+
"{0} 是插槽,一个实例里只有一个,不写 min、max": "{0} is a slot, one to an instance: it has no min or max",
|
|
385
411
|
"{0} 要写成 true 或 false": "{0} is true or false",
|
|
386
412
|
"{0} 要写成不小于 0 的整数": "{0} is a whole number from 0",
|
|
387
413
|
"{0} 的 min 比 max 大,没有实例能满足": "{0}: min is over max, which no instance can meet",
|
|
@@ -425,7 +451,6 @@ export default {
|
|
|
425
451
|
"旅行与地点": "Travel & Places",
|
|
426
452
|
"物品": "Objects",
|
|
427
453
|
"符号": "Symbols",
|
|
428
|
-
"旗帜": "Flags",
|
|
429
454
|
"自定义": "Custom",
|
|
430
455
|
"添加自定义表情符号": "Add custom emoji",
|
|
431
456
|
"无结果": "No results",
|
|
@@ -775,7 +800,7 @@ export default {
|
|
|
775
800
|
"在左侧插入": "Insert left",
|
|
776
801
|
"在右侧插入": "Insert right",
|
|
777
802
|
"均分列宽": "Even column widths",
|
|
778
|
-
"
|
|
803
|
+
"自动调整列宽": "Auto-fit column widths",
|
|
779
804
|
"列": "Column",
|
|
780
805
|
"行": "Row",
|
|
781
806
|
"已一键优化": "Optimized",
|
|
@@ -816,7 +841,7 @@ export default {
|
|
|
816
841
|
"点击后拖动调整表格的高度": "Click, then drag to resize the table's height",
|
|
817
842
|
"拖动调整表格的高度,超出的行上下滚动": "Drag to resize the table's height; rows beyond it scroll",
|
|
818
843
|
"点击后可拖动调整表格的高度": "Click, then drag to resize the table's height",
|
|
819
|
-
"
|
|
844
|
+
"已自动调整列宽": "Column widths auto-fitted",
|
|
820
845
|
"列宽已按内容调整,无需再调。": "The column widths already fit their content.",
|
|
821
846
|
"已均分列宽": "Column widths evened",
|
|
822
847
|
"跨块删除只支持正文、标题和列表里的文字。": "Deleting across blocks works only on text in paragraphs, headings and lists.",
|
|
@@ -877,6 +902,7 @@ export default {
|
|
|
877
902
|
"折叠列表标题": "Toggle title",
|
|
878
903
|
"折叠块标题": "Toggle title",
|
|
879
904
|
"空白折叠区块。点击或拖动区块到这里。": "Empty toggle. Click here, or drag blocks into it.",
|
|
905
|
+
"空白内容区。点击或拖动区块到这里。": "Empty content area. Click here, or drag blocks into it.",
|
|
880
906
|
"先选中点开详情卡片的文字。": "Select the text that should open the detail card first.",
|
|
881
907
|
"选中的文字里已经有链接,先移除链接(Ctrl+K)再做详情卡片的入口。": "The selected text already has a link; remove it (Ctrl+K) before making it open a detail card.",
|
|
882
908
|
"这里不能新建详情卡片。": "A detail card can't be added here.",
|
|
@@ -1095,6 +1121,13 @@ export default {
|
|
|
1095
1121
|
"检查没有通过:{0};{1};{2}": "The checks didn't pass: {0}; {1}; {2}",
|
|
1096
1122
|
"检查没有通过:{0};{1};{2} 等 {3} 条": "The checks didn't pass: {0}; {1}; {2} and more, {3} in all",
|
|
1097
1123
|
"写作偏好不能移到回收区": "The writing preferences can't be moved to trash",
|
|
1124
|
+
"写作偏好不在文档目录里,不能移动,下面也不能放文档": "The writing preferences aren't in the outline: they can't be moved or have documents under them",
|
|
1125
|
+
"找不到要放到它下面的文档": "Can't find the document to put it under",
|
|
1126
|
+
"找不到要放在它后面的文档": "Can't find the document to put it after",
|
|
1127
|
+
"要放在它后面的文档不在这一层": "The document to put it after isn't at that level",
|
|
1128
|
+
"文档目录刚在别处改过": "The outline was just changed elsewhere",
|
|
1129
|
+
"文档不能放在它自己后面": "A document can't go after itself",
|
|
1130
|
+
"文档不能移到它自己或它下面的文档下面": "A document can't move under itself or a document under it",
|
|
1098
1131
|
"还没有写作偏好": "There are no writing preferences yet",
|
|
1099
1132
|
"回收区里没有这一项": "That's not in the trash",
|
|
1100
1133
|
"文件太大(最多 5GB)": "File too large (at most 5 GB)",
|
|
@@ -1136,7 +1169,6 @@ export default {
|
|
|
1136
1169
|
"底稿": "Source",
|
|
1137
1170
|
"块": "Block",
|
|
1138
1171
|
"(整块)": " (whole)",
|
|
1139
|
-
"没有“建议”的批注只是标记了一个位置,不用改。": "One with no suggestion only marks a place: nothing to change there.",
|
|
1140
1172
|
"(还没有 id,按原文找)": "(no id yet: find it by its words)",
|
|
1141
1173
|
"建议@note": "Suggestion",
|
|
1142
1174
|
"正文": "Body",
|
|
@@ -1161,4 +1193,31 @@ export default {
|
|
|
1161
1193
|
"「{0}」里的组件 {1} 有错误,没有生成文件:可以让 AI 检查这份文档并修好": "Component {1} in “{0}” has an error, so no file was made: an AI agent can check this document and fix it",
|
|
1162
1194
|
"「{0}」没有通过导出前的检查,没有生成文件:{1}": "“{0}” didn't pass the checks before export, so no file was made: {1}",
|
|
1163
1195
|
"「{0}」没有通过导出前的检查,没有生成文件:{1}(共 {2} 处:可以让 AI 检查这份文档,列出全部并修好)": "“{0}” didn't pass the checks before export, so no file was made: {1} ({2} in all: ask the AI to check this document, list them all and fix them)",
|
|
1196
|
+
"字体与布局": "Font and layout",
|
|
1197
|
+
"字体": "Font",
|
|
1198
|
+
"等线体": "Sans",
|
|
1199
|
+
"衬线体": "Serif",
|
|
1200
|
+
"混合体": "Mixed",
|
|
1201
|
+
"无衬线字体,屏幕上读起来清楚,适合任何文档": "A sans-serif font, clear on screen, for any document",
|
|
1202
|
+
"标题和正文都用衬线字体,适合长文章": "A serif font for headings and text alike, for long reads",
|
|
1203
|
+
"标题用衬线字体,正文用等线体": "Headings in a serif font, the text in sans",
|
|
1204
|
+
"字体大小": "Font size",
|
|
1205
|
+
"小号": "Small",
|
|
1206
|
+
"大号": "Large",
|
|
1207
|
+
"正文间距": "Spacing",
|
|
1208
|
+
"紧凑": "Compact",
|
|
1209
|
+
"宽松": "Loose",
|
|
1210
|
+
"正文宽度": "Text width",
|
|
1211
|
+
"偏窄": "Narrow",
|
|
1212
|
+
"偏宽": "Wide",
|
|
1213
|
+
"默认字号下一行约 {0} 个字": "About {0} characters a line at the default font size",
|
|
1214
|
+
"字体与布局没有保存:{0}": "Font and layout not saved: {0}",
|
|
1215
|
+
"写作偏好没有字体与布局": "Writing preferences have no font and layout",
|
|
1216
|
+
"字体与布局不对": "The font and layout given are not right",
|
|
1217
|
+
"这份文档的标题和说明读不出来": "This document's title and description could not be read",
|
|
1218
|
+
"应用到全部文档": "Apply to all documents",
|
|
1219
|
+
"这份文档的字体与布局用到全部文档,新建的文档也一样": "Use this document's font and layout in every document, new ones too",
|
|
1220
|
+
"已应用到全部文档": "Applied to all documents",
|
|
1221
|
+
"已应用到全部文档,替换了 {0} 篇的单独设置": "Applied to all documents; own settings replaced in {0}",
|
|
1222
|
+
"没有应用到全部文档:{0}": "Not applied to all documents: {0}",
|
|
1164
1223
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Where a line may break inside a word: one rule for the kit's runtime (runtime/spacing.js: the
|
|
2
|
+
// reading page holds each long run in a span that breaks anywhere), its flowcharts' line model
|
|
3
|
+
// (runtime/flowchart.js piecesOf) and the page editor (page/long-runs.mjs: a text block holding one
|
|
4
|
+
// breaks anywhere as a whole). Ordinary words stay whole at a line's end, in a Chinese page as in an
|
|
5
|
+
// English one; a run of more than LONG_RUN characters with no place to break inside it (a path, a
|
|
6
|
+
// URL, a long identifier) breaks where its line ends, never carried whole to the next line leaving
|
|
7
|
+
// the line before it short (the user's words of 2026-11-07, and of 2026-11-06 before them).
|
|
8
|
+
//
|
|
9
|
+
// A run is what stands between spaces, Chinese, Japanese or Korean characters, full-width forms and
|
|
10
|
+
// the general punctuation (dashes, curly quotes, an ellipsis, a zero-width space: where a line breaks
|
|
11
|
+
// already, or what a path or an identifier does not hold): letters, digits and the marks of paths and
|
|
12
|
+
// URLs (. : / \ _ = & % # @ …). Inside one Chrome breaks a line after a hyphen and after a question
|
|
13
|
+
// mark, and at no other mark of ASCII (Chrome 156: not at a slash, a dot, a colon, a backslash or an
|
|
14
|
+
// underscore), so a run is counted by its pieces there: state-of-the-art is three short pieces, never
|
|
15
|
+
// long; D:\code\FinHarness\FinHarness-Demo.html is long by its first piece (30 characters).
|
|
16
|
+
// DOM-free.
|
|
17
|
+
|
|
18
|
+
/** A run longer than this many characters (by its longest piece) breaks anywhere. */
|
|
19
|
+
export const LONG_RUN = 20;
|
|
20
|
+
|
|
21
|
+
const RUNS = /[^\s\u2000-\u206f\u2e80-\u9fff\uac00-\ud7af\uf900-\ufaff\ufe30-\ufe4f\uff00-\uffef]+/gu;
|
|
22
|
+
const PIECES = /(?<=[-?])/u;
|
|
23
|
+
|
|
24
|
+
const long = (run) => run.length > LONG_RUN && run.split(PIECES).some((piece) => [...piece].length > LONG_RUN);
|
|
25
|
+
|
|
26
|
+
/** The long runs of `text`, in order: { index, length } each (code units). */
|
|
27
|
+
export function longRuns(text) {
|
|
28
|
+
const found = [];
|
|
29
|
+
if (text.length <= LONG_RUN) return found;
|
|
30
|
+
for (const match of text.matchAll(RUNS)) if (long(match[0])) found.push({ index: match.index, length: match[0].length });
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Whether `text` holds a long run. */
|
|
35
|
+
export function holdsLongRun(text) {
|
|
36
|
+
if (text.length <= LONG_RUN) return false;
|
|
37
|
+
for (const match of text.matchAll(RUNS)) if (long(match[0])) return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parse } from "parse5";
|
|
2
|
-
import { isInstance } from "./components/index.mjs";
|
|
2
|
+
import { instanceSlots, isInstance } from "./components/index.mjs";
|
|
3
3
|
import { mayHoldOldNames, newAttributeName, newAttributeValue, newScriptNames, newStyleNames } from "./old-names.mjs";
|
|
4
4
|
|
|
5
5
|
// A file's text with the format's old names new (old-names.mjs says which, and where the tools read
|
|
@@ -50,7 +50,10 @@ export function newNames(html) {
|
|
|
50
50
|
}
|
|
51
51
|
const visit = (node) => {
|
|
52
52
|
for (const child of node.childNodes ?? []) {
|
|
53
|
-
if (isInstance(child))
|
|
53
|
+
if (isInstance(child)) {
|
|
54
|
+
instanceSlots(child).forEach(visit); // what its slots hold is the document's, its own region the component's
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
54
57
|
if (child.tagName) element(child);
|
|
55
58
|
visit(child.tagName === "template" && child.content ? child.content : child);
|
|
56
59
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeAtomic } from "./files.mjs";
|
|
5
|
+
import { layoutOf, listDocumentSlugs, pageSlug, slugPattern } from "./workspace.mjs";
|
|
6
|
+
|
|
7
|
+
// The workspace's outline: which document sits under which, in what order (the sidebar's tree,
|
|
8
|
+
// <workspace>/.gallery/outline.json, docs/workbench.md "Workspace commands"):
|
|
9
|
+
// { version: 1, children: { "": [top-level slugs], "<slug>": [slugs under it] } }
|
|
10
|
+
// Hierarchy and order only: a document's folder never moves with it, and no file of a document
|
|
11
|
+
// names where it is (links between documents are relative links between their files). Read, it
|
|
12
|
+
// lists every document of the workspace once but the 写作偏好 page (listed apart, in .gallery/pages/):
|
|
13
|
+
// slugs that are gone drop out, documents it does not know yet come last at the top.
|
|
14
|
+
// Written by the workspace's server (src/workbench/service.mjs: the workbench's moves, a new, copied,
|
|
15
|
+
// restored or imported document, gallery new --under and gallery move while it runs, in the queue its
|
|
16
|
+
// saves go through), and by a gallery command when no server runs (src/cli/tree.mjs), with the same
|
|
17
|
+
// functions, each change read, made and written under the outline's lock (withOutlineLock: a file
|
|
18
|
+
// that two processes cannot both make), so that two writers never lose each other's change.
|
|
19
|
+
|
|
20
|
+
/** The outline file's lock (.gallery/outline.lock): who holds it, until when it counts. */
|
|
21
|
+
const LOCK_STALE = 10_000; // a lock older than this was left by a process that ended: taken over
|
|
22
|
+
const LOCK_WAIT = 15_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run `task` holding the outline's lock of workspace `root`: a file made with "wx", which one
|
|
26
|
+
* process alone can make, removed after; one older than 10 s is a crashed writer's and is taken over.
|
|
27
|
+
* Waits for it 15 s at most, then takes it all the same (a writer that holds it that long holds
|
|
28
|
+
* nothing any more).
|
|
29
|
+
*/
|
|
30
|
+
export async function withOutlineLock(root, task) {
|
|
31
|
+
const { gallery } = layoutOf(root);
|
|
32
|
+
const lock = path.join(gallery, "outline.lock");
|
|
33
|
+
const mine = `${process.pid} ${randomBytes(4).toString("hex")}`;
|
|
34
|
+
await fs.mkdir(gallery, { recursive: true });
|
|
35
|
+
for (const until = Date.now() + LOCK_WAIT; ;) {
|
|
36
|
+
try {
|
|
37
|
+
await fs.writeFile(lock, mine, { flag: "wx" });
|
|
38
|
+
break;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code !== "EEXIST") throw error;
|
|
41
|
+
const age = await fs.stat(lock).then(({ mtimeMs }) => Date.now() - mtimeMs, () => 0);
|
|
42
|
+
if (age > LOCK_STALE || Date.now() > until) await fs.rm(lock, { force: true });
|
|
43
|
+
else await new Promise((resolve) => setTimeout(resolve, 20));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return await task();
|
|
48
|
+
} finally {
|
|
49
|
+
// Removed only while it is still this writer's (one taken over meanwhile is the next writer's).
|
|
50
|
+
if ((await fs.readFile(lock, "utf8").catch(() => null)) === mine) await fs.rm(lock, { force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const readJson = (file) => fs.readFile(file, "utf8").then(JSON.parse).catch(() => null);
|
|
55
|
+
|
|
56
|
+
/** The outline of workspace `root` as it is read: every document but the 写作偏好 page once (above). */
|
|
57
|
+
export async function readOutline(root) {
|
|
58
|
+
return (await outlineAndFile(root)).outline;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The outline as it is read and what its file holds: { outline, stored: the file's children, or null }.
|
|
63
|
+
* A change is written when what it makes is not what the file holds (writtenAs), a document the file
|
|
64
|
+
* does not hold yet too (one made since, listed last at the top only as it is read), so that the file
|
|
65
|
+
* keeps every place a command or the workbench gave.
|
|
66
|
+
*/
|
|
67
|
+
export async function outlineAndFile(root) {
|
|
68
|
+
const instructions = await pageSlug(root);
|
|
69
|
+
const slugs = (await listDocumentSlugs(root)).filter((slug) => slug !== instructions);
|
|
70
|
+
const stored = await readJson(layoutOf(root).outline);
|
|
71
|
+
const outline = { version: 1, children: ordered(stored, slugs) };
|
|
72
|
+
return { outline, stored: stored?.children ?? null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether `children` is what the outline's file holds (`stored`, outlineAndFile's): the same lists
|
|
77
|
+
* (an empty one as none), in whatever order the file has its keys. Also whether an outline a
|
|
78
|
+
* workbench changed (POST outline's `base`) is the one read now.
|
|
79
|
+
*/
|
|
80
|
+
export function writtenAs(children, stored) {
|
|
81
|
+
if (!stored || typeof stored !== "object") return false;
|
|
82
|
+
const lists = (value) => Object.keys(value).filter((key) => Array.isArray(value[key]) && value[key].length > 0);
|
|
83
|
+
const [ours, theirs] = [lists(children), lists(stored)];
|
|
84
|
+
return ours.length === theirs.length && ours.every((key) => JSON.stringify(children[key]) === JSON.stringify(stored[key]));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The outline's children of the documents `slugs`, in the order `stored` (the file's value) gives.
|
|
88
|
+
function ordered(stored, slugs) {
|
|
89
|
+
const children = { "": [] };
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
const place = (parent) => {
|
|
92
|
+
for (const slug of stored?.children?.[parent] ?? []) {
|
|
93
|
+
if (!slugs.includes(slug) || seen.has(slug)) continue;
|
|
94
|
+
seen.add(slug);
|
|
95
|
+
(children[parent] ??= []).push(slug);
|
|
96
|
+
place(slug);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
place("");
|
|
100
|
+
for (const slug of slugs) if (!seen.has(slug)) children[""].push(slug);
|
|
101
|
+
return children;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether `value` is an outline to store: each document at most once, never under itself. */
|
|
105
|
+
export function validOutline(value) {
|
|
106
|
+
const children = value?.children;
|
|
107
|
+
if (!children || typeof children !== "object" || Array.isArray(children)) return false;
|
|
108
|
+
const listed = new Set();
|
|
109
|
+
for (const [parent, list] of Object.entries(children)) {
|
|
110
|
+
if (parent !== "" && !slugPattern.test(parent)) return false;
|
|
111
|
+
if (!Array.isArray(list)) return false;
|
|
112
|
+
for (const slug of list) {
|
|
113
|
+
if (typeof slug !== "string" || !slugPattern.test(slug) || listed.has(slug) || slug === parent) return false;
|
|
114
|
+
listed.add(slug);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Write outline `value` ({ children }) of workspace `root` (.gallery/ made when there is none yet). */
|
|
121
|
+
export async function writeOutline(root, value) {
|
|
122
|
+
const file = layoutOf(root).outline;
|
|
123
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
124
|
+
return writeAtomic(file, `${JSON.stringify({ version: 1, children: value.children }, null, 2)}\n`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The document `slug` sits under in `children` ("" at the top), or null when the outline holds it not. */
|
|
128
|
+
export function parentIn(children, slug) {
|
|
129
|
+
for (const [parent, list] of Object.entries(children)) if (list.includes(slug)) return parent;
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The documents above `slug` in `children`, from the top down (itself not among them). */
|
|
134
|
+
export function ancestorsIn(children, slug) {
|
|
135
|
+
const above = [];
|
|
136
|
+
for (let parent = parentIn(children, slug); parent; parent = parentIn(children, parent)) above.unshift(parent);
|
|
137
|
+
return above;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Every document under `slug` in `children` ("": all), depth first, in the sidebar's order. */
|
|
141
|
+
export function descendantsIn(children, slug) {
|
|
142
|
+
const found = [];
|
|
143
|
+
const walk = (parent) => (children[parent] ?? []).forEach((child) => { found.push(child); walk(child); });
|
|
144
|
+
walk(slug);
|
|
145
|
+
return found;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Document `slug` moved in `children` (an outline as read: changed in place) with the documents under
|
|
150
|
+
* it, to where `where` says: `parent` ("" the top; undefined: where it is now, or `after`'s parent),
|
|
151
|
+
* right after `after` (one of that parent's), first (`first`), else last. Resolves to { ok: true,
|
|
152
|
+
* moved: whether its place changed, parent, index, from: { parent, index } } or { ok: false, reason }:
|
|
153
|
+
* "missing" (no such document in the outline), "parent" (no such parent), "after" (no such document
|
|
154
|
+
* to go after), "level" (`after` is not under `parent`), "itself" (after itself), "inside" (under
|
|
155
|
+
* itself or one of the documents under it).
|
|
156
|
+
*/
|
|
157
|
+
export function moveIn(children, slug, { parent = undefined, after = null, first = false } = {}) {
|
|
158
|
+
const from = parentIn(children, slug);
|
|
159
|
+
if (from === null) return { ok: false, reason: "missing" };
|
|
160
|
+
let to = parent;
|
|
161
|
+
if (after !== null) {
|
|
162
|
+
if (after === slug) return { ok: false, reason: "itself" };
|
|
163
|
+
const its = parentIn(children, after);
|
|
164
|
+
if (its === null) return { ok: false, reason: "after" };
|
|
165
|
+
if (to !== undefined && to !== its) return { ok: false, reason: "level" };
|
|
166
|
+
to = its;
|
|
167
|
+
}
|
|
168
|
+
to ??= from;
|
|
169
|
+
if (to !== "" && parentIn(children, to) === null) return { ok: false, reason: "parent" };
|
|
170
|
+
if (to === slug || ancestorsIn(children, to).includes(slug)) return { ok: false, reason: "inside" };
|
|
171
|
+
const index = children[from].indexOf(slug);
|
|
172
|
+
children[from].splice(index, 1);
|
|
173
|
+
const list = (children[to] ??= []);
|
|
174
|
+
const at = after !== null ? list.indexOf(after) + 1 : first ? 0 : list.length;
|
|
175
|
+
list.splice(at, 0, slug);
|
|
176
|
+
for (const key of Object.keys(children)) if (key !== "" && children[key].length === 0) delete children[key];
|
|
177
|
+
return { ok: true, moved: to !== from || at !== index, parent: to, index: at, from: { parent: from, index } };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Move document `slug` of workspace `root` where `where` says (moveIn), under the outline's lock: the
|
|
182
|
+
* outline read, changed and written when that is not what its file holds (writtenAs: a move, or a
|
|
183
|
+
* document the file does not hold yet placed where it is read). Resolves to moveIn's answer, with
|
|
184
|
+
* `written`: whether the file was written.
|
|
185
|
+
*/
|
|
186
|
+
export function moveDocument(root, slug, where) {
|
|
187
|
+
return withOutlineLock(root, async () => {
|
|
188
|
+
const { outline, stored } = await outlineAndFile(root);
|
|
189
|
+
const result = moveIn(outline.children, slug, where);
|
|
190
|
+
const written = result.ok && !writtenAs(outline.children, stored);
|
|
191
|
+
if (written) await writeOutline(root, outline);
|
|
192
|
+
return { ...result, ...(result.ok ? { written } : {}) };
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// The reader's preferences of Gallery's UI (the workbench's 设置): switches (and a width) kept in localStorage,
|
|
2
2
|
// which the workbench and every report page share (one origin), so the page editor follows a change
|
|
3
|
-
// at once (the storage event reaches every other page of the origin). Each
|
|
3
|
+
// at once (the storage event reaches every other page of the origin). Each has its default (off, but for
|
|
4
|
+
// showChanges) until changed.
|
|
4
5
|
// editFrame 显示编辑框: the dashed frame around the text being edited (editor/page/frame.mjs)
|
|
5
6
|
// quietNotices 减少通知: the notices of everyday operations (what was just done, how to go on) are not
|
|
6
7
|
// shown, only what the reader must act on or would not know otherwise (editor/page/quiet.mjs)
|
|
8
|
+
// showChanges 显示变更: what an Agent wrote marked on the page, with the bar to keep or revert it
|
|
9
|
+
// (editor/page/changes/); on until switched off, and off, what comes under review is kept as it comes
|
|
7
10
|
// outlineWidth the outline's width in full, as the reader dragged it (px; 0: its default), for every report
|
|
11
|
+
// outlineCollapsed the outline as its strip where the room would give it the list in full: a click on its
|
|
12
|
+
// guide (editor/page/outline/), and again on the guide in the strip's card to have the list back;
|
|
13
|
+
// for every report, as its width
|
|
8
14
|
//
|
|
9
15
|
// Whether the outline (目录, the top bar's 更多; editor/page/outline/) shows is no preference of the UI
|
|
10
16
|
// but the report's, as what is folded in it is: switched on for one report, the others keep
|
|
@@ -19,10 +25,10 @@
|
|
|
19
25
|
import { KEYS, OLD_KEYS } from "./renamed-storage.mjs";
|
|
20
26
|
|
|
21
27
|
const KEY = KEYS.preferences;
|
|
22
|
-
/** The preferences, each with its default (a switch's false, a width's 0). */
|
|
23
|
-
export const PREFERENCES = { editFrame: false, quietNotices: false, outlineWidth: 0 };
|
|
28
|
+
/** The preferences, each with its default (a switch's false but 显示变更's, a width's 0). */
|
|
29
|
+
export const PREFERENCES = { editFrame: false, quietNotices: false, showChanges: true, outlineWidth: 0, outlineCollapsed: false };
|
|
24
30
|
|
|
25
|
-
/** The key they are kept under (the page's head reads the outline's width before it shows: editor/page/margin.mjs). */
|
|
31
|
+
/** The key they are kept under (the page's head reads the outline's width and whether it is collapsed before it shows: editor/page/margin.mjs). */
|
|
26
32
|
export const PREFERENCES_KEY = KEY;
|
|
27
33
|
|
|
28
34
|
function stored() {
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// src/workbench/app/session/. Every message is { protocol: PROTOCOL, v: VERSION, type, slug, ... },
|
|
4
4
|
// posted to the other window for this origin only.
|
|
5
5
|
//
|
|
6
|
-
// report -> workbench ready once the editor started: { commands, title, icon, state, annotations, card }
|
|
7
|
-
// state when the state, the title, the page's icon, how many 批注 it
|
|
8
|
-
// detail card is open changed: { title, icon,
|
|
9
|
-
// (icon: the page's icon, an emoji, or null;
|
|
6
|
+
// report -> workbench ready once the editor started: { commands, title, icon, look, state, annotations, card }
|
|
7
|
+
// state when the state, the title, the page's icon, the look it shows, how many 批注 it
|
|
8
|
+
// has or whether a detail card is open changed: { title, icon, look, state,
|
|
9
|
+
// annotations, card } (icon: the page's icon, an emoji, or null; look: the
|
|
10
|
+
// document's look on its <html>, shared/document-look.mjs; card: a detail card is open)
|
|
10
11
|
// reply to a command, once it is done: { id, ok, ... }
|
|
11
12
|
// key a key combination the workbench registered, pressed in the page: { key }
|
|
12
13
|
// notice something to tell the reader, as the workbench's toast: { text, tone, hold,
|