@heycys/gallery 0.1.1 → 0.1.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/columns/page.mjs +4 -4
- package/browser/editor/components/embed/index.mjs +2 -2
- package/browser/editor/components/flowchart/add.mjs +7 -6
- package/browser/editor/components/flowchart/page.mjs +1 -1
- package/browser/editor/components/icons.mjs +8 -0
- package/browser/editor/components/image/crop.mjs +6 -9
- package/browser/editor/components/installed/index.mjs +11 -4
- package/browser/editor/components/installed/page.mjs +27 -16
- package/browser/editor/components/installed/served.mjs +6 -5
- package/browser/editor/components/instances.mjs +18 -0
- package/browser/editor/components/list/page.mjs +3 -6
- package/browser/editor/components/registry.mjs +13 -4
- package/browser/editor/components/table/index.mjs +59 -2
- package/browser/editor/components/table/page.mjs +1 -1
- package/browser/editor/page/annotate/card.mjs +3 -6
- package/browser/editor/page/autosave.mjs +310 -37
- package/browser/editor/page/board/client.mjs +57 -0
- package/browser/editor/page/board/hint.mjs +145 -0
- package/browser/editor/page/board/history.mjs +119 -0
- package/browser/editor/page/board/index.mjs +970 -0
- package/browser/editor/page/board/settings.mjs +209 -0
- package/browser/editor/page/caret.mjs +1 -1
- package/browser/editor/page/changes/bar.mjs +6 -14
- package/browser/editor/page/changes/index.mjs +2 -2
- package/browser/editor/page/clipboard/import/html.mjs +29 -6
- package/browser/editor/page/clipboard/import/inline.mjs +32 -0
- package/browser/editor/page/clipboard/import/markdown.mjs +58 -13
- package/browser/editor/page/clipboard/import/read.mjs +6 -1
- package/browser/editor/page/clipboard/import/to-blocks.mjs +44 -12
- package/browser/editor/page/clipboard/import/yuque.mjs +622 -0
- package/browser/editor/page/clipboard/index.mjs +124 -13
- package/browser/editor/page/clipboard/own/blocks.mjs +11 -11
- package/browser/editor/page/clipboard/own/chart-keys.mjs +41 -28
- package/browser/editor/page/clipboard/own/selection.mjs +1 -1
- package/browser/editor/page/columns/index.mjs +48 -46
- package/browser/editor/page/commit.mjs +80 -22
- package/browser/editor/page/conflicts.mjs +7 -7
- package/browser/editor/page/core.mjs +31 -15
- package/browser/editor/page/custom-page.mjs +106 -0
- package/browser/editor/page/editor.mjs +51 -14
- package/browser/editor/page/emoji-picker/styles.mjs +7 -12
- package/browser/editor/page/emoji-picker/upload.mjs +2 -2
- package/browser/editor/page/handle/index.mjs +5 -8
- package/browser/editor/page/history.mjs +24 -4
- package/browser/editor/page/links/index.mjs +5 -3
- package/browser/editor/page/links/pages.mjs +27 -9
- package/browser/editor/page/look.mjs +54 -3
- package/browser/editor/page/margin.mjs +18 -3
- package/browser/editor/page/media-menu/index.mjs +6 -7
- package/browser/editor/page/menu.mjs +18 -7
- package/browser/editor/page/notices.mjs +2 -2
- package/browser/editor/page/page-objects.mjs +13 -2
- package/browser/editor/page/panel.mjs +4 -9
- package/browser/editor/page/present-links.mjs +11 -2
- package/browser/editor/page/resize/index.mjs +42 -29
- package/browser/editor/page/served.mjs +14 -0
- package/browser/editor/page/session.mjs +48 -4
- package/browser/editor/page/slash-menu/index.mjs +3 -3
- package/browser/editor/page/table/cells.mjs +102 -46
- package/browser/editor/page/table/icons.mjs +2 -0
- package/browser/editor/page/table/index.mjs +29 -10
- package/browser/editor/page/table/pictures.mjs +299 -0
- package/browser/editor/page/table/views.mjs +150 -0
- package/browser/editor/page/text-select/index.mjs +26 -12
- package/browser/editor/page/todo-writes.mjs +75 -0
- package/browser/editor/page/typing/index.mjs +53 -3
- package/browser/editor/page/ui.mjs +13 -0
- package/browser/shared/block-packs.mjs +404 -0
- package/browser/shared/board-places.mjs +224 -0
- package/browser/shared/card-size.mjs +59 -0
- package/browser/shared/colors.mjs +3 -2
- package/browser/shared/component-catalog.mjs +323 -46
- package/browser/shared/component-source.mjs +37 -0
- package/browser/shared/components/index.mjs +12 -3
- package/browser/shared/components/installed.mjs +35 -3
- package/browser/shared/components/page.mjs +18 -8
- package/browser/shared/components/state.mjs +5 -1
- package/browser/shared/document-look.mjs +10 -5
- package/browser/shared/drafts.mjs +432 -0
- package/browser/shared/extension-sidebar.mjs +126 -0
- package/browser/shared/extensions.mjs +79 -0
- package/browser/shared/file-table.mjs +101 -0
- package/browser/shared/i18n/en.mjs +307 -27
- package/browser/shared/json-layout.mjs +47 -0
- package/browser/shared/net-cache.mjs +133 -0
- package/browser/shared/net-keys.mjs +32 -0
- package/browser/shared/net-policy.mjs +177 -0
- package/browser/shared/outline-shape.mjs +128 -0
- package/browser/shared/outline.mjs +331 -67
- package/browser/shared/pack-manifest.mjs +77 -0
- package/browser/shared/page-address.mjs +6 -3
- package/browser/shared/page-cards.mjs +308 -0
- package/browser/shared/page-links.mjs +69 -6
- package/browser/shared/pages.mjs +388 -0
- package/browser/shared/paths.mjs +6 -0
- package/browser/shared/preview-server.mjs +3 -7
- package/browser/shared/session.mjs +8 -1
- package/browser/shared/shipped-blocks.mjs +299 -0
- package/browser/shared/table-chart.mjs +33 -1
- package/browser/shared/todos.mjs +83 -0
- package/browser/shared/user-directory.mjs +24 -0
- package/browser/shared/versions.mjs +14 -0
- package/browser/shared/web-links.mjs +134 -0
- package/browser/shared/workspace-columns.mjs +120 -0
- package/browser/shared/workspace-name.mjs +154 -0
- package/browser/shared/workspace-query.mjs +125 -0
- package/browser/shared/workspace-registry.mjs +272 -0
- package/browser/shared/workspace.mjs +171 -20
- package/dist/chunk-2NRBY6AJ.mjs +2 -0
- package/dist/chunk-32ZUEFZ7.mjs +2 -0
- package/dist/chunk-35NQ4PLQ.mjs +2 -0
- package/dist/chunk-3KJHLHUO.mjs +2 -0
- package/dist/chunk-3NJACR4W.mjs +2 -0
- package/dist/chunk-4AV456RQ.mjs +2 -0
- package/dist/chunk-4OZWXZE5.mjs +5 -0
- package/dist/chunk-5XYHDBJ5.mjs +2 -0
- package/dist/chunk-5ZJA7CXT.mjs +2 -0
- package/dist/chunk-6A53MZYZ.mjs +2 -0
- package/dist/chunk-6EXH3ORZ.mjs +166 -0
- package/dist/chunk-6EZ7FNIK.mjs +3 -0
- package/dist/{chunk-V5JTW2LH.mjs → chunk-6QBHU4DF.mjs} +1 -1
- package/dist/{chunk-Y6RLRPG3.mjs → chunk-6VTU6KEO.mjs} +1 -1
- package/dist/chunk-B4P2CM2J.mjs +3 -0
- package/dist/{chunk-FDLXYYG4.mjs → chunk-BPL64HGA.mjs} +1 -1
- package/dist/chunk-BVEFGE4B.mjs +2 -0
- package/dist/{chunk-LS4F7JUM.mjs → chunk-BZ5RAF7L.mjs} +1 -1
- package/dist/chunk-CRJEG7MT.mjs +2 -0
- package/dist/{chunk-LCTHTZIL.mjs → chunk-D4CKRD3V.mjs} +91 -91
- package/dist/chunk-D4EURAX4.mjs +4 -0
- package/dist/chunk-DQCE7TJY.mjs +5 -0
- package/dist/chunk-DR73Y2UJ.mjs +3 -0
- package/dist/{chunk-TMYZB6AV.mjs → chunk-EKNHM3P4.mjs} +1 -1
- package/dist/{chunk-GB5SOYQJ.mjs → chunk-ERLAIBOA.mjs} +2 -2
- package/dist/chunk-EURLTOTZ.mjs +4 -0
- package/dist/chunk-FJYN7RTH.mjs +4 -0
- package/dist/chunk-FWNRPMCJ.mjs +12 -0
- package/dist/chunk-FWW4CSAZ.mjs +2 -0
- package/dist/{chunk-2B5E4CDJ.mjs → chunk-GPIWQ4X4.mjs} +1 -1
- package/dist/chunk-GY3FAUVM.mjs +2 -0
- package/dist/chunk-HLYV7RIH.mjs +2 -0
- package/dist/chunk-HMYTM3GT.mjs +15 -0
- package/dist/chunk-IA4PSIMD.mjs +2 -0
- package/dist/chunk-JEUK3ZO7.mjs +2 -0
- package/dist/chunk-KTKHRC6H.mjs +2 -0
- package/dist/chunk-L7UP52JO.mjs +2 -0
- package/dist/{chunk-SYGN7GLU.mjs → chunk-MHW7ZKOU.mjs} +1 -1
- package/dist/chunk-N76HWBA3.mjs +10 -0
- package/dist/chunk-N7AZZLGT.mjs +548 -0
- package/dist/chunk-NAMN3RAF.mjs +2 -0
- package/dist/{chunk-5ZKJ3H4P.mjs → chunk-NFMCP2HI.mjs} +3 -3
- package/dist/chunk-NG375ILQ.mjs +2 -0
- package/dist/chunk-NVRNLK25.mjs +6 -0
- package/dist/chunk-NWFBE2YA.mjs +2 -0
- package/dist/chunk-NWR2LT5B.mjs +2 -0
- package/dist/chunk-OJKMINIU.mjs +20 -0
- package/dist/{chunk-4FS7Z7WR.mjs → chunk-OUUP3WKT.mjs} +1 -1
- package/dist/{chunk-BGGDRNYQ.mjs → chunk-OW5NWPOJ.mjs} +1 -1
- package/dist/chunk-PAREWQGB.mjs +4 -0
- package/dist/chunk-POQSE4C2.mjs +2 -0
- package/dist/chunk-PUOQ65NC.mjs +2 -0
- package/dist/{chunk-XNROSH2T.mjs → chunk-PVER7LSC.mjs} +2 -2
- package/dist/chunk-PZDLJJTJ.mjs +3 -0
- package/dist/chunk-Q47PLKIJ.mjs +15 -0
- package/dist/chunk-Q4UEPUWF.mjs +2 -0
- package/dist/chunk-QKXMPUXC.mjs +2 -0
- package/dist/chunk-QOUEFUCH.mjs +16 -0
- package/dist/chunk-RFDZXFJF.mjs +13 -0
- package/dist/chunk-RKPWLGZ3.mjs +4 -0
- package/dist/chunk-RMECZA2K.mjs +2 -0
- package/dist/{chunk-7DTINW6U.mjs → chunk-SQ6Y2ZMA.mjs} +2 -2
- package/dist/chunk-SX4LJYT6.mjs +8 -0
- package/dist/chunk-SXHBYZST.mjs +16 -0
- package/dist/chunk-TANZRINB.mjs +2 -0
- package/dist/chunk-TEZAWDSQ.mjs +2 -0
- package/dist/chunk-UBBRTJ2R.mjs +24 -0
- package/dist/chunk-UFNYFFKO.mjs +20 -0
- package/dist/chunk-UGWYKHBL.mjs +170 -0
- package/dist/chunk-UKSHDZZ6.mjs +2 -0
- package/dist/chunk-UNOYVIYR.mjs +2 -0
- package/dist/{chunk-E7LWZRXY.mjs → chunk-V6RIYQZZ.mjs} +1 -1
- package/dist/chunk-VJPFVNNW.mjs +15 -0
- package/dist/chunk-VS3ZMVGX.mjs +5 -0
- package/dist/chunk-VTKNEYT3.mjs +6 -0
- package/dist/chunk-VXACTLO3.mjs +3 -0
- package/dist/chunk-YI43RG4P.mjs +2 -0
- package/dist/chunk-YQGT4AB2.mjs +6 -0
- package/dist/chunk-ZB4H7ZN4.mjs +2 -0
- package/dist/{chunk-IVDLMMBD.mjs → chunk-ZHEHPMBK.mjs} +1 -1
- package/dist/chunk-ZTHICSSF.mjs +2 -0
- package/dist/chunk-ZUNVAXLH.mjs +2 -0
- package/dist/chunk-ZWPMCK7P.mjs +2 -0
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/{chunk-U6KNPJPR.mjs → update-fetch.mjs} +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/board.css +95 -0
- package/kit/board.js +250 -0
- package/kit/charts.js +5 -71
- package/kit/code.js +14 -14
- package/kit/report.css +255 -17
- package/kit/runtime.js +1600 -125
- package/kit/state.js +10 -1
- package/kit/tables.js +128 -43
- package/kit/ui.js +77 -35
- package/package.json +2 -1
- package/packs/gallery/card/component.json +14 -0
- package/packs/gallery/card/example.html +10 -0
- package/packs/gallery/card/preview.svg +1 -0
- package/packs/gallery/card/style.css +6 -0
- package/packs/gallery/card/template.html +6 -0
- package/packs/gallery/card/writing.md +14 -0
- package/packs/gallery/chart-card/component.json +15 -0
- package/packs/gallery/chart-card/example.html +14 -0
- package/packs/gallery/chart-card/kit.js +130 -0
- package/packs/gallery/chart-card/preview.svg +1 -0
- package/packs/gallery/chart-card/style.css +42 -0
- package/packs/gallery/chart-card/template.html +13 -0
- package/packs/gallery/chart-card/writing.md +20 -0
- package/packs/gallery/chart-view/component.json +20 -0
- package/packs/gallery/chart-view/example.html +13 -0
- package/packs/gallery/chart-view/kit.js +355 -0
- package/packs/gallery/chart-view/preview.svg +1 -0
- package/packs/gallery/chart-view/style.css +94 -0
- package/packs/gallery/chart-view/template.html +3 -0
- package/packs/gallery/chart-view/writing.md +22 -0
- package/packs/gallery/countdown/component.json +18 -0
- package/packs/gallery/countdown/example.html +3 -0
- package/packs/gallery/countdown/kit.js +156 -0
- package/packs/gallery/countdown/preview.svg +1 -0
- package/packs/gallery/countdown/style.css +130 -0
- package/packs/gallery/countdown/template.html +3 -0
- package/packs/gallery/countdown/writing.md +22 -0
- package/packs/gallery/habit/component.json +14 -0
- package/packs/gallery/habit/example.html +3 -0
- package/packs/gallery/habit/kit.js +295 -0
- package/packs/gallery/habit/preview.svg +1 -0
- package/packs/gallery/habit/style.css +246 -0
- package/packs/gallery/habit/template.html +3 -0
- package/packs/gallery/habit/writing.md +21 -0
- package/packs/gallery/heatmap/component.json +18 -0
- package/packs/gallery/heatmap/example.html +3 -0
- package/packs/gallery/heatmap/kit.js +351 -0
- package/packs/gallery/heatmap/preview.svg +1 -0
- package/packs/gallery/heatmap/style.css +201 -0
- package/packs/gallery/heatmap/template.html +3 -0
- package/packs/gallery/heatmap/writing.md +21 -0
- package/packs/gallery/kanban/component.json +18 -0
- package/packs/gallery/kanban/example.html +12 -0
- package/packs/gallery/kanban/kit.js +540 -0
- package/packs/gallery/kanban/preview.svg +1 -0
- package/packs/gallery/kanban/style.css +212 -0
- package/packs/gallery/kanban/template.html +3 -0
- package/packs/gallery/kanban/writing.md +20 -0
- package/packs/gallery/metric/component.json +22 -0
- package/packs/gallery/metric/example.html +15 -0
- package/packs/gallery/metric/kit.js +248 -0
- package/packs/gallery/metric/preview.svg +1 -0
- package/packs/gallery/metric/style.css +92 -0
- package/packs/gallery/metric/template.html +3 -0
- package/packs/gallery/metric/writing.md +23 -0
- package/packs/gallery/note/component.json +14 -0
- package/packs/gallery/note/example.html +3 -0
- package/packs/gallery/note/kit.js +102 -0
- package/packs/gallery/note/preview.svg +1 -0
- package/packs/gallery/note/style.css +76 -0
- package/packs/gallery/note/template.html +3 -0
- package/packs/gallery/note/writing.md +18 -0
- package/packs/gallery/pack.json +8 -0
- package/packs/gallery/recent/component.json +13 -0
- package/packs/gallery/recent/example.html +1 -0
- package/packs/gallery/recent/kit.js +238 -0
- package/packs/gallery/recent/preview.svg +1 -0
- package/packs/gallery/recent/style.css +158 -0
- package/packs/gallery/recent/template.html +1 -0
- package/packs/gallery/recent/writing.md +15 -0
- package/packs/gallery/static/component.json +13 -0
- package/packs/gallery/static/example.html +11 -0
- package/packs/gallery/static/preview.svg +1 -0
- package/packs/gallery/static/style.css +138 -0
- package/packs/gallery/static/template.html +1 -0
- package/packs/gallery/static/writing.md +18 -0
- package/packs/gallery/stats/component.json +13 -0
- package/packs/gallery/stats/example.html +1 -0
- package/packs/gallery/stats/kit.js +166 -0
- package/packs/gallery/stats/preview.svg +1 -0
- package/packs/gallery/stats/style.css +151 -0
- package/packs/gallery/stats/template.html +1 -0
- package/packs/gallery/stats/writing.md +11 -0
- package/packs/gallery/today/component.json +12 -0
- package/packs/gallery/today/example.html +1 -0
- package/packs/gallery/today/kit.js +185 -0
- package/packs/gallery/today/preview.svg +1 -0
- package/packs/gallery/today/style.css +120 -0
- package/packs/gallery/today/template.html +1 -0
- package/packs/gallery/today/writing.md +14 -0
- package/packs/gallery/todos/component.json +19 -0
- package/packs/gallery/todos/example.html +3 -0
- package/packs/gallery/todos/kit.js +342 -0
- package/packs/gallery/todos/preview.svg +1 -0
- package/packs/gallery/todos/style.css +267 -0
- package/packs/gallery/todos/template.html +3 -0
- package/packs/gallery/todos/writing.md +30 -0
- package/packs/gallery/weather/component.json +19 -0
- package/packs/gallery/weather/example.html +3 -0
- package/packs/gallery/weather/kit.js +312 -0
- package/packs/gallery/weather/preview.svg +1 -0
- package/packs/gallery/weather/style.css +258 -0
- package/packs/gallery/weather/template.html +3 -0
- package/packs/gallery/weather/writing.md +26 -0
- package/packs/gallery-soft/pack.json +8 -0
- package/release.json +12 -10
- package/sdk/doc-kit.d.ts +396 -5
- package/sdk/doc-state.d.ts +2 -0
- package/skill/SKILL.md +56 -23
- package/skill/references/block-script.md +140 -0
- package/skill/references/building-blocks.md +257 -0
- package/skill/references/extensions.md +59 -0
- package/skill/references/pages-and-data.md +221 -0
- package/skill/references/writing-html.md +6 -4
- package/skill/sdk/doc-kit.d.ts +396 -5
- package/skill/sdk/doc-state.d.ts +2 -0
- package/web/assets/index-BK2e_07w.js +13 -0
- package/web/assets/index-CWFI83mF.css +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-3HIRPVLU.mjs +0 -2
- package/dist/chunk-5MHMDLZD.mjs +0 -4
- package/dist/chunk-64K7IPLO.mjs +0 -255
- package/dist/chunk-6QXGZM36.mjs +0 -8
- package/dist/chunk-6RGJOMJP.mjs +0 -16
- package/dist/chunk-AJ73XJF3.mjs +0 -2
- package/dist/chunk-AUZGSZJH.mjs +0 -13
- package/dist/chunk-BFE45AKL.mjs +0 -2
- package/dist/chunk-BXO2ZY5P.mjs +0 -2
- package/dist/chunk-CEH4MRCC.mjs +0 -2
- package/dist/chunk-CY2FNNAB.mjs +0 -16
- package/dist/chunk-DFUDV4LX.mjs +0 -2
- package/dist/chunk-DKXG4XDG.mjs +0 -2
- package/dist/chunk-EZQ34M3F.mjs +0 -124
- package/dist/chunk-EZY3WC6Z.mjs +0 -15
- package/dist/chunk-FUKQNOQS.mjs +0 -9
- package/dist/chunk-JPOSLGOC.mjs +0 -6
- package/dist/chunk-L3QKH4OK.mjs +0 -26
- package/dist/chunk-LAE5NQ4B.mjs +0 -2
- package/dist/chunk-LEF6DRCK.mjs +0 -59
- package/dist/chunk-NO5NJCHO.mjs +0 -2
- package/dist/chunk-NQPVAU63.mjs +0 -2
- package/dist/chunk-NSKUTQ5J.mjs +0 -5
- package/dist/chunk-O35FIXRB.mjs +0 -2
- package/dist/chunk-P2CAZPDP.mjs +0 -3
- package/dist/chunk-PL6GHLGD.mjs +0 -15
- package/dist/chunk-QJIR2BLP.mjs +0 -6
- package/dist/chunk-QUPLVPB2.mjs +0 -7
- package/dist/chunk-R7BNENLY.mjs +0 -4
- package/dist/chunk-S6O4UHLC.mjs +0 -2
- package/dist/chunk-SGJWQKEI.mjs +0 -2
- package/dist/chunk-TX6JH7TR.mjs +0 -2
- package/dist/chunk-TYK6VNAB.mjs +0 -10
- package/dist/chunk-UALRNVSE.mjs +0 -4
- package/dist/chunk-V3HXVQEM.mjs +0 -2
- package/dist/chunk-V5VXUJKG.mjs +0 -2
- package/dist/chunk-VBXMADMP.mjs +0 -2
- package/dist/chunk-VNV55RUA.mjs +0 -2
- package/dist/chunk-VPD2PS35.mjs +0 -2
- package/dist/chunk-W4VKN3GF.mjs +0 -7
- package/dist/chunk-W5BXXPDJ.mjs +0 -3
- package/dist/chunk-XUNHRCOG.mjs +0 -6
- package/skill/references/building-components.md +0 -367
- package/web/assets/index-Bh74DfsV.css +0 -1
- package/web/assets/index-UW0RyFFt.js +0 -13
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
|
+
import { escapeMarkup, escapeText } from "./escape.mjs";
|
|
7
|
+
import { writeAtomic } from "./files.mjs";
|
|
8
|
+
import { laidOut } from "./json-layout.mjs";
|
|
9
|
+
import { DOCUMENT_FOLDERS, isSlugName, keptName, layoutOf, listTrash, markerOf, slugPattern, slugTaken } from "./workspace.mjs";
|
|
10
|
+
|
|
11
|
+
// A workspace's custom pages (自定义页: notes/plans/component-pages-plan-2026-11-07.md §5, the spec
|
|
12
|
+
// custom-blocks-spec-2026-11-07.md §5): a page is a folder pages/<slug>/ at the workspace's root, as
|
|
13
|
+
// components/ is, holding page.html (the page an Agent writes: its head, its look, and one or more
|
|
14
|
+
// boards, <div class="gallery-board">, of cards, each <section class="gallery-card" data-card-id
|
|
15
|
+
// data-x data-y data-w data-h> around one instance of a custom block) and page.json ({ title, icon }).
|
|
16
|
+
// Only an Agent makes one (gallery page new); the workbench lists them under 新建 and 搜索, shows one
|
|
17
|
+
// at #@<slug> and renames or trashes it (src/workbench/service.mjs). A page's slug and a document's
|
|
18
|
+
// are one pool (./workspace.mjs slugTaken): no new document takes a page's, nor a new page a
|
|
19
|
+
// document's, and `pages` is a name no new document takes. A page writes no script of its own: the
|
|
20
|
+
// blocks' scripts and styles are put in by the server as it shows it (src/render/page-html.mjs).
|
|
21
|
+
// The home page is pages/home, listed first: the default one (首页, HOME_TITLE, holding a card of each
|
|
22
|
+
// of HOME_CARDS) is made once per workspace by src/editor/home-page.mjs, as the server starts and by
|
|
23
|
+
// gallery init, and gallery.json's pages.homeMade says it was (markHomeMade); nothing makes it again.
|
|
24
|
+
// Node's built-ins only, but ./escape.mjs, ./files.mjs, ./json-layout.mjs and ./workspace.mjs.
|
|
25
|
+
|
|
26
|
+
/** A custom page's two files. */
|
|
27
|
+
export const PAGE_FILES = { html: "page.html", json: "page.json" };
|
|
28
|
+
/** The home page's slug (首页): listed first, never trashed. */
|
|
29
|
+
export const HOME = "home";
|
|
30
|
+
/** The default home page's title. */
|
|
31
|
+
export const HOME_TITLE = "首页";
|
|
32
|
+
/**
|
|
33
|
+
* The blocks of the default home page's cards (the plan's §5.2: 问候与日期, 便签, 我的待办, 最近编辑, 工作区统计,
|
|
34
|
+
* 编辑热力图), in the order they are placed, each at the first free spot by its default size (a home of
|
|
35
|
+
* 12 x 6 cells, none left empty: shared/board-places.mjs freeSpot); 恢复默认卡片 (the page service's
|
|
36
|
+
* restore-defaults) puts back those of them no card of the home page holds.
|
|
37
|
+
*/
|
|
38
|
+
export const HOME_CARDS = Object.freeze(["gallery/today", "gallery/note", "gallery/todos", "gallery/recent", "gallery/stats", "gallery/heatmap"]);
|
|
39
|
+
/**
|
|
40
|
+
* The default cards a home page made before the workspace's data came had (the plan's phase 2: 问候与日期
|
|
41
|
+
* and 便签): what a home whose hints say nothing of its cards was told of, so that it is told once of the
|
|
42
|
+
* others (homeCardsTold, the home page's note of new cards).
|
|
43
|
+
*/
|
|
44
|
+
export const FIRST_HOME_CARDS = Object.freeze(["gallery/today", "gallery/note"]);
|
|
45
|
+
/** The board a new page has, and the board gallery page add puts a card on when none is named. */
|
|
46
|
+
export const FIRST_BOARD = "main";
|
|
47
|
+
/** Whether `name` is one of a custom page's own files. */
|
|
48
|
+
export const isPageFile = (name) => name === PAGE_FILES.html || name === PAGE_FILES.json;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Where custom page `slug` is: <root>/pages/<slug>/, and its two files and its own files' folder
|
|
52
|
+
* (assets/, which the server serves at /pages/<slug>/assets/<file>). Where pages/ has no folder of
|
|
53
|
+
* that name, a scratch copy's of .gallery/try/ that holds a page.html (gallery page try's and gallery
|
|
54
|
+
* page check's, try-<8 hex>: src/cli/page-try.mjs, served and written as any page's, listed nowhere,
|
|
55
|
+
* gone once its command ends), as a document's slug finds one (workspace.mjs documentDirectory).
|
|
56
|
+
*/
|
|
57
|
+
export function pageFiles(slug, root) {
|
|
58
|
+
const { customPages, scratch } = layoutOf(root);
|
|
59
|
+
const own = path.join(customPages, slug);
|
|
60
|
+
const copy = path.join(scratch, slug);
|
|
61
|
+
const folder = !existsSync(own) && existsSync(path.join(copy, PAGE_FILES.html)) ? copy : own;
|
|
62
|
+
return { folder, html: path.join(folder, PAGE_FILES.html), json: path.join(folder, PAGE_FILES.json), assets: path.join(folder, PAGE_ASSETS) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The folder of a custom page's own files (pictures a card shows), in its folder: served at /pages/<slug>/assets/<file>. */
|
|
66
|
+
export const PAGE_ASSETS = "assets";
|
|
67
|
+
|
|
68
|
+
/** Where the server serves file `value` of custom page `slug`'s assets/ (`value`: assets/<file>, as a page writes it). */
|
|
69
|
+
export const pageAssetAddress = (slug, value) => `/pages/${slug}/${value}`;
|
|
70
|
+
|
|
71
|
+
/** Whether custom page `slug` is there: its folder holds page.html. */
|
|
72
|
+
export const pageExists = (slug, root) => isSlugName(slug) && existsSync(pageFiles(slug, root).html);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Why `slug` names no new custom page in workspace `root`, or null: not lowercase kebab-case; a name
|
|
76
|
+
* the workspace keeps (exports, components, pages…); assets or materials, which a document called
|
|
77
|
+
* pages keeps in that folder; taken by a document, the 写作偏好 page or another custom page (one pool,
|
|
78
|
+
* ./workspace.mjs slugTaken); a slug the trash holds, whose address would open the new page.
|
|
79
|
+
*/
|
|
80
|
+
export async function pageSlugRefused(slug, root) {
|
|
81
|
+
if (typeof slug !== "string" || !slugPattern.test(slug)) return `"${slug}" is no slug: lowercase letters, digits and single hyphens (reading-list).`;
|
|
82
|
+
if (keptName(slug) || DOCUMENT_FOLDERS.includes(slug)) return `"${slug}" is a name the workspace keeps for itself: choose another slug.`;
|
|
83
|
+
if (existsSync(pageFiles(slug, root).folder)) return `A custom page "${slug}" exists already: ${pageFiles(slug, root).folder}`;
|
|
84
|
+
if (slugTaken(slug, root)) return `"${slug}" is a document's slug (documents and custom pages share one set of names): choose another slug.`;
|
|
85
|
+
if ((await listTrash(root)).some((entry) => entry.slug === slug)) return `"${slug}" is the slug of something in the trash: choose another slug.`;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const readJson = (file) => fs.readFile(file, "utf8").then((text) => JSON.parse(text.replace(/^/, ""))).catch(() => null);
|
|
90
|
+
|
|
91
|
+
/** A page.json's fields as a list shows them: { title (the slug when it names none), icon (or null) }. */
|
|
92
|
+
export function pageMetaOf(slug, value) {
|
|
93
|
+
const title = typeof value?.title === "string" && value.title.trim() ? value.title.trim() : slug;
|
|
94
|
+
const icon = typeof value?.icon === "string" && value.icon.trim() ? value.icon.trim() : null;
|
|
95
|
+
return { title, icon };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Custom page `slug` as a list shows it: { slug, title, icon }, or null when it is not there. */
|
|
99
|
+
export async function readPageMeta(slug, root) {
|
|
100
|
+
if (!pageExists(slug, root)) return null;
|
|
101
|
+
return { slug, ...pageMetaOf(slug, await readJson(pageFiles(slug, root).json)) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What gallery.json says of the custom pages (the spec §2): { order: the sidebar's order of their
|
|
106
|
+
* slugs, homeMade: whether the default home page was made }; nothing said, or a gallery.json that does
|
|
107
|
+
* not read: { order: [], homeMade: false }.
|
|
108
|
+
*/
|
|
109
|
+
export function pagesSettings(root) {
|
|
110
|
+
let value = null;
|
|
111
|
+
try {
|
|
112
|
+
value = markerOf(readFileSync(layoutOf(root).marker, "utf8")).value;
|
|
113
|
+
} catch {}
|
|
114
|
+
const pages = value?.pages && typeof value.pages === "object" && !Array.isArray(value.pages) ? value.pages : {};
|
|
115
|
+
const order = Array.isArray(pages.order) ? pages.order.filter((slug, at, all) => typeof slug === "string" && all.indexOf(slug) === at) : [];
|
|
116
|
+
return { order, homeMade: pages.homeMade === true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Write into workspace `root`'s gallery.json that the default home page was made (the spec §2's
|
|
121
|
+
* pages.homeMade), every other key and value of it kept as it was (its pages.order too), and the file's
|
|
122
|
+
* own layout with them (homeMadeIn: a write no reader asked for, on a workspace's first start of a
|
|
123
|
+
* Gallery that makes the home page, changes no line but those it adds). Resolves to true once it says
|
|
124
|
+
* so; false where gallery.json does not read as Gallery's, which nothing then writes over (the home
|
|
125
|
+
* waits for a gallery.json that reads: made once, and said so). Throws where it cannot be written
|
|
126
|
+
* (writeAtomic's EPERM, EBUSY…: makeDefaultHome says so).
|
|
127
|
+
*/
|
|
128
|
+
export async function markHomeMade(root) {
|
|
129
|
+
const file = layoutOf(root).marker;
|
|
130
|
+
for (let tries = 0; tries < 3; tries++) {
|
|
131
|
+
const text = await fs.readFile(file, "utf8").catch(() => null);
|
|
132
|
+
if (text === null) return false;
|
|
133
|
+
const { value, ours } = markerOf(text);
|
|
134
|
+
if (!ours || !value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
135
|
+
const pages = value.pages && typeof value.pages === "object" && !Array.isArray(value.pages) ? value.pages : {};
|
|
136
|
+
if (pages.homeMade === true) return true;
|
|
137
|
+
const wanted = { ...value, pages: { ...pages, homeMade: true } };
|
|
138
|
+
let next = null;
|
|
139
|
+
try {
|
|
140
|
+
next = homeMadeIn(text);
|
|
141
|
+
} catch {}
|
|
142
|
+
// What the text says once written is checked: any doubt (a key twice, say), the whole file written as JSON.stringify lays it out.
|
|
143
|
+
if (next === null || !isDeepStrictEqual(markerOf(next).value, wanted)) next = `${JSON.stringify(wanted, null, 2)}\n`;
|
|
144
|
+
if (await writeAtomic(file, next, { expect: text })) return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// gallery.json's text (`text`, a JSON object that reads: markerOf) with pages.homeMade true, written into
|
|
150
|
+
// it as it stands: its line ends, indentation, BOM, what stands on one line kept, the new member laid out
|
|
151
|
+
// as the one before it is (on a line of its own at its indentation, the file's own indentation inside
|
|
152
|
+
// it, or on its line after it, as tightly as it is), a value that is not true or a pages that is no
|
|
153
|
+
// object (or an empty one) replaced in its place. Never more than that member's text changes.
|
|
154
|
+
function homeMadeIn(text) {
|
|
155
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
156
|
+
const top = objectAt(text, text.indexOf("{"));
|
|
157
|
+
const unit = top.members.map((member) => ownLine(text, top.open, member)).find(Boolean) ?? " ";
|
|
158
|
+
const blanksBefore = (member) => /[ \t]*$/.exec(text.slice(0, member.from))[0];
|
|
159
|
+
// `value`'s text where `member` stands: lines of its own from `indent`, or on one line (spaced as `spaced` says).
|
|
160
|
+
const laid = (value, indent, spaced) => (indent !== null ? JSON.stringify(value, null, unit).split("\n").join(eol + indent) : spaced ? laidOut(value, "") : JSON.stringify(value));
|
|
161
|
+
const added = (object, key, value) => {
|
|
162
|
+
const last = object.members.at(-1);
|
|
163
|
+
const indent = ownLine(text, object.open, last);
|
|
164
|
+
const gap = indent !== null ? eol + indent : blanksBefore(last);
|
|
165
|
+
return `${text.slice(0, last.end)},${gap}${JSON.stringify(key)}:${last.colon}${laid(value, indent, gap !== "")}${text.slice(last.end)}`;
|
|
166
|
+
};
|
|
167
|
+
const replaced = (object, member, value) => `${text.slice(0, member.value)}${laid(value, ownLine(text, object.open, member), blanksBefore(member) !== "")}${text.slice(member.end)}`;
|
|
168
|
+
const pages = top.members.findLast(({ key }) => key === "pages");
|
|
169
|
+
if (!pages) return added(top, "pages", { homeMade: true });
|
|
170
|
+
const inner = text[pages.value] === "{" ? objectAt(text, pages.value) : null;
|
|
171
|
+
if (!inner?.members.length) return replaced(top, pages, { homeMade: true });
|
|
172
|
+
const made = inner.members.findLast(({ key }) => key === "homeMade");
|
|
173
|
+
return made ? replaced(inner, made, true) : added(inner, "homeMade", true);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const blank = (char) => char === " " || char === "\t" || char === "\n" || char === "\r";
|
|
177
|
+
|
|
178
|
+
// Where the JSON value starting at `at` in `text` ends (the index past it), in JSON that reads; past the
|
|
179
|
+
// text's end, a throw (homeMadeIn's caller then lays the whole file out), never a loop that does not end.
|
|
180
|
+
function pastValue(text, at) {
|
|
181
|
+
if (text[at] === '"') {
|
|
182
|
+
let next = at + 1;
|
|
183
|
+
while (text[next] !== '"') {
|
|
184
|
+
if (next >= text.length) throw new Error("unended string");
|
|
185
|
+
next += text[next] === "\\" ? 2 : 1;
|
|
186
|
+
}
|
|
187
|
+
return next + 1;
|
|
188
|
+
}
|
|
189
|
+
if (text[at] === "{" || text[at] === "[") {
|
|
190
|
+
for (let next = at, depth = 0; ; next++) {
|
|
191
|
+
if (next >= text.length) throw new Error("unended " + text[at]);
|
|
192
|
+
const char = text[next];
|
|
193
|
+
if (char === '"') next = pastValue(text, next) - 1;
|
|
194
|
+
else if (char === "{" || char === "[") depth++;
|
|
195
|
+
else if ((char === "}" || char === "]") && --depth === 0) return next + 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
let next = at;
|
|
199
|
+
while (next < text.length && !blank(text[next]) && !",}]".includes(text[next])) next++;
|
|
200
|
+
return next;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The object whose "{" is at `open` in `text`: { open, members: [{ key, from (its key's quote), value
|
|
205
|
+
* (where its value starts), end (past it), colon (the blanks after its colon) }] }. Also what
|
|
206
|
+
* ./workspace-name.mjs writes gallery.json's name by.
|
|
207
|
+
*/
|
|
208
|
+
export function objectAt(text, open) {
|
|
209
|
+
const members = [];
|
|
210
|
+
let at = open + 1;
|
|
211
|
+
const skip = () => {
|
|
212
|
+
while (blank(text[at])) at++;
|
|
213
|
+
};
|
|
214
|
+
skip();
|
|
215
|
+
while (text[at] !== "}") {
|
|
216
|
+
const from = at;
|
|
217
|
+
at = pastValue(text, at);
|
|
218
|
+
const key = JSON.parse(text.slice(from, at));
|
|
219
|
+
skip();
|
|
220
|
+
const colon = ++at;
|
|
221
|
+
skip();
|
|
222
|
+
const value = at;
|
|
223
|
+
at = pastValue(text, at);
|
|
224
|
+
members.push({ key, from, value, end: at, colon: text.slice(colon, value) });
|
|
225
|
+
skip();
|
|
226
|
+
if (text[at] === ",") at++;
|
|
227
|
+
skip();
|
|
228
|
+
}
|
|
229
|
+
return { open, members };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The indentation `member` of the object opening at `open` stands at on a line of its own, or null where it shares its line. */
|
|
233
|
+
export function ownLine(text, open, member) {
|
|
234
|
+
const start = text.lastIndexOf("\n", member.from - 1) + 1;
|
|
235
|
+
const before = text.slice(start, member.from);
|
|
236
|
+
return start > open && /^[ \t]*$/.test(before) ? before : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// .gallery/hints.json: { home: <when it was closed>, homeCards: [the default cards the reader knows of] },
|
|
240
|
+
// or {} where there is none or it does not read.
|
|
241
|
+
function hintsOf(root) {
|
|
242
|
+
try {
|
|
243
|
+
const value = JSON.parse(readFileSync(layoutOf(root).hints, "utf8"));
|
|
244
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
245
|
+
} catch {
|
|
246
|
+
return {};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Whether the reader closed the home page's first-open note (the plan's §5.2): once for the workspace,
|
|
252
|
+
* whichever browser or port shows it, kept in .gallery/hints.json (what Gallery keeps of its own, never
|
|
253
|
+
* gallery.json, which is the workspace's settings and wakes every page's watcher as it is written).
|
|
254
|
+
*/
|
|
255
|
+
export const homeHintClosed = (root) => typeof hintsOf(root).home === "string";
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The default cards (of HOME_CARDS) the reader knows of in workspace `root`: those its home page was made
|
|
259
|
+
* with, or the reader was told of since (the home page's note of new cards, closed or taken up, and
|
|
260
|
+
* 恢复默认卡片 run), kept in .gallery/hints.json as `homeCards`; FIRST_HOME_CARDS where it says none, the
|
|
261
|
+
* cards of a home made before the others came.
|
|
262
|
+
*/
|
|
263
|
+
export function homeCardsTold(root) {
|
|
264
|
+
const told = hintsOf(root).homeCards;
|
|
265
|
+
return Array.isArray(told) && told.every((id) => typeof id === "string") ? told : [...FIRST_HOME_CARDS];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The default cards the home page's note tells of (the plan's 3.4: a home made before them is not
|
|
270
|
+
* changed, and is told once that 恢复默认卡片 brings them): those of HOME_CARDS the reader was not told of
|
|
271
|
+
* (homeCardsTold) and no card of the home page holds (`held`: the blocks its cards are). Empty where
|
|
272
|
+
* there is none to tell.
|
|
273
|
+
*/
|
|
274
|
+
export const homeCardsNew = (root, held) => HOME_CARDS.filter((id) => !homeCardsTold(root).includes(id) && !held.includes(id));
|
|
275
|
+
|
|
276
|
+
/** Every default card told of (HOME_CARDS, kept as hints.json's `homeCards`): the home page's note of new cards says nothing again. */
|
|
277
|
+
export async function tellHomeCards(root) {
|
|
278
|
+
const { hints, gallery } = layoutOf(root);
|
|
279
|
+
await fs.mkdir(gallery, { recursive: true });
|
|
280
|
+
await writeAtomic(hints, `${JSON.stringify({ ...hintsOf(root), homeCards: [...HOME_CARDS] }, null, 2)}\n`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The home page's first-open note closed for good (`at`: when, an ISO time), the file's other notes kept. */
|
|
284
|
+
export async function closeHomeHint(root, at = new Date().toISOString()) {
|
|
285
|
+
const { hints, gallery } = layoutOf(root);
|
|
286
|
+
await fs.mkdir(gallery, { recursive: true });
|
|
287
|
+
await writeAtomic(hints, `${JSON.stringify({ ...hintsOf(root), home: at }, null, 2)}\n`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Pages in the sidebar's order (the spec §2): home first, then those gallery.json's pages.order
|
|
292
|
+
* names, in its order, then the rest by title (then slug). Pure: `pages` [{ slug, title }].
|
|
293
|
+
*/
|
|
294
|
+
export function inPageOrder(pages, order = []) {
|
|
295
|
+
const rank = (slug) => (slug === HOME ? -1 : order.includes(slug) ? order.indexOf(slug) : Infinity);
|
|
296
|
+
const collator = new Intl.Collator("zh-CN", { numeric: true });
|
|
297
|
+
return [...pages].sort((a, b) => rank(a.slug) - rank(b.slug) || collator.compare(a.title, b.title) || (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The custom pages of workspace `root`, in the sidebar's order: [{ slug, title, icon }]. Folders of pages/ that hold no page.html are passed over. */
|
|
301
|
+
export async function listPages(root) {
|
|
302
|
+
const { customPages: pages } = layoutOf(root);
|
|
303
|
+
const names = await fs.readdir(pages, { withFileTypes: true }).catch(() => []);
|
|
304
|
+
const found = await Promise.all(names.filter((entry) => entry.isDirectory() && isSlugName(entry.name)).map((entry) => readPageMeta(entry.name, root)));
|
|
305
|
+
return inPageOrder(found.filter(Boolean), pagesSettings(root).order);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* A page's version as the event stream says it (`page <version>`, the spec §7): its page.html and
|
|
310
|
+
* page.json as they are, 16 hex digits. The page compares it with the one it was served with.
|
|
311
|
+
*/
|
|
312
|
+
export function pageVersion(html, json = "") {
|
|
313
|
+
return createHash("sha256").update(html ?? "").update("\u0000").update(json ?? "").digest("hex").slice(0, 16);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** A page's version as its files are now (pageVersion). */
|
|
317
|
+
export async function pageVersionNow(slug, root) {
|
|
318
|
+
const { html, json } = pageFiles(slug, root);
|
|
319
|
+
const [page, meta] = await Promise.all([fs.readFile(html, "utf8").catch(() => ""), fs.readFile(json, "utf8").catch(() => "")]);
|
|
320
|
+
return pageVersion(page, meta);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The page.html of a new custom page: its title (in `<title>` only: the page draws no big title, its
|
|
325
|
+
* name is the sidebar's, the tab's and the top bar's) and one empty board (`main`), the spec §5's
|
|
326
|
+
* structure. `lang`: its page's lang ("zh-CN" by default).
|
|
327
|
+
*/
|
|
328
|
+
export function newPageHtml(slug, title, { lang = "zh-CN" } = {}) {
|
|
329
|
+
return `<!doctype html>
|
|
330
|
+
<html lang="${escapeMarkup(lang)}">
|
|
331
|
+
<head>
|
|
332
|
+
<meta charset="utf-8">
|
|
333
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
334
|
+
<title>${escapeText(title)}</title>
|
|
335
|
+
<style>
|
|
336
|
+
/* This page's own look. Colors of its own are written for both themes:
|
|
337
|
+
:root[data-doc-effective-theme="dark"] { … } holds the dark ones. */
|
|
338
|
+
</style>
|
|
339
|
+
</head>
|
|
340
|
+
<body>
|
|
341
|
+
<main class="gallery-page" data-page-id="${slug}">
|
|
342
|
+
<div class="gallery-board" data-board-id="${FIRST_BOARD}">
|
|
343
|
+
</div>
|
|
344
|
+
</main>
|
|
345
|
+
</body>
|
|
346
|
+
</html>
|
|
347
|
+
`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Make custom page `slug` in workspace `root`: pages/<slug>/ with page.html (newPageHtml) and page.json
|
|
352
|
+
* ({ title, icon? }). Throws (code EEXIST) when pageSlugRefused refuses the slug; resolves to its files.
|
|
353
|
+
*/
|
|
354
|
+
export async function createPage(slug, title, { root, icon = null, lang } = {}) {
|
|
355
|
+
const refused = await pageSlugRefused(slug, root);
|
|
356
|
+
if (refused) throw Object.assign(new Error(refused), { code: "EEXIST" });
|
|
357
|
+
const files = pageFiles(slug, root);
|
|
358
|
+
await fs.mkdir(layoutOf(root).customPages, { recursive: true });
|
|
359
|
+
await fs.mkdir(files.folder, { recursive: false }); // fails when it was made meanwhile
|
|
360
|
+
const name = title.replace(/\s+/g, " ").trim();
|
|
361
|
+
await fs.writeFile(files.html, newPageHtml(slug, name, { lang }));
|
|
362
|
+
await fs.writeFile(files.json, `${JSON.stringify({ title: name, ...(icon ? { icon } : {}) }, null, 2)}\n`);
|
|
363
|
+
return files;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Custom page `slug`'s title written into its page.json (the workbench's 重命名), every other field as
|
|
368
|
+
* it was; page.html is not touched (its <title> and header are the Agent's). Resolves to its meta, or
|
|
369
|
+
* null when the page is not there; throws for a title that is empty or longer than 200 characters.
|
|
370
|
+
*/
|
|
371
|
+
export async function renamePage(slug, title, root) {
|
|
372
|
+
const name = typeof title === "string" ? title.replace(/\s+/g, " ").trim() : "";
|
|
373
|
+
if (!name || name.length > 200) throw Object.assign(new Error("A page's title is 1 to 200 characters."), { code: "title" });
|
|
374
|
+
if (!pageExists(slug, root)) return null;
|
|
375
|
+
const { json } = pageFiles(slug, root);
|
|
376
|
+
for (let tries = 0; tries < 3; tries++) {
|
|
377
|
+
const text = await fs.readFile(json, "utf8").catch(() => null);
|
|
378
|
+
let value = {};
|
|
379
|
+
try {
|
|
380
|
+
value = text === null ? {} : JSON.parse(text.replace(/^/, ""));
|
|
381
|
+
} catch {}
|
|
382
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) value = {};
|
|
383
|
+
const next = `${JSON.stringify({ ...value, title: name }, null, 2)}\n`;
|
|
384
|
+
const written = text === null ? (await fs.writeFile(json, next, { flag: "wx" }).then(() => true, () => false)) : await writeAtomic(json, next, { expect: text });
|
|
385
|
+
if (written) break;
|
|
386
|
+
}
|
|
387
|
+
return readPageMeta(slug, root);
|
|
388
|
+
}
|
package/browser/shared/paths.mjs
CHANGED
|
@@ -51,6 +51,12 @@ export const previewLogFile = workspace.serverLog;
|
|
|
51
51
|
const inPackage = (built, ...source) => path.join(packageRoot, ...(release ? built : source));
|
|
52
52
|
export const kitSourceDirectory = path.join(packageRoot, "src", "render", "kit"); // the source checkout's only
|
|
53
53
|
export const starterDirectory = inPackage(["starter"], "src", "render", "starter");
|
|
54
|
+
/**
|
|
55
|
+
* Gallery's own block packs (自定义块包), a folder each (src/packs/<pack>/: its pack.json and a folder of each
|
|
56
|
+
* of its blocks, <pack>/<name>/, the scope <pack>): what a workspace's first start installs as any shared
|
|
57
|
+
* pack is installed, and gallery block add copies a block of (shared/block-packs.mjs, shared/shipped-blocks.mjs).
|
|
58
|
+
*/
|
|
59
|
+
export const packsDirectory = inPackage(["packs"], "src", "packs");
|
|
54
60
|
export const vendorDirectory = inPackage(["vendor"], "public", "vendor");
|
|
55
61
|
export const kitPublicDirectory = inPackage(["kit"], "public", "kit");
|
|
56
62
|
/** The editor's browser modules, at their path under src/ (the preview serves them so: src/server/routes.mjs). */
|
|
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { packageRoot } from "./paths.mjs";
|
|
7
|
+
import { compareVersions } from "./versions.mjs";
|
|
7
8
|
import { layoutOf } from "./workspace.mjs";
|
|
8
9
|
|
|
9
10
|
// Where a workspace's Gallery server listens (one a gallery command started, kind "cli", or npm run
|
|
@@ -168,13 +169,8 @@ export const portIsFree = (port) => new Promise((resolve) => {
|
|
|
168
169
|
}));
|
|
169
170
|
});
|
|
170
171
|
|
|
171
|
-
/** -1, 0 or 1 as version `a` is older than, the same as or newer than `b` (
|
|
172
|
-
export
|
|
173
|
-
const parts = (version) => String(version ?? "0").split(/[.+-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
|
|
174
|
-
const [x, y] = [parts(a), parts(b)];
|
|
175
|
-
for (let index = 0; index < 3; index++) if ((x[index] ?? 0) !== (y[index] ?? 0)) return (x[index] ?? 0) < (y[index] ?? 0) ? -1 : 1;
|
|
176
|
-
return 0;
|
|
177
|
-
}
|
|
172
|
+
/** -1, 0 or 1 as version `a` is older than, the same as or newer than `b` (./versions.mjs, which gallery --version reads too). */
|
|
173
|
+
export { compareVersions };
|
|
178
174
|
|
|
179
175
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
180
176
|
// Whether process `pid` is gone within `ms` (asked every 100 ms).
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// The session between a report page and a workbench that shows it in an iframe of the same origin
|
|
2
2
|
// (docs/workbench.md): the page's side is src/editor/page/session.mjs, the workbench's
|
|
3
3
|
// src/workbench/app/session/. Every message is { protocol: PROTOCOL, v: VERSION, type, slug, ... },
|
|
4
|
-
// posted to the other window for this origin only
|
|
4
|
+
// posted to the other window for this origin only; or, for a draft of another workspace, between the
|
|
5
|
+
// page and a workbench of another origin each side's server vouched for (the page's ?shell=<origin>,
|
|
6
|
+
// GET /__gallery/workspace/shell; the workbench's POST elsewhere: src/workbench/elsewhere.mjs).
|
|
5
7
|
//
|
|
6
8
|
// report -> workbench ready once the editor started: { commands, title, icon, look, state, annotations, card }
|
|
7
9
|
// state when the state, the title, the page's icon, the look it shows, how many 批注 it
|
|
@@ -31,9 +33,14 @@
|
|
|
31
33
|
// workspace a page was made under this one (a page block's box, the page itself
|
|
32
34
|
// staying as it is): the workbench reads the list of pages again and
|
|
33
35
|
// shows this page's row open
|
|
36
|
+
// consent a block's card asks to be let online (its 允许访问: the kit's net.js):
|
|
37
|
+
// { block (its id), hosts (those of its needs waiting) }: the workbench
|
|
38
|
+
// opens its consent dialog, which writes what the reader says
|
|
34
39
|
// annotations-removed
|
|
35
40
|
// a 批注 deleted from its card: { items }: the workbench says so, as
|
|
36
41
|
// it does for 全部删除, and its 撤销 puts them back
|
|
42
|
+
// bye the page goes (pagehide), to a workbench of another origin alone, which
|
|
43
|
+
// cannot see it go: its session ends there
|
|
37
44
|
// workbench -> report command { id, name, ... }
|
|
38
45
|
|
|
39
46
|
export const PROTOCOL = "gallery/session";
|