@panphora/clayjs 1.15.2 → 1.16.0
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/README.md +4 -5
- package/THIRD-PARTY-NOTICES.md +16 -0
- package/compat/highlight-11.9.0.min.js +1213 -0
- package/compat/highlight-LICENSE +29 -0
- package/compat/journal-assets.lock.json +22 -0
- package/compat/overtype-2.6.1.min.js +116 -0
- package/compat/overtype-LICENSE +21 -0
- package/dist/all.js +90 -0
- package/dist/clay-dom.js +84 -0
- package/dist/clay-events.js +103 -0
- package/dist/clay-internals.js +85 -0
- package/dist/clay-options.js +93 -0
- package/dist/clay-router.js +98 -0
- package/dist/clay-ui.js +95 -0
- package/dist/clay-utils.js +84 -0
- package/dist/clay.js +146 -0
- package/dist/clay.standalone.js +83090 -58706
- package/dist/m/attrs/add-from-T5LJXFFI.js +1 -0
- package/dist/m/attrs/onaftersave-OKSWEJQF.js +1 -0
- package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +1 -0
- package/dist/m/attrs/save-freeze-FUP4Q4X3.js +1 -0
- package/dist/m/c-2AZDZWCK.js +1 -0
- package/dist/m/c-2JL6E75N.js +1 -0
- package/dist/m/c-2M73252O.js +1 -0
- package/dist/m/c-2SPVQSHC.js +1 -0
- package/dist/m/c-33HUYEDI.js +1 -0
- package/dist/m/c-33JJ6CBX.js +2 -0
- package/dist/m/c-35NBGMZB.js +1 -0
- package/dist/m/c-3GJ4SDE2.js +1 -0
- package/dist/m/c-3IQRECYS.js +1 -0
- package/dist/m/c-3OWDMNJR.js +2 -0
- package/dist/m/c-46XYM55L.js +1 -0
- package/dist/m/c-4B6H4G5V.js +1 -0
- package/dist/m/c-4DRMS4QG.js +9 -0
- package/dist/m/c-4FFHZM32.js +1 -0
- package/dist/m/c-4JZDZFF6.js +1 -0
- package/dist/m/c-4L3BTFRK.js +1 -0
- package/dist/m/c-4Q2XTQ2N.js +2 -0
- package/dist/m/c-4WKFKEKF.js +1 -0
- package/dist/m/c-4YSCRICO.js +1 -0
- package/dist/m/c-54RCDCBU.js +1 -0
- package/dist/m/c-5M4YEKCX.js +4 -0
- package/dist/m/c-5R4PTRG2.js +1 -0
- package/dist/m/c-5V3MOVJT.js +1 -0
- package/dist/m/c-5VYOWDTI.js +1 -0
- package/dist/m/c-5Y6TNX5W.js +34 -0
- package/dist/m/c-5YIHJ3TJ.js +1 -0
- package/dist/m/c-66O32MV3.js +28 -0
- package/dist/m/c-6HMB24EV.js +1 -0
- package/dist/m/c-7DJDKPGL.js +1 -0
- package/dist/m/c-7O6AFR65.js +1 -0
- package/dist/m/c-7SWMYKAI.js +1 -0
- package/dist/m/c-7VGZ7RU5.js +1 -0
- package/dist/m/c-A7LS5UZW.js +1 -0
- package/dist/m/c-ANQTJ3B5.js +2 -0
- package/dist/m/c-AQLFEATB.js +1 -0
- package/dist/m/c-AYVOREBE.js +1 -0
- package/dist/m/c-B2ZXPOS3.js +3 -0
- package/dist/m/c-BBVGTQG7.js +1 -0
- package/dist/m/c-BMIZKMPV.js +1 -0
- package/dist/m/c-BTDI4Y23.js +1 -0
- package/dist/m/c-BWFVSBDF.js +1 -0
- package/dist/m/c-BYTKL6UX.js +1 -0
- package/dist/m/c-CFHW5AMY.js +6 -0
- package/dist/m/c-CJZSLMVQ.js +1 -0
- package/dist/m/c-CYWLPPUS.js +1 -0
- package/dist/m/c-CZUSOB7V.js +1 -0
- package/dist/m/c-DBE6ORSI.js +1 -0
- package/dist/m/c-DCFSEJZG.js +1 -0
- package/dist/m/c-DNZ5IFM6.js +1 -0
- package/dist/m/c-EDHUR4MX.js +1 -0
- package/dist/m/c-EJTGYSLN.js +1 -0
- package/dist/m/c-EY6UOM54.js +1 -0
- package/dist/m/c-F3CKJTJS.js +1 -0
- package/dist/m/c-F7Y6FLPY.js +1 -0
- package/dist/m/c-FA4DZVTC.js +1 -0
- package/dist/m/c-FILLTJA7.js +1 -0
- package/dist/m/c-G2BUZITR.js +1 -0
- package/dist/m/c-GB3JTV6E.js +1 -0
- package/dist/m/c-GBGWXTX4.js +1 -0
- package/dist/m/c-GHBCCVG4.js +1 -0
- package/dist/m/c-GJAEGGXY.js +1 -0
- package/dist/m/c-GOWBDM6J.js +1 -0
- package/dist/m/c-GQ6F6IDK.js +1 -0
- package/dist/m/c-H4LG442Q.js +1 -0
- package/dist/m/c-HA6INT6J.js +1 -0
- package/dist/m/c-HPURYVZS.js +2 -0
- package/dist/m/c-HRDCD3WN.js +67 -0
- package/dist/m/c-HT33EQRN.js +1 -0
- package/dist/m/c-HVYXIS72.js +1 -0
- package/dist/m/c-IB52R237.js +1 -0
- package/dist/m/c-IJUK2GPW.js +1 -0
- package/dist/m/c-IKWNE4R4.js +1 -0
- package/dist/m/c-IU3IYEU6.js +1 -0
- package/dist/m/c-IZ2EBRAJ.js +1 -0
- package/dist/m/c-J7CA54IP.js +1 -0
- package/dist/m/c-JAX2667S.js +6 -0
- package/dist/m/c-KI7M4AY3.js +1 -0
- package/dist/m/c-KIPQJX4B.js +1 -0
- package/dist/m/c-KKWLDOPM.js +1 -0
- package/dist/m/c-KR4UW4KW.js +1 -0
- package/dist/m/c-KY3VJGUA.js +8 -0
- package/dist/m/c-L4WQSC27.js +1 -0
- package/dist/m/c-LFPHCXY3.js +1 -0
- package/dist/m/c-LR4CGEPT.js +91 -0
- package/dist/m/c-LRPUO2MX.js +1 -0
- package/dist/m/c-LTGMXXKO.js +1 -0
- package/dist/m/c-LXENEBV7.js +1 -0
- package/dist/m/c-LZJPNR2W.js +1 -0
- package/dist/m/c-MMUIUUZG.js +2 -0
- package/dist/m/c-MTQHWQC3.js +1 -0
- package/dist/m/c-N326R2JD.js +1 -0
- package/dist/m/c-N7JPUPGC.js +1 -0
- package/dist/m/c-NMJRXKPC.js +8 -0
- package/dist/m/c-NRPJEVJS.js +1 -0
- package/dist/m/c-NUHP2XDB.js +1 -0
- package/dist/m/c-NZMNXVF6.js +1 -0
- package/dist/m/c-OCSZ4QGN.js +1 -0
- package/dist/m/c-OIBUTDXI.js +1 -0
- package/dist/m/c-OIDRZKAF.js +1 -0
- package/dist/m/c-OYTFP5QN.js +1 -0
- package/dist/m/c-P7KNDG34.js +1 -0
- package/dist/m/c-PPOSCAQD.js +1 -0
- package/dist/m/c-PQ5LK6QE.js +993 -0
- package/dist/m/c-PZ4XEPUP.js +2 -0
- package/dist/m/c-QHIWXHRO.js +1 -0
- package/dist/m/c-QHWJ5ZIJ.js +1 -0
- package/dist/m/c-QIX5BJYN.js +1 -0
- package/dist/m/c-QNKREOL6.js +1 -0
- package/dist/m/c-QQHEX6HA.js +1 -0
- package/dist/m/c-R7RMANJI.js +9 -0
- package/dist/m/c-RI4NT75Y.js +1 -0
- package/dist/m/c-RM7J7BV2.js +1 -0
- package/dist/m/c-RUJKSUQW.js +1 -0
- package/dist/m/c-RVMWGXEJ.js +1 -0
- package/dist/m/c-S7H2DRCB.js +1 -0
- package/dist/m/c-S7LK4KUE.js +1 -0
- package/dist/m/c-SGCCC4FW.js +25 -0
- package/dist/m/c-SGIA4IEQ.js +188 -0
- package/dist/m/c-SWRJFCNN.js +1 -0
- package/dist/m/c-T4VO4OCB.js +1 -0
- package/dist/m/c-TCCA5BZN.js +2 -0
- package/dist/m/c-TQEMVJCS.js +1 -0
- package/dist/m/c-TQZ2JYHD.js +1 -0
- package/dist/m/c-TR6RMDQ7.js +3 -0
- package/dist/m/c-TRS6TM6O.js +1 -0
- package/dist/m/c-UGHH4MB3.js +1 -0
- package/dist/m/c-UKHTLWUW.js +1 -0
- package/dist/m/c-UL2YUY52.js +1 -0
- package/dist/m/c-UNN7LWCT.js +1 -0
- package/dist/m/c-UXBMRDZB.js +235 -0
- package/dist/m/c-V7PZZRG7.js +1 -0
- package/dist/m/c-VAK7SG37.js +1 -0
- package/dist/m/c-VDSS4K3Z.js +1 -0
- package/dist/m/c-VFX45IZQ.js +1 -0
- package/dist/m/c-VHNSBD2M.js +1 -0
- package/dist/m/c-VKEDWYDF.js +1 -0
- package/dist/m/c-VL52ADDR.js +1 -0
- package/dist/m/c-WN5AY3OZ.js +1 -0
- package/dist/m/c-WNYBLIOZ.js +1 -0
- package/dist/m/c-WRBKEJD4.js +1 -0
- package/dist/m/c-WWDBYXLK.js +1 -0
- package/dist/m/c-WYFDLITL.js +1 -0
- package/dist/m/c-XN6MBKI6.js +67 -0
- package/dist/m/c-XNNW2CWK.js +1 -0
- package/dist/m/c-XPELB6E5.js +1 -0
- package/dist/m/c-XQXZAO7N.js +1 -0
- package/dist/m/c-Y6USGTS5.js +5 -0
- package/dist/m/c-YDH7Q3NY.js +1 -0
- package/dist/m/c-YG2NJYS6.js +135 -0
- package/dist/m/c-Z36J63PP.js +1 -0
- package/dist/m/c-Z4VRC7GU.js +7 -0
- package/dist/m/c-Z6K52OLP.js +19 -0
- package/dist/m/c-ZDY2MDAA.js +21 -0
- package/dist/m/c-ZE46QDIH.js +1 -0
- package/dist/m/c-ZUCOKOHT.js +1 -0
- package/dist/m/core/admin-attrs-VGNLAUDK.js +1 -0
- package/dist/m/core/autosave-E4PEFPZ6.js +1 -0
- package/dist/m/core/conflict-notice-VQ6Y64PH.js +1 -0
- package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
- package/dist/m/core/host-permission-XN3K2IW3.js +1 -0
- package/dist/m/core/is-edit-mode-JXHDVGUL.js +1 -0
- package/dist/m/core/persist-HKHA66IY.js +3 -0
- package/dist/m/core/recovery-TBBSICE2.js +1 -0
- package/dist/m/core/save-WUCHTQBE.js +1 -0
- package/dist/m/core/save-core-FJVHT5M3.js +1 -0
- package/dist/m/core/snapshot-F3SWDZMM.js +1 -0
- package/dist/m/core/unsaved-warning-STKBHE6G.js +1 -0
- package/dist/m/dom/all-APS5ZSKP.js +1 -0
- package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
- package/dist/m/events/index-23DPTUT4.js +1 -0
- package/dist/m/internals/index-WOVAP3RU.js +1 -0
- package/dist/m/lib/cache-bust-CGWT6ZJI.js +1 -0
- package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
- package/dist/m/loader-HXX6YNH5.js +1 -0
- package/dist/m/manifest-F22DEDAC.js +1 -0
- package/dist/m/manifest-edit-C3C7376B.js +1 -0
- package/dist/m/manifest.json +3491 -0
- package/dist/m/options/options-2LH76JQ3.js +1 -0
- package/dist/m/page/editor-XUTB2HB2.js +189 -0
- package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +6 -0
- package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
- package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
- package/dist/m/plugins/demo-ZIBFAA3R.js +1 -0
- package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
- package/dist/m/plugins/indicator-PV4YW2ID.js +1 -0
- package/dist/m/plugins/live-NUTTXSBS.js +1 -0
- package/dist/m/plugins/menu-HCKO3DSP.js +138 -0
- package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
- package/dist/m/plugins/page-KSUH7G6J.js +2 -0
- package/dist/m/plugins/page-view-W73OW7WI.js +1 -0
- package/dist/m/plugins/people-AJJ32YCG.js +1 -0
- package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
- package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
- package/dist/m/plugins/richclay-RC3L2F6X.js +517 -0
- package/dist/m/plugins/richclay-RC3L2F6X.js.LEGAL.txt +1 -0
- package/dist/m/plugins/sortable-YRMQX3ZU.js +1 -0
- package/dist/m/plugins/source-KHK5UUSK.js +1 -0
- package/dist/m/plugins/undo-OF2L3CXU.js +1 -0
- package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
- package/dist/m/plugins/upgrade-ZNYODP5Y.js +1 -0
- package/dist/m/plugins/upload-XSGNILUY.js +1 -0
- package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +21 -0
- package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +1 -0
- package/dist/m/plugins/wire-ZWVWKF76.js +1 -0
- package/dist/m/reader-core-ILWLXVMP.js +68 -0
- package/dist/m/route/route-KOCZSEGN.js +1 -0
- package/dist/m/sync/live-sync-4AUVXVJU.js +1 -0
- package/dist/m/ui/index-EVHKMKBL.js +1 -0
- package/dist/m/utils/index-4TJCVLT7.js +1 -0
- package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
- package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
- package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
- package/dist/offline-manifest.json +3800 -0
- package/dist/page/themes/light-1.css +406 -0
- package/dist/server/core.cjs +23662 -22860
- package/dist/server/offline-runtime.cjs +4954 -0
- package/dist/server/offline-runtime.mjs +4923 -0
- package/dist/server/rooms.mjs +9 -0
- package/docs/guide.md +1078 -669
- package/entries/all.js +2 -0
- package/entries/clay-data.js +1 -1
- package/entries/clay-dom.js +2 -0
- package/entries/clay-events.js +2 -0
- package/entries/clay-internals.js +2 -0
- package/entries/clay-options.js +2 -0
- package/entries/clay-router.js +2 -0
- package/entries/clay-ui.js +2 -0
- package/entries/clay-utils.js +2 -0
- package/entries/clay.js +2 -0
- package/entries/sap.js +6 -6
- package/fonts/ibm-plex-mono-LICENSE +93 -0
- package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-LICENSE +93 -0
- package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex.css +71 -0
- package/fonts/inter-LICENSE +93 -0
- package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
- package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-greek-wght-normal.woff2 +0 -0
- package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
- package/fonts/inter.css +62 -0
- package/fonts/newsreader-LICENSE +93 -0
- package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
- package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
- package/fonts/newsreader.css +26 -0
- package/fonts/work-sans-LICENSE +93 -0
- package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
- package/fonts/work-sans.css +8 -0
- package/package.json +20 -3
- package/packed-contract.json +1 -0
- package/src/attrs/add-from.js +27 -4
- package/src/attrs/save-freeze.js +7 -52
- package/src/core/admin-attrs.js +119 -12
- package/src/core/admin-contenteditable.js +12 -10
- package/src/core/admin-inputs.js +14 -15
- package/src/core/admin-onclick.js +2 -0
- package/src/core/autosave.js +74 -21
- package/src/core/conflict-download.js +22 -0
- package/src/core/conflict-notice-ui.js +1173 -0
- package/src/core/conflict-notice.js +140 -697
- package/src/core/edit-mode.js +147 -14
- package/src/core/etag.js +30 -12
- package/src/core/host-meta.js +72 -8
- package/src/core/host-permission.js +29 -0
- package/src/core/is-edit-mode.js +145 -12
- package/src/core/mode-key.js +125 -0
- package/src/core/mode-lifecycle.js +544 -0
- package/src/core/mode-receipt.js +30 -0
- package/src/core/mode-refusal.js +13 -0
- package/src/core/mode-resources.js +52 -0
- package/src/core/mode-support.js +82 -0
- package/src/core/mode-switch.js +259 -0
- package/src/core/mode-transition.js +112 -0
- package/src/core/page-data-write.js +305 -0
- package/src/core/page-data.js +26 -227
- package/src/core/page-format-wire.js +4 -0
- package/src/core/page-format.js +58 -0
- package/src/core/persist.js +37 -9
- package/src/core/preserve-authored.js +114 -0
- package/src/core/recovery.js +772 -0
- package/src/core/runtime-url.js +105 -0
- package/src/core/save-core.js +109 -12
- package/src/core/save-replace.js +72 -0
- package/src/core/save-size-notice.js +134 -0
- package/src/core/save-size.js +140 -0
- package/src/core/save.js +805 -118
- package/src/core/snapshot.js +121 -11
- package/src/core/source-map.js +157 -9
- package/src/core/unsaved-warning.js +14 -4
- package/src/core/upgrade-run.js +408 -0
- package/src/core/version.js +18 -0
- package/src/lib/assets.js +30 -0
- package/src/lib/autosave-debug-gate.js +50 -0
- package/src/lib/autosave-scheduler.js +141 -0
- package/src/lib/boot-imports.js +8 -0
- package/src/lib/derived.js +3 -1
- package/src/lib/dirty-gate.js +15 -0
- package/src/lib/doc-storage.js +33 -0
- package/src/lib/effect.js +9 -4
- package/src/lib/mutation.js +4 -2
- package/src/lib/offline-runtime.js +226 -0
- package/src/lib/preload.js +48 -0
- package/src/lib/root-attrs.js +1 -1
- package/src/lib/trash.js +18 -0
- package/src/lib/undo/capture-move.js +152 -0
- package/src/live/local-channel.js +61 -0
- package/src/live/local-lane.js +60 -9
- package/src/live/server-lane.js +6 -3
- package/src/loader-logic.js +76 -15
- package/src/loader.js +218 -30
- package/src/page/adopt-boot.js +76 -22
- package/src/page/adopt.js +25 -4
- package/src/page/awareness.js +230 -0
- package/src/page/blocks.js +79 -5
- package/src/page/blocksel.js +20 -4
- package/src/page/board-face.js +13 -0
- package/src/page/board-panel.js +5 -17
- package/src/page/board-style.js +2 -13
- package/src/page/board-view.js +1 -11
- package/src/page/board-views.js +17 -0
- package/src/page/board.js +2 -2
- package/src/page/boot-pack.js +26 -0
- package/src/page/boot.js +237 -0
- package/src/page/checklist.js +1 -1
- package/src/page/chrome.js +411 -61
- package/src/page/csv-import.js +17 -2
- package/src/page/document-title-dom.js +40 -0
- package/src/page/document-title-runtime.js +76 -0
- package/src/page/document-title.js +9 -0
- package/src/page/drag.js +48 -13
- package/src/page/drop.js +2 -1
- package/src/page/editor.js +330 -86
- package/src/page/feature-loader.js +72 -0
- package/src/page/features/board.js +12 -0
- package/src/page/features/code.js +13 -0
- package/src/page/features/media.js +290 -0
- package/src/page/features/registry.js +23 -0
- package/src/page/features/table.js +383 -0
- package/src/page/{tool-view.js → features/tool.js} +33 -2
- package/src/page/format-codec.js +215 -0
- package/src/page/format-gate.js +179 -0
- package/src/page/format-level.js +157 -0
- package/src/page/highlight-slot.js +23 -0
- package/src/page/highlight.js +30 -16
- package/src/page/history-intent.js +78 -0
- package/src/page/inline.js +18 -25
- package/src/page/items.js +52 -11
- package/src/page/link.js +1 -0
- package/src/page/markdown.js +86 -1
- package/src/page/media-views.js +3 -0
- package/src/page/media.js +33 -245
- package/src/page/outline-cache.js +11 -0
- package/src/page/placeholder.js +27 -7
- package/src/page/projection.js +196 -50
- package/src/page/reader-patch.js +306 -0
- package/src/page/recover.js +1 -1
- package/src/page/rich/page-tool.js +1 -4
- package/src/page/runtime-owner.js +27 -0
- package/src/page/save-projection.js +7 -4
- package/src/page/schema.js +14 -0
- package/src/page/table-rect-key.js +3 -0
- package/src/page/table-select.js +4 -3
- package/src/page/table-style.js +1 -1
- package/src/page/table-views.js +3 -0
- package/src/page/table.js +8 -293
- package/src/page/themes/light-1.css +139 -2
- package/src/page/themes/light-1.source.css +269 -0
- package/src/page/tool-capability.js +3 -0
- package/src/page/tool-views.js +3 -0
- package/src/page/view.js +390 -107
- package/src/page/ybind/awareness.js +172 -0
- package/src/page/ybind/binding.js +9 -1
- package/src/page/ybind/bridge.js +50 -47
- package/src/page/ybind/decode.js +52 -19
- package/src/page/ybind/doc.js +27 -2
- package/src/page/ybind/group.js +12 -3
- package/src/page/ybind/index.js +2 -0
- package/src/page/ybind/lineage.js +71 -0
- package/src/page/ybind/marks.js +25 -1
- package/src/page/ybind/migrate.js +48 -13
- package/src/page/ybind/moves.js +32 -21
- package/src/page/ybind/positions.js +50 -0
- package/src/page/ybind/provider.js +84 -22
- package/src/page/ybind/recovery-proof.js +191 -0
- package/src/page/ybind/recovery-replay.js +130 -0
- package/src/page/ybind/tokens.js +15 -6
- package/src/page/ybind/walk.js +10 -3
- package/src/page/ybind/writer.js +48 -4
- package/src/plugins/ai-edit.js +167 -32
- package/src/plugins/bind-text.js +218 -0
- package/src/plugins/cms.js +93 -0
- package/src/plugins/indicator.js +57 -5
- package/src/plugins/live.js +282 -34
- package/src/plugins/menu/claygoo.svg.js +1 -0
- package/src/plugins/menu/seo-head.js +323 -0
- package/src/plugins/menu/seo.js +849 -0
- package/src/plugins/menu.js +425 -0
- package/src/plugins/page-view.js +131 -0
- package/src/plugins/page.js +1022 -275
- package/src/plugins/people.js +158 -31
- package/src/plugins/richclay.js +119 -9
- package/src/plugins/sortable.js +370 -60
- package/src/plugins/source.js +189 -366
- package/src/plugins/undo.js +289 -19
- package/src/plugins/unsent.js +132 -0
- package/src/plugins/upgrade-run.js +1 -0
- package/src/plugins/upgrade.js +210 -0
- package/src/plugins/upload-task.js +571 -0
- package/src/plugins/upload-ui.js +265 -0
- package/src/plugins/upload.js +34 -153
- package/src/plugins/visitor-drafts/inert.js +32 -0
- package/src/plugins/visitor-drafts/owner-chip.js +3 -0
- package/src/plugins/visitor-drafts/prompt.js +2 -2
- package/src/plugins/visitor-drafts-owner.js +85 -0
- package/src/plugins/visitor-drafts.js +31 -42
- package/src/plugins/wire.js +82 -1
- package/src/reader-core.js +26 -0
- package/src/recovery/conflicts.js +287 -0
- package/src/recovery/identity.js +154 -0
- package/src/recovery/store.js +792 -0
- package/src/recovery/undo.js +257 -0
- package/src/server/checkpoint/index.js +68 -4
- package/src/server/offline-runtime/index.js +214 -0
- package/src/server/rooms/index.js +2 -1
- package/src/server/rooms/live-data-rooms.js +2 -2
- package/src/server/rooms/page-admission.js +33 -0
- package/src/server/rooms/page-lineage.js +56 -0
- package/src/server/rooms/page-rebase.js +11 -5
- package/src/server/rooms/page-state.js +2 -0
- package/src/server/rooms/page-write-admission.js +9 -3
- package/src/source/session.js +705 -0
- package/src/sync/conflict-revert.js +5 -4
- package/src/sync/conflicts-edit.js +119 -0
- package/src/sync/conflicts-facade.js +49 -0
- package/src/sync/conflicts.js +99 -1
- package/src/sync/editing-marker.js +37 -6
- package/src/sync/editing-sender.js +8 -1
- package/src/sync/live-sync-edit.js +63 -0
- package/src/sync/live-sync.js +364 -119
- package/src/sync/local-presence.js +187 -0
- package/src/sync/merge-declarations.js +10 -0
- package/src/sync/merge-engine.js +12 -0
- package/src/sync/presence-store.js +412 -0
- package/src/sync/presence-target.js +243 -0
- package/src/sync/presence.js +72 -41
- package/src/sync/saved-handoff.js +200 -0
- package/src/sync/saved-stream.js +535 -0
- package/src/sync/version-guard.js +92 -0
- package/src/sync/wire-profile.js +81 -0
- package/src/ui/bevel-controls.js +2 -8
- package/src/ui/page-scheme.js +7 -0
- package/src/ui/skins/cms.js +1 -1
- package/src/ui/theme-config.js +150 -0
- package/src/ui/theme.js +6 -138
- package/src/ui/toast-lazy.js +11 -0
- package/src/vendor/LICENSES-optional.md +253 -0
- package/src/vendor/highlight.vendor.js +3 -0
- package/src/vendor/hljs-bash.vendor.js +2 -0
- package/src/vendor/hljs-css.vendor.js +2 -0
- package/src/vendor/hljs-javascript.vendor.js +2 -0
- package/src/vendor/hljs-json.vendor.js +2 -0
- package/src/vendor/hljs-python.vendor.js +2 -0
- package/src/vendor/hljs-xml.vendor.js +2 -0
- package/src/vendor/hyper-html-api-data.vendor.js +1 -1
- package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
- package/src/vendor/hyper-morph-identity.vendor.js +2 -0
- package/src/vendor/hyper-morph-json.vendor.js +3 -0
- package/src/vendor/hyper-morph.vendor.js +6 -6
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +169 -84
- package/src/vendor/marked.vendor.js +68 -0
- package/src/vendor/overtype.vendor.js +994 -0
- package/src/vendor/richclay.vendor.js +17 -17
- package/src/vendor/sap-core.vendor.js +5 -5
- package/src/vendor/sap-ownership.vendor.js +11 -7
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// A selection as a peer can name it, and a peer's target read back in this tab's
|
|
2
|
+
// document: stable block ids, guarded Yjs relative positions, and the winning
|
|
3
|
+
// copy's coordinates. Nothing here is stored or saved; a target is built from the
|
|
4
|
+
// current document and thrown away.
|
|
5
|
+
import * as Y from "../../vendor/yjs.vendor.js";
|
|
6
|
+
import { toBase64, fromBase64 } from "../../vendor/yjs.vendor.js";
|
|
7
|
+
import { state as pmState } from "../../vendor/prosemirror.vendor.js";
|
|
8
|
+
import { yBefore, yAfter } from "./tokens.js";
|
|
9
|
+
import { posAtY, yIndexAt, cellPoint, cellRange } from "./positions.js";
|
|
10
|
+
import { BlockSelection } from "../blocks.js";
|
|
11
|
+
|
|
12
|
+
// A block selection publishes its ids, bounded so one person can never make a
|
|
13
|
+
// target too large for the host to accept.
|
|
14
|
+
export const MAX_BLOCK_IDS = 128;
|
|
15
|
+
|
|
16
|
+
const blockIdOf = ($pos) => ($pos.depth ? $pos.node(1).attrs.id : null);
|
|
17
|
+
const inTable = ($pos) => !!$pos.depth && $pos.node(1).type.name === "table";
|
|
18
|
+
|
|
19
|
+
const relativeTo = (pd, doc, w, pos, assoc) =>
|
|
20
|
+
toBase64(Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(pd.flow, yIndexAt(doc, w.holes, pos, w.visible), assoc)));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One selection as a peer publishes it, or null when it has no page coordinates
|
|
24
|
+
* (a mixed selection, a caret with no block, a table range across cells).
|
|
25
|
+
*/
|
|
26
|
+
export function encodeSelection({ pd, w, doc }, selection) {
|
|
27
|
+
if (!pd || !w || !doc || !selection) return null;
|
|
28
|
+
if (selection instanceof pmState.TextSelection) {
|
|
29
|
+
const $anchor = selection.$anchor, $head = selection.$head;
|
|
30
|
+
if (inTable($anchor) || inTable($head)) {
|
|
31
|
+
const anchor = cellPoint(doc, $anchor.pos), head = cellPoint(doc, $head.pos);
|
|
32
|
+
if (!anchor || !head || anchor.table !== head.table || anchor.row !== head.row || anchor.col !== head.col) return null;
|
|
33
|
+
return { kind: "cell", block: anchor.table, row: anchor.row, col: anchor.col };
|
|
34
|
+
}
|
|
35
|
+
const from = Math.min(selection.from, selection.to);
|
|
36
|
+
const end = ($pos) => {
|
|
37
|
+
const block = blockIdOf($pos);
|
|
38
|
+
if (block === null) return null;
|
|
39
|
+
return { block, rel: relativeTo(pd, doc, w, $pos.pos, selection.empty || $pos.pos === from ? 0 : -1) };
|
|
40
|
+
};
|
|
41
|
+
const anchor = end($anchor), head = end($head);
|
|
42
|
+
return anchor && head ? { kind: "text", anchor, head } : null;
|
|
43
|
+
}
|
|
44
|
+
if (selection instanceof BlockSelection || selection instanceof pmState.NodeSelection) {
|
|
45
|
+
const ids = [];
|
|
46
|
+
if (selection instanceof BlockSelection) {
|
|
47
|
+
const { lo, hi } = selection.span();
|
|
48
|
+
doc.forEach((node, _at, index) => { if (index >= lo && index <= hi) ids.push(node.attrs.id); });
|
|
49
|
+
} else {
|
|
50
|
+
const node = doc.nodeAt(selection.from);
|
|
51
|
+
if (node) ids.push(node.attrs.id);
|
|
52
|
+
}
|
|
53
|
+
if (!ids.length) return null;
|
|
54
|
+
if (ids.length > MAX_BLOCK_IDS) {
|
|
55
|
+
const focused = blockIdOf(selection.$from);
|
|
56
|
+
return focused ? { kind: "blocks", ids: [focused] } : null;
|
|
57
|
+
}
|
|
58
|
+
return { kind: "blocks", ids };
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const indexOf = (pd, end) => {
|
|
64
|
+
if (!end || typeof end !== "object" || typeof end.block !== "string" || !end.block || typeof end.rel !== "string" || !end.rel) return null;
|
|
65
|
+
let rel;
|
|
66
|
+
try { rel = Y.decodeRelativePosition(fromBase64(end.rel)); } catch { return null; }
|
|
67
|
+
let abs;
|
|
68
|
+
try { abs = Y.createAbsolutePositionFromRelativePosition(rel, pd.ydoc); } catch { return null; }
|
|
69
|
+
// A position that names another shared type is not this document's: the peer
|
|
70
|
+
// wrote against a document this tab does not hold.
|
|
71
|
+
if (!abs || abs.type !== pd.flow) return null;
|
|
72
|
+
return abs.index;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Whether the flow still holds text at that index: a join keeps it, a deletion
|
|
76
|
+
// and an undone Restore do not.
|
|
77
|
+
const aliveAt = (pd, index) => {
|
|
78
|
+
let at = 0;
|
|
79
|
+
for (let it = pd.flow._start; it; it = it.right) {
|
|
80
|
+
const end = at + it.length;
|
|
81
|
+
if (index < end) return !it.deleted;
|
|
82
|
+
at = end;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const liveMarker = (pd, id) => {
|
|
88
|
+
for (let it = pd.flow._start; it; it = it.right) if (!it.deleted && it.content instanceof Y.ContentEmbed && it.content.embed?.b === id) return true;
|
|
89
|
+
return false;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Where a flow index sits inside its own block, read from the flow itself: the
|
|
93
|
+
// marker the index follows and how far into that block's text it is. A moved
|
|
94
|
+
// block leaves its old text behind as a hidden copy, so the index still names the
|
|
95
|
+
// block that was moved and the offset the person had.
|
|
96
|
+
const offsetInBlock = (pd, index) => {
|
|
97
|
+
let at = 0, id = null, start = 0;
|
|
98
|
+
for (let it = pd.flow._start; it; it = it.right) {
|
|
99
|
+
if (it.content instanceof Y.ContentEmbed && it.content.embed?.b !== undefined) {
|
|
100
|
+
id = it.content.embed.b;
|
|
101
|
+
start = at + it.length;
|
|
102
|
+
}
|
|
103
|
+
at += it.length;
|
|
104
|
+
if (at > index && id !== null) return { id, offset: index - start };
|
|
105
|
+
}
|
|
106
|
+
return id === null ? null : { id, offset: index - start };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const placeInBlock = (doc, id, offset) => {
|
|
110
|
+
let found = null;
|
|
111
|
+
doc.forEach((node, pos) => { if (found === null && node.attrs.id === id && node.isTextblock) found = pos + 1 + Math.min(Math.max(offset, 0), node.content.size); });
|
|
112
|
+
return found;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// A failed resolution is null, never a neighbouring block: the caller hides it
|
|
116
|
+
// instead of drawing it somewhere plausible. A block that moved is the same
|
|
117
|
+
// logical block at a new place, so it is placed by its id and the offset the
|
|
118
|
+
// person had; a block that is hidden or gone has no place here at all.
|
|
119
|
+
const placeEnd = ({ pd, w, doc }, end) => {
|
|
120
|
+
const index = indexOf(pd, end);
|
|
121
|
+
if (index === null) return null;
|
|
122
|
+
const pos = posAtY(doc, w.holes, w.tokens, index, w.visible);
|
|
123
|
+
if (blockIdOf(doc.resolve(pos)) === end.block) return pos;
|
|
124
|
+
const spot = offsetInBlock(pd, index);
|
|
125
|
+
if (spot && spot.id === end.block) {
|
|
126
|
+
const moved = placeInBlock(doc, end.block, spot.offset);
|
|
127
|
+
if (moved !== null) return moved;
|
|
128
|
+
}
|
|
129
|
+
// A block that still exists in the flow but not in the document is hidden, and a
|
|
130
|
+
// position that landed elsewhere is not this caret's. A block the peer named that
|
|
131
|
+
// no longer exists anywhere was joined into the one that now holds its text, and
|
|
132
|
+
// the relative position says where in it.
|
|
133
|
+
if (liveMarker(pd, end.block) || !aliveAt(pd, index)) return null;
|
|
134
|
+
// A position inside a hole is text this tab does not show; only a position in
|
|
135
|
+
// visible text may stand in for a block that was joined away.
|
|
136
|
+
if (w.holes.some((hole) => index >= yBefore(w.holes, hole.at) && index < yAfter(w.holes, hole.at))) return null;
|
|
137
|
+
return doc.resolve(pos).parent.inlineContent ? pos : null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A published selection read back in this tab's current document: page
|
|
142
|
+
* positions, or null when any part of it no longer names this document's text.
|
|
143
|
+
* A hidden block, a deleted one, a position from another document or bytes that
|
|
144
|
+
* do not decode are all nulls, and a null is a caret that is not drawn.
|
|
145
|
+
*/
|
|
146
|
+
export function decodeSelection({ pd, w, doc }, selection) {
|
|
147
|
+
if (!pd || !w || !doc || !selection || typeof selection !== "object") return null;
|
|
148
|
+
if (selection.kind === "text") {
|
|
149
|
+
const anchor = placeEnd({ pd, w, doc }, selection.anchor);
|
|
150
|
+
const head = placeEnd({ pd, w, doc }, selection.head);
|
|
151
|
+
if (anchor === null || head === null) return null;
|
|
152
|
+
return { kind: "text", from: Math.min(anchor, head), to: Math.max(anchor, head) };
|
|
153
|
+
}
|
|
154
|
+
if (selection.kind === "cell") {
|
|
155
|
+
const cell = cellRange(doc, { table: selection.block, row: selection.row, col: selection.col, offset: 0 });
|
|
156
|
+
if (!cell) return null;
|
|
157
|
+
return { kind: "cell", from: cell.from, to: cell.to, block: cell.block, row: cell.row, col: cell.col };
|
|
158
|
+
}
|
|
159
|
+
if (selection.kind === "blocks") {
|
|
160
|
+
const ids = [];
|
|
161
|
+
const ranges = [];
|
|
162
|
+
for (const id of Array.isArray(selection.ids) ? selection.ids : []) {
|
|
163
|
+
doc.forEach((node, at) => {
|
|
164
|
+
if (node.attrs.id !== id || ids.includes(id)) return;
|
|
165
|
+
ids.push(id);
|
|
166
|
+
ranges.push({ from: at, to: at + node.nodeSize });
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return ranges.length ? { kind: "blocks", ids, ranges } : null;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
@@ -63,8 +63,16 @@ export function createYPageBinding({ store, ydoc = new Y.Doc(), onNotice = () =>
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// What the page's awareness UI asks of the mapping: the document, and the bridge's
|
|
67
|
+
// current walk, which is the winning-copy view a remote position is read in and a
|
|
68
|
+
// local one is written from. The UI never reaches into the bridge itself.
|
|
69
|
+
const awareness = {
|
|
70
|
+
generation: () => (Number.isSafeInteger(store.value?.g) && store.value.g >= 0 ? store.value.g : 0),
|
|
71
|
+
map: () => ({ pd, w: bridge.walk }),
|
|
72
|
+
};
|
|
73
|
+
|
|
66
74
|
return {
|
|
67
|
-
ydoc, pd, provider, bridge, stats, flush,
|
|
75
|
+
ydoc, pd, provider, bridge, stats, flush, awareness,
|
|
68
76
|
doc: () => keepViewPos(docOf(pd, walk(pd))),
|
|
69
77
|
attach: (view) => bridge.attach(view),
|
|
70
78
|
dispatch: (tr) => bridge.dispatch(tr),
|
package/src/page/ybind/bridge.js
CHANGED
|
@@ -3,31 +3,17 @@ import { REMOTE_META, LOCAL, HISTORY_META, attrKeys, sameJson, parseKey, mintOp
|
|
|
3
3
|
import { walk, hashText } from "./walk.js";
|
|
4
4
|
import { docOf } from "./doc.js";
|
|
5
5
|
import { localGroup } from "./group.js";
|
|
6
|
-
import {
|
|
6
|
+
import { yBefore, yAfter } from "./tokens.js";
|
|
7
|
+
import { yIndexAt, posAtY, cellPoint, cellAt } from "./positions.js";
|
|
7
8
|
import { state as pmState } from "../../vendor/prosemirror.vendor.js";
|
|
8
9
|
import { repairSiblings } from "../model.js";
|
|
9
10
|
import { createOriginIndex, installUndoOrigins } from "./origins.js";
|
|
10
11
|
import { reduceRecovery, canonicalRevision, restoreKey, physicalKeyOf, authorKey, RECOVERED_PREFIX, parsePhysicalKey, parseGoneIntents } from "./recovery.js";
|
|
11
12
|
import { moveBlocks } from "./moves.js";
|
|
12
13
|
import { createBoardCommands } from "./board-commands.js";
|
|
14
|
+
import { continuesGroup, intentOf } from "../history-intent.js";
|
|
13
15
|
import { MOVE, BlockSelection } from "../blocks.js";
|
|
14
16
|
|
|
15
|
-
const yIndexAt = (doc, holes, pos, visible) => yBefore(holes, tokenIndex(doc, new Map(), visible)[pos]);
|
|
16
|
-
|
|
17
|
-
export function posAtY(doc, holes, tokens, y, visible = null) {
|
|
18
|
-
let lo = 0, hi = tokens;
|
|
19
|
-
while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (yBefore(holes, mid) <= y) lo = mid; else hi = mid - 1; }
|
|
20
|
-
const idx = tokenIndex(doc, new Map(), visible);
|
|
21
|
-
let first = -1;
|
|
22
|
-
for (let p = 0; p < idx.length; p++) {
|
|
23
|
-
if (idx[p] < lo) continue;
|
|
24
|
-
if (idx[p] > lo) break;
|
|
25
|
-
if (first < 0) first = p;
|
|
26
|
-
if (doc.resolve(p).parent.inlineContent) return p;
|
|
27
|
-
}
|
|
28
|
-
return first < 0 ? doc.content.size : first;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
17
|
export const shapeOf = (doc) => { const out = []; doc.forEach((b) => out.push([b.type.name, b.attrs.id, JSON.stringify(Object.fromEntries(attrKeys(b.type).map((k) => [k, b.attrs[k]]))), b.isTextblock || b.type.name === "table" ? JSON.stringify(b.content.toJSON()) : null])); return JSON.stringify(out); };
|
|
32
18
|
export const bareShape = (doc) => { const out = []; doc.forEach((b) => out.push([b.type.name, JSON.stringify(Object.fromEntries(attrKeys(b.type).map((k) => [k, b.attrs[k]]))), b.isTextblock || b.type.name === "table" ? JSON.stringify(b.content.toJSON()) : null])); return JSON.stringify(out); };
|
|
33
19
|
export function sameBareShape(a, b) {
|
|
@@ -41,6 +27,22 @@ export function sameBareShape(a, b) {
|
|
|
41
27
|
return true;
|
|
42
28
|
}
|
|
43
29
|
|
|
30
|
+
const opaqueRanges = (doc) => {
|
|
31
|
+
const out = [];
|
|
32
|
+
doc.forEach((node, pos) => { if (node.type.name === "opaque_block") out.push([pos, pos + node.nodeSize]); });
|
|
33
|
+
return out;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// A command that would rewrite an opaque block, or cut through it, is refused
|
|
37
|
+
// before the view or the document changes. A command that merely touches its
|
|
38
|
+
// neighbours keeps working.
|
|
39
|
+
const touchesOpaque = (step, ranges) => {
|
|
40
|
+
const from = typeof step.from === "number" ? step.from : typeof step.pos === "number" ? step.pos : null;
|
|
41
|
+
if (from === null) return false;
|
|
42
|
+
const to = typeof step.to === "number" ? step.to : from + 1;
|
|
43
|
+
return ranges.some(([start, end]) => from < end && to > start);
|
|
44
|
+
};
|
|
45
|
+
|
|
44
46
|
const tableSpan = (doc, pos) => {
|
|
45
47
|
const $pos = doc.resolve(pos);
|
|
46
48
|
return $pos.depth && $pos.node(1).type.name === "table" ? [$pos.before(1), $pos.after(1)] : null;
|
|
@@ -62,27 +64,6 @@ export function docDiff(oldDoc, newDoc) {
|
|
|
62
64
|
return { start, endA, endB };
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
const cellPoint = (doc, pos) => {
|
|
66
|
-
const $pos = doc.resolve(pos);
|
|
67
|
-
if ($pos.depth !== 3 || $pos.parent.type.name !== "table_cell") return null;
|
|
68
|
-
return { table: $pos.node(1).attrs.id, row: $pos.node(2).attrs.id, col: $pos.parent.attrs.col, offset: $pos.parentOffset };
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const cellAt = (doc, point) => {
|
|
72
|
-
let found = null;
|
|
73
|
-
doc.forEach((table, at) => {
|
|
74
|
-
if (found !== null || table.type.name !== "table" || table.attrs.id !== point.table) return;
|
|
75
|
-
table.forEach((row, rowAt) => {
|
|
76
|
-
if (found !== null || row.attrs.id !== point.row) return;
|
|
77
|
-
row.forEach((cell, cellAt) => {
|
|
78
|
-
if (found === null && cell.attrs.col === point.col) found = at + 2 + rowAt + cellAt + 1 + Math.min(point.offset, cell.content.size);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
return found;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
|
|
86
67
|
export function keepViewPos(derived, current = null) {
|
|
87
68
|
const previous = new Map();
|
|
88
69
|
current?.forEach((block) => { if (block.attrs.id) previous.set(block.attrs.id, block); });
|
|
@@ -207,6 +188,9 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
207
188
|
return docOf(pd, w);
|
|
208
189
|
};
|
|
209
190
|
let saved = null;
|
|
191
|
+
let lastIntent = null;
|
|
192
|
+
let groupOpen = false;
|
|
193
|
+
let joining = false;
|
|
210
194
|
const render = (own = false, fresh = null) => {
|
|
211
195
|
stats.renders++;
|
|
212
196
|
const keep = saved;
|
|
@@ -244,6 +228,7 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
244
228
|
view.dispatch(tr);
|
|
245
229
|
};
|
|
246
230
|
const onBefore = (t) => {
|
|
231
|
+
if (groupOpen && !joining && t.origin === LOCAL) { um.stopCapturing(); groupOpen = false; }
|
|
247
232
|
if (!view || t.origin === LOCAL || t.origin === um || saved) return;
|
|
248
233
|
const sel = view.state.selection;
|
|
249
234
|
if (!(sel instanceof pmState.TextSelection)) return;
|
|
@@ -259,18 +244,19 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
259
244
|
saved = { anchor: end(sel.$anchor), head: end(sel.$head) };
|
|
260
245
|
};
|
|
261
246
|
const onAfter = (t) => {
|
|
247
|
+
if (t.origin !== LOCAL) { lastIntent = null; groupOpen = false; }
|
|
262
248
|
if (!view) { bumpRecovery(); return; }
|
|
263
249
|
if (t.origin === LOCAL) { saved = null; return; }
|
|
264
250
|
render(t.origin === um);
|
|
265
251
|
};
|
|
266
252
|
const portMove = (tr, move) => {
|
|
267
253
|
const result = moveBlocks({ pd, view, um, origins, render }, move);
|
|
268
|
-
if (result.status
|
|
269
|
-
stats.
|
|
270
|
-
return
|
|
254
|
+
if (result.status !== "moved") {
|
|
255
|
+
if (result.status === "refused") stats.moveRefusals = (stats.moveRefusals ?? 0) + 1;
|
|
256
|
+
return result;
|
|
271
257
|
}
|
|
272
258
|
const sel = tr.selection;
|
|
273
|
-
if (sel instanceof pmState.TextSelection) return
|
|
259
|
+
if (sel instanceof pmState.TextSelection) return result;
|
|
274
260
|
const doc = view.state.doc;
|
|
275
261
|
let from = null, to = null;
|
|
276
262
|
doc.forEach((node, pos) => {
|
|
@@ -278,13 +264,13 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
278
264
|
if (from === null) from = pos;
|
|
279
265
|
to = pos + node.nodeSize;
|
|
280
266
|
});
|
|
281
|
-
if (from === null) return
|
|
267
|
+
if (from === null) return result;
|
|
282
268
|
const next = sel instanceof pmState.NodeSelection
|
|
283
269
|
? pmState.NodeSelection.create(doc, from)
|
|
284
270
|
: sel.anchor <= sel.head ? new BlockSelection(doc.resolve(from), doc.resolve(to)) : new BlockSelection(doc.resolve(to), doc.resolve(from));
|
|
285
271
|
const reselect = view.state.tr.setSelection(next).setMeta(REMOTE_META, true).setMeta("addToHistory", false);
|
|
286
272
|
view.dispatch(tr.scrolledIntoView ? reselect.scrollIntoView() : reselect);
|
|
287
|
-
return
|
|
273
|
+
return result;
|
|
288
274
|
};
|
|
289
275
|
const board = createBoardCommands({ pd, um, origins, render: () => (view ? render() : refresh()) });
|
|
290
276
|
pd.ydoc.on("beforeTransaction", onBefore);
|
|
@@ -321,16 +307,33 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
321
307
|
},
|
|
322
308
|
dispatch(tr) {
|
|
323
309
|
const move = tr.getMeta(MOVE);
|
|
324
|
-
if (move
|
|
310
|
+
if (move) return portMove(tr, move);
|
|
311
|
+
const ranges = opaqueRanges(view.state.doc);
|
|
312
|
+
if (ranges.length && tr.steps.some((step) => touchesOpaque(step, ranges))) return { status: "refused", code: "unsupported-format" };
|
|
325
313
|
const { state, transactions } = view.state.applyTransaction(tr);
|
|
326
314
|
const local = transactions.filter((t) => t.docChanged && !t.getMeta(REMOTE_META));
|
|
327
315
|
view.updateState(state);
|
|
328
|
-
if (!local.length)
|
|
316
|
+
if (!local.length) {
|
|
317
|
+
if (transactions.some((t) => t.selectionSet && !t.getMeta(REMOTE_META))) {
|
|
318
|
+
lastIntent = null;
|
|
319
|
+
if (groupOpen) { um.stopCapturing(); groupOpen = false; }
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
329
323
|
w = walk(pd);
|
|
330
324
|
const history = transactions.every((t) => t.getMeta("addToHistory") !== false);
|
|
331
325
|
const register = registerAuthor;
|
|
332
326
|
registerAuthor = null;
|
|
333
|
-
|
|
327
|
+
const intent = history ? intentOf(local, view) : null;
|
|
328
|
+
const join = groupOpen && continuesGroup(lastIntent, intent);
|
|
329
|
+
lastIntent = intent;
|
|
330
|
+
joining = join;
|
|
331
|
+
try {
|
|
332
|
+
localGroup(pd, local, w, stats, um, history, register, { join, open: intent !== null });
|
|
333
|
+
} finally {
|
|
334
|
+
joining = false;
|
|
335
|
+
}
|
|
336
|
+
groupOpen = intent !== null;
|
|
334
337
|
const derived = refresh();
|
|
335
338
|
if (!sameBareShape(derived, view.state.doc)) {
|
|
336
339
|
stats.mismatch++;
|
package/src/page/ybind/decode.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createPageDoc, attrKeys, defaultOf, sameJson, PAGE_ROOTS } from "./layo
|
|
|
6
6
|
import { resolveBoardFields } from "./board.js";
|
|
7
7
|
import { resolveTableFields } from "./table.js";
|
|
8
8
|
import { markName } from "./marks.js";
|
|
9
|
+
import { entriesOfYjs } from "../format-codec.js";
|
|
9
10
|
import { walk } from "./walk.js";
|
|
10
11
|
import { docFromValue, YDecodeError } from "./provider.js";
|
|
11
12
|
import { parseGoneIntents, parsePhysicalKey, RecoveryError } from "./recovery.js";
|
|
@@ -25,6 +26,13 @@ const isPlain = (value) => value !== null && typeof value === "object" && !Array
|
|
|
25
26
|
const originId = (client, clock) => `${client}.${clock}`;
|
|
26
27
|
const boundaryIdentity = (record) => JSON.stringify([record.blockId, record.kind, record.recoveryOf ?? null]);
|
|
27
28
|
|
|
29
|
+
// A mark name this runtime has no schema for is kept when the codec reads it as a
|
|
30
|
+
// valid descriptor, a named mark or the reserved compact-token bridge, and refused
|
|
31
|
+
// when it is malformed.
|
|
32
|
+
const keptMark = (key, value) => {
|
|
33
|
+
try { entriesOfYjs({ [markName(key)]: value }); return true; } catch { return false; }
|
|
34
|
+
};
|
|
35
|
+
|
|
28
36
|
function validateFlow(pd) {
|
|
29
37
|
if (pd.flow._map.size) unsupported("The page flow must contain text and block markers");
|
|
30
38
|
for (let it = pd.flow._start; it; it = it.right) {
|
|
@@ -32,7 +40,7 @@ function validateFlow(pd) {
|
|
|
32
40
|
const c = it.content;
|
|
33
41
|
if (c instanceof Y.ContentString) continue;
|
|
34
42
|
if (c instanceof Y.ContentFormat) {
|
|
35
|
-
if (c.value !== null && !Object.hasOwn(schema.marks, markName(c.key))) unsupported(`Unsupported page mark: ${c.key}`);
|
|
43
|
+
if (c.value !== null && !Object.hasOwn(schema.marks, markName(c.key)) && !keptMark(c.key, c.value)) unsupported(`Unsupported page mark: ${c.key}`);
|
|
36
44
|
continue;
|
|
37
45
|
}
|
|
38
46
|
if (c instanceof Y.ContentEmbed) {
|
|
@@ -132,8 +140,45 @@ function validateOrigins(pd) {
|
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
|
|
143
|
+
// The text a block's runs carry, as one record states it: characters and the mark
|
|
144
|
+
// key of each, a hard break as an atom.
|
|
145
|
+
function textOf(block) {
|
|
146
|
+
let text = "";
|
|
147
|
+
const units = [];
|
|
148
|
+
for (const run of block.runs) {
|
|
149
|
+
if (run.hole) continue;
|
|
150
|
+
if (run.br) {
|
|
151
|
+
if (block.t === "code_block") unsupported("A code block cannot contain a hard-break atom");
|
|
152
|
+
text += ATOM;
|
|
153
|
+
units.push(BREAK);
|
|
154
|
+
} else {
|
|
155
|
+
text += run.text;
|
|
156
|
+
const key = markKey([...run.marks].sort((a, b) => a.type.rank - b.type.rank));
|
|
157
|
+
for (let i = 0; i < run.text.length; i++) units.push(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const out = { c: text };
|
|
161
|
+
if (units.some(Boolean)) out.m = runsOf(units);
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The attributes a block's Y attribute map, or a recovered root, states for it.
|
|
166
|
+
const attributeFields = (block, attrs) => (block.fields ? Object.entries(block.fields) : attrs ? attrs.entries() : []);
|
|
167
|
+
|
|
168
|
+
// A block type this runtime has no schema for keeps the record it stands for: its
|
|
169
|
+
// own type, its text and marks and every attribute a newer peer wrote on it.
|
|
170
|
+
function opaqueBlockOf(pd, block, index) {
|
|
171
|
+
const attrs = pd.attrs.get(block.baseId);
|
|
172
|
+
const out = { t: block.t, pos: String(index + 1).padStart(8, "0") };
|
|
173
|
+
for (const [key, value] of attributeFields(block, attrs)) if (key !== "t" && key !== "pos") out[key] = copy(value);
|
|
174
|
+
return Object.assign(out, textOf(block));
|
|
175
|
+
}
|
|
176
|
+
|
|
135
177
|
function blockOf(pd, block, index) {
|
|
136
|
-
if (!Object.hasOwn(DEFAULTS, block.t))
|
|
178
|
+
if (!Object.hasOwn(DEFAULTS, block.t)) {
|
|
179
|
+
if (typeof block.t !== "string" || !block.t || block.t === "opaque_block") unsupported(`Unsupported page block type: ${block.t}`);
|
|
180
|
+
return opaqueBlockOf(pd, block, index);
|
|
181
|
+
}
|
|
137
182
|
const type = schema.nodes[block.t];
|
|
138
183
|
const attrs = pd.attrs.get(block.baseId);
|
|
139
184
|
const table = block.t === "table" && !block.fields ? resolveTableFields(pd.tableFields, block.baseId, { cols: attrs?.get("cols"), rows: attrs?.get("rows") }) : null;
|
|
@@ -145,24 +190,12 @@ function blockOf(pd, block, index) {
|
|
|
145
190
|
}
|
|
146
191
|
if (block.t === "table") out.rows = copy((block.fields ? block.fields.rows : table.rows) ?? {});
|
|
147
192
|
if (block.t === "board") Object.assign(out, block.fields ? { columns: copy(block.fields.columns ?? {}), cards: copy(block.fields.cards ?? {}) } : resolveBoardFields(pd.boardFields, block.baseId));
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (run.hole) continue;
|
|
153
|
-
if (run.br) {
|
|
154
|
-
if (block.t === "code_block") unsupported("A code block cannot contain a hard-break atom");
|
|
155
|
-
text += ATOM;
|
|
156
|
-
units.push(BREAK);
|
|
157
|
-
} else {
|
|
158
|
-
text += run.text;
|
|
159
|
-
const key = markKey([...run.marks].sort((a, b) => a.type.rank - b.type.rank));
|
|
160
|
-
for (let i = 0; i < run.text.length; i++) units.push(key);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
out.c = text;
|
|
164
|
-
if (units.some(Boolean)) out.m = runsOf(units);
|
|
193
|
+
const known = new Set(attrKeys(type));
|
|
194
|
+
for (const [key, value] of attributeFields(block, attrs)) {
|
|
195
|
+
if (key === "t" || key === "pos" || known.has(key) || Object.hasOwn(out, key)) continue;
|
|
196
|
+
out[key] = copy(value);
|
|
165
197
|
}
|
|
198
|
+
if (type.isTextblock) Object.assign(out, textOf(block));
|
|
166
199
|
return out;
|
|
167
200
|
}
|
|
168
201
|
|
package/src/page/ybind/doc.js
CHANGED
|
@@ -14,8 +14,24 @@ export const fieldsOf = (pd, b, type) => {
|
|
|
14
14
|
return out;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
const copy = (value) => (value === undefined ? value : JSON.parse(JSON.stringify(value)));
|
|
18
|
+
|
|
19
|
+
// Every attribute a block record holds, known or not, without the structural
|
|
20
|
+
// position: the record an opaque block keeps and writes back unchanged.
|
|
21
|
+
const rawOf = (pd, b) => {
|
|
22
|
+
const out = {};
|
|
23
|
+
const m = pd.attrs.get(b.baseId);
|
|
24
|
+
if (m) m.forEach((value, key) => { if (key !== "pos") out[key] = copy(value); });
|
|
25
|
+
out.t = b.t;
|
|
26
|
+
return out;
|
|
27
|
+
};
|
|
28
|
+
|
|
17
29
|
export function docOf(pd, w) {
|
|
18
30
|
const blocks = w.visible.map((b) => {
|
|
31
|
+
if (b.opaque) {
|
|
32
|
+
const fields = b.recovered ? { ...copy(b.fields ?? {}) } : rawOf(pd, b);
|
|
33
|
+
return { b, type: schema.nodes.opaque_block, fields, opaque: true };
|
|
34
|
+
}
|
|
19
35
|
const type = schema.nodes[b.t] ?? schema.nodes.paragraph;
|
|
20
36
|
return { b, type, fields: { ...(b.fields ?? fieldsOf(pd, b, type)) } };
|
|
21
37
|
});
|
|
@@ -42,9 +58,18 @@ export function docOf(pd, w) {
|
|
|
42
58
|
});
|
|
43
59
|
byId.get(lowest).fields.parent = null;
|
|
44
60
|
}
|
|
45
|
-
const nodes = blocks.map(({ b, type, fields }) => {
|
|
61
|
+
const nodes = blocks.map(({ b, type, fields, opaque }) => {
|
|
62
|
+
if (opaque) {
|
|
63
|
+
const content = [];
|
|
64
|
+
for (const r of b.runs) {
|
|
65
|
+
if (r.hole) continue;
|
|
66
|
+
if (r.br) content.push(schema.nodes.hard_break.create());
|
|
67
|
+
else content.push(schema.text(r.text, r.marks));
|
|
68
|
+
}
|
|
69
|
+
return type.create({ id: b.id, pos: null, raw: fields }, content);
|
|
70
|
+
}
|
|
46
71
|
const attrs = { id: b.id, pos: null, ...fields };
|
|
47
|
-
if (type.name === "table" && Object.keys(attrs.rows ?? {}).length) return nodeFromBlock(schema, b.id, { t: "table", ...attrs });
|
|
72
|
+
if (type.name === "table" && Object.keys(attrs.rows ?? {}).length && Object.keys(attrs.cols ?? {}).length) return nodeFromBlock(schema, b.id, { t: "table", ...attrs });
|
|
48
73
|
if (!type.isTextblock) return type.create(attrs);
|
|
49
74
|
const content = [];
|
|
50
75
|
for (const r of b.runs) {
|
package/src/page/ybind/group.js
CHANGED
|
@@ -6,6 +6,7 @@ import { LOCAL, HISTORY_META, attrKeys, defaultOf, mintId, mintOp } from "./layo
|
|
|
6
6
|
import { tokensOf, tokenIndex, textTokenRange, projectionOf, yAfter, sameTok } from "./tokens.js";
|
|
7
7
|
import { CHUNK_CHARS, insertTokens, applyRegion, markRange } from "./writer.js";
|
|
8
8
|
import { walk } from "./walk.js";
|
|
9
|
+
import { isOpaque, yjsFields } from "../format-codec.js";
|
|
9
10
|
|
|
10
11
|
const { AddMarkStep, RemoveMarkStep, AttrStep, ReplaceStep, Mapping } = transform;
|
|
11
12
|
|
|
@@ -23,7 +24,7 @@ const touchesTable = (doc, from, to) => {
|
|
|
23
24
|
return found;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
export function localGroup(pd, transactions, w, stats, um = null, history = true, onFirstWrite = null) {
|
|
27
|
+
export function localGroup(pd, transactions, w, stats, um = null, history = true, onFirstWrite = null, { join = false, open = false } = {}) {
|
|
27
28
|
const first = () => { if (onFirstWrite) { const fn = onFirstWrite; onFirstWrite = null; fn(); } };
|
|
28
29
|
const steps = [], docs = [], maps = [];
|
|
29
30
|
for (const tr of transactions) tr.steps.forEach((s, i) => { steps.push(s); docs.push(tr.docs[i]); maps.push(s.getMap()); });
|
|
@@ -106,10 +107,18 @@ export function localGroup(pd, transactions, w, stats, um = null, history = true
|
|
|
106
107
|
if (to <= from) continue;
|
|
107
108
|
const name = step.mark.type.name;
|
|
108
109
|
const val = step instanceof AddMarkStep ? (step.mark.attrs && Object.keys(step.mark.attrs).length ? { ...step.mark.attrs } : true) : null;
|
|
110
|
+
// An opaque mark is not a live key: adding it writes each descriptor it
|
|
111
|
+
// carries, and removing it erases nothing the command cannot understand.
|
|
112
|
+
if (isOpaque(step.mark)) {
|
|
113
|
+
if (val === null) continue;
|
|
114
|
+
const fields = yjsFields(step.mark.attrs.entries);
|
|
115
|
+
tx(() => { for (const [key, value] of Object.entries(fields)) markRange(markCtx, from, to, key, value, at); });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
109
118
|
tx(() => markRange(markCtx, from, to, name, val, at));
|
|
110
119
|
}
|
|
111
120
|
};
|
|
112
|
-
um?.stopCapturing();
|
|
121
|
+
if (!join) um?.stopCapturing();
|
|
113
122
|
const timeout = um?.captureTimeout;
|
|
114
123
|
if (um) um.captureTimeout = Infinity;
|
|
115
124
|
try {
|
|
@@ -117,7 +126,7 @@ export function localGroup(pd, transactions, w, stats, um = null, history = true
|
|
|
117
126
|
else pd.ydoc.transact((tr) => { tr.meta.set(HISTORY_META, history); first(); write(); }, LOCAL);
|
|
118
127
|
} finally {
|
|
119
128
|
if (um) um.captureTimeout = timeout;
|
|
120
|
-
um?.stopCapturing();
|
|
129
|
+
if (!open) um?.stopCapturing();
|
|
121
130
|
}
|
|
122
131
|
stats.groups++;
|
|
123
132
|
stats.lastGroup = { chunked, structural, regional, span };
|
package/src/page/ybind/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export * from "./tokens.js";
|
|
|
3
3
|
export * from "./writer.js";
|
|
4
4
|
export * from "./group.js";
|
|
5
5
|
export * from "./bridge.js";
|
|
6
|
+
export * from "./positions.js";
|
|
7
|
+
export * from "./awareness.js";
|
|
6
8
|
export * from "./seed.js";
|
|
7
9
|
export * from "./origins.js";
|
|
8
10
|
export * from "./recovery.js";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as Y from "../../vendor/yjs.vendor.js";
|
|
2
|
+
import { parseModel } from "../projection.js";
|
|
3
|
+
import { blockFromItem } from "../adopt.js";
|
|
4
|
+
import { seedClientId } from "./migrate.js";
|
|
5
|
+
import { entriesOf } from "./parts.js";
|
|
6
|
+
import { digestHex } from "./recovery-proof.js";
|
|
7
|
+
|
|
8
|
+
// The seed client of an article: the Yjs client id seedPageFromArticle would write,
|
|
9
|
+
// without building the seed. Two articles with the same blocks share it.
|
|
10
|
+
export function seedLineage(article) {
|
|
11
|
+
const items = parseModel(article, { seed: article.getAttribute("data-rev") ?? "" });
|
|
12
|
+
if (!items.length) return { client: null };
|
|
13
|
+
const blocks = {};
|
|
14
|
+
items.forEach((item, i) => {
|
|
15
|
+
const block = blockFromItem(item, null);
|
|
16
|
+
block.pos = String(i + 1).padStart(8, "0");
|
|
17
|
+
blocks[item.id] = block;
|
|
18
|
+
});
|
|
19
|
+
return { client: seedClientId(blocks) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function valueClients(value) {
|
|
23
|
+
const clients = new Set();
|
|
24
|
+
for (const [, text] of entriesOf(value?.y ?? {})) {
|
|
25
|
+
try {
|
|
26
|
+
for (const client of Y.decodeStateVector(Y.encodeStateVectorFromUpdateV2(Y.fromBase64(text))).keys()) clients.add(client);
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
return clients;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sharesLineage(stored, incoming) {
|
|
33
|
+
const theirs = valueClients(stored);
|
|
34
|
+
const mine = valueClients(incoming);
|
|
35
|
+
if (!theirs.size || !mine.size || [...mine].some((client) => theirs.has(client))) return true;
|
|
36
|
+
const kept = new Map(entriesOf(stored?.y ?? {}));
|
|
37
|
+
return entriesOf(incoming?.y ?? {}).some(([key, text]) => kept.get(key) === text);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// May a journal written under `a` be replayed into a page under `b`? Same page
|
|
41
|
+
// generation, and the same base bytes: an externally replaced article is another
|
|
42
|
+
// generation even when it shows the same text.
|
|
43
|
+
export function sameLineage(a, b) {
|
|
44
|
+
if (!a || !b || !a.generation || !b.generation) return false;
|
|
45
|
+
return a.generation === b.generation && a.baseDigest === b.baseDigest;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The lineage a capsule records for its immutable base: the digest the host persists
|
|
49
|
+
// and the state vector the page's own document had at the same moment.
|
|
50
|
+
export async function lineageOf(generation, base) {
|
|
51
|
+
const bytes = base instanceof Uint8Array ? base : new Uint8Array(0);
|
|
52
|
+
return {
|
|
53
|
+
generation,
|
|
54
|
+
baseDigest: await digestHex(bytes),
|
|
55
|
+
baseVector: bytes.length ? Y.encodeStateVectorFromUpdateV2(bytes) : new Uint8Array(0),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The existing ancestry check, with the empty case refused: an update with no client of
|
|
60
|
+
// its own shares lineage with nothing, and neither does a page with no clients.
|
|
61
|
+
export function sharesAncestry(stored, incoming) {
|
|
62
|
+
const theirs = valueClients(stored);
|
|
63
|
+
const mine = valueClients(incoming);
|
|
64
|
+
if (!theirs.size || !mine.size) return false;
|
|
65
|
+
return sharesLineage(stored, incoming);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function seedOnly(doc, client) {
|
|
69
|
+
for (const id of Y.decodeStateVector(Y.encodeStateVector(doc)).keys()) if (id !== client) return false;
|
|
70
|
+
return Y.createDeleteSetFromStructStore(doc.store).clients.size === 0;
|
|
71
|
+
}
|