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