@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,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* undo.js — the durable half of a refused undo step.
|
|
3
|
+
*
|
|
4
|
+
* The undo stack belongs to this tab and this editing session: a reload starts with an
|
|
5
|
+
* empty history, and the entry a person tried to take back is live DOM nodes and a
|
|
6
|
+
* compiled replay plan, neither of which a reload can use. What a reload can keep is
|
|
7
|
+
* the losing side as plain data — the label, the direction, why it was refused, the
|
|
8
|
+
* targets with the page's own name for each, and every step's before and after.
|
|
9
|
+
*
|
|
10
|
+
* One internal channel, `clay.undo`, on the shared recovery service: registered the
|
|
11
|
+
* first time a step is refused or the page keeps one of its own, never at boot and
|
|
12
|
+
* never by a reader. A refused record is never the kind a save can cover, and this
|
|
13
|
+
* channel registers nothing a save could read back, so a normal save dismisses
|
|
14
|
+
* nothing here that a person has not dealt with. A record is still only a recovery
|
|
15
|
+
* copy: it is not a save and the page never renders from it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { representedEtag } from '../core/etag.js';
|
|
19
|
+
import { storage } from '../lib/doc-storage.js';
|
|
20
|
+
import { plainCopy, storedContext } from './conflicts.js';
|
|
21
|
+
|
|
22
|
+
export const UNDO_CHANNEL = 'clay.undo';
|
|
23
|
+
export const UNDO_VERSION = 1;
|
|
24
|
+
export const UNDO_KIND = 'conflict';
|
|
25
|
+
export const UNDO_TEXT = 'text';
|
|
26
|
+
|
|
27
|
+
// The same cheap read the conflicts channel keeps for the notice's shell: one small
|
|
28
|
+
// count in this document's own storage, so the next edit session learns that copies
|
|
29
|
+
// are waiting without opening the database. It is written here, on stage and on
|
|
30
|
+
// dismissal, and never read from the reader path, which loads neither module.
|
|
31
|
+
const HINT_KEY = 'undo';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How many copies this document's store is holding for review, without opening the
|
|
35
|
+
* database. Zero means a boot has nothing to show.
|
|
36
|
+
*
|
|
37
|
+
* @returns {number}
|
|
38
|
+
*/
|
|
39
|
+
export function pendingUndo() {
|
|
40
|
+
const stored = storage.local.get(HINT_KEY);
|
|
41
|
+
return Number.isFinite(stored?.count) && stored.count > 0 ? stored.count : 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function noteHint(count) {
|
|
45
|
+
if (count > 0) storage.local.set(HINT_KEY, { count, at: Date.now() });
|
|
46
|
+
else storage.local.remove(HINT_KEY);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function recoveryService() {
|
|
50
|
+
const clay = globalThis.window?.clay ?? globalThis.clay;
|
|
51
|
+
return clay?.recovery ?? null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let registered = null;
|
|
55
|
+
|
|
56
|
+
function channel() {
|
|
57
|
+
const service = recoveryService();
|
|
58
|
+
if (!service || typeof service.register !== 'function') return null;
|
|
59
|
+
if (registered?.service === service) return registered.channel;
|
|
60
|
+
try {
|
|
61
|
+
registered = {
|
|
62
|
+
service,
|
|
63
|
+
channel: service.register({ channel: UNDO_CHANNEL, version: UNDO_VERSION }),
|
|
64
|
+
};
|
|
65
|
+
} catch (_) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return registered.channel;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// One refused step and one direction are one record: the key is what a repeated click
|
|
72
|
+
// lands on, and what lets the older copy go.
|
|
73
|
+
const keyOf = (entryId, direction) => `refused:${entryId}:${direction}`;
|
|
74
|
+
|
|
75
|
+
function elementFor(id) {
|
|
76
|
+
if (id == null || typeof document === 'undefined') return null;
|
|
77
|
+
const escaped = globalThis.CSS?.escape ? globalThis.CSS.escape(String(id)) : String(id).replace(/["\\]/g, '\\$&');
|
|
78
|
+
try {
|
|
79
|
+
return document.querySelector(`[data-id="${escaped}"]`);
|
|
80
|
+
} catch (_) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The registry itself stays with the ledger: `clay.conflicts` is where a page registered
|
|
86
|
+
// its resolver, and asking it here costs no module the reader carries and no download.
|
|
87
|
+
function pageRegistry() {
|
|
88
|
+
const clay = globalThis.window?.clay ?? globalThis.clay;
|
|
89
|
+
const conflicts = clay?.conflicts;
|
|
90
|
+
return typeof conflicts?.contextFor === 'function' ? conflicts : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The page's name for one refused target, asked of the same resolver registry the
|
|
95
|
+
* conflict notice uses — the ledger's own `contextFor`, so the order, the throw and the
|
|
96
|
+
* fields kept are one behaviour and not two.
|
|
97
|
+
*
|
|
98
|
+
* The subject is the target's id and the live element that id finds, which a resolver
|
|
99
|
+
* written for the conflict ledger answers nothing for: it reads the record's own nodes,
|
|
100
|
+
* and a refused step has none. Such a page gets the generic shape until it names this
|
|
101
|
+
* subject too.
|
|
102
|
+
*
|
|
103
|
+
* @param {{id: ?string}} target
|
|
104
|
+
* @returns {Object}
|
|
105
|
+
*/
|
|
106
|
+
export function contextForTarget({ id = null } = {}) {
|
|
107
|
+
const target = id == null ? null : String(id);
|
|
108
|
+
const registry = pageRegistry();
|
|
109
|
+
const found = registry ? registry.contextFor({ source: 'undo', id: target, element: elementFor(target) }, null) : null;
|
|
110
|
+
return found ?? { kind: null, id: target, label: null, field: null };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// One context per refused target, in the targets' own order.
|
|
114
|
+
function contextsOf(targets) {
|
|
115
|
+
return targets.map((target) => storedContext(contextForTarget(target)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A step refused twice is the same losing material. The older record goes only once
|
|
119
|
+
// the newer one is durable, and only at the revision this tab can see, so a copy
|
|
120
|
+
// another tab replaced or a person dismissed is never removed by a guess.
|
|
121
|
+
async function supersede(ch, key, id) {
|
|
122
|
+
let listed;
|
|
123
|
+
try {
|
|
124
|
+
listed = await ch.list();
|
|
125
|
+
} catch (_) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!listed?.ok) return;
|
|
129
|
+
for (const record of listed.records) {
|
|
130
|
+
if (record.key !== key || String(record.id) === String(id)) continue;
|
|
131
|
+
try {
|
|
132
|
+
await ch.dismiss(record.id, { revision: record.revision });
|
|
133
|
+
} catch (_) {}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The whole stage, flush and supersede of one refusal, one call at a time: two refusals
|
|
138
|
+
// in the same tick would otherwise each delete the other's record as the older copy.
|
|
139
|
+
let keeping = Promise.resolve();
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Keep what a refused undo or redo would have restored, before anything in this tab
|
|
143
|
+
* can be lost to a reload.
|
|
144
|
+
*
|
|
145
|
+
* The engine's own `describe(entryId)` is the source: it is plain data already, with
|
|
146
|
+
* the refusal on it and every step's endpoints, so nothing here reads a DOM node or a
|
|
147
|
+
* replay plan. A store that refuses the write leaves the copy in memory — the service
|
|
148
|
+
* keeps it — and answers with the failure.
|
|
149
|
+
*
|
|
150
|
+
* @param {Object} engine - the started undo scope (`clay.undo`)
|
|
151
|
+
* @param {{entryId: string, direction: string, code?: string}} receipt
|
|
152
|
+
* @returns {Promise<{ok: boolean, code?: string}>}
|
|
153
|
+
*/
|
|
154
|
+
export function keepRefusal(engine, receipt) {
|
|
155
|
+
const run = keeping.then(() => keepRefusalNow(engine, receipt));
|
|
156
|
+
keeping = run.catch(() => {});
|
|
157
|
+
return run;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function keepRefusalNow(engine, receipt) {
|
|
161
|
+
const ch = channel();
|
|
162
|
+
if (!ch) return { ok: false, code: 'unavailable' };
|
|
163
|
+
let described = null;
|
|
164
|
+
try {
|
|
165
|
+
described = engine?.describe?.(receipt?.entryId) ?? null;
|
|
166
|
+
} catch (_) {
|
|
167
|
+
described = null;
|
|
168
|
+
}
|
|
169
|
+
if (!described) return { ok: false, code: 'unknown-entry' };
|
|
170
|
+
const refused = described.refused && typeof described.refused === 'object' ? described.refused : {};
|
|
171
|
+
const direction = receipt?.direction ?? refused.direction ?? null;
|
|
172
|
+
const targets = (Array.isArray(refused.targets) ? refused.targets : []).map((target) => ({
|
|
173
|
+
id: target?.id == null ? null : String(target.id),
|
|
174
|
+
kind: target?.kind == null ? null : String(target.kind),
|
|
175
|
+
}));
|
|
176
|
+
const key = keyOf(described.id ?? receipt?.entryId, direction);
|
|
177
|
+
const payload = plainCopy({
|
|
178
|
+
label: described.label ?? null,
|
|
179
|
+
direction,
|
|
180
|
+
code: receipt?.code ?? refused.code ?? null,
|
|
181
|
+
targets,
|
|
182
|
+
context: contextsOf(targets),
|
|
183
|
+
steps: described.steps ?? [],
|
|
184
|
+
}, [], '');
|
|
185
|
+
let staged;
|
|
186
|
+
try {
|
|
187
|
+
staged = ch.stage({ key, kind: UNDO_KIND, baseEtag: representedEtag() ?? null, base: null, payload });
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error('clayjs: the refused step could not be kept for a later reload; it is still in this tab\'s history', err);
|
|
190
|
+
return { ok: false, code: 'error' };
|
|
191
|
+
}
|
|
192
|
+
const written = await ch.flush();
|
|
193
|
+
if (written?.ok) {
|
|
194
|
+
await supersede(ch, key, staged.id);
|
|
195
|
+
await kept();
|
|
196
|
+
}
|
|
197
|
+
return written?.ok ? written : { ok: false, code: written?.code || 'error' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Keep one of the page's own records on the same channel: an app's unsent text or its
|
|
202
|
+
* own unreviewed conflict material, staged under `app:<key>`.
|
|
203
|
+
*
|
|
204
|
+
* @param {{key: string, label?: ?string, context?: ?Object, payload?: ?Object}} spec
|
|
205
|
+
* @returns {Promise<{ok: boolean, code?: string}>}
|
|
206
|
+
*/
|
|
207
|
+
export async function keep({ key, label = null, context = null, payload = null } = {}) {
|
|
208
|
+
const ch = channel();
|
|
209
|
+
if (!ch) return { ok: false, code: 'unavailable' };
|
|
210
|
+
const name = key == null ? '' : String(key);
|
|
211
|
+
if (!name) return { ok: false, code: 'invalid' };
|
|
212
|
+
const body = payload && typeof payload === 'object' ? payload : {};
|
|
213
|
+
const record = plainCopy({ ...body, label, context: storedContext(context) }, [], '');
|
|
214
|
+
try {
|
|
215
|
+
ch.stage({ key: `app:${name}`, kind: typeof body.text === 'string' ? UNDO_TEXT : UNDO_KIND, payload: record });
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('clayjs: the app\'s recovery copy could not be kept; it is still in this tab only', err);
|
|
218
|
+
return { ok: false, code: 'error' };
|
|
219
|
+
}
|
|
220
|
+
const written = await ch.flush();
|
|
221
|
+
if (written?.ok) await kept();
|
|
222
|
+
return written;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* What this document's store holds for review, from every writer: a record belongs to
|
|
227
|
+
* the document, not to the tab that made it.
|
|
228
|
+
*
|
|
229
|
+
* @returns {Promise<{ok: boolean, code?: string, records: Object[], status?: Object}>}
|
|
230
|
+
*/
|
|
231
|
+
export async function kept() {
|
|
232
|
+
const ch = channel();
|
|
233
|
+
if (!ch) return { ok: true, records: [] };
|
|
234
|
+
const listed = await ch.list();
|
|
235
|
+
if (listed?.ok) noteHint(Array.isArray(listed.records) ? listed.records.length : 0);
|
|
236
|
+
return listed;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Remove one kept record, at the revision the reviewer was looking at. A record that
|
|
241
|
+
* moved since then is left exactly as it is.
|
|
242
|
+
*
|
|
243
|
+
* @param {*} id
|
|
244
|
+
* @param {{revision?: number}} [options]
|
|
245
|
+
* @returns {Promise<{ok: boolean, code?: string}>}
|
|
246
|
+
*/
|
|
247
|
+
export async function dismissKept(id, { revision } = {}) {
|
|
248
|
+
const ch = channel();
|
|
249
|
+
if (!ch) return { ok: false, code: 'unavailable' };
|
|
250
|
+
const receipt = await ch.dismiss(id, { revision });
|
|
251
|
+
if (receipt?.ok) {
|
|
252
|
+
try {
|
|
253
|
+
await kept();
|
|
254
|
+
} catch (_) {}
|
|
255
|
+
}
|
|
256
|
+
return receipt;
|
|
257
|
+
}
|
|
@@ -3,12 +3,14 @@ import { parseJsonRelaxed } from 'hyper-morph/json-parse';
|
|
|
3
3
|
import { cloneJson, formatBy, serializeLive, stampOf, textHash, unionRanges } from '../rooms/index.js';
|
|
4
4
|
import { blocksFromYValue } from '../../page/ybind/decode.js';
|
|
5
5
|
import { YDecodeError } from '../../page/ybind/provider.js';
|
|
6
|
-
import { canon, fragmentCleaner, renderBlocks } from '../../page/projection.js';
|
|
6
|
+
import { MARKS, OPAQUE, canon, fragmentCleaner, renderBlocks } from '../../page/projection.js';
|
|
7
7
|
import { classifyFile, readBack } from '../../page/adopt.js';
|
|
8
8
|
import { merge3, mergeJson } from 'hyper-morph';
|
|
9
9
|
import { mergePageModels } from '../../page/merge.js';
|
|
10
|
+
import { documentTitle } from '../../page/document-title.js';
|
|
10
11
|
|
|
11
12
|
const PAGE = /^page(\.[0-9]+)?$/;
|
|
13
|
+
const TITLE_MODE = 'clay-seo-title-mode';
|
|
12
14
|
const NAME = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
13
15
|
const attr = (node, name) => node.attrs.find((item) => item.name === name)?.value;
|
|
14
16
|
const escapeAttr = (value) => value.replace(/&/g, '&').replace(/"/g, '"');
|
|
@@ -36,9 +38,19 @@ function inspect(html) {
|
|
|
36
38
|
const tags = new Map();
|
|
37
39
|
const duplicates = new Set();
|
|
38
40
|
const articles = [];
|
|
41
|
+
const titleModes = [];
|
|
42
|
+
let head = null;
|
|
43
|
+
let body = null;
|
|
44
|
+
let title = null;
|
|
39
45
|
function visit(node, inert = false) {
|
|
40
46
|
if (!node.tagName) { for (const child of node.childNodes ?? []) visit(child, inert); return; }
|
|
41
47
|
const blocked = inert || node.tagName === 'template' || node.tagName === 'noscript';
|
|
48
|
+
if (!blocked && node.tagName === 'head') head = node;
|
|
49
|
+
if (!blocked && node.tagName === 'body') body = node;
|
|
50
|
+
if (!blocked && node.parentNode === head && node.tagName === 'title' && !title) title = node;
|
|
51
|
+
if (!blocked && node.parentNode === head && node.tagName === 'meta' && (attr(node, 'name') ?? '').trim().toLowerCase() === TITLE_MODE) {
|
|
52
|
+
titleModes.push(attr(node, 'content'));
|
|
53
|
+
}
|
|
42
54
|
if (!blocked && node.tagName === 'article' && attr(node, 'data-page') !== undefined) articles.push(node);
|
|
43
55
|
if (!blocked && node.tagName === 'script' && attr(node, 'src') === undefined &&
|
|
44
56
|
(attr(node, 'clay') ?? '').split(/\s+/).includes('live')) {
|
|
@@ -54,7 +66,7 @@ function inspect(html) {
|
|
|
54
66
|
}
|
|
55
67
|
visit(root);
|
|
56
68
|
for (const name of duplicates) tags.delete(name);
|
|
57
|
-
return { article: articles.length === 1 ? articles[0] : null, tags };
|
|
69
|
+
return { article: articles.length === 1 ? articles[0] : null, tags, head, body, title, titleModes };
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
function startTag(html, node, store) {
|
|
@@ -87,7 +99,7 @@ function projection(capture) {
|
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
function placements(html, capture) {
|
|
90
|
-
const { article, tags } = inspect(html);
|
|
102
|
+
const { article, tags, head, body, title, titleModes } = inspect(html);
|
|
91
103
|
if (!article?.sourceCodeLocation?.startTag || !article.sourceCodeLocation.endTag ||
|
|
92
104
|
capture.stores.some((store) => !tags.get(store.name)?.sourceCodeLocation?.endTag ||
|
|
93
105
|
(attr(tags.get(store.name), 'clay') ?? '').split(/\s+/).includes('ephemeral'))) return null;
|
|
@@ -97,7 +109,7 @@ function placements(html, capture) {
|
|
|
97
109
|
const tag = tags.get(store.name).sourceCodeLocation;
|
|
98
110
|
return tag.startOffset < articleEnd && tag.endOffset > articleStart;
|
|
99
111
|
})) return null;
|
|
100
|
-
return { article, tags };
|
|
112
|
+
return { article, tags, head, body, title, titleModes };
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
function replace(html, edits) {
|
|
@@ -105,6 +117,46 @@ function replace(html, edits) {
|
|
|
105
117
|
out.slice(0, edit.start) + edit.text + out.slice(edit.end), html);
|
|
106
118
|
}
|
|
107
119
|
|
|
120
|
+
const customTitleOf = (titleModes) =>
|
|
121
|
+
titleModes.some((content) => String(content ?? '').trim().toLowerCase() === 'custom');
|
|
122
|
+
|
|
123
|
+
const escapeTitle = (value) => value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
124
|
+
|
|
125
|
+
function titleTextOf(node) {
|
|
126
|
+
let out = '';
|
|
127
|
+
const walk = (current) => {
|
|
128
|
+
for (const child of current.childNodes ?? []) {
|
|
129
|
+
if (child.nodeName === '#text') out += child.value;
|
|
130
|
+
else walk(child);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
if (node) walk(node);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A title with no element of its own goes after the head's start tag, else before its
|
|
138
|
+
// first child, else before the body starts.
|
|
139
|
+
function titleAt(placement) {
|
|
140
|
+
const head = placement.head?.sourceCodeLocation;
|
|
141
|
+
if (head?.startTag) return head.startTag.endOffset;
|
|
142
|
+
const inside = (placement.head?.childNodes ?? []).find((node) => node.sourceCodeLocation);
|
|
143
|
+
if (inside) return inside.sourceCodeLocation.startOffset;
|
|
144
|
+
const body = placement.body?.sourceCodeLocation;
|
|
145
|
+
if (body?.startTag) return body.startTag.startOffset;
|
|
146
|
+
const first = (placement.body?.childNodes ?? []).find((node) => node.sourceCodeLocation);
|
|
147
|
+
return first ? first.sourceCodeLocation.startOffset : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function titleEdit(placement, title) {
|
|
151
|
+
if (placement.title) {
|
|
152
|
+
const loc = placement.title.sourceCodeLocation;
|
|
153
|
+
if (!loc?.startTag || !loc.endTag) return null;
|
|
154
|
+
return { start: loc.startTag.endOffset, end: loc.endTag.startOffset, text: escapeTitle(title) };
|
|
155
|
+
}
|
|
156
|
+
const at = titleAt(placement);
|
|
157
|
+
return at === null ? null : { start: at, end: at, text: `<title>${escapeTitle(title)}</title>` };
|
|
158
|
+
}
|
|
159
|
+
|
|
108
160
|
// Whether the room's blocks already carry everything the hand-edited article
|
|
109
161
|
// changed since its tag was written: merging those changes in changes nothing.
|
|
110
162
|
function holdsEdit(found, blocks, doc) {
|
|
@@ -151,6 +203,11 @@ function adoptionNeeded(html, createDocument) {
|
|
|
151
203
|
}
|
|
152
204
|
}
|
|
153
205
|
|
|
206
|
+
// An article that carries a semantic envelope holds content this renderer cannot
|
|
207
|
+
// project from the room's blocks. Rewriting it would drop that content, so the
|
|
208
|
+
// checkpoint refuses instead, leaving the file as it stands.
|
|
209
|
+
const holdsEnvelope = (html) => html.includes(`${OPAQUE}="`) || html.includes(`${MARKS}="`);
|
|
210
|
+
|
|
154
211
|
// recovery: render the room's blocks even over an article it has not merged, for
|
|
155
212
|
// a recovered copy of its edits (the file itself keeps the outside edit).
|
|
156
213
|
export function renderCheckpoint(currentHtml, capture, { createDocument, recovery = false }) {
|
|
@@ -175,7 +232,9 @@ export function renderCheckpoint(currentHtml, capture, { createDocument, recover
|
|
|
175
232
|
// A tab that merged this article says so on the frame that carried the
|
|
176
233
|
// merge; otherwise the blocks must show the edit.
|
|
177
234
|
if (!recovery && found.kind === 'adopt' && capture.adopted !== found.A && !holdsEdit(found, decoded.blocks, doc)) return { kind: 'refused', reason: 'needs-adoption' };
|
|
235
|
+
const current = currentHtml.slice(articleLoc.startTag.endOffset, articleLoc.endTag.startOffset);
|
|
178
236
|
rendered = renderBlocks(decoded.blocks, { clean: fragmentCleaner(doc) });
|
|
237
|
+
if (current !== rendered.html && holdsEnvelope(current)) return { kind: 'refused', reason: 'unknown-page-content' };
|
|
179
238
|
}
|
|
180
239
|
catch (error) {
|
|
181
240
|
if (error instanceof YDecodeError || error instanceof SyntaxError) return { kind: 'refused', reason: 'invalid-document' };
|
|
@@ -196,6 +255,10 @@ export function renderCheckpoint(currentHtml, capture, { createDocument, recover
|
|
|
196
255
|
articleOpen.slice(revLoc.endOffset - articleLoc.startTag.startOffset) : articleOpen;
|
|
197
256
|
edits.push({ start: articleLoc.startTag.startOffset, end: articleLoc.startTag.endOffset, text: articleStart.slice(0, -1) + rev + '>' });
|
|
198
257
|
edits.push({ start: articleLoc.startTag.endOffset, end: articleLoc.endTag.startOffset, text: rendered.html });
|
|
258
|
+
if (!customTitleOf(placement.titleModes)) {
|
|
259
|
+
const edit = titleEdit(placement, documentTitle(rendered.model));
|
|
260
|
+
if (edit) edits.push(edit);
|
|
261
|
+
}
|
|
199
262
|
return { kind: 'ok', html: replace(currentHtml, edits), revision: rendered.rev };
|
|
200
263
|
}
|
|
201
264
|
|
|
@@ -208,6 +271,7 @@ export function isCheckpointCurrent(currentHtml, capture) {
|
|
|
208
271
|
const articleLoc = placement.article.sourceCodeLocation;
|
|
209
272
|
if (attr(placement.article, 'data-rev') !== rendered.rev ||
|
|
210
273
|
currentHtml.slice(articleLoc.startTag.endOffset, articleLoc.endTag.startOffset) !== rendered.html) return false;
|
|
274
|
+
if (!customTitleOf(placement.titleModes) && titleTextOf(placement.title) !== documentTitle(rendered.model)) return false;
|
|
211
275
|
return capture.stores.every((store) => {
|
|
212
276
|
const node = placement.tags.get(store.name);
|
|
213
277
|
const loc = node.sourceCodeLocation;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { parse } from "../../vendor/parse5.vendor.js";
|
|
2
|
+
import {
|
|
3
|
+
canonicalizeRuntimeURL,
|
|
4
|
+
parseRuntimeURL,
|
|
5
|
+
resolveRuntimeURL,
|
|
6
|
+
} from "../../lib/offline-runtime.js";
|
|
7
|
+
|
|
8
|
+
export { canonicalizeRuntimeURL, parseRuntimeURL, resolveRuntimeURL };
|
|
9
|
+
|
|
10
|
+
export const DESCRIPTOR_ID = "clay-runtime-map";
|
|
11
|
+
|
|
12
|
+
const RUNTIME_REL = new Set(["stylesheet", "preload", "modulepreload"]);
|
|
13
|
+
|
|
14
|
+
function attributeOf(node, name) {
|
|
15
|
+
for (const attr of node.attrs ?? []) {
|
|
16
|
+
if (attr.name === name) return attr;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isCandidate(node) {
|
|
22
|
+
if (node.tagName === "script") return Boolean(attributeOf(node, "src"));
|
|
23
|
+
if (node.tagName !== "link") return false;
|
|
24
|
+
if (!attributeOf(node, "href")) return false;
|
|
25
|
+
const rel = String(attributeOf(node, "rel")?.value ?? "").toLowerCase();
|
|
26
|
+
return rel.split(/\s+/).some((token) => RUNTIME_REL.has(token));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function valueBounds(raw) {
|
|
30
|
+
const eq = raw.indexOf("=");
|
|
31
|
+
if (eq < 0) return null;
|
|
32
|
+
let index = eq + 1;
|
|
33
|
+
while (index < raw.length && /\s/.test(raw[index])) index += 1;
|
|
34
|
+
const quote = raw[index] === '"' || raw[index] === "'" ? raw[index] : "";
|
|
35
|
+
if (!quote) return { start: index, end: raw.length, quote };
|
|
36
|
+
const close = raw.lastIndexOf(quote);
|
|
37
|
+
return { start: index + 1, end: close > index ? close : raw.length, quote };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function elements(doc) {
|
|
41
|
+
const found = [];
|
|
42
|
+
const walk = (node) => {
|
|
43
|
+
if (node.tagName) found.push(node);
|
|
44
|
+
for (const child of node.childNodes ?? []) walk(child);
|
|
45
|
+
if (node.content) walk(node.content);
|
|
46
|
+
};
|
|
47
|
+
walk(doc);
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function candidates(doc, html) {
|
|
52
|
+
const found = [];
|
|
53
|
+
for (const node of elements(doc)) {
|
|
54
|
+
if (!isCandidate(node)) continue;
|
|
55
|
+
const name = node.tagName === "script" ? "src" : "href";
|
|
56
|
+
const location = node.sourceCodeLocation?.attrs?.[name];
|
|
57
|
+
if (!location) continue;
|
|
58
|
+
const bounds = valueBounds(html.slice(location.startOffset, location.endOffset));
|
|
59
|
+
if (!bounds) continue;
|
|
60
|
+
found.push({
|
|
61
|
+
node,
|
|
62
|
+
value: attributeOf(node, name).value,
|
|
63
|
+
raw: html.slice(location.startOffset + bounds.start, location.startOffset + bounds.end),
|
|
64
|
+
valueStart: location.startOffset + bounds.start,
|
|
65
|
+
valueEnd: location.startOffset + bounds.end,
|
|
66
|
+
startOffset: node.sourceCodeLocation.startTag?.startOffset ?? node.sourceCodeLocation.startOffset,
|
|
67
|
+
quote: bounds.quote,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function applySplices(html, splices) {
|
|
74
|
+
const ordered = [...splices].sort((a, b) => b.start - a.start);
|
|
75
|
+
let out = html;
|
|
76
|
+
for (const splice of ordered) {
|
|
77
|
+
out = out.slice(0, splice.start) + splice.text + out.slice(splice.end);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function attributeText(value, quote) {
|
|
83
|
+
const text = value.replace(/&/g, "&");
|
|
84
|
+
if (quote === "'") return text.replace(/'/g, "'");
|
|
85
|
+
if (quote === '"') return text.replace(/"/g, """);
|
|
86
|
+
return /[\s"'<>=`]/.test(text) ? `"${text.replace(/"/g, """)}"` : text;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The attribute text for `replacement`, keeping the file's own spelling of the tail it shares with
|
|
90
|
+
// `value`, so a query written with a bare `&` keeps it. `raw` is how `value` is written in the file.
|
|
91
|
+
// A shared head that holds `&` could split a character reference, so that case escapes everything.
|
|
92
|
+
function respelled(raw, value, replacement, quote) {
|
|
93
|
+
let shared = 0;
|
|
94
|
+
while (
|
|
95
|
+
shared < value.length &&
|
|
96
|
+
shared < replacement.length &&
|
|
97
|
+
value[value.length - 1 - shared] === replacement[replacement.length - 1 - shared]
|
|
98
|
+
) shared += 1;
|
|
99
|
+
const head = value.slice(0, value.length - shared);
|
|
100
|
+
const newHead = replacement.slice(0, replacement.length - shared);
|
|
101
|
+
if (head.includes("&") || !raw.startsWith(head)) return attributeText(replacement, quote);
|
|
102
|
+
if (!quote && /[\s"'<>=`]/.test(newHead)) return attributeText(replacement, quote);
|
|
103
|
+
return attributeText(newHead, quote) + raw.slice(head.length);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function descriptorTag(version, entries) {
|
|
107
|
+
const descriptor = { schema: 1, version, entries };
|
|
108
|
+
const json = JSON.stringify(descriptor).replace(/</g, "\\u003c");
|
|
109
|
+
return `<script type="application/json" id="${DESCRIPTOR_ID}" clay="no-save no-watch no-snapshot">${json}</script>`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function projectRuntimeHTML(html, { releases, selectedVersion, documentURL, descriptor = true } = {}) {
|
|
113
|
+
const nodes = candidates(parse(html, { sourceCodeLocationInfo: true }), html);
|
|
114
|
+
const resolved = nodes.map((node) => ({
|
|
115
|
+
node,
|
|
116
|
+
result: resolveRuntimeURL(node.value, { releases, selectedVersion, documentURL }),
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
const versions = new Set();
|
|
120
|
+
let missing = false;
|
|
121
|
+
let absent = false;
|
|
122
|
+
for (const { node, result } of resolved) {
|
|
123
|
+
if (!result) {
|
|
124
|
+
const parsed = parseRuntimeURL(node.value, { documentURL });
|
|
125
|
+
if (parsed && (parsed.kind === "rolling" || parsed.kind === "exact")) absent = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (result.available === false) missing = true;
|
|
129
|
+
versions.add(result.version);
|
|
130
|
+
}
|
|
131
|
+
if (missing || absent || versions.size > 1) {
|
|
132
|
+
return {
|
|
133
|
+
html,
|
|
134
|
+
mappings: [],
|
|
135
|
+
skipped: [],
|
|
136
|
+
unavailable: {
|
|
137
|
+
reason: missing ? "release-missing" : absent ? "path-missing" : "mixed-versions",
|
|
138
|
+
versions: [...versions].sort(),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const mappings = [];
|
|
144
|
+
const splices = [];
|
|
145
|
+
const version = versions.size === 1 ? [...versions][0] : null;
|
|
146
|
+
let insertAt = null;
|
|
147
|
+
|
|
148
|
+
for (const { node, result } of resolved) {
|
|
149
|
+
if (!result) continue;
|
|
150
|
+
splices.push({
|
|
151
|
+
start: node.valueStart,
|
|
152
|
+
end: node.valueEnd,
|
|
153
|
+
text: respelled(node.raw, node.value, result.localURL, node.quote),
|
|
154
|
+
});
|
|
155
|
+
mappings.push({ original: node.value, local: result.localURL });
|
|
156
|
+
if (insertAt === null) insertAt = node.startOffset;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!mappings.length) return { html, mappings, skipped: [], unavailable: null };
|
|
160
|
+
|
|
161
|
+
if (descriptor) splices.push({ start: insertAt, end: insertAt, text: descriptorTag(version, mappings) });
|
|
162
|
+
return { html: applySplices(html, splices), mappings, skipped: [], unavailable: null };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function descriptorSpan(node) {
|
|
166
|
+
const location = node.sourceCodeLocation;
|
|
167
|
+
if (!location) return null;
|
|
168
|
+
const start = location.startTag?.startOffset ?? location.startOffset;
|
|
169
|
+
const end = location.endTag ? location.endTag.endOffset : location.endOffset;
|
|
170
|
+
return { start, end };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function descriptorText(html, node) {
|
|
174
|
+
const location = node.sourceCodeLocation;
|
|
175
|
+
if (!location) return "";
|
|
176
|
+
const start = location.startTag?.endOffset ?? location.startOffset;
|
|
177
|
+
const end = location.endTag ? location.endTag.startOffset : location.endOffset;
|
|
178
|
+
return html.slice(start, end);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function canonicalizeRuntimeHTML(html, { releases, documentURL } = {}) {
|
|
182
|
+
const doc = parse(html, { sourceCodeLocationInfo: true });
|
|
183
|
+
const nodes = candidates(doc, html);
|
|
184
|
+
const splices = [];
|
|
185
|
+
let descriptor = null;
|
|
186
|
+
let descriptorRemoved = false;
|
|
187
|
+
|
|
188
|
+
for (const node of elements(doc)) {
|
|
189
|
+
if (node.tagName !== "script" || attributeOf(node, "id")?.value !== DESCRIPTOR_ID) continue;
|
|
190
|
+
const span = descriptorSpan(node);
|
|
191
|
+
if (!span) continue;
|
|
192
|
+
if (!descriptorRemoved) {
|
|
193
|
+
try {
|
|
194
|
+
descriptor = JSON.parse(descriptorText(html, node));
|
|
195
|
+
} catch {
|
|
196
|
+
descriptor = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
descriptorRemoved = true;
|
|
200
|
+
splices.push({ start: span.start, end: span.end, text: "" });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let changed = 0;
|
|
204
|
+
for (const node of nodes) {
|
|
205
|
+
const parsed = parseRuntimeURL(node.value, { documentURL });
|
|
206
|
+
if (!parsed || parsed.kind !== "local") continue;
|
|
207
|
+
const replacement = canonicalizeRuntimeURL(node.value, { descriptor, releases, documentURL });
|
|
208
|
+
if (replacement === node.value) continue;
|
|
209
|
+
splices.push({ start: node.valueStart, end: node.valueEnd, text: respelled(node.raw, node.value, replacement, node.quote) });
|
|
210
|
+
changed += 1;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { html: applySplices(html, splices), changed, descriptorRemoved };
|
|
214
|
+
}
|
|
@@ -40,7 +40,8 @@ export {
|
|
|
40
40
|
postReceipt,
|
|
41
41
|
} from './live-provenance.js';
|
|
42
42
|
|
|
43
|
-
export { isHybridPageValue, isYPageValue, inspectPage, pageBaseline } from './page-admission.js';
|
|
43
|
+
export { isHybridPageValue, isYPageValue, inspectPage, pageBaseline, captureFormat, checkPageFormatAdmission } from './page-admission.js';
|
|
44
|
+
export { PAGE_FORMAT, PAGE_FORMAT_HEADER, PAGE_FORMAT_EXTENSION, PAGE_FORMAT_UPGRADE, formatCapability, pageFormatFloor, withPageFormatFloor } from '../../core/page-format.js';
|
|
44
45
|
|
|
45
46
|
export { splitPage, composePage, pageRev } from './page-state.js';
|
|
46
47
|
|
|
@@ -422,9 +422,9 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
422
422
|
return reservePageTarget(key, nodeId, room, html, { source, base, epoch, names, format: 'yjs' });
|
|
423
423
|
},
|
|
424
424
|
|
|
425
|
-
reservePageInitial(key, { nodeId, currentHtml, html, ifMatch }) {
|
|
425
|
+
reservePageInitial(key, { nodeId, currentHtml, html, ifMatch, pageWrite = null, createDocument }) {
|
|
426
426
|
const room = pageSource(key, nodeId);
|
|
427
|
-
checkPageInitial({ currentHtml, html, ifMatch, room });
|
|
427
|
+
checkPageInitial({ currentHtml, html, ifMatch, room, pageWrite, createDocument });
|
|
428
428
|
return reservePageTarget(key, nodeId, room, html, { source: 'initial', names: [], format: 'yjs' });
|
|
429
429
|
},
|
|
430
430
|
|
|
@@ -3,6 +3,9 @@ import { parseJsonRelaxed } from 'hyper-morph/json-parse';
|
|
|
3
3
|
import { cloneJson } from './live-data-ops.js';
|
|
4
4
|
import { liveTags } from './live-data-tags.js';
|
|
5
5
|
import { recent } from './recent.js';
|
|
6
|
+
import { project } from '../../page/projection.js';
|
|
7
|
+
import { blocksFromPageValues } from '../../page/ybind/decode.js';
|
|
8
|
+
import { pageFormatDecision, pageFormatFloor, pageFormatRefusal, requiredPageFormat, withPageFormatFloor } from '../../page/format-level.js';
|
|
6
9
|
|
|
7
10
|
const PAGE = /^page(\.[0-9]+)?$/;
|
|
8
11
|
const PART = /^(.+)\.(\d+)\.(\d+)$/;
|
|
@@ -77,3 +80,33 @@ export function pageBaseline(tags) {
|
|
|
77
80
|
});
|
|
78
81
|
return createHash('sha256').update(canon(source), 'utf8').digest('hex');
|
|
79
82
|
}
|
|
83
|
+
|
|
84
|
+
// The floor a prospective page value needs: 1 when the model the capture would
|
|
85
|
+
// store holds a newer runtime's mark, block or field, 0 when it holds only what
|
|
86
|
+
// the frozen floor-0 registry knows. Invalid content refuses here rather than
|
|
87
|
+
// answering with a level, so a malformed capture cannot buy admission.
|
|
88
|
+
export function captureFormat(html) {
|
|
89
|
+
const answer = inspectPage(html ?? '');
|
|
90
|
+
if (answer.kind !== 'yjs') return 0;
|
|
91
|
+
return requiredPageFormat(project(blocksFromPageValues([answer.tags[0][1]])));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The floor decision for one page write, taken before anything is mutated.
|
|
95
|
+
// `floor` is the document's established floor, `required` what the captured
|
|
96
|
+
// content needs (its own marker included) and `capability` what the writer
|
|
97
|
+
// declares in X-Clay-Page-Format. `admitted` lists the capabilities of the page
|
|
98
|
+
// editors the room has admitted, so a floor rise is refused while an editor that
|
|
99
|
+
// could not read the result is connected. On success the returned body carries
|
|
100
|
+
// the marker the room must persist, so a stale save that omitted it cannot lower
|
|
101
|
+
// the floor.
|
|
102
|
+
export function checkPageFormatAdmission({ currentHtml, html, capability = 0, admitted = [] }) {
|
|
103
|
+
const established = pageFormatFloor(currentHtml ?? '');
|
|
104
|
+
const decision = pageFormatDecision({
|
|
105
|
+
floor: established,
|
|
106
|
+
required: Math.max(captureFormat(html), pageFormatFloor(html ?? '')),
|
|
107
|
+
capability,
|
|
108
|
+
admitted,
|
|
109
|
+
});
|
|
110
|
+
if (!decision.ok) throw pageFormatRefusal(decision.minimum);
|
|
111
|
+
return { ...decision, html: withPageFormatFloor(html, decision.floor) };
|
|
112
|
+
}
|