@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,705 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session.js — the source map at work: fetch, model, pair, render, adopt.
|
|
3
|
+
*
|
|
4
|
+
* On by default in edit mode; `exclude=source` turns it off. The mechanism lives in
|
|
5
|
+
* core/source-map.js; this file is the
|
|
6
|
+
* session around it: fetch the bytes this document was loaded from, pair them to
|
|
7
|
+
* the page, render the save through them, verify, and re-pair when the file moves
|
|
8
|
+
* under us. plugins/source.js is the thin facade over it and imports it on demand, so
|
|
9
|
+
* nothing here — and with it core/source-map.js and parse5 — is evaluated until a save
|
|
10
|
+
* needs it or code reads `clay.source.ready`.
|
|
11
|
+
*
|
|
12
|
+
* FOUR MOMENTS
|
|
13
|
+
*
|
|
14
|
+
* boot fetch the source, model it, refuse anything that is not this
|
|
15
|
+
* document, and pair it against a save clone.
|
|
16
|
+
* save render; verify; send the render if it verifies and today's full
|
|
17
|
+
* serialization if it does not, counting the second case.
|
|
18
|
+
* accepted re-model against the bytes the host took, so the next save is
|
|
19
|
+
* measured from what is on disk rather than from what was there at
|
|
20
|
+
* boot.
|
|
21
|
+
* sync-applied re-pair, because a morph replaces live nodes and the map is keyed
|
|
22
|
+
* by node identity.
|
|
23
|
+
*
|
|
24
|
+
* The accepted-save re-model is deferred and coalesced, so it never sits between a save
|
|
25
|
+
* landing and the page hearing about it. A sync frame's re-pair shares that idle refresh,
|
|
26
|
+
* so a burst of frames costs one walk, and a save, `text()` or `locate()` that comes
|
|
27
|
+
* before it runs it first, so none of them work from a map the morph made stale.
|
|
28
|
+
*
|
|
29
|
+
* NOTHING HERE BLOCKS BOOT. The install is async and the loader does not wait for
|
|
30
|
+
* it. Only the save that asked for it waits, and a save after a failed install is the
|
|
31
|
+
* ordinary full serialization rather than an error.
|
|
32
|
+
*
|
|
33
|
+
* ON DEMAND
|
|
34
|
+
*
|
|
35
|
+
* Nothing here runs at import: `start` is the one entry, and the facade calls it when a
|
|
36
|
+
* save needs the parser or a reader asks for `clay.source.ready`. `release` takes an
|
|
37
|
+
* install back — the renderer, a scheduled refresh and the installed flag — and bumps
|
|
38
|
+
* `state.session`, so an answer that arrives after the session ended installs nothing.
|
|
39
|
+
*
|
|
40
|
+
* WHEN IT DOES NOTHING
|
|
41
|
+
*
|
|
42
|
+
* Any of these leaves the save pipeline untouched, and each says so once in the
|
|
43
|
+
* console rather than repeatedly at save time:
|
|
44
|
+
*
|
|
45
|
+
* the source fetch fails, redirects, or is not text/html
|
|
46
|
+
* the fetched bytes disagree with the live document outside <html>
|
|
47
|
+
* the render throws
|
|
48
|
+
* the render does not verify
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
model,
|
|
53
|
+
checkSource,
|
|
54
|
+
locate,
|
|
55
|
+
nodeAt,
|
|
56
|
+
pair,
|
|
57
|
+
pairCaptured,
|
|
58
|
+
render,
|
|
59
|
+
verify,
|
|
60
|
+
verifyForSave
|
|
61
|
+
} from '../core/source-map.js';
|
|
62
|
+
import {
|
|
63
|
+
attachCaptureArtifact,
|
|
64
|
+
captureSaveClone,
|
|
65
|
+
currentCaptureContext,
|
|
66
|
+
originalSnapshotNode,
|
|
67
|
+
setSaveRenderer
|
|
68
|
+
} from '../core/snapshot.js';
|
|
69
|
+
import { onSaveAccepted } from '../core/save-core.js';
|
|
70
|
+
import { lastSeenEtag } from '../core/etag.js';
|
|
71
|
+
|
|
72
|
+
const state = {
|
|
73
|
+
installed: false,
|
|
74
|
+
refused: null,
|
|
75
|
+
// How many sessions have been uninstalled. An install applies nothing when the session
|
|
76
|
+
// that asked for it has ended in the meantime, so a late answer cannot put a renderer
|
|
77
|
+
// or a map back into view.
|
|
78
|
+
session: 0,
|
|
79
|
+
model: null,
|
|
80
|
+
map: null,
|
|
81
|
+
stats: null,
|
|
82
|
+
saves: 0,
|
|
83
|
+
reprints: 0,
|
|
84
|
+
lastReprint: null,
|
|
85
|
+
partialReprints: 0,
|
|
86
|
+
lastPartialReprint: null,
|
|
87
|
+
// What the last render produced, so the save the host accepts can be counted and
|
|
88
|
+
// announced as what it was. Cleared once reported.
|
|
89
|
+
lastOutcome: null,
|
|
90
|
+
lastRenderMs: null,
|
|
91
|
+
lastBytes: null,
|
|
92
|
+
timing: { fetch: null, model: null, pair: null, refresh: null },
|
|
93
|
+
refreshes: 0,
|
|
94
|
+
// How many times the map has been rebuilt from the page since this module
|
|
95
|
+
// installed. A candidate's map is only valid while this is unchanged: a morph
|
|
96
|
+
// replaces live nodes, and a disk frame replaces the model.
|
|
97
|
+
repairs: 0,
|
|
98
|
+
refreshQueued: false,
|
|
99
|
+
pendingBytes: null,
|
|
100
|
+
refetch: false,
|
|
101
|
+
// How many times newer file bytes have replaced the model: an accepted save, a disk
|
|
102
|
+
// frame, or a newer fetch. A fetch answers about the file at the moment the host read
|
|
103
|
+
// it, so this is what tells a late answer that something newer already landed.
|
|
104
|
+
adoptions: 0,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// How many times a boot fetch asks again after a save or a disk frame overtook it before
|
|
108
|
+
// it gives up and leaves saves on the full serialization.
|
|
109
|
+
const SUPERSEDED_RETRIES = 2;
|
|
110
|
+
|
|
111
|
+
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
112
|
+
|
|
113
|
+
const PROJECTED_MAP_PREFIX = '<script type="application/json" id="clay-runtime-map"';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Bytes a host projected for its own delivery are not the file: the runtime URLs in them
|
|
117
|
+
* point at the local mirror and may carry a descriptor the file never held. Modelling them
|
|
118
|
+
* would make this tab's next save copy the projection back into the file.
|
|
119
|
+
*/
|
|
120
|
+
const isProjected = (text) => text.includes(PROJECTED_MAP_PREFIX) || text.includes('/_/clayjs/');
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The bytes this document was loaded from.
|
|
124
|
+
*
|
|
125
|
+
* Every guard here is about one failure: writing somebody else's page into this
|
|
126
|
+
* file. `redirect: 'manual'` and the `redirected` check catch a host that answers a
|
|
127
|
+
* logged-out request with a login page at another URL; the content-type check
|
|
128
|
+
* catches one that answers with JSON or an error page; `cache: 'no-store'` keeps a
|
|
129
|
+
* stale cached copy of an older version of this document out of the model. The
|
|
130
|
+
* shape check in checkSource is the backstop for whatever gets past all three.
|
|
131
|
+
*/
|
|
132
|
+
async function fetchSource(url) {
|
|
133
|
+
const res = await fetch(url, {
|
|
134
|
+
cache: 'no-store',
|
|
135
|
+
redirect: 'manual',
|
|
136
|
+
credentials: 'same-origin',
|
|
137
|
+
headers: { 'Clay-Source': '1' }
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) throw new Error(`source fetch: status ${res.status}`);
|
|
140
|
+
if (res.redirected || res.type === 'opaqueredirect') throw new Error('source fetch: redirected');
|
|
141
|
+
const type = (res.headers.get('content-type') || '').toLowerCase();
|
|
142
|
+
if (!type.startsWith('text/html')) throw new Error(`source fetch: content-type ${type || 'missing'}`);
|
|
143
|
+
const text = await res.text();
|
|
144
|
+
if (text.includes(PROJECTED_MAP_PREFIX)) throw new Error('source fetch: projected response');
|
|
145
|
+
return text;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pair the model against the page.
|
|
150
|
+
*
|
|
151
|
+
* Over a SAVE CLONE, not the live DOM. The clone is in the file's own domain: edit
|
|
152
|
+
* mode deactivated back to the inert attribute forms, [no-save] regions gone, every
|
|
153
|
+
* document transform run. Pairing the live DOM instead would compare
|
|
154
|
+
* `contenteditable="true"` against the file's `inert-contenteditable="true"` and lose
|
|
155
|
+
* the two strongest signature tiers on every activated element and every ancestor of
|
|
156
|
+
* one. The map is keyed through snapshot provenance to the LIVE node, because the
|
|
157
|
+
* clone is rebuilt on every save and its nodes are new objects each time.
|
|
158
|
+
*
|
|
159
|
+
* This capture is an INSPECTION, not a save, and it happens at boot and again on every
|
|
160
|
+
* incoming live-sync frame. So it takes neither of the two things a save capture does
|
|
161
|
+
* on its way past: it does not close the undo batch, which at boot and mid-typing
|
|
162
|
+
* would split the user's undo history at a point they did not make, and it does not
|
|
163
|
+
* run the page's own [onbeforesave] and [onbeforesnapshot] handlers, which are author
|
|
164
|
+
* JavaScript that can do anything and are meant to run once per save. The cost is that
|
|
165
|
+
* a page using those handlers pairs against a tree slightly behind the one it will
|
|
166
|
+
* save, which costs some formatting fidelity on the nodes a handler touches and
|
|
167
|
+
* nothing else, because the render is verified either way.
|
|
168
|
+
*/
|
|
169
|
+
function pairAgainstPage(m) {
|
|
170
|
+
const clone = captureSaveClone({ flushUndo: false, authored: false });
|
|
171
|
+
return pair(clone, m, originalSnapshotNode);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The version a set of bytes names for itself, or null when nothing stamped it.
|
|
176
|
+
*
|
|
177
|
+
* A host writes `documentetag` on the root of the bytes it stored, so a fetch answers
|
|
178
|
+
* with the version it read, which is not necessarily the one this page was built from.
|
|
179
|
+
*/
|
|
180
|
+
function stampedVersion(m) {
|
|
181
|
+
const html = m.doc && m.doc.childNodes.find((node) => node.nodeName === 'html');
|
|
182
|
+
const attr = html && Array.isArray(html.attrs)
|
|
183
|
+
? html.attrs.find((attribute) => attribute.name === 'documentetag')
|
|
184
|
+
: null;
|
|
185
|
+
return attr && attr.value ? attr.value : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The version this session represents: the file the reader handed it, else the version this
|
|
190
|
+
* tab last saw, else — for an explicit edit boot, which has no reader — the version the
|
|
191
|
+
* document was served with.
|
|
192
|
+
*/
|
|
193
|
+
function heldVersion(context) {
|
|
194
|
+
const represented = context && context.represented && context.represented.etag;
|
|
195
|
+
if (represented) return represented;
|
|
196
|
+
const seen = lastSeenEtag();
|
|
197
|
+
if (seen) return seen;
|
|
198
|
+
return context === null ? document.documentElement.getAttribute('documentetag') || null : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A fetch is an answer about the file as it was when the host read it, and a boot fetch
|
|
203
|
+
* can come back after the file has moved on. Bytes that name a version this page does
|
|
204
|
+
* not hold are that newer file: adopting them would pair this page against content it
|
|
205
|
+
* never received, and the next save would print its older bytes back over them. The
|
|
206
|
+
* stamp and the model go together, so neither is taken from a late answer.
|
|
207
|
+
*/
|
|
208
|
+
function staleVersion(m, context) {
|
|
209
|
+
const stamp = stampedVersion(m);
|
|
210
|
+
const held = heldVersion(context);
|
|
211
|
+
if (!stamp || !held || stamp === held) return null;
|
|
212
|
+
return `source refused: the fetch answered version ${stamp} while this page holds ${held}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function install(sourceUrl, context = null, attempt = 0, render = true, latest = null) {
|
|
216
|
+
const session = state.session;
|
|
217
|
+
const t0 = now();
|
|
218
|
+
const generation = state.adoptions;
|
|
219
|
+
state.refused = null;
|
|
220
|
+
// Bytes this page already holds for the file — the reader's receipt, an accepted save,
|
|
221
|
+
// a disk frame — are the file, so the install starts from them instead of asking the
|
|
222
|
+
// host for a copy it has just written. Every check below runs on whichever answer it
|
|
223
|
+
// gets, so this is a request saved and not a guard skipped.
|
|
224
|
+
const receipt = latest ? latest() : null;
|
|
225
|
+
const receiptBytes = receipt && typeof receipt.bytes === 'string' && !isProjected(receipt.bytes)
|
|
226
|
+
? receipt.bytes
|
|
227
|
+
: null;
|
|
228
|
+
return (receiptBytes !== null ? Promise.resolve(receiptBytes) : fetchSource(sourceUrl)).then((src) => {
|
|
229
|
+
if (session !== state.session) return null;
|
|
230
|
+
const t1 = now();
|
|
231
|
+
const m = model(src);
|
|
232
|
+
const refused = checkSource(m, document);
|
|
233
|
+
if (refused) throw new Error(`source refused: ${refused}`);
|
|
234
|
+
const stale = staleVersion(m, context);
|
|
235
|
+
if (stale) throw new Error(stale);
|
|
236
|
+
// A save was accepted, or a disk frame carried newer bytes, while this answer was on
|
|
237
|
+
// its way. Those bytes are the file now, so ask again rather than adopt an older
|
|
238
|
+
// answer; a page that will not hold still is refused instead of retried forever.
|
|
239
|
+
if (state.adoptions !== generation) {
|
|
240
|
+
if (attempt < SUPERSEDED_RETRIES) return install(sourceUrl, context, attempt + 1, render, latest);
|
|
241
|
+
throw new Error('source refused: a save replaced the file while the fetch was in flight');
|
|
242
|
+
}
|
|
243
|
+
const t2 = now();
|
|
244
|
+
const { map, stats } = pairAgainstPage(m);
|
|
245
|
+
const t3 = now();
|
|
246
|
+
|
|
247
|
+
state.model = m;
|
|
248
|
+
state.map = map;
|
|
249
|
+
state.stats = stats;
|
|
250
|
+
state.timing = { fetch: t1 - t0, model: t2 - t1, pair: t3 - t2, refresh: null };
|
|
251
|
+
state.installed = true;
|
|
252
|
+
if (render) setSaveRenderer(renderSave);
|
|
253
|
+
return summary();
|
|
254
|
+
}).catch((err) => {
|
|
255
|
+
// Not an error the page can do anything about, and not a failure of the save:
|
|
256
|
+
// saves go out exactly as they would without this plugin. Said once.
|
|
257
|
+
if (session !== state.session) return null;
|
|
258
|
+
state.refused = String(err && err.message ? err.message : err);
|
|
259
|
+
console.info(`clayjs: source map off (${state.refused}); saves use the full serialization`);
|
|
260
|
+
return null;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Render one save, or hand back today's bytes.
|
|
266
|
+
*
|
|
267
|
+
* Exported so a test can drive the whole path, including the fallback, without a
|
|
268
|
+
* server: `renderSave(clone, today, { breakText: true })` makes every text node
|
|
269
|
+
* reprint, which changes the bytes while leaving the tree identical, and the verifier
|
|
270
|
+
* must still pass it — a tree comparison is the right answer there, and reading that
|
|
271
|
+
* pass as "the verifier is blind" is a mistake this project has already made once.
|
|
272
|
+
* `{ corrupt: 'drop-first-text' }` is the one that must be REJECTED: it changes the
|
|
273
|
+
* tree, which is exactly what the verifier is for.
|
|
274
|
+
*
|
|
275
|
+
* A render that does not verify is not thrown away whole. verify says where it first
|
|
276
|
+
* differs; that element is printed in full and the render tried again, widening to the
|
|
277
|
+
* parent while it still fails. Everything outside the printed elements is still the
|
|
278
|
+
* author's bytes. Only when the widening reaches <html>, or the failure has no node to
|
|
279
|
+
* point at (the prologue, a parse-error count), is today's full serialization sent.
|
|
280
|
+
*
|
|
281
|
+
* Nothing is counted or announced here. A render is not a save: an autosave that
|
|
282
|
+
* found nothing changed, a refused save and a failed request all rendered. The
|
|
283
|
+
* outcome is recorded, and `adopt` reports it when the host accepts these exact bytes.
|
|
284
|
+
*
|
|
285
|
+
* A render that belongs to a save capture also hands that capture a private artifact:
|
|
286
|
+
* the model of the bytes it just verified, paired with the clone it verified them
|
|
287
|
+
* against, so the save the host accepts does not parse and pair those same bytes a
|
|
288
|
+
* second time. It is a candidate, not an answer: `adopt` installs it only if the host
|
|
289
|
+
* took these exact bytes and nothing has replaced the page's pairing since. A page
|
|
290
|
+
* with its own [onbeforesave] or [onbeforesnapshot] handlers gets none, because those
|
|
291
|
+
* handlers can edit the clone after the capture and the inspection path is what
|
|
292
|
+
* describes the live page there.
|
|
293
|
+
*/
|
|
294
|
+
export function renderSave(clone, today, opts = {}) {
|
|
295
|
+
if (!state.installed) return today;
|
|
296
|
+
// A refresh still waiting for idle time runs now, so this save never renders from a
|
|
297
|
+
// map a morph made stale. It pairs against the page, not this clone: onbeforesave may
|
|
298
|
+
// have changed the clone, and the map must describe the live page.
|
|
299
|
+
refreshNow();
|
|
300
|
+
state.lastOutcome = null;
|
|
301
|
+
const capture = currentCaptureContext();
|
|
302
|
+
const adoptable = !!capture && !capture.authoredHandlers;
|
|
303
|
+
const print = new Set();
|
|
304
|
+
let firstDiff = null;
|
|
305
|
+
for (let round = 0; round <= MAX_PRINT_ROUNDS; round++) {
|
|
306
|
+
let out;
|
|
307
|
+
try {
|
|
308
|
+
out = render(clone, state.map, state.model, originalSnapshotNode, { ...opts, print });
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return fallback('render threw: ' + (err && err.message ? err.message : err), today);
|
|
311
|
+
}
|
|
312
|
+
// The verifier's retained parse is a cost, so it is only asked for when there is a
|
|
313
|
+
// capture that could adopt it.
|
|
314
|
+
const v = adoptable
|
|
315
|
+
? verifyForSave(out.text, today, document, clone, state.model.parseErrors)
|
|
316
|
+
: verify(out.text, today, document, clone, state.model.parseErrors);
|
|
317
|
+
if (v.ok) {
|
|
318
|
+
state.lastRenderMs = out.ms;
|
|
319
|
+
state.lastBytes = out.text.length;
|
|
320
|
+
state.lastOutcome = print.size
|
|
321
|
+
? { text: out.text, scope: 'partial', reason: firstDiff, printed: print.size }
|
|
322
|
+
: { text: out.text, scope: null };
|
|
323
|
+
if (v.model) offerCandidate(v.model, out.text, clone, capture);
|
|
324
|
+
return out.text;
|
|
325
|
+
}
|
|
326
|
+
if (firstDiff === null) firstDiff = v.diff;
|
|
327
|
+
if (!v.at || !widen(clone, nodeAt(clone, v.at), print)) return fallback(firstDiff, today);
|
|
328
|
+
}
|
|
329
|
+
return fallback(firstDiff, today);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Hand this capture the model of the bytes it just verified, or nothing.
|
|
334
|
+
*
|
|
335
|
+
* The pairing is the whole point of the artifact: it must describe the live page, so
|
|
336
|
+
* it is built by walking the verified clone and that model together. Anything that
|
|
337
|
+
* does not correspond, node for node, leaves the capture without an artifact, and the
|
|
338
|
+
* accepted save takes the inspection path it takes today.
|
|
339
|
+
*/
|
|
340
|
+
function offerCandidate(m, html, clone, capture) {
|
|
341
|
+
const paired = pairCaptured(clone, m, originalSnapshotNode);
|
|
342
|
+
if (!paired) return;
|
|
343
|
+
attachCaptureArtifact({
|
|
344
|
+
captureId: capture.captureId,
|
|
345
|
+
html,
|
|
346
|
+
source: {
|
|
347
|
+
model: m,
|
|
348
|
+
map: paired.map,
|
|
349
|
+
stats: paired.stats,
|
|
350
|
+
repairGeneration: state.repairs,
|
|
351
|
+
outcome: state.lastOutcome,
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const MAX_PRINT_ROUNDS = 8;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Add the next element to print: the one holding `node`, or, when that is already
|
|
360
|
+
* inside a printed element, that element's parent. false when the only element left is
|
|
361
|
+
* the root, which is the full serialization by another name.
|
|
362
|
+
*/
|
|
363
|
+
function widen(clone, node, print) {
|
|
364
|
+
let el = node && node.nodeType === 1 ? node : node && node.parentNode;
|
|
365
|
+
for (let a = el; a && a !== clone; a = a.parentNode) {
|
|
366
|
+
if (print.has(a)) { print.delete(a); el = a.parentNode; break; }
|
|
367
|
+
}
|
|
368
|
+
if (!el || el.nodeType !== 1 || el === clone) return false;
|
|
369
|
+
print.add(el);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Send today's full serialization, and remember that this render was a fallback. */
|
|
374
|
+
function fallback(reason, today) {
|
|
375
|
+
state.lastOutcome = { text: today, scope: 'full', reason };
|
|
376
|
+
return today;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The fallback, and the alarm.
|
|
381
|
+
*
|
|
382
|
+
* Reported for a save the host accepted. The bytes that went out were today's, so the
|
|
383
|
+
* floor of this whole mechanism is the behaviour it replaces. The event and the
|
|
384
|
+
* counter are how a reprint gets fixed
|
|
385
|
+
* in the renderer or in the page, which is the only place it should ever be fixed: a
|
|
386
|
+
* verifier loosened to make this counter look better would pass exactly the writes it
|
|
387
|
+
* exists to stop.
|
|
388
|
+
*/
|
|
389
|
+
function reprint(reason) {
|
|
390
|
+
state.reprints++;
|
|
391
|
+
state.lastReprint = reason;
|
|
392
|
+
console.warn('clayjs: source map did not verify, saving the full serialization instead:', reason);
|
|
393
|
+
document.dispatchEvent(new CustomEvent('clay:save-reprinted', { detail: { reason, scope: 'full' } }));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Part of the document was printed rather than copied. Counted apart from a full
|
|
398
|
+
* reprint because they are different news: this one kept the author's bytes everywhere
|
|
399
|
+
* else, and its rate says how often the renderer and the page disagree about one
|
|
400
|
+
* element, which is where the next renderer fix is.
|
|
401
|
+
*/
|
|
402
|
+
function partialReprint(reason, printed) {
|
|
403
|
+
state.partialReprints++;
|
|
404
|
+
state.lastPartialReprint = reason;
|
|
405
|
+
console.info(`clayjs: source map printed ${printed} element(s) in full to match the page:`, reason);
|
|
406
|
+
document.dispatchEvent(new CustomEvent('clay:save-reprinted', { detail: { reason, scope: 'partial', printed } }));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The host took these bytes, so this is what the file holds now.
|
|
411
|
+
*
|
|
412
|
+
* Deferred, like the re-pair below and for the same reason: re-modelling walks the
|
|
413
|
+
* whole document, and doing it inline would put that walk between the save landing
|
|
414
|
+
* and the page being told about it. Only the most recent accepted bytes matter, so a
|
|
415
|
+
* burst of saves costs one refresh.
|
|
416
|
+
*
|
|
417
|
+
* It is also where a save is counted, because it is the one place that knows a save
|
|
418
|
+
* reached the file.
|
|
419
|
+
*
|
|
420
|
+
* A capture that rendered its own verified bytes brought a candidate with them, and
|
|
421
|
+
* when the host took exactly those bytes that candidate IS the file: installing it is
|
|
422
|
+
* a few assignments instead of a parse of the accepted bytes and a whole-document
|
|
423
|
+
* re-pair. Everything else — a raw save, a fallback, a candidate whose pairing a morph
|
|
424
|
+
* has since invalidated, a disk version that arrived while the save was in flight —
|
|
425
|
+
* takes the guarded repair path, which is the behaviour this replaces.
|
|
426
|
+
*/
|
|
427
|
+
function adopt(bytes, accepted) {
|
|
428
|
+
state.adoptions++;
|
|
429
|
+
if (!state.installed) return;
|
|
430
|
+
report(bytes, accepted && accepted.artifact ? accepted.artifact.source.outcome : null);
|
|
431
|
+
if (adoptCandidate(bytes, accepted)) return;
|
|
432
|
+
state.pendingBytes = bytes;
|
|
433
|
+
scheduleRefresh();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Install a capture's candidate model, if it is this save's and still describes the page.
|
|
438
|
+
*
|
|
439
|
+
* Three things have to agree, and each is a different mistake. The bytes: the artifact
|
|
440
|
+
* is the model of the string that exact render returned, not of a later one. The
|
|
441
|
+
* capture: `save.js` keeps the artifact with its request, so the ID rules out a
|
|
442
|
+
* candidate that reached an accepted listener by any other route. The generation: a
|
|
443
|
+
* morph replaces live nodes and a disk frame replaces the model, so a map built before
|
|
444
|
+
* one is keyed to a page that is no longer here.
|
|
445
|
+
*/
|
|
446
|
+
function adoptCandidate(bytes, accepted) {
|
|
447
|
+
const artifact = accepted && accepted.artifact;
|
|
448
|
+
if (!artifact || !artifact.source) return false;
|
|
449
|
+
if (artifact.html !== bytes) return false;
|
|
450
|
+
if (accepted.captureId === null || accepted.captureId === undefined) return false;
|
|
451
|
+
if (artifact.captureId !== accepted.captureId) return false;
|
|
452
|
+
if (artifact.source.repairGeneration !== state.repairs) return false;
|
|
453
|
+
if (state.refreshQueued || state.pendingBytes !== null) return false;
|
|
454
|
+
state.model = artifact.source.model;
|
|
455
|
+
state.map = artifact.source.map;
|
|
456
|
+
state.stats = artifact.source.stats;
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Count and announce an accepted save, if these are the bytes the last render
|
|
462
|
+
* produced. Bytes this module did not render (a caller of `saveHtml` with its own
|
|
463
|
+
* string, or an older render) are not its save to report.
|
|
464
|
+
*/
|
|
465
|
+
function report(bytes, fromCapture = null) {
|
|
466
|
+
const outcome = state.lastOutcome && state.lastOutcome.text === bytes ? state.lastOutcome : fromCapture;
|
|
467
|
+
if (!outcome || outcome.text !== bytes) return;
|
|
468
|
+
if (state.lastOutcome === outcome) state.lastOutcome = null;
|
|
469
|
+
state.saves++;
|
|
470
|
+
if (outcome.scope === 'full') reprint(outcome.reason);
|
|
471
|
+
else if (outcome.scope === 'partial') partialReprint(outcome.reason, outcome.printed);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* A morph replaced live nodes, so the map is keyed by objects that are no longer in
|
|
476
|
+
* the page, and it has to be re-paired either way. A DISK frame also carries the bytes
|
|
477
|
+
* now on disk, written by somebody else: those become the model, the same as bytes this
|
|
478
|
+
* tab saved, or the next save would copy the old formatting back over theirs. A peer
|
|
479
|
+
* frame changed nothing on disk, so the model stays. The re-pair runs in idle time, or
|
|
480
|
+
* earlier if a save, `text()` or `locate()` needs it first.
|
|
481
|
+
*/
|
|
482
|
+
function queueRepair(event) {
|
|
483
|
+
const detail = event && event.detail;
|
|
484
|
+
if (detail && detail.source === 'disk' && typeof detail.html === 'string') {
|
|
485
|
+
// Newer bytes than anything already in flight, adopted or not: a fetch that started
|
|
486
|
+
// before this frame cannot answer for the file the frame describes, so the boot
|
|
487
|
+
// fetch asks again rather than installing the version it asked about.
|
|
488
|
+
state.adoptions++;
|
|
489
|
+
}
|
|
490
|
+
if (!state.installed) return;
|
|
491
|
+
if (detail && detail.source === 'disk' && typeof detail.html === 'string') {
|
|
492
|
+
if (isProjected(detail.html)) state.refetch = true;
|
|
493
|
+
else state.pendingBytes = detail.html;
|
|
494
|
+
}
|
|
495
|
+
scheduleRefresh();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Re-model if a save was accepted, re-pair either way, once, off the critical path.
|
|
500
|
+
*
|
|
501
|
+
* Coalescing is what makes this safe as well as cheap. Saves are serialized, so the
|
|
502
|
+
* most recent accepted bytes are the file; a refresh that skipped an intermediate
|
|
503
|
+
* save would still land on the right answer, and one that ran them out of order could
|
|
504
|
+
* not.
|
|
505
|
+
*/
|
|
506
|
+
function scheduleRefresh() {
|
|
507
|
+
if (state.refreshQueued) return;
|
|
508
|
+
state.refreshQueued = true;
|
|
509
|
+
if (typeof requestIdleCallback === 'function') requestIdleCallback(refreshNow, { timeout: 2000 });
|
|
510
|
+
else setTimeout(refreshNow, 0);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Run a queued refresh now. A no-op when none is queued, which is also what makes the
|
|
515
|
+
* idle callback of a refresh that `text()` or `locate()` already ran harmless.
|
|
516
|
+
*/
|
|
517
|
+
function refreshNow() {
|
|
518
|
+
if (!state.refreshQueued) return;
|
|
519
|
+
state.refreshQueued = false;
|
|
520
|
+
if (!state.installed) return;
|
|
521
|
+
const bytes = state.pendingBytes;
|
|
522
|
+
state.pendingBytes = null;
|
|
523
|
+
const refetch = state.refetch && bytes === null;
|
|
524
|
+
if (refetch) state.refetch = false;
|
|
525
|
+
// Counted even when the re-pair below fails: either way this page is not the page a
|
|
526
|
+
// candidate captured before it was built against.
|
|
527
|
+
state.repairs++;
|
|
528
|
+
const t = now();
|
|
529
|
+
// The two halves fail independently, so they are tried independently. A refused
|
|
530
|
+
// re-model used to skip the re-pair with it, and the re-pair is the half that
|
|
531
|
+
// cannot be skipped: a morph replaced live nodes, so the old map is keyed by
|
|
532
|
+
// objects no longer in the page, and every save after that reprints the whole
|
|
533
|
+
// document until something else queues a refresh. The previous model still
|
|
534
|
+
// describes real bytes, so re-pairing against it is the right answer.
|
|
535
|
+
let m = state.model;
|
|
536
|
+
if (bytes !== null) {
|
|
537
|
+
try {
|
|
538
|
+
const next = model(bytes);
|
|
539
|
+
const refused = checkSource(next, document);
|
|
540
|
+
if (refused) throw new Error(refused);
|
|
541
|
+
m = next;
|
|
542
|
+
} catch (err) {
|
|
543
|
+
console.warn('clayjs: source map kept the previous model, the accepted bytes did not model:', err);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
const { map, stats } = pairAgainstPage(m);
|
|
548
|
+
state.model = m;
|
|
549
|
+
state.map = map;
|
|
550
|
+
state.stats = stats;
|
|
551
|
+
state.refreshes++;
|
|
552
|
+
state.timing.refresh = now() - t;
|
|
553
|
+
} catch (err) {
|
|
554
|
+
// The map is now stale rather than wrong: it still describes the pairing as of
|
|
555
|
+
// the last successful refresh. Renders off a stale map verify or fall back like
|
|
556
|
+
// any other, so this costs formatting fidelity and nothing else.
|
|
557
|
+
console.warn('clayjs: source map could not re-pair, continuing on the previous map:', err);
|
|
558
|
+
}
|
|
559
|
+
if (refetch) refetchModel();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Take the model from the file itself, after a disk frame arrived with bytes this host
|
|
564
|
+
* projected rather than bytes on disk.
|
|
565
|
+
*
|
|
566
|
+
* The re-pair has already run against the previous model, which still describes real
|
|
567
|
+
* bytes, so a fetch that fails or is refused costs nothing: the model stays as it was
|
|
568
|
+
* and the same warning the refused branch gives says so. Anything that replaced the
|
|
569
|
+
* model while the fetch was in flight is newer than these bytes and keeps the answer.
|
|
570
|
+
*/
|
|
571
|
+
function refetchModel() {
|
|
572
|
+
const generation = state.repairs;
|
|
573
|
+
const adoption = state.adoptions;
|
|
574
|
+
fetchSource(window.location.href)
|
|
575
|
+
.then((src) => {
|
|
576
|
+
const next = model(src);
|
|
577
|
+
const refused = checkSource(next, document);
|
|
578
|
+
if (refused) throw new Error(refused);
|
|
579
|
+
return next;
|
|
580
|
+
})
|
|
581
|
+
.then((m) => {
|
|
582
|
+
// A save landed, or a newer fetch answered, while this one was in flight: its bytes
|
|
583
|
+
// describe a file this page has already moved past.
|
|
584
|
+
if (state.repairs !== generation || state.adoptions !== adoption) return;
|
|
585
|
+
try {
|
|
586
|
+
state.repairs++;
|
|
587
|
+
state.adoptions++;
|
|
588
|
+
const { map, stats } = pairAgainstPage(m);
|
|
589
|
+
state.model = m;
|
|
590
|
+
state.map = map;
|
|
591
|
+
state.stats = stats;
|
|
592
|
+
state.refreshes++;
|
|
593
|
+
} catch (err) {
|
|
594
|
+
console.warn('clayjs: source map could not re-pair, continuing on the previous map:', err);
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
.catch((err) => {
|
|
598
|
+
console.warn('clayjs: source map kept the previous model, the accepted bytes did not model:', err);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function summary() {
|
|
603
|
+
const s = state.stats;
|
|
604
|
+
return {
|
|
605
|
+
installed: state.installed,
|
|
606
|
+
refused: state.refused,
|
|
607
|
+
sourceBytes: state.model ? state.model.src.length : null,
|
|
608
|
+
paired: s ? s.paired : 0,
|
|
609
|
+
unresolved: s ? s.unresolved : 0,
|
|
610
|
+
unmatchedLive: s ? s.unmatchedLive.length : 0,
|
|
611
|
+
unmatchedSource: s ? s.unmatchedSource.length : 0,
|
|
612
|
+
attrDiffs: s ? s.attrDiffs.length : 0,
|
|
613
|
+
saves: state.saves,
|
|
614
|
+
reprints: state.reprints,
|
|
615
|
+
lastReprint: state.lastReprint,
|
|
616
|
+
partialReprints: state.partialReprints,
|
|
617
|
+
lastPartialReprint: state.lastPartialReprint,
|
|
618
|
+
lastRenderMs: state.lastRenderMs,
|
|
619
|
+
lastBytes: state.lastBytes,
|
|
620
|
+
refreshes: state.refreshes,
|
|
621
|
+
timing: state.timing,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* The bytes this module believes are on disk right now. A refresh still queued from a save
|
|
627
|
+
* or a disk frame runs first, so this is never the file before that.
|
|
628
|
+
*/
|
|
629
|
+
export function text() {
|
|
630
|
+
refreshNow();
|
|
631
|
+
return state.model ? state.model.src : null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Where a live element is in `text()`: `{ from, to, line, column }`, or null.
|
|
636
|
+
*
|
|
637
|
+
* Offsets and `column` are UTF-16 code units into `text()`, not bytes. Slice `text()` with them
|
|
638
|
+
* and the answer is exact; hand them to something that counts bytes and it is wrong on any
|
|
639
|
+
* document with non-ASCII content, which is the kind of mistake that shows up months later in
|
|
640
|
+
* one customer's file.
|
|
641
|
+
*
|
|
642
|
+
* null whenever this module cannot answer, which includes the whole document when the session
|
|
643
|
+
* never installed or refused it. `stats().installed` and `stats().refused` say which, and an
|
|
644
|
+
* agent that cannot tell "this element is new" from "this document has no map" will eventually
|
|
645
|
+
* write into a file it was never modelling.
|
|
646
|
+
*/
|
|
647
|
+
export function locateLive(node) {
|
|
648
|
+
refreshNow();
|
|
649
|
+
return state.installed && node ? locate(node, state.map, state.model) : null;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** What did not pair, for working out why a document reprints more than it should. */
|
|
653
|
+
export function unpaired() {
|
|
654
|
+
refreshNow();
|
|
655
|
+
return state.stats
|
|
656
|
+
? { live: state.stats.unmatchedLive.slice(0, 50), source: state.stats.unmatchedSource.slice(0, 50) }
|
|
657
|
+
: null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export function stats() {
|
|
661
|
+
refreshNow();
|
|
662
|
+
return summary();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export const installed = () => state.installed;
|
|
666
|
+
|
|
667
|
+
/** The facade's dispose: take the install back, and invalidate whatever was still loading. */
|
|
668
|
+
export function release() {
|
|
669
|
+
uninstall();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Take an install back: the renderer, a scheduled refresh, and the installed state that
|
|
674
|
+
* lets the hook and the listener do anything. The map and the model stay, so `text()` and
|
|
675
|
+
* `locateLive()` keep answering about the last file this module modelled while a view is
|
|
676
|
+
* reading it.
|
|
677
|
+
*/
|
|
678
|
+
function uninstall() {
|
|
679
|
+
state.session++;
|
|
680
|
+
state.installed = false;
|
|
681
|
+
state.refreshQueued = false;
|
|
682
|
+
state.pendingBytes = null;
|
|
683
|
+
state.refetch = false;
|
|
684
|
+
setSaveRenderer(null);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The install a session's demand asks for.
|
|
689
|
+
*
|
|
690
|
+
* `context` is the mode entry's own context, when there was one: `represented` is the file
|
|
691
|
+
* the reader handed that session. `latest` is the facade's newest receipt, read when the
|
|
692
|
+
* install starts so a save or disk frame that landed while the parser loaded is the file
|
|
693
|
+
* this session models rather than an answer older than it.
|
|
694
|
+
*/
|
|
695
|
+
export function start({ context = null, latest = null } = {}) {
|
|
696
|
+
return install(window.location.href, context, 0, true, latest);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Bytes the host took. Re-models against them, or records them for the install to use. */
|
|
700
|
+
export { adopt as adoptAccepted };
|
|
701
|
+
|
|
702
|
+
// Registered once, when the session first loads: both bodies return while no session is
|
|
703
|
+
// installed, so a page that never demands the parser never runs either.
|
|
704
|
+
onSaveAccepted(adopt);
|
|
705
|
+
document.addEventListener('clay:sync-applied', queueRepair);
|