@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,299 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { beforeCatalogRead } from "./component-catalog.mjs";
|
|
5
|
+
import { packsDirectory } from "./paths.mjs";
|
|
6
|
+
import { PACKAGE_MANIFEST, layoutOf } from "./workspace.mjs";
|
|
7
|
+
|
|
8
|
+
// Gallery's own custom blocks (自带块): the blocks of Gallery's own block packs (./block-packs.mjs), a
|
|
9
|
+
// folder each in the package (src/packs/<pack>/<name>/ in the source checkout, packs/<pack>/<name>/ in
|
|
10
|
+
// the release), each a block of the scope <pack> as any workspace's is, its manifest's id <pack>/<name>:
|
|
11
|
+
// the pack gallery's (Gallery 风格) and gallery-soft's (柔光风格). A workspace's first start installs each
|
|
12
|
+
// pack as any shared pack is installed (./block-packs.mjs installOfficialPacks); gallery block add <name>
|
|
13
|
+
// (gallery's, or <pack>/<name>) copies one unchanged into a workspace, components/<pack>/<name>/, and records its
|
|
14
|
+
// version and a fingerprint of what it copied in .gallery/blocks.json; adding it again updates a copy
|
|
15
|
+
// that is as it was put there, and never one whose fingerprint has changed since, which is someone's
|
|
16
|
+
// work (gallery block new <id> --recipe <name> starts a block of one's own from a shipped one instead).
|
|
17
|
+
// The scopes of Gallery's packs are kept for these: gallery block new refuses them. A custom page's
|
|
18
|
+
// service (phase 2) adds one the same way when a card asks for it.
|
|
19
|
+
//
|
|
20
|
+
// A workspace's copies follow Gallery (the user, 2026-11-09: a home page made with 1.0.0's cards kept
|
|
21
|
+
// their plain look after 1.1.0 brought the cards' design): a copy that is as it was added and behind
|
|
22
|
+
// Gallery's is updated in place, as gallery block add would update it, without the command
|
|
23
|
+
// (followShipped): as a server starts (src/server/index.mjs, production.mjs), whenever its catalog
|
|
24
|
+
// reads the workspace's blocks (readCatalog, by followOnRead's hook) and when the page service reads
|
|
25
|
+
// them (its library, an add), once a process for each block and version, told once in the server's
|
|
26
|
+
// log. A copy changed since it was added is someone's work and stays as it is: the log says so once,
|
|
27
|
+
// and 设置 › 组件列表 and gallery block list say that Gallery has a newer one and how to take it.
|
|
28
|
+
|
|
29
|
+
/** The scope of Gallery's own blocks that a bare name names (gallery block add card): the pack gallery's. */
|
|
30
|
+
export const SHIPPED_SCOPE = "gallery";
|
|
31
|
+
|
|
32
|
+
// What a system writes into a folder it shows, which nobody wrote: macOS's Finder (.DS_Store, and ._
|
|
33
|
+
// files beside each on a disk of another format), Windows' Explorer (Thumbs.db, desktop.ini). No
|
|
34
|
+
// fingerprint counts them, no add takes them out, and a recipe copies none.
|
|
35
|
+
const SYSTEM_FILE = /^(?:\.DS_Store|\._.*|Thumbs\.db|ehthumbs\.db|desktop\.ini)$/i;
|
|
36
|
+
|
|
37
|
+
/** Whether a file called `name` (its last part) is one a system wrote into a folder it showed. */
|
|
38
|
+
export const isSystemFile = (name) => SYSTEM_FILE.test(name);
|
|
39
|
+
|
|
40
|
+
/** The files of a folder, by their paths in it with "/", sorted, what a system wrote there left out. */
|
|
41
|
+
export function filesIn(folder) {
|
|
42
|
+
return fs.readdirSync(folder, { recursive: true, withFileTypes: true }).filter((entry) => entry.isFile() && !SYSTEM_FILE.test(entry.name))
|
|
43
|
+
.map((entry) => path.relative(folder, path.join(entry.parentPath, entry.name)).split(path.sep).join("/")).sort();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The names of a block's files in the order they are written: its manifest last, what makes a folder a
|
|
48
|
+
* block to whoever watches (a running server's watcher, the catalog), so that a block that appears is
|
|
49
|
+
* whole, and one a write stopped partway through is a folder with no manifest, which loads as nothing.
|
|
50
|
+
*/
|
|
51
|
+
export const writingOrder = (names) => [...names.filter((name) => name !== PACKAGE_MANIFEST), ...(names.includes(PACKAGE_MANIFEST) ? [PACKAGE_MANIFEST] : [])];
|
|
52
|
+
|
|
53
|
+
/** What a folder holds, as a hash of each file's path and bytes (what a system wrote there aside), or null when there is no such folder. */
|
|
54
|
+
export function fingerprintOf(folder) {
|
|
55
|
+
if (!fs.existsSync(folder)) return null;
|
|
56
|
+
return fingerprintOfFiles(filesIn(folder).map((name) => ({ name, data: fs.readFileSync(path.join(folder, name)) })));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What fingerprintOf says of a folder that holds `files` alone ([{ name, data }], each by its path in it with "/"). */
|
|
60
|
+
export function fingerprintOfFiles(files) {
|
|
61
|
+
const hash = createHash("sha256");
|
|
62
|
+
const named = files.filter(({ name }) => !SYSTEM_FILE.test(name.split("/").at(-1))).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
63
|
+
for (const { name, data } of named) hash.update(`${name}\u0000`).update(data).update("\n");
|
|
64
|
+
return hash.digest("hex").slice(0, 16);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The folders right in `directory`, by name, or none where it is not there.
|
|
68
|
+
const foldersOf = (directory) => {
|
|
69
|
+
try {
|
|
70
|
+
return fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** The ids of Gallery's own packs in `directory` (the package's): each folder of it, by name. */
|
|
77
|
+
export const shippedPackIds = (directory = packsDirectory) => foldersOf(directory);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Gallery's own blocks in `directory` (the package's packs): [{ name, id, pack, folder, version, title:
|
|
81
|
+
* { zh, en }, description: { zh, en }, places }], by pack and name; a folder whose manifest does not read
|
|
82
|
+
* as its pack's block of its name is none.
|
|
83
|
+
*/
|
|
84
|
+
export function shippedBlocks(directory = packsDirectory) {
|
|
85
|
+
return foldersOf(directory).flatMap((pack) => foldersOf(path.join(directory, pack)).flatMap((name) => {
|
|
86
|
+
const folder = path.join(directory, pack, name);
|
|
87
|
+
try {
|
|
88
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(folder, PACKAGE_MANIFEST), "utf8"));
|
|
89
|
+
if (manifest.id !== `${pack}/${name}`) return [];
|
|
90
|
+
return [{ name, id: manifest.id, pack, folder, version: manifest.version, title: manifest.name, description: manifest.description, places: manifest.places ?? ["document"] }];
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The shipped block `written` names (card, gallery/card, gallery-soft/todos: a bare name is the pack gallery's), or null. */
|
|
98
|
+
export function shippedBlock(written, directory = packsDirectory) {
|
|
99
|
+
const id = written.includes("/") ? written : `${SHIPPED_SCOPE}/${written}`;
|
|
100
|
+
return shippedBlocks(directory).find((block) => block.id === id) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Whether `id` is of a scope of Gallery's own packs (gallery/…, gallery-soft/…). */
|
|
104
|
+
export const isShippedId = (id, directory = packsDirectory) => shippedPackIds(directory).includes(String(id).split("/")[0]);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* .gallery/blocks.json: { "<id>": { version, fingerprint } } of each block added or installed (one of
|
|
108
|
+
* Gallery's own, or of a pack), and `packs`, { "<pack id>": { version, fingerprint, blocks, official? } } of
|
|
109
|
+
* each pack installed (./block-packs.mjs); {} for none or a file that does not read.
|
|
110
|
+
*/
|
|
111
|
+
export function recordOf(root) {
|
|
112
|
+
try {
|
|
113
|
+
const record = JSON.parse(fs.readFileSync(layoutOf(root).blocks, "utf8"));
|
|
114
|
+
return record && typeof record === "object" && !Array.isArray(record) ? record : {};
|
|
115
|
+
} catch {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** .gallery/blocks.json as `record` says. */
|
|
121
|
+
export function writeRecord(root, record) {
|
|
122
|
+
const file = layoutOf(root).blocks;
|
|
123
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
124
|
+
fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Where a block of the workspace at `root` is. */
|
|
128
|
+
const folderIn = (root, id) => path.join(layoutOf(root).components, ...id.split("/"));
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* What the workspace at `root` holds of the shipped `block`: { added (a folder of its id is there),
|
|
132
|
+
* version (what its manifest says, or null), changed (its files are not what was added: changed since,
|
|
133
|
+
* or put there by some other hand), behind (it is as added, and Gallery's is another now) }.
|
|
134
|
+
*/
|
|
135
|
+
export function shippedState(root, block) {
|
|
136
|
+
const folder = folderIn(root, block.id);
|
|
137
|
+
const now = fingerprintOf(folder);
|
|
138
|
+
if (now === null) return { added: false, version: null, changed: false, behind: false };
|
|
139
|
+
let version = null;
|
|
140
|
+
try {
|
|
141
|
+
version = JSON.parse(fs.readFileSync(path.join(folder, PACKAGE_MANIFEST), "utf8")).version ?? null;
|
|
142
|
+
} catch {}
|
|
143
|
+
const kept = recordOf(root)[block.id];
|
|
144
|
+
const shipped = fingerprintOf(block.folder);
|
|
145
|
+
const changed = now !== shipped && now !== kept?.fingerprint;
|
|
146
|
+
return { added: true, version, changed, behind: !changed && now !== shipped };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Copies the shipped `block` into the workspace at `root`, unchanged: { did: "added", "updated" (from a
|
|
151
|
+
* copy as it was added, `from` its version) or "same" (nothing to do), folder }. A copy whose files
|
|
152
|
+
* changed since it was added (or that no add put there) is left as it is: an error with code "changed".
|
|
153
|
+
* The manifest is written last, what makes a folder a block to a running server's watcher, so that a
|
|
154
|
+
* block that appears is whole; files of the old copy that the new one has not go first.
|
|
155
|
+
*/
|
|
156
|
+
export function addShipped(root, block) {
|
|
157
|
+
const folder = folderIn(root, block.id);
|
|
158
|
+
const state = shippedState(root, block);
|
|
159
|
+
if (state.changed) throw Object.assign(new Error(`${block.id} was changed after it was added`), { code: "changed", folder });
|
|
160
|
+
const shipped = fingerprintOf(block.folder);
|
|
161
|
+
const record = recordOf(root);
|
|
162
|
+
if (state.added && !state.behind) {
|
|
163
|
+
if (record[block.id]?.fingerprint !== shipped) writeRecord(root, { ...record, [block.id]: { version: block.version, fingerprint: shipped } });
|
|
164
|
+
return { did: "same", folder };
|
|
165
|
+
}
|
|
166
|
+
const files = filesIn(block.folder);
|
|
167
|
+
if (state.added) for (const old of filesIn(folder)) if (!files.includes(old)) fs.rmSync(path.join(folder, old));
|
|
168
|
+
for (const file of writingOrder(files)) {
|
|
169
|
+
fs.mkdirSync(path.dirname(path.join(folder, file)), { recursive: true });
|
|
170
|
+
fs.copyFileSync(path.join(block.folder, file), path.join(folder, file));
|
|
171
|
+
}
|
|
172
|
+
writeRecord(root, { ...record, [block.id]: { version: block.version, fingerprint: shipped } });
|
|
173
|
+
return { did: state.added ? "updated" : "added", from: state.version, folder };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// A version's numbers (1.10.0: [1, 10, 0]), or null for one that is not three of them.
|
|
177
|
+
const numbersOf = (version) => (/^\d+\.\d+\.\d+$/.test(String(version ?? "")) ? version.split(".").map(Number) : null);
|
|
178
|
+
|
|
179
|
+
/** Whether version `a` comes after `b` (both x.y.z); `b` none or not of that form: whether they differ. */
|
|
180
|
+
export function versionAfter(a, b) {
|
|
181
|
+
const [x, y] = [numbersOf(a), numbersOf(b)];
|
|
182
|
+
if (!x || !y) return a !== b;
|
|
183
|
+
const at = x.findIndex((number, index) => number !== y[index]);
|
|
184
|
+
return at !== -1 && x[at] > y[at];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The version of the shipped `block` that the workspace at `root` does not take because its copy was
|
|
189
|
+
* changed since it was added (or no add put it there), when Gallery's comes after the copy's; else null.
|
|
190
|
+
*/
|
|
191
|
+
export function shippedNewer(root, block) {
|
|
192
|
+
const state = shippedState(root, block);
|
|
193
|
+
return state.changed && versionAfter(block.version, state.version) ? block.version : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const looked = new Map(); // a workspace's root and a shipped block's folder -> the version of it looked at
|
|
197
|
+
let told = () => {}; // where followShipped tells what it did when it is not given a log: the server's (followOnRead)
|
|
198
|
+
|
|
199
|
+
// What followShipped did, as lines of the log.
|
|
200
|
+
function followedLines(root, { updated, kept, failed }) {
|
|
201
|
+
const lines = [];
|
|
202
|
+
if (updated.length) lines.push(`Gallery's own blocks in ${root}, as they were added, updated to this Gallery's: ${updated.map(({ id, from, to }) => `${id} ${from ?? "?"} -> ${to}`).join(", ")}`);
|
|
203
|
+
for (const { id, version, shipped } of kept) lines.push(`${id} in ${root} was changed after it was added, so it stays at ${version ?? "its version"}: Gallery's is ${shipped} (设置 › 组件列表 says how to take it)`);
|
|
204
|
+
for (const { id, to, why } of failed) lines.push(`${id} in ${root} could not be updated to Gallery's ${to}: ${why}`);
|
|
205
|
+
return lines;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Bring the workspace at `root`'s copies of Gallery's own blocks (of `ids`, by default each one) up to
|
|
210
|
+
* Gallery's (in `directory`): one that is as it was added and behind is updated as addShipped updates
|
|
211
|
+
* it (the manifest written last); one changed since it was added is kept as it is. Each block and
|
|
212
|
+
* version is looked at once a process, a later call for it doing nothing, so that what reads the blocks
|
|
213
|
+
* again and again (a server's catalog, its page service) pays once. `log`: told once of what was done,
|
|
214
|
+
* a line each (by default the one followOnRead set: none in a command). Never throws: { updated: [{ id,
|
|
215
|
+
* from, to }], kept: [{ id, version, shipped }] (changed, Gallery's newer), failed: [{ id, to, why }] }.
|
|
216
|
+
*/
|
|
217
|
+
export function followShipped(root, { ids = null, directory = packsDirectory, log = null } = {}) {
|
|
218
|
+
const done = { updated: [], kept: [], failed: [] };
|
|
219
|
+
const key = path.resolve(root);
|
|
220
|
+
for (const block of shippedBlocks(directory)) {
|
|
221
|
+
if (ids && !ids.includes(block.id)) continue;
|
|
222
|
+
const at = `${key}\u0000${path.resolve(block.folder)}`;
|
|
223
|
+
if (looked.get(at) === block.version) continue;
|
|
224
|
+
looked.set(at, block.version);
|
|
225
|
+
try {
|
|
226
|
+
const state = shippedState(root, block);
|
|
227
|
+
if (state.changed && versionAfter(block.version, state.version)) done.kept.push({ id: block.id, version: state.version, shipped: block.version });
|
|
228
|
+
if (!state.behind) continue;
|
|
229
|
+
if (addShipped(root, block).did === "updated") done.updated.push({ id: block.id, from: state.version, to: block.version });
|
|
230
|
+
} catch (error) {
|
|
231
|
+
done.failed.push({ id: block.id, to: block.version, why: error?.message ?? String(error) });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const line of followedLines(key, done)) (log ?? told)(line);
|
|
235
|
+
return done;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const following = new Set(); // the workspaces whose server runs in this process (followOnRead), by their resolved root
|
|
239
|
+
const keyOf = (root) => (process.platform === "win32" ? path.resolve(root).toLowerCase() : path.resolve(root));
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* From now on in this process, the catalog brings the copies of Gallery's own blocks of the workspace at
|
|
243
|
+
* `root` up to Gallery's before it reads them (followShipped, its ids those of the scopes of Gallery's
|
|
244
|
+
* packs it finds), and what is done of it is told to `log` (a line): a server's, set as it starts for
|
|
245
|
+
* its own workspace (another's blocks are its own server's to follow, and a test's process reads the
|
|
246
|
+
* repository's reports/ too, which nothing of it is to write).
|
|
247
|
+
*/
|
|
248
|
+
export function followOnRead(root, log) {
|
|
249
|
+
following.add(keyOf(root));
|
|
250
|
+
told = log;
|
|
251
|
+
beforeCatalogRead((read, ids) => {
|
|
252
|
+
if (following.has(keyOf(read))) followShipped(read, { ids });
|
|
253
|
+
}, shippedPackIds());
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// The words of a shipped block's files that name it, and what a recipe's copy names instead: its id,
|
|
257
|
+
// and the id with "-" for "/" (a class, a chart type).
|
|
258
|
+
const namesOf = (id) => [id, id.replace("/", "-")];
|
|
259
|
+
const escaped = (words) => words.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The shipped block whose preview.svg the file `bytes` is, byte for byte, as Gallery ships it or as a
|
|
263
|
+
* recipe of it for the block `id` has it (every word that named it naming `id`), or null: a block made
|
|
264
|
+
* by --recipe keeps its recipe's picture until someone draws its own, which gallery block check notes.
|
|
265
|
+
*/
|
|
266
|
+
export function shippedPreviewOf(bytes, id, directory = packsDirectory) {
|
|
267
|
+
for (const block of shippedBlocks(directory)) {
|
|
268
|
+
if (block.id === id) continue; // a shipped block's own picture is its own
|
|
269
|
+
let shipped;
|
|
270
|
+
try {
|
|
271
|
+
shipped = fs.readFileSync(path.join(block.folder, "preview.svg"));
|
|
272
|
+
} catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (shipped.equals(bytes) || Buffer.from(recipeFiles(block, id)["preview.svg"] ?? "").equals(bytes)) return block;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The files of a new block `id` made from the shipped `block` (gallery block new <id> --recipe <name>),
|
|
282
|
+
* by name, the manifest last: each as Gallery ships it, every word that names the shipped block naming
|
|
283
|
+
* the new one, all in one pass, so that a new name holding an old one (local/gallery-card, made of
|
|
284
|
+
* gallery/card) is not renamed again; its version 0.1.0.
|
|
285
|
+
*/
|
|
286
|
+
export function recipeFiles(block, id) {
|
|
287
|
+
const [from, to] = [namesOf(block.id), namesOf(id)];
|
|
288
|
+
const names = new RegExp(from.map(escaped).join("|"), "g");
|
|
289
|
+
const files = {};
|
|
290
|
+
for (const file of filesIn(block.folder)) {
|
|
291
|
+
const bytes = fs.readFileSync(path.join(block.folder, file));
|
|
292
|
+
if (/\.(?:json|html|md|css|js|svg)$/.test(file)) {
|
|
293
|
+
let text = bytes.toString("utf8").replace(names, (name) => to[from.indexOf(name)]);
|
|
294
|
+
if (file === PACKAGE_MANIFEST) text = text.replace(/"version":\s*"[^"]*"/, '"version": "0.1.0"');
|
|
295
|
+
files[file] = text;
|
|
296
|
+
} else files[file] = bytes;
|
|
297
|
+
}
|
|
298
|
+
return Object.fromEntries(writingOrder(Object.keys(files)).map((name) => [name, files[name]]));
|
|
299
|
+
}
|
|
@@ -13,11 +13,21 @@ import { isMissingValue, numberReading } from "./number-reading.mjs";
|
|
|
13
13
|
// copy or delete either block (a copied pair takes a key of its own:
|
|
14
14
|
// src/editor/page/clipboard/own/chart-keys.mjs, and the commit makes sure of it against the file:
|
|
15
15
|
// src/editor/source/chart-keys.mjs).
|
|
16
|
+
//
|
|
17
|
+
// A view of a table (plan 阶段 4: a task board, a count, a chart card of a custom block's) is the same
|
|
18
|
+
// link: an instance of a custom block whose manifest's needs hold "tables" names the table by that key
|
|
19
|
+
// (VIEW_FROM), reads its rows as a chart does (the kit's runtime/table-reading.js, context.table()) and
|
|
20
|
+
// changes them only by asking the page editor (an intent: docs/editor.md, "Table views"). A key is one
|
|
21
|
+
// table's, which any number of charts and views read: TABLE_READERS are the attributes that read one.
|
|
16
22
|
|
|
17
23
|
/** On a chart's host: the key of the table it draws. */
|
|
18
24
|
export const CHART_DATA = "data-doc-chart-data";
|
|
19
25
|
/** On a table's frame (.doc-table-wrap): the key a chart reads it by. */
|
|
20
26
|
export const CHART_SOURCE = "data-doc-chart-source";
|
|
27
|
+
/** On an instance of a custom block whose needs hold "tables": the key of the table it is a view of (context.table()). */
|
|
28
|
+
export const VIEW_FROM = "data-doc-from";
|
|
29
|
+
/** The attributes that name a table by its key, a chart's host's and a view's. */
|
|
30
|
+
export const TABLE_READERS = [CHART_DATA, VIEW_FROM];
|
|
21
31
|
/** A key: a letter, then letters, digits, "_" or "-", 32 at most. */
|
|
22
32
|
export const CHART_KEY = /^[A-Za-z][\w-]{0,31}$/;
|
|
23
33
|
|
|
@@ -38,6 +48,28 @@ const FORMULA = /\\\(([\s\S]*?)\\\)|\\\[([\s\S]*?)\\\]|\$\$([\s\S]*?)\$\$/g;
|
|
|
38
48
|
*/
|
|
39
49
|
export const cellWords = (text) => text.replace(FORMULA, (_, inline, display, dollars) => inline ?? display ?? dollars).replaceAll(ZWSP, "").replace(/\s+/g, " ").trim();
|
|
40
50
|
|
|
51
|
+
// A formula KaTeX drew on a page: its TeX is its annotation (its drawing's text is its glyphs, twice over).
|
|
52
|
+
const TEX = 'annotation[encoding="application/x-tex"]';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A cell's text on a page as the file writes it, as near as the page tells: a formula KaTeX drew as its
|
|
56
|
+
* TeX, the editor's chip of an empty one as nothing (the file's \(\)). Its words are then cellWords'.
|
|
57
|
+
* For the kit, which reads a table on a page (runtime/table-reading.js: a chart's, a view's and a package's rows).
|
|
58
|
+
* @param {Node} node
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export function cellText(node) {
|
|
62
|
+
if (node.nodeType === 3) return /** @type {Text} */ (node).data;
|
|
63
|
+
if (node.nodeType !== 1) return "";
|
|
64
|
+
const element = /** @type {Element} */ (node);
|
|
65
|
+
if (element.hasAttribute("data-ge-empty")) return "";
|
|
66
|
+
const tex = element.classList.contains("katex") ? element.querySelector(TEX) : null;
|
|
67
|
+
if (tex) return tex.textContent ?? "";
|
|
68
|
+
let text = "";
|
|
69
|
+
for (const child of element.childNodes) text += cellText(child);
|
|
70
|
+
return text;
|
|
71
|
+
}
|
|
72
|
+
|
|
41
73
|
// A header's data-sort-type that says its column holds no numbers: its values are its words.
|
|
42
74
|
const WORD_TYPES = new Set(["text", "date"]);
|
|
43
75
|
|
|
@@ -66,7 +98,7 @@ export function cellValue(cell, type = null) {
|
|
|
66
98
|
* row of header cells alone; null when it has neither), its header cells' data-sort-type (null where
|
|
67
99
|
* none), its rows of data in the file's order (never a <tfoot>'s), each cell { words, sort } (its
|
|
68
100
|
* words, cellWords', and its data-sort-value, or null), and whether a cell of the header or the rows
|
|
69
|
-
* spans rows or columns (
|
|
101
|
+
* spans rows or columns (runtime/table-reading.js readTable reads it from the page,
|
|
70
102
|
* chart-declarations.mjs tableCells from the file).
|
|
71
103
|
*
|
|
72
104
|
* `data` is what d3.csvParse gives: an object for each row of data, in their order, its values by
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { parse } from "parse5";
|
|
2
|
+
import { TODO } from "./components/list.mjs";
|
|
3
|
+
import { sha256 } from "./files.mjs";
|
|
4
|
+
import { attr, BLOCK_LEVEL, elementsOf, hasClass } from "./html-tree.mjs";
|
|
5
|
+
|
|
6
|
+
// A document's to-dos as its pages show them (the items of its to-do lists, ul.doc-todo): what the
|
|
7
|
+
// workspace's facts list (src/workbench/index.mjs, workspace.query("todos")) and what a check of one
|
|
8
|
+
// from another page finds it by (src/editor/todos.mjs, workspace.setTodo: the plan's 5.1), the one
|
|
9
|
+
// reading for both, so that a ref the facts gave names the item the write looks for. Pure but for the
|
|
10
|
+
// parser and the hash: no DOM.
|
|
11
|
+
|
|
12
|
+
const SKIPPED = new Set(["script", "style", "template", "noscript", "svg", "math"]);
|
|
13
|
+
const HEADING = /^h([1-6])$/;
|
|
14
|
+
const tidy = (text) => text.replace(/[\s]+/g, " ").trim();
|
|
15
|
+
|
|
16
|
+
// The words an element holds, as a reader reads them: its text, a line break a space, nothing of a script,
|
|
17
|
+
// a style or a drawing; `stop(child)`: a child left out with what it holds.
|
|
18
|
+
function wordsOf(node, stop = () => false) {
|
|
19
|
+
let text = "";
|
|
20
|
+
const visit = (current) => {
|
|
21
|
+
for (const child of current.childNodes ?? []) {
|
|
22
|
+
if (child.nodeName === "#text") text += child.value;
|
|
23
|
+
else if (child.tagName === "br") text += " ";
|
|
24
|
+
else if (child.tagName && !SKIPPED.has(child.tagName) && !stop(child)) visit(child);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
visit(node);
|
|
28
|
+
return tidy(text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A to-do's own words: what the item holds before its lists and the blocks it holds (a paragraph, a
|
|
32
|
+
// picture), or, where it holds no words of its own (an item written <li><p>…</p></li>), its first block's.
|
|
33
|
+
function itemWords(item) {
|
|
34
|
+
const own = wordsOf(item, (child) => BLOCK_LEVEL.has(child.tagName));
|
|
35
|
+
if (own) return own;
|
|
36
|
+
const first = elementsOf(item).find((child) => BLOCK_LEVEL.has(child.tagName) && child.tagName !== "ul" && child.tagName !== "ol");
|
|
37
|
+
return first ? wordsOf(first, (child) => child.tagName === "ul" || child.tagName === "ol") : "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The to-dos of a document's `text` (its page's HTML, as its pages see it), in their order: [{ text,
|
|
42
|
+
* done, block, path, nth }], `path` the headings above it (their words, the outermost first: an h2 under
|
|
43
|
+
* an h1 of the body, the document's title never one), `nth` which of the to-dos of the same words under
|
|
44
|
+
* the same path it is (0 the first). With `at`, each says where its <li> starts in `text` too (`at`, the
|
|
45
|
+
* offset of its start tag), which the editor's units name it by (source/units.mjs elStart).
|
|
46
|
+
*/
|
|
47
|
+
export function todosOf(text, { at = false } = {}) {
|
|
48
|
+
const root = parse(String(text), { sourceCodeLocationInfo: at });
|
|
49
|
+
const headings = []; // [{ level, words }]
|
|
50
|
+
const todos = [];
|
|
51
|
+
const seen = new Map();
|
|
52
|
+
const visit = (node) => {
|
|
53
|
+
for (const child of elementsOf(node)) {
|
|
54
|
+
if (SKIPPED.has(child.tagName)) continue;
|
|
55
|
+
const heading = HEADING.exec(child.tagName);
|
|
56
|
+
if (heading && !hasClass(child, "doc-title")) {
|
|
57
|
+
const level = Number(heading[1]);
|
|
58
|
+
while (headings.length && headings.at(-1).level >= level) headings.pop();
|
|
59
|
+
const words = wordsOf(child);
|
|
60
|
+
if (words) headings.push({ level, words });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (child.tagName === "li" && child.parentNode?.tagName === "ul" && hasClass(child.parentNode, TODO)) {
|
|
64
|
+
const path = headings.map(({ words }) => words);
|
|
65
|
+
const words = itemWords(child);
|
|
66
|
+
const key = JSON.stringify([words, path]);
|
|
67
|
+
const nth = seen.get(key) ?? 0;
|
|
68
|
+
seen.set(key, nth + 1);
|
|
69
|
+
todos.push({ text: words, done: attr(child, "data-checked") !== undefined, block: attr(child, "data-block-id") ?? null, path, nth, ...(at ? { at: child.sourceCodeLocation?.startOffset ?? -1 } : {}) });
|
|
70
|
+
}
|
|
71
|
+
visit(child);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
visit(root);
|
|
75
|
+
return todos;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The version of a document's file the facts read its to-dos from (the hash of its text, as a to-do's
|
|
80
|
+
* ref carries it: workspace.query("todos")), which a check of one finds it by its place in (`nth`) while
|
|
81
|
+
* the file is still that one.
|
|
82
|
+
*/
|
|
83
|
+
export const fileVersion = (source) => sha256(source).slice(0, 16);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// The user's own folders for Gallery, outside every workspace: "cache", what may be thrown away (the
|
|
5
|
+
// update check's answer, src/cli/updates.mjs), and "data", what Gallery keeps of the machine (the
|
|
6
|
+
// registry of the workspaces used on it, ./workspace-registry.mjs). One function, so that every system
|
|
7
|
+
// finds them alike. Node's built-ins only: gallery --version reads it, and stays as fast as Node's start.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The folder of `kind` ("cache" or "data"): GALLERY_CACHE_DIR or GALLERY_DATA_DIR when set (the tests
|
|
11
|
+
* name folders of their own); else on Windows %LOCALAPPDATA%\gallery for both; on macOS
|
|
12
|
+
* ~/Library/Caches/gallery and ~/Library/Application Support/gallery; elsewhere $XDG_CACHE_HOME/gallery
|
|
13
|
+
* (~/.cache/gallery) and $XDG_STATE_HOME/gallery (~/.local/state/gallery). Only a path: nothing is made.
|
|
14
|
+
*/
|
|
15
|
+
export function userDirectory(kind, { env = process.env, platform = process.platform, home = os.homedir() } = {}) {
|
|
16
|
+
const cache = kind === "cache";
|
|
17
|
+
const own = cache ? env.GALLERY_CACHE_DIR : env.GALLERY_DATA_DIR;
|
|
18
|
+
if (own) return path.resolve(own);
|
|
19
|
+
if (platform === "win32") return path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "gallery");
|
|
20
|
+
if (platform === "darwin") return path.join(home, "Library", cache ? "Caches" : "Application Support", "gallery");
|
|
21
|
+
return cache
|
|
22
|
+
? path.join(env.XDG_CACHE_HOME || path.join(home, ".cache"), "gallery")
|
|
23
|
+
: path.join(env.XDG_STATE_HOME || path.join(home, ".local", "state"), "gallery");
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Gallery's versions compared, with nothing imported: the preview server's choice of a server to use
|
|
2
|
+
// or replace (./preview-server.mjs) and gallery --version's notes (src/cli/updates.mjs, which loads
|
|
3
|
+
// nothing it can do without) read it.
|
|
4
|
+
|
|
5
|
+
/** -1, 0 or 1 as version `a` is older than, the same as or newer than `b` (numbers by dots: 0.1.0 < 0.1.10 < 0.2.0). */
|
|
6
|
+
export function compareVersions(a, b) {
|
|
7
|
+
const parts = (version) => String(version ?? "0").split(/[.+-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
|
|
8
|
+
const [x, y] = [parts(a), parts(b)];
|
|
9
|
+
for (let index = 0; index < 3; index++) if ((x[index] ?? 0) !== (y[index] ?? 0)) return (x[index] ?? 0) < (y[index] ?? 0) ? -1 : 1;
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A released version as npm writes one, numbers alone (0.1.2): what a registry's answer must be to be believed. */
|
|
14
|
+
export const RELEASED = /^\d{1,6}\.\d{1,6}\.\d{1,6}$/;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { iconKind } from "./emoji/icon-value.mjs";
|
|
2
|
+
|
|
3
|
+
// A web link of the sidebar (docs/workbench.md "Web links"): a folder at the workspace's root, named by
|
|
4
|
+
// a slug from the pool documents and custom pages share, holding link.json alone:
|
|
5
|
+
// { "url": "https://open-meteo.com/en/docs", "title": "天气接口文档", "icon": "🌤️", "siteIcon": "data:image/png;base64,…" }
|
|
6
|
+
// `url` an http or https address of 2048 characters at most, `title` 1 to 200 characters, `icon` an
|
|
7
|
+
// emoji or an icon of the 图标 tab, the user's (no picture: a link keeps no files), `siteIcon` the site's
|
|
8
|
+
// own icon, fetched once as the link is added (src/editor/unfurl.mjs siteIcon) and kept in the file as a
|
|
9
|
+
// data: address (PNG, ICO, SVG, JPEG, WebP or GIF, 16 KB at most: SITE_ICON_BYTES), shown when the
|
|
10
|
+
// user chose none (the grid globe when neither is there). The workbench shows it in its
|
|
11
|
+
// content area in a sandboxed frame (features/link/LinkView.tsx), never as a document: no session, no
|
|
12
|
+
// export, no check, no search of its text. A folder holding doc.html (or report.html) is a document
|
|
13
|
+
// whatever else it holds. What is read and checked here is the same on the server, in the gallery
|
|
14
|
+
// command and in the workbench; reading and writing the file is ./workspace.mjs's (Node's).
|
|
15
|
+
|
|
16
|
+
/** A link's file in its folder. */
|
|
17
|
+
export const LINK_FILE = "link.json";
|
|
18
|
+
export const LINK_LIMITS = { url: 2048, title: 200 };
|
|
19
|
+
/** The largest site icon a link keeps, in bytes (its data: address holds them in base64). */
|
|
20
|
+
export const SITE_ICON_BYTES = 16 * 1024;
|
|
21
|
+
/** The media types of a site icon a link keeps. */
|
|
22
|
+
export const SITE_ICON_TYPES = ["image/png", "image/x-icon", "image/vnd.microsoft.icon", "image/svg+xml", "image/jpeg", "image/webp", "image/gif"];
|
|
23
|
+
|
|
24
|
+
/** Whether `value` is a site icon a link keeps: a base64 data: address of one of SITE_ICON_TYPES, SITE_ICON_BYTES at most. */
|
|
25
|
+
export function isSiteIcon(value) {
|
|
26
|
+
if (typeof value !== "string") return false;
|
|
27
|
+
const match = /^data:([a-z+./-]+);base64,([A-Za-z0-9+/]+={0,2})$/.exec(value);
|
|
28
|
+
if (!match || !SITE_ICON_TYPES.includes(match[1])) return false;
|
|
29
|
+
const bytes = (match[2].length * 3) / 4 - (match[2].endsWith("==") ? 2 : match[2].endsWith("=") ? 1 : 0);
|
|
30
|
+
return bytes > 0 && bytes <= SITE_ICON_BYTES;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The sandbox of a web page's frame, the embed block's (Notion's): scripts, its own origin (a page of
|
|
35
|
+
* another site, which can then not reach the workbench's), forms, popups that leave the sandbox, a
|
|
36
|
+
* top navigation only by the reader's own click.
|
|
37
|
+
*/
|
|
38
|
+
export const WEB_SANDBOX = "allow-scripts allow-popups allow-top-navigation-by-user-activation allow-forms allow-same-origin allow-storage-access-by-user-activation allow-popups-to-escape-sandbox";
|
|
39
|
+
|
|
40
|
+
const tidy = (value) => (typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "");
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Address `value` as a link takes it: { url } (the URL's own spelling, a scheme written in) or
|
|
44
|
+
* { refused, message } — "url" (no address), "scheme" (not http or https: javascript:, file:, data:…),
|
|
45
|
+
* "long" (over 2048 characters). An address written without a scheme (example.com/a) is taken as https.
|
|
46
|
+
*/
|
|
47
|
+
export function linkAddress(value) {
|
|
48
|
+
const written = tidy(value);
|
|
49
|
+
if (!written) return { refused: "url", message: "网址不能是空的" };
|
|
50
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:/i.test(written) ? written : `https://${written}`;
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(withScheme);
|
|
54
|
+
} catch {
|
|
55
|
+
return { refused: "url", message: "这不是一个网址" };
|
|
56
|
+
}
|
|
57
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return { refused: "scheme", message: "只能链接 http 或 https 的网址" };
|
|
58
|
+
if (!url.hostname) return { refused: "url", message: "这不是一个网址" };
|
|
59
|
+
if (url.href.length > LINK_LIMITS.url) return { refused: "long", message: "网址太长了(最多 2048 个字符)" };
|
|
60
|
+
return { url: url.href };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether `icon` is one a link takes: an emoji or an icon of the 图标 tab. */
|
|
64
|
+
export const linkIcon = (icon) => ["emoji", "glyph"].includes(iconKind(icon));
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A link's file as it is to be written: { value: { url, title, icon?, siteIcon? } } or { refused, message }
|
|
68
|
+
* (linkAddress's, "title": empty or over 200 characters, "icon": no emoji nor icon of the 图标 tab). A site
|
|
69
|
+
* icon that is none (isSiteIcon) is left out, never refused: the add never fails for it.
|
|
70
|
+
*/
|
|
71
|
+
export function linkValue({ url, title, icon = null, siteIcon = null }) {
|
|
72
|
+
const address = linkAddress(url);
|
|
73
|
+
if (address.refused) return address;
|
|
74
|
+
const name = tidy(title);
|
|
75
|
+
if (!name || name.length > LINK_LIMITS.title) return { refused: "title", message: "名称不能是空的,也不能超过 200 个字" };
|
|
76
|
+
if (icon !== null && icon !== undefined && icon !== "" && !linkIcon(icon)) return { refused: "icon", message: "图标只能是表情或图标页签里的图标" };
|
|
77
|
+
return { value: { url: address.url, title: name, ...(icon ? { icon } : {}), ...(isSiteIcon(siteIcon) ? { siteIcon } : {}) } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** What link.json's text says, as a link: { url, title, icon?, siteIcon? }, or null for a file that is no link (one written by hand wrong). */
|
|
81
|
+
export function readLinkText(text) {
|
|
82
|
+
let value;
|
|
83
|
+
try {
|
|
84
|
+
value = JSON.parse(String(text).replace(/^/, ""));
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
89
|
+
const made = linkValue({ url: value.url, title: value.title, icon: linkIcon(value.icon) ? value.icon : null, siteIcon: value.siteIcon });
|
|
90
|
+
return made.value ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether address `url` is this Gallery's own (`port` its server's): the workbench's host by any of
|
|
95
|
+
* its names (127.0.0.1, localhost, ::1) and port. A link there is refused: framed with the sandbox's
|
|
96
|
+
* allow-same-origin and scripts, the workbench's own page could reach the workbench.
|
|
97
|
+
*/
|
|
98
|
+
export function ownAddress(url, port) {
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = new URL(url);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
106
|
+
const given = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
107
|
+
return ["127.0.0.1", "localhost", "::1", "0.0.0.0"].includes(host) && String(port) === given;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A slug made of words: their Latin letters and digits, lowercase, kebab-case, 60 characters at most
|
|
112
|
+
* (cut where a word ends); "" when none of them is a word of three letters (a title in Chinese, or one
|
|
113
|
+
* whose Latin is a version alone: "… v2.2 …"). gallery import's, and a link's.
|
|
114
|
+
*/
|
|
115
|
+
export const kebab = (text) => {
|
|
116
|
+
const words = String(text ?? "").normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
117
|
+
if (!/[a-z]{3}/.test(words)) return "";
|
|
118
|
+
return words.length <= 60 ? words : words.slice(0, 61).replace(/-[^-]*$/, "") || words.slice(0, 60);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The stem of a new link's slug (a number added after it when it is taken: the workspace's freeSlug):
|
|
123
|
+
* its title's Latin words, else its site's name (open-meteo of open-meteo.com, www passed over), else "link".
|
|
124
|
+
*/
|
|
125
|
+
export function linkStem(title, url) {
|
|
126
|
+
const words = kebab(title);
|
|
127
|
+
if (words) return words;
|
|
128
|
+
let host = "";
|
|
129
|
+
try {
|
|
130
|
+
host = new URL(url).hostname.replace(/^www\./, "");
|
|
131
|
+
} catch {}
|
|
132
|
+
const site = kebab(host.split(".")[0] ?? "");
|
|
133
|
+
return site || "link";
|
|
134
|
+
}
|