@panphora/clayjs 1.15.2 → 1.17.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 (551) 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 +84291 -57611
  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-ZUE7PSCI.js +1 -0
  21. package/dist/m/attrs/save-freeze-MYNVXREN.js +1 -0
  22. package/dist/m/c-2AZDZWCK.js +1 -0
  23. package/dist/m/c-2SPVQSHC.js +1 -0
  24. package/dist/m/c-2YYDMLNZ.js +1 -0
  25. package/dist/m/c-336SGU5W.js +1 -0
  26. package/dist/m/c-33JJ6CBX.js +2 -0
  27. package/dist/m/c-3GJ4SDE2.js +1 -0
  28. package/dist/m/c-3H6BZDNJ.js +6 -0
  29. package/dist/m/c-3OWDMNJR.js +2 -0
  30. package/dist/m/c-3RUU4TPX.js +91 -0
  31. package/dist/m/c-3XIRD4NU.js +1 -0
  32. package/dist/m/c-46XYM55L.js +1 -0
  33. package/dist/m/c-4DFEJ2PO.js +3 -0
  34. package/dist/m/c-4JZDZFF6.js +1 -0
  35. package/dist/m/c-4L3BTFRK.js +1 -0
  36. package/dist/m/c-4Q2XTQ2N.js +2 -0
  37. package/dist/m/c-4WKFKEKF.js +1 -0
  38. package/dist/m/c-56QRZTZH.js +1 -0
  39. package/dist/m/c-5J4E7O4Y.js +4 -0
  40. package/dist/m/c-5LKDWQTL.js +19 -0
  41. package/dist/m/c-5QWQ7A5L.js +1 -0
  42. package/dist/m/c-5RJFTLSF.js +1 -0
  43. package/dist/m/c-5V3MOVJT.js +1 -0
  44. package/dist/m/c-5VYOWDTI.js +1 -0
  45. package/dist/m/c-5YIHJ3TJ.js +1 -0
  46. package/dist/m/c-6AZOGAPE.js +1 -0
  47. package/dist/m/c-6CRUUXOX.js +5 -0
  48. package/dist/m/c-6HMB24EV.js +1 -0
  49. package/dist/m/c-6KEVEQ7L.js +1 -0
  50. package/dist/m/c-6NC5I2TV.js +1 -0
  51. package/dist/m/c-74MGYWZF.js +1 -0
  52. package/dist/m/c-7DJDKPGL.js +1 -0
  53. package/dist/m/c-7OLLJPWP.js +1 -0
  54. package/dist/m/c-7SWMYKAI.js +1 -0
  55. package/dist/m/c-A3E3NIMC.js +1 -0
  56. package/dist/m/c-A7LS5UZW.js +1 -0
  57. package/dist/m/c-AMSNPOCX.js +1 -0
  58. package/dist/m/c-AQLFEATB.js +1 -0
  59. package/dist/m/c-B3JM5ZXC.js +1 -0
  60. package/dist/m/c-BBVGTQG7.js +1 -0
  61. package/dist/m/c-BPF2NSPV.js +1 -0
  62. package/dist/m/c-BQ6LOBEE.js +1 -0
  63. package/dist/m/c-BTDI4Y23.js +1 -0
  64. package/dist/m/c-BWFVSBDF.js +1 -0
  65. package/dist/m/c-BYTKL6UX.js +1 -0
  66. package/dist/m/c-CFHW5AMY.js +6 -0
  67. package/dist/m/c-CJ3A7ZKE.js +1 -0
  68. package/dist/m/c-CQHPGKX2.js +1 -0
  69. package/dist/m/c-CYWLPPUS.js +1 -0
  70. package/dist/m/c-CZUSOB7V.js +1 -0
  71. package/dist/m/c-DCFSEJZG.js +1 -0
  72. package/dist/m/c-DCXOXBF3.js +1 -0
  73. package/dist/m/c-DFS4XZJV.js +2 -0
  74. package/dist/m/c-DNENWY4O.js +1 -0
  75. package/dist/m/c-DNZ5IFM6.js +1 -0
  76. package/dist/m/c-E4LIR5X5.js +1 -0
  77. package/dist/m/c-EJTGYSLN.js +1 -0
  78. package/dist/m/c-EVCQ3BKA.js +1 -0
  79. package/dist/m/c-EZU56NQV.js +34 -0
  80. package/dist/m/c-FDNHHZY5.js +1 -0
  81. package/dist/m/c-FILLTJA7.js +1 -0
  82. package/dist/m/c-FRLHSY7E.js +1 -0
  83. package/dist/m/c-FROJUZMV.js +1 -0
  84. package/dist/m/c-G2JPKCTP.js +1 -0
  85. package/dist/m/c-GB6TU7WV.js +1 -0
  86. package/dist/m/c-GFJJEBTU.js +1 -0
  87. package/dist/m/c-GJAEGGXY.js +1 -0
  88. package/dist/m/c-GOWBDM6J.js +1 -0
  89. package/dist/m/c-GQ6F6IDK.js +1 -0
  90. package/dist/m/c-GSFIGKOP.js +1 -0
  91. package/dist/m/c-GY422N3Q.js +1 -0
  92. package/dist/m/c-H4LG442Q.js +1 -0
  93. package/dist/m/c-HA6INT6J.js +1 -0
  94. package/dist/m/c-HAWFNBYG.js +1 -0
  95. package/dist/m/c-HDDEEL77.js +1 -0
  96. package/dist/m/c-HFKUVACH.js +1 -0
  97. package/dist/m/c-HHDFCNHV.js +1 -0
  98. package/dist/m/c-HPURYVZS.js +2 -0
  99. package/dist/m/c-I2ZATQCD.js +2 -0
  100. package/dist/m/c-IEM4NTCS.js +1 -0
  101. package/dist/m/c-IJUK2GPW.js +1 -0
  102. package/dist/m/c-IW6GGOU2.js +1 -0
  103. package/dist/m/c-JAX2667S.js +6 -0
  104. package/dist/m/c-JDCX26UP.js +25 -0
  105. package/dist/m/c-JRKY44DK.js +1 -0
  106. package/dist/m/c-JU3NK25H.js +1 -0
  107. package/dist/m/c-KAU6E43D.js +1 -0
  108. package/dist/m/c-KBS3UV6B.js +1 -0
  109. package/dist/m/c-KGVGROZT.js +1 -0
  110. package/dist/m/c-KI7M4AY3.js +1 -0
  111. package/dist/m/c-KIPQJX4B.js +1 -0
  112. package/dist/m/c-KQ6TYJAP.js +135 -0
  113. package/dist/m/c-KU3ZFADW.js +1 -0
  114. package/dist/m/c-L4WQSC27.js +1 -0
  115. package/dist/m/c-LFPHCXY3.js +1 -0
  116. package/dist/m/c-LRPUO2MX.js +1 -0
  117. package/dist/m/c-LWLBHLVC.js +3 -0
  118. package/dist/m/c-LZJPNR2W.js +1 -0
  119. package/dist/m/c-M2L47S2X.js +7 -0
  120. package/dist/m/c-MFP2ZWD4.js +1 -0
  121. package/dist/m/c-MKSQ4Y57.js +1 -0
  122. package/dist/m/c-MTQHWQC3.js +1 -0
  123. package/dist/m/c-N326R2JD.js +1 -0
  124. package/dist/m/c-N7JPUPGC.js +1 -0
  125. package/dist/m/c-NRPJEVJS.js +1 -0
  126. package/dist/m/c-NUFFJVVJ.js +1 -0
  127. package/dist/m/c-NUHP2XDB.js +1 -0
  128. package/dist/m/c-NUHSPT5P.js +1 -0
  129. package/dist/m/c-O72HVYSA.js +1 -0
  130. package/dist/m/c-OCSZ4QGN.js +1 -0
  131. package/dist/m/c-OIBUTDXI.js +1 -0
  132. package/dist/m/c-OIDRZKAF.js +1 -0
  133. package/dist/m/c-OW5QIGG3.js +1 -0
  134. package/dist/m/c-P7KNDG34.js +1 -0
  135. package/dist/m/c-PGRCK73F.js +1 -0
  136. package/dist/m/c-PQ5LK6QE.js +993 -0
  137. package/dist/m/c-PS5ARVWR.js +9 -0
  138. package/dist/m/c-PZ4XEPUP.js +2 -0
  139. package/dist/m/c-Q622NS2Y.js +1 -0
  140. package/dist/m/c-Q7NQD65V.js +1 -0
  141. package/dist/m/c-QH63LQOS.js +1 -0
  142. package/dist/m/c-QHIWXHRO.js +1 -0
  143. package/dist/m/c-QHWJ5ZIJ.js +1 -0
  144. package/dist/m/c-QICP2YR5.js +1 -0
  145. package/dist/m/c-QJ4OM3G6.js +1 -0
  146. package/dist/m/c-QNKREOL6.js +1 -0
  147. package/dist/m/c-R3L7ZJGB.js +2 -0
  148. package/dist/m/c-R7IWVINQ.js +1 -0
  149. package/dist/m/c-RM7J7BV2.js +1 -0
  150. package/dist/m/c-RSMOODDU.js +1 -0
  151. package/dist/m/c-RVMWGXEJ.js +1 -0
  152. package/dist/m/c-RYUXUUBX.js +1 -0
  153. package/dist/m/c-S4ZEH46B.js +9 -0
  154. package/dist/m/c-S7H2DRCB.js +1 -0
  155. package/dist/m/c-S7LK4KUE.js +1 -0
  156. package/dist/m/c-SDAXDM2D.js +1 -0
  157. package/dist/m/c-SGIA4IEQ.js +188 -0
  158. package/dist/m/c-SMBZG5X4.js +1 -0
  159. package/dist/m/c-SMGU7RT3.js +235 -0
  160. package/dist/m/c-SPIORRYS.js +1 -0
  161. package/dist/m/c-SWLRT66R.js +1 -0
  162. package/dist/m/c-SZGYW5HT.js +1 -0
  163. package/dist/m/c-TCCA5BZN.js +2 -0
  164. package/dist/m/c-TCJTVAFN.js +1 -0
  165. package/dist/m/c-TJ6WOJL3.js +67 -0
  166. package/dist/m/c-TQEMVJCS.js +1 -0
  167. package/dist/m/c-TRS6TM6O.js +1 -0
  168. package/dist/m/c-TYLXX4NF.js +21 -0
  169. package/dist/m/c-U62OB4BT.js +1 -0
  170. package/dist/m/c-U6YWUAR3.js +8 -0
  171. package/dist/m/c-UDCSU5G5.js +2 -0
  172. package/dist/m/c-UDUUJPFZ.js +1 -0
  173. package/dist/m/c-UGHH4MB3.js +1 -0
  174. package/dist/m/c-UL2YUY52.js +1 -0
  175. package/dist/m/c-VAK7SG37.js +1 -0
  176. package/dist/m/c-VDSS4K3Z.js +1 -0
  177. package/dist/m/c-VE6ZMYMC.js +1 -0
  178. package/dist/m/c-VG5XIN4T.js +1 -0
  179. package/dist/m/c-VHM5XQHK.js +1 -0
  180. package/dist/m/c-VHNSBD2M.js +1 -0
  181. package/dist/m/c-VI5RMSVY.js +1 -0
  182. package/dist/m/c-VKEDWYDF.js +1 -0
  183. package/dist/m/c-VRZN56XY.js +1 -0
  184. package/dist/m/c-W5MCHEGA.js +1 -0
  185. package/dist/m/c-WDC5GVKU.js +1 -0
  186. package/dist/m/c-WN5AY3OZ.js +1 -0
  187. package/dist/m/c-WNYBLIOZ.js +1 -0
  188. package/dist/m/c-WRBKEJD4.js +1 -0
  189. package/dist/m/c-WWXIDH4O.js +28 -0
  190. package/dist/m/c-WYFDLITL.js +1 -0
  191. package/dist/m/c-XCNTK76U.js +1 -0
  192. package/dist/m/c-XMS6DPBR.js +1 -0
  193. package/dist/m/c-XN6MBKI6.js +67 -0
  194. package/dist/m/c-XPELB6E5.js +1 -0
  195. package/dist/m/c-XQXZAO7N.js +1 -0
  196. package/dist/m/c-Y5HBBIQ4.js +1 -0
  197. package/dist/m/c-ZE46QDIH.js +1 -0
  198. package/dist/m/c-ZUCOKOHT.js +1 -0
  199. package/dist/m/c-ZVPVXTJF.js +4 -0
  200. package/dist/m/c-ZVRKXKM7.js +1 -0
  201. package/dist/m/c-ZWIOJGMT.js +1 -0
  202. package/dist/m/core/admin-attrs-QHOKPJ7W.js +1 -0
  203. package/dist/m/core/autosave-HRYGUWV6.js +1 -0
  204. package/dist/m/core/conflict-notice-6MROJN2L.js +1 -0
  205. package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
  206. package/dist/m/core/host-permission-FNXVEFNE.js +1 -0
  207. package/dist/m/core/is-edit-mode-KTWA3O5N.js +1 -0
  208. package/dist/m/core/persist-JFYRHMRJ.js +3 -0
  209. package/dist/m/core/recovery-FLTCC2OC.js +1 -0
  210. package/dist/m/core/save-HQZ4S667.js +1 -0
  211. package/dist/m/core/save-core-KGQWFUNH.js +1 -0
  212. package/dist/m/core/snapshot-EGQZNT45.js +1 -0
  213. package/dist/m/core/unsaved-warning-IDJLD66H.js +1 -0
  214. package/dist/m/dom/all-APS5ZSKP.js +1 -0
  215. package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
  216. package/dist/m/events/index-RN2JNGBV.js +1 -0
  217. package/dist/m/internals/index-PVXGPUDA.js +1 -0
  218. package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
  219. package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
  220. package/dist/m/loader-S4I7GKXR.js +1 -0
  221. package/dist/m/manifest-4C419D67.js +1 -0
  222. package/dist/m/manifest-edit-B077F288.js +1 -0
  223. package/dist/m/manifest.json +3988 -0
  224. package/dist/m/options/options-EJMS3K24.js +1 -0
  225. package/dist/m/page/editor-NNJA4OKD.js +202 -0
  226. package/dist/m/plugins/ai-edit-QN4PMMNL.js +6 -0
  227. package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
  228. package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
  229. package/dist/m/plugins/demo-RM5IR6AO.js +1 -0
  230. package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
  231. package/dist/m/plugins/indicator-URDBBOQO.js +1 -0
  232. package/dist/m/plugins/live-HFWD4OIH.js +1 -0
  233. package/dist/m/plugins/menu-3HIEQSI6.js +138 -0
  234. package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
  235. package/dist/m/plugins/page-JOKB6XPR.js +2 -0
  236. package/dist/m/plugins/page-view-NXOJHNW6.js +1 -0
  237. package/dist/m/plugins/people-SGPJKANK.js +1 -0
  238. package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
  239. package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
  240. package/dist/m/plugins/richclay-NRQDSWX6.js +517 -0
  241. package/dist/m/plugins/richclay-NRQDSWX6.js.LEGAL.txt +1 -0
  242. package/dist/m/plugins/sortable-GWT4PWDV.js +1 -0
  243. package/dist/m/plugins/source-4ZRSAIYM.js +1 -0
  244. package/dist/m/plugins/undo-VDA5P7FH.js +1 -0
  245. package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
  246. package/dist/m/plugins/upgrade-W7FG5VFG.js +1 -0
  247. package/dist/m/plugins/upload-WLYKK6TL.js +1 -0
  248. package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +21 -0
  249. package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +1 -0
  250. package/dist/m/plugins/wire-4N6GYAF6.js +1 -0
  251. package/dist/m/reader-core-DXYCURFF.js +68 -0
  252. package/dist/m/route/route-KOCZSEGN.js +1 -0
  253. package/dist/m/sync/live-sync-J7VMYTKS.js +1 -0
  254. package/dist/m/ui/index-3JLVI27O.js +1 -0
  255. package/dist/m/utils/index-4TJCVLT7.js +1 -0
  256. package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
  257. package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
  258. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
  259. package/dist/offline-manifest.json +4034 -0
  260. package/dist/page/themes/light-1.css +430 -0
  261. package/dist/server/core.cjs +23748 -22855
  262. package/dist/server/offline-runtime.cjs +4954 -0
  263. package/dist/server/offline-runtime.mjs +4923 -0
  264. package/dist/server/rooms.mjs +9 -0
  265. package/docs/guide.md +1078 -669
  266. package/entries/all.js +2 -0
  267. package/entries/clay-data.js +1 -1
  268. package/entries/clay-dom.js +2 -0
  269. package/entries/clay-events.js +2 -0
  270. package/entries/clay-internals.js +2 -0
  271. package/entries/clay-options.js +2 -0
  272. package/entries/clay-router.js +2 -0
  273. package/entries/clay-ui.js +2 -0
  274. package/entries/clay-utils.js +2 -0
  275. package/entries/clay.js +2 -0
  276. package/entries/sap.js +6 -6
  277. package/fonts/ibm-plex-mono-LICENSE +93 -0
  278. package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  279. package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  280. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  281. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  282. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  283. package/fonts/ibm-plex-sans-LICENSE +93 -0
  284. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  285. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  286. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  287. package/fonts/ibm-plex.css +71 -0
  288. package/fonts/inter-LICENSE +93 -0
  289. package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  290. package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
  291. package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
  292. package/fonts/inter-greek-wght-normal.woff2 +0 -0
  293. package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  294. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  295. package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
  296. package/fonts/inter.css +62 -0
  297. package/fonts/newsreader-LICENSE +93 -0
  298. package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
  299. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  300. package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
  301. package/fonts/newsreader.css +26 -0
  302. package/fonts/work-sans-LICENSE +93 -0
  303. package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
  304. package/fonts/work-sans.css +8 -0
  305. package/package.json +20 -3
  306. package/packed-contract.json +1 -0
  307. package/src/attrs/add-from.js +27 -4
  308. package/src/attrs/save-freeze.js +7 -52
  309. package/src/core/admin-attrs.js +119 -12
  310. package/src/core/admin-contenteditable.js +12 -10
  311. package/src/core/admin-inputs.js +14 -15
  312. package/src/core/admin-onclick.js +2 -0
  313. package/src/core/autosave.js +74 -21
  314. package/src/core/boot-phase.js +2 -1
  315. package/src/core/conflict-download.js +19 -23
  316. package/src/core/conflict-notice-ui.js +1173 -0
  317. package/src/core/conflict-notice.js +140 -697
  318. package/src/core/download.js +32 -0
  319. package/src/core/edit-mode.js +147 -14
  320. package/src/core/etag.js +30 -12
  321. package/src/core/host-meta.js +90 -8
  322. package/src/core/host-permission.js +29 -0
  323. package/src/core/is-edit-mode.js +145 -12
  324. package/src/core/mode-key.js +125 -0
  325. package/src/core/mode-lifecycle.js +544 -0
  326. package/src/core/mode-receipt.js +30 -0
  327. package/src/core/mode-refusal.js +13 -0
  328. package/src/core/mode-resources.js +52 -0
  329. package/src/core/mode-support.js +82 -0
  330. package/src/core/mode-switch.js +259 -0
  331. package/src/core/mode-transition.js +112 -0
  332. package/src/core/page-data-write.js +305 -0
  333. package/src/core/page-data.js +26 -227
  334. package/src/core/page-format-wire.js +4 -0
  335. package/src/core/page-format.js +58 -0
  336. package/src/core/persist.js +37 -9
  337. package/src/core/preserve-authored.js +114 -0
  338. package/src/core/recovery.js +772 -0
  339. package/src/core/runtime-url.js +105 -0
  340. package/src/core/save-core.js +113 -12
  341. package/src/core/save-replace.js +72 -0
  342. package/src/core/save-size-notice.js +134 -0
  343. package/src/core/save-size.js +140 -0
  344. package/src/core/save.js +806 -119
  345. package/src/core/snapshot.js +159 -11
  346. package/src/core/source-map.js +157 -9
  347. package/src/core/unsaved-warning.js +14 -4
  348. package/src/core/upgrade-run.js +408 -0
  349. package/src/core/version.js +18 -0
  350. package/src/lib/assets.js +30 -0
  351. package/src/lib/autosave-debug-gate.js +50 -0
  352. package/src/lib/autosave-scheduler.js +140 -0
  353. package/src/lib/boot-imports.js +8 -0
  354. package/src/lib/derived.js +3 -1
  355. package/src/lib/dirty-gate.js +15 -0
  356. package/src/lib/doc-storage.js +33 -0
  357. package/src/lib/effect.js +9 -4
  358. package/src/lib/mutation.js +4 -2
  359. package/src/lib/offline-runtime.js +226 -0
  360. package/src/lib/preload.js +48 -0
  361. package/src/lib/root-attrs.js +1 -1
  362. package/src/lib/trash.js +18 -0
  363. package/src/lib/undo/capture-move.js +152 -0
  364. package/src/live/local-channel.js +61 -0
  365. package/src/live/local-lane.js +60 -9
  366. package/src/live/server-lane.js +6 -3
  367. package/src/loader-logic.js +76 -15
  368. package/src/loader.js +218 -30
  369. package/src/page/adopt-boot.js +76 -22
  370. package/src/page/adopt.js +25 -4
  371. package/src/page/awareness.js +230 -0
  372. package/src/page/blocks.js +113 -6
  373. package/src/page/blocksel.js +20 -4
  374. package/src/page/board-face.js +13 -0
  375. package/src/page/board-panel.js +5 -17
  376. package/src/page/board-style.js +2 -13
  377. package/src/page/board-view.js +1 -11
  378. package/src/page/board-views.js +3 -0
  379. package/src/page/board.js +2 -2
  380. package/src/page/boot-pack.js +26 -0
  381. package/src/page/boot.js +237 -0
  382. package/src/page/chart-tables.js +11 -0
  383. package/src/page/chart.js +6 -10
  384. package/src/page/checklist.js +1 -1
  385. package/src/page/checkpoint-proof.js +289 -0
  386. package/src/page/chrome.js +431 -63
  387. package/src/page/csv-import.js +17 -2
  388. package/src/page/document-title-dom.js +52 -0
  389. package/src/page/document-title-runtime.js +90 -0
  390. package/src/page/document-title.js +12 -0
  391. package/src/page/drag.js +48 -13
  392. package/src/page/drop.js +2 -1
  393. package/src/page/editor.js +399 -90
  394. package/src/page/emoji-data.js +162 -0
  395. package/src/page/feature-loader.js +72 -0
  396. package/src/page/features/board.js +21 -0
  397. package/src/page/features/chart.js +11 -0
  398. package/src/page/features/code.js +13 -0
  399. package/src/page/features/dates.js +180 -0
  400. package/src/page/features/emoji.js +187 -0
  401. package/src/page/features/media.js +290 -0
  402. package/src/page/features/mentions.js +188 -0
  403. package/src/page/features/registry.js +28 -0
  404. package/src/page/features/table.js +383 -0
  405. package/src/page/{tool-view.js → features/tool.js} +33 -2
  406. package/src/page/format-codec.js +215 -0
  407. package/src/page/format-command.js +9 -0
  408. package/src/page/format-gate.js +179 -0
  409. package/src/page/format-host.js +54 -0
  410. package/src/page/format-level.js +157 -0
  411. package/src/page/format-native-commands.js +142 -0
  412. package/src/page/format-native.js +42 -0
  413. package/src/page/highlight-slot.js +23 -0
  414. package/src/page/highlight.js +30 -16
  415. package/src/page/history-intent.js +78 -0
  416. package/src/page/inline.js +45 -28
  417. package/src/page/items.js +67 -14
  418. package/src/page/link.js +1 -0
  419. package/src/page/markdown.js +112 -3
  420. package/src/page/media-views.js +3 -0
  421. package/src/page/media.js +33 -245
  422. package/src/page/mention-picker.js +99 -0
  423. package/src/page/merge.js +1 -1
  424. package/src/page/outline-cache.js +11 -0
  425. package/src/page/placeholder.js +27 -7
  426. package/src/page/projection.js +240 -51
  427. package/src/page/reader-patch.js +306 -0
  428. package/src/page/recover.js +1 -1
  429. package/src/page/rich/page-tool.js +1 -4
  430. package/src/page/runtime-owner.js +27 -0
  431. package/src/page/save-projection.js +46 -4
  432. package/src/page/save-states.js +1 -1
  433. package/src/page/schema.js +47 -0
  434. package/src/page/table-rect-key.js +3 -0
  435. package/src/page/table-select.js +4 -3
  436. package/src/page/table-style.js +1 -1
  437. package/src/page/table-views.js +3 -0
  438. package/src/page/table.js +8 -293
  439. package/src/page/themes/content.css +25 -1
  440. package/src/page/themes/light-1.css +163 -2
  441. package/src/page/themes/light-1.source.css +269 -0
  442. package/src/page/tool-capability.js +3 -0
  443. package/src/page/tool-views.js +3 -0
  444. package/src/page/view-hooks.js +32 -0
  445. package/src/page/view.js +390 -107
  446. package/src/page/ybind/awareness.js +172 -0
  447. package/src/page/ybind/binding.js +40 -5
  448. package/src/page/ybind/bridge.js +50 -47
  449. package/src/page/ybind/decode.js +52 -19
  450. package/src/page/ybind/doc.js +27 -2
  451. package/src/page/ybind/group.js +12 -3
  452. package/src/page/ybind/index.js +2 -0
  453. package/src/page/ybind/lineage.js +71 -0
  454. package/src/page/ybind/marks.js +26 -1
  455. package/src/page/ybind/migrate.js +48 -13
  456. package/src/page/ybind/moves.js +32 -21
  457. package/src/page/ybind/positions.js +50 -0
  458. package/src/page/ybind/provider.js +97 -24
  459. package/src/page/ybind/recovery-proof.js +191 -0
  460. package/src/page/ybind/recovery-replay.js +488 -0
  461. package/src/page/ybind/tokens.js +15 -6
  462. package/src/page/ybind/walk.js +10 -3
  463. package/src/page/ybind/writer.js +48 -4
  464. package/src/plugins/ai-edit.js +167 -32
  465. package/src/plugins/bind-text.js +218 -0
  466. package/src/plugins/cms.js +93 -0
  467. package/src/plugins/indicator.js +57 -5
  468. package/src/plugins/live.js +319 -35
  469. package/src/plugins/menu/claygoo.svg.js +1 -0
  470. package/src/plugins/menu/seo-head.js +323 -0
  471. package/src/plugins/menu/seo.js +849 -0
  472. package/src/plugins/menu.js +425 -0
  473. package/src/plugins/page-view.js +131 -0
  474. package/src/plugins/page.js +1131 -270
  475. package/src/plugins/people.js +158 -31
  476. package/src/plugins/richclay.js +119 -9
  477. package/src/plugins/sortable.js +370 -60
  478. package/src/plugins/source.js +189 -366
  479. package/src/plugins/undo.js +289 -19
  480. package/src/plugins/unsent.js +132 -0
  481. package/src/plugins/upgrade-run.js +1 -0
  482. package/src/plugins/upgrade.js +210 -0
  483. package/src/plugins/upload-task.js +571 -0
  484. package/src/plugins/upload-ui.js +265 -0
  485. package/src/plugins/upload.js +34 -153
  486. package/src/plugins/visitor-drafts/inert.js +32 -0
  487. package/src/plugins/visitor-drafts/intro.js +2 -1
  488. package/src/plugins/visitor-drafts/owner-chip.js +3 -0
  489. package/src/plugins/visitor-drafts/prompt.js +2 -2
  490. package/src/plugins/visitor-drafts-owner.js +85 -0
  491. package/src/plugins/visitor-drafts.js +31 -42
  492. package/src/plugins/wire.js +82 -1
  493. package/src/reader-core.js +26 -0
  494. package/src/recovery/conflicts.js +287 -0
  495. package/src/recovery/identity.js +154 -0
  496. package/src/recovery/store.js +792 -0
  497. package/src/recovery/undo.js +257 -0
  498. package/src/server/checkpoint/index.js +68 -4
  499. package/src/server/offline-runtime/index.js +214 -0
  500. package/src/server/rooms/index.js +2 -1
  501. package/src/server/rooms/live-data-rooms.js +2 -2
  502. package/src/server/rooms/page-admission.js +33 -0
  503. package/src/server/rooms/page-lineage.js +56 -0
  504. package/src/server/rooms/page-rebase.js +11 -5
  505. package/src/server/rooms/page-state.js +2 -0
  506. package/src/server/rooms/page-write-admission.js +9 -3
  507. package/src/source/session.js +705 -0
  508. package/src/sync/conflict-revert.js +5 -4
  509. package/src/sync/conflicts-edit.js +119 -0
  510. package/src/sync/conflicts-facade.js +49 -0
  511. package/src/sync/conflicts.js +99 -1
  512. package/src/sync/editing-marker.js +37 -6
  513. package/src/sync/editing-sender.js +8 -1
  514. package/src/sync/live-sync-edit.js +63 -0
  515. package/src/sync/live-sync.js +364 -119
  516. package/src/sync/local-presence.js +187 -0
  517. package/src/sync/merge-declarations.js +10 -0
  518. package/src/sync/merge-engine.js +12 -0
  519. package/src/sync/presence-store.js +412 -0
  520. package/src/sync/presence-target.js +243 -0
  521. package/src/sync/presence.js +72 -41
  522. package/src/sync/saved-handoff.js +200 -0
  523. package/src/sync/saved-stream.js +544 -0
  524. package/src/sync/version-guard.js +92 -0
  525. package/src/sync/wire-profile.js +81 -0
  526. package/src/ui/bevel-controls.js +2 -8
  527. package/src/ui/page-scheme.js +7 -0
  528. package/src/ui/skins/cms.js +1 -1
  529. package/src/ui/theme-config.js +150 -0
  530. package/src/ui/theme.js +6 -138
  531. package/src/ui/toast-lazy.js +11 -0
  532. package/src/vendor/LICENSES-optional.md +253 -0
  533. package/src/vendor/highlight.vendor.js +3 -0
  534. package/src/vendor/hljs-bash.vendor.js +2 -0
  535. package/src/vendor/hljs-css.vendor.js +2 -0
  536. package/src/vendor/hljs-javascript.vendor.js +2 -0
  537. package/src/vendor/hljs-json.vendor.js +2 -0
  538. package/src/vendor/hljs-python.vendor.js +2 -0
  539. package/src/vendor/hljs-xml.vendor.js +2 -0
  540. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  541. package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
  542. package/src/vendor/hyper-morph-identity.vendor.js +2 -0
  543. package/src/vendor/hyper-morph-json.vendor.js +3 -0
  544. package/src/vendor/hyper-morph.vendor.js +6 -6
  545. package/src/vendor/hyper-undo.vendor.js +1 -1
  546. package/src/vendor/hypercms.vendor.js +169 -84
  547. package/src/vendor/marked.vendor.js +68 -0
  548. package/src/vendor/overtype.vendor.js +994 -0
  549. package/src/vendor/richclay.vendor.js +17 -17
  550. package/src/vendor/sap-core.vendor.js +5 -5
  551. package/src/vendor/sap-ownership.vendor.js +11 -7
@@ -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
+ }
@@ -1,8 +1,21 @@
1
1
  import { schema } from "../schema.js";
2
+ import { entriesOfYjs, isOpaque, opaqueMark, yjsFields } from "../format-codec.js";
3
+ import { NATIVE_NAMES, nativeAttrs } from "../format-native.js";
2
4
 
3
5
  export const markName = (k) => k.split("!")[0].split("~")[0];
4
6
  export const isCancel = (k) => k.includes("!");
5
7
  export const marksAttrs = (marks) => { const o = {}; for (const m of marks) o[m.type.name] = m.attrs && Object.keys(m.attrs).length ? { ...m.attrs } : true; return o; };
8
+ // The Yjs fields a set of editor marks stands for. An opaque mark is not a live
9
+ // key of its own: its named descriptors go under their own names and its compact
10
+ // tokens under the reserved one.
11
+ export const marksFields = (marks) => {
12
+ const o = {};
13
+ for (const m of marks) {
14
+ if (isOpaque(m)) { for (const [key, value] of Object.entries(yjsFields(m.attrs.entries))) o[key] = JSON.parse(JSON.stringify(value)); continue; }
15
+ o[m.type.name] = m.attrs && Object.keys(m.attrs).length ? { ...m.attrs } : true;
16
+ }
17
+ return o;
18
+ };
6
19
  export const marksKey = (marks) => marks.map((m) => m.type.name + (m.attrs && Object.keys(m.attrs).length ? JSON.stringify(m.attrs) : "")).sort().join("|");
7
20
  export function marksFromAttributes(a, blockType) {
8
21
  if (blockType === "code_block") return [];
@@ -14,5 +27,17 @@ export function marksFromAttributes(a, blockType) {
14
27
  if (v === null || v === undefined || isCancel(k) || cancelled.has(k)) continue;
15
28
  best.set(markName(k), v);
16
29
  }
17
- return [...best].filter(([n]) => schema.marks[n]).map(([n, v]) => schema.marks[n].create(v === true ? null : v));
30
+ const marks = [];
31
+ let unknown = null;
32
+ for (const [n, v] of best) {
33
+ if (schema.marks[n] && (!NATIVE_NAMES.has(n) || nativeAttrs(n, v))) marks.push(schema.marks[n].create(v === true ? null : v));
34
+ else (unknown ??= {})[n] = v;
35
+ }
36
+ // The walk asks this of every text run on every keystroke, and almost none carry a
37
+ // mark from a newer version, so the codec runs only when one is there.
38
+ if (unknown) {
39
+ const opaque = entriesOfYjs(unknown);
40
+ if (opaque.length) marks.push(opaqueMark(opaque));
41
+ }
42
+ return marks;
18
43
  }
@@ -1,10 +1,11 @@
1
1
  import * as Y from "../../vendor/yjs.vendor.js";
2
2
  import { schema } from "../schema.js";
3
3
  import { nodeFromBlock } from "../model.js";
4
- import { flatten, marksOf } from "../inline.js";
4
+ import { flatten, markKey, marksOf } from "../inline.js";
5
5
  import { ATOM, BREAK, DEFAULTS, blocksOfValues, orderedIds, unitsOf, runsOf, canon, hash64, parseModel } from "../projection.js";
6
6
  import { blockFromItem } from "../adopt.js";
7
7
  import { createPageDoc, attrKeys, defaultOf, SEED } from "./layout.js";
8
+ import { marksFields } from "./marks.js";
8
9
  import { blocksFromYValue } from "./decode.js";
9
10
  import { PART, enc } from "./provider.js";
10
11
  import { boardKey } from "./board.js";
@@ -34,13 +35,54 @@ function sourceBlocks(values) {
34
35
  return blocksOfValues(values);
35
36
  }
36
37
 
38
+ // A mark key as the record keeps it: the codec validates every descriptor, and the
39
+ // known codes and the opaque ones come back in the order the binding writes them.
40
+ const canonicalKey = (key, id) => {
41
+ let marks;
42
+ try { marks = marksOf(schema, key); } catch { return fail(`Invalid mark key on block: ${id}`); }
43
+ return markKey([...marks].sort((a, b) => a.type.rank - b.type.rank));
44
+ };
45
+
46
+ const markUnits = (block, id) => {
47
+ const units = unitsOf(block.m, block.c.length);
48
+ if (!units) fail(`Invalid mark runs on block: ${id}`);
49
+ for (let i = 0; i < units.length; i++) {
50
+ const key = units[i];
51
+ if (typeof key !== "string") fail(`Invalid mark key on block: ${id}`);
52
+ if (key.startsWith("@") && (key !== BREAK || block.c[i] !== ATOM)) fail(`Unsupported inline atom on block: ${id}`);
53
+ if (block.t === "code_block" && key) fail(`Unsupported formatting in code block: ${id}`);
54
+ if (!key.startsWith("@")) units[i] = canonicalKey(key, id);
55
+ }
56
+ return units;
57
+ };
58
+
59
+ // A block type this runtime has no schema for keeps the record it stands for: its
60
+ // own type, its text and marks and every other field a newer peer wrote on it.
61
+ function opaqueRecord(id, block, pos) {
62
+ const out = { t: block.t, pos };
63
+ for (const key of Object.keys(block)) {
64
+ if (key === "t" || key === "pos" || key === "c" || key === "m") continue;
65
+ out[key] = copy(block[key]);
66
+ }
67
+ if (block.c != null && typeof block.c !== "string") fail(`Invalid text on block: ${id}`);
68
+ out.c = block.c ?? "";
69
+ if (typeof block.c === "string" && block.m != null) {
70
+ const units = markUnits(block, id);
71
+ if (units.some(Boolean)) out.m = runsOf(units);
72
+ } else if (block.m != null) fail(`Mark runs require flat text on block: ${id}`);
73
+ return out;
74
+ }
75
+
37
76
  function normalizeBlock(id, block, index) {
38
- if (!Object.hasOwn(DEFAULTS, block.t)) fail(`Unsupported hybrid block type: ${block.t}`);
77
+ const known = Object.hasOwn(DEFAULTS, block.t);
78
+ if (!known && (typeof block.t !== "string" || !block.t || block.t === "opaque_block")) fail(`Unsupported hybrid block type: ${block.t}`);
79
+ const pos = String(index + 1).padStart(8, "0");
80
+ if (!known) return opaqueRecord(id, block, pos);
39
81
  const type = schema.nodes[block.t];
40
82
  const fields = attrKeys(type);
41
83
  const allowed = new Set(["t", "pos", "c", "m", ...fields, ...(block.t === "table" ? ["rows"] : [])]);
42
- for (const key of Object.keys(block)) if (!allowed.has(key)) fail(`Unsupported field on block ${id}: ${key}`);
43
- const out = { t: block.t, pos: String(index + 1).padStart(8, "0") };
84
+ const out = { t: block.t, pos };
85
+ for (const key of Object.keys(block)) if (!allowed.has(key)) out[key] = copy(block[key]);
44
86
  for (const key of fields) {
45
87
  const value = block[key] === undefined ? defaultOf(type, key) : block[key];
46
88
  if (canon(value) !== canon(defaultOf(type, key))) out[key] = copy(value);
@@ -56,14 +98,7 @@ function normalizeBlock(id, block, index) {
56
98
  }
57
99
  if (block.c != null && typeof block.c !== "string" && !Array.isArray(block.c)) fail(`Invalid text on block: ${id}`);
58
100
  if (typeof block.c === "string" && block.m != null) {
59
- const units = unitsOf(block.m, block.c.length);
60
- if (!units) fail(`Invalid mark runs on block: ${id}`);
61
- for (let i = 0; i < units.length; i++) {
62
- const key = units[i];
63
- if (typeof key !== "string") fail(`Invalid mark key on block: ${id}`);
64
- if (key.startsWith("@") && (key !== BREAK || block.c[i] !== ATOM)) fail(`Unsupported inline atom on block: ${id}`);
65
- if (block.t === "code_block" && key) fail(`Unsupported formatting in code block: ${id}`);
66
- }
101
+ markUnits(block, id);
67
102
  } else if (block.m != null) fail(`Mark runs require flat text on block: ${id}`);
68
103
  const node = nodeFromBlock(schema, id, block);
69
104
  node.check();
@@ -107,7 +142,7 @@ export function buildPageValue(values, clientID) {
107
142
  if (key === BREAK) {
108
143
  for (let i = 0; i < length; i++) pd.flow.insertEmbed(pd.flow.length, { br: 1 }, {});
109
144
  } else {
110
- const attrs = Object.fromEntries(marksOf(schema, key).map((mark) => [`${mark.type.name}~${clientID}-m${(op++).toString(36)}`, Object.keys(mark.attrs).length ? copy(mark.attrs) : true]));
145
+ const attrs = Object.fromEntries(Object.entries(marksFields(marksOf(schema, key))).map(([name, value]) => [`${name}~${clientID}-m${(op++).toString(36)}`, value && typeof value === "object" ? copy(value) : value]));
111
146
  pd.flow.insert(pd.flow.length, c.slice(offset, offset + length), attrs);
112
147
  }
113
148
  offset += length;
@@ -59,6 +59,7 @@ const MAX_INDENT = 8;
59
59
  const ATTR_RULES = {
60
60
  indent: (value, block) => block.t === "list_item" && Number.isInteger(value) && value >= 0 && value <= MAX_INDENT,
61
61
  open: (value, block) => block.t === "toggle" && typeof value === "boolean",
62
+ parent: (value, block, byId) => value === null || (typeof value === "string" && byId.get(value)?.t === "toggle" && !inSubtree(byId, value, block.id)),
62
63
  };
63
64
 
64
65
  const attrChanges = (args, byId) => {
@@ -69,7 +70,7 @@ const attrChanges = (args, byId) => {
69
70
  const block = byId.get(id);
70
71
  if (!block || !fields || typeof fields !== "object" || Array.isArray(fields)) return null;
71
72
  for (const [field, value] of Object.entries(fields)) {
72
- if (!ATTR_RULES[field] || !ATTR_RULES[field](value, block)) return null;
73
+ if (!ATTR_RULES[field] || !ATTR_RULES[field](value, block, byId)) return null;
73
74
  out.push({ id, field, value });
74
75
  }
75
76
  }
@@ -118,6 +119,9 @@ export function moveBlocks({ pd, view, um, origins, render }, args = {}) {
118
119
  const hasParent = args?.parent !== undefined;
119
120
 
120
121
  const current = walk(pd, origins);
122
+ // An opaque block is an operation this runtime must refuse before writing: it
123
+ // has no turn-into, it cannot be carried, and its record is never rewritten.
124
+ const protectedIds = new Set(current.visible.filter((seg) => seg.opaque).map((seg) => seg.id));
121
125
  const list = visibleBlocks(pd, current, view);
122
126
  const byId = new Map(list.map((block) => [block.id, block]));
123
127
  const order = list.map((block) => block.id);
@@ -126,10 +130,12 @@ export function moveBlocks({ pd, view, um, origins, render }, args = {}) {
126
130
  for (const id of ids) if (!byId.has(id)) return refuse("missing-source");
127
131
  const changes = attrChanges(args, byId);
128
132
  if (!changes) return refuse("invalid-attrs");
133
+ const parentChanges = changes.filter((change) => change.field === "parent");
129
134
 
130
135
  const roots = new Set(ids.filter((id) => !ids.some((other) => other !== id && isAncestor(byId, other, id))));
131
136
  const selected = list.filter((block) => ids.some((root) => inSubtree(byId, block.id, root)));
132
137
  const selectedSet = new Set(selected.map((block) => block.id));
138
+ if (selected.some((block) => protectedIds.has(block.id)) || changes.some((change) => protectedIds.has(change.id))) return refuse("unsupported-format");
133
139
 
134
140
  const segments = new Map(current.visible.map((seg) => [seg.id, seg]));
135
141
  for (const block of selected) {
@@ -174,14 +180,33 @@ export function moveBlocks({ pd, view, um, origins, render }, args = {}) {
174
180
  const movedIds = selected.map((block) => block.id);
175
181
  if (sameOrder && !movedRoots.length && !changes.length) return { status: "unchanged" };
176
182
 
183
+ const caret = (() => {
184
+ const sel = view?.state.selection;
185
+ if (!(sel instanceof pmState.TextSelection)) return null;
186
+ const at = (pos) => { const $pos = view.state.doc.resolve(pos); return $pos.depth ? { id: $pos.node(1).attrs.id, offset: $pos.parentOffset } : null; };
187
+ const anchor = at(sel.anchor), head = at(sel.head);
188
+ return anchor && head && (selectedSet.has(anchor.id) || selectedSet.has(head.id)) ? { anchor, head } : null;
189
+ })();
190
+ const restoreCaret = () => {
191
+ if (!caret || !view) return;
192
+ const place = ({ id, offset }) => {
193
+ let found = null;
194
+ view.state.doc.forEach((node, pos) => { if (found === null && node.attrs.id === id) found = pos + 1 + Math.min(offset, node.content.size); });
195
+ return found;
196
+ };
197
+ const anchor = place(caret.anchor), head = place(caret.head);
198
+ if (anchor !== null && head !== null) view.dispatch(view.state.tr.setSelection(pmState.TextSelection.create(view.state.doc, anchor, head)).setMeta(REMOTE_META, true).setMeta("addToHistory", false));
199
+ };
177
200
  if (sameOrder) {
178
201
  um?.stopCapturing();
179
202
  pd.ydoc.transact(() => {
180
- for (const block of movedRoots) setParent(pd, segments.get(block.id), block.id, parent);
181
- for (const change of changes) setAttr(pd, segments.get(change.id), change.id, change.field, change.value);
203
+ if (parentChanges.length) for (const change of parentChanges) setParent(pd, segments.get(change.id), change.id, change.value);
204
+ else for (const block of movedRoots) setParent(pd, segments.get(block.id), block.id, parent);
205
+ for (const change of changes) if (change.field !== "parent") setAttr(pd, segments.get(change.id), change.id, change.field, change.value);
182
206
  }, LOCAL);
183
207
  um?.stopCapturing();
184
208
  render();
209
+ restoreCaret();
185
210
  return { status: "moved", ids: movedIds };
186
211
  }
187
212
 
@@ -216,13 +241,6 @@ export function moveBlocks({ pd, view, um, origins, render }, args = {}) {
216
241
  const baseline = intents.restores.find((restore) => restore.key === winner?.key)?.baseline ?? { type: seg.t, attrs: seg.fields ?? {} };
217
242
  promotions.set(seg.sourceKey, { covers: removalsOf.map((removal) => removal.key), baseline: JSON.parse(JSON.stringify(baseline)) });
218
243
  }
219
- const caret = (() => {
220
- const sel = view?.state.selection;
221
- if (!(sel instanceof pmState.TextSelection)) return null;
222
- const at = (pos) => { const $pos = view.state.doc.resolve(pos); return $pos.depth ? { id: $pos.node(1).attrs.id, offset: $pos.parentOffset } : null; };
223
- const anchor = at(sel.anchor), head = at(sel.head);
224
- return anchor && head && (selectedSet.has(anchor.id) || selectedSet.has(head.id)) ? { anchor, head } : null;
225
- })();
226
244
  if (!removals.length) throw new Error("The selected blocks have no live physical markers to retire");
227
245
 
228
246
  um?.stopCapturing();
@@ -270,19 +288,12 @@ export function moveBlocks({ pd, view, um, origins, render }, args = {}) {
270
288
  pd.gone.set(copyActiveKey(physicalKeyOf(dest[0])), true);
271
289
  }
272
290
  if (spans.length) origins.recordSpans(spans);
273
- for (const block of movedRoots) setParent(pd, segments.get(block.id), block.id, parent);
274
- for (const change of changes) setAttr(pd, segments.get(change.id), change.id, change.field, change.value);
291
+ if (parentChanges.length) for (const change of parentChanges) setParent(pd, segments.get(change.id), change.id, change.value);
292
+ else for (const block of movedRoots) setParent(pd, segments.get(block.id), block.id, parent);
293
+ for (const change of changes) if (change.field !== "parent") setAttr(pd, segments.get(change.id), change.id, change.field, change.value);
275
294
  }, LOCAL);
276
295
  um?.stopCapturing();
277
296
  render();
278
- if (caret && view) {
279
- const place = ({ id, offset }) => {
280
- let found = null;
281
- view.state.doc.forEach((node, pos) => { if (found === null && node.attrs.id === id) found = pos + 1 + Math.min(offset, node.content.size); });
282
- return found;
283
- };
284
- const anchor = place(caret.anchor), head = place(caret.head);
285
- if (anchor !== null && head !== null) view.dispatch(view.state.tr.setSelection(pmState.TextSelection.create(view.state.doc, anchor, head)).setMeta(REMOTE_META, true).setMeta("addToHistory", false));
286
- }
297
+ restoreCaret();
287
298
  return { status: "moved", ids: movedIds };
288
299
  }
@@ -0,0 +1,50 @@
1
+ // Where a position sits on the page: the map between a ProseMirror position in
2
+ // the projected document and an index in the page's Yjs `flow`. Extracted from
3
+ // bridge.js so the same functions keep this tab's own caret through a peer's edit
4
+ // and place a peer's caret here, instead of each approximating the other.
5
+ import { tokenIndex, yBefore } from "./tokens.js";
6
+
7
+ export const yIndexAt = (doc, holes, pos, visible) => yBefore(holes, tokenIndex(doc, new Map(), visible)[pos]);
8
+
9
+ export function posAtY(doc, holes, tokens, y, visible = null) {
10
+ let lo = 0, hi = tokens;
11
+ while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (yBefore(holes, mid) <= y) lo = mid; else hi = mid - 1; }
12
+ const idx = tokenIndex(doc, new Map(), visible);
13
+ let first = -1;
14
+ for (let p = 0; p < idx.length; p++) {
15
+ if (idx[p] < lo) continue;
16
+ if (idx[p] > lo) break;
17
+ if (first < 0) first = p;
18
+ if (doc.resolve(p).parent.inlineContent) return p;
19
+ }
20
+ return first < 0 ? doc.content.size : first;
21
+ }
22
+
23
+ export const cellPoint = (doc, pos) => {
24
+ const $pos = doc.resolve(pos);
25
+ if ($pos.depth !== 3 || $pos.parent.type.name !== "table_cell") return null;
26
+ return { table: $pos.node(1).attrs.id, row: $pos.node(2).attrs.id, col: $pos.parent.attrs.col, offset: $pos.parentOffset };
27
+ };
28
+
29
+ // The cell a point names, as its own range: what an outline draws and what a
30
+ // character offset would pretend to know.
31
+ export const cellRange = (doc, point) => {
32
+ let found = null;
33
+ doc.forEach((table, at) => {
34
+ if (found !== null || table.type.name !== "table" || table.attrs.id !== point.table) return;
35
+ table.forEach((row, rowAt) => {
36
+ if (found !== null || row.attrs.id !== point.row) return;
37
+ row.forEach((cell, cellAt) => {
38
+ if (found === null && cell.attrs.col === point.col) found = { from: at + 2 + rowAt + cellAt, to: at + 2 + rowAt + cellAt + cell.nodeSize, block: point.table, row: point.row, col: point.col };
39
+ });
40
+ });
41
+ });
42
+ return found;
43
+ };
44
+
45
+ export const cellAt = (doc, point) => {
46
+ const range = cellRange(doc, point);
47
+ if (!range) return null;
48
+ const cell = doc.nodeAt(range.from);
49
+ return range.from + 1 + Math.min(point.offset ?? 0, cell ? cell.content.size : 0);
50
+ };
@@ -3,6 +3,7 @@ import { toBase64, fromBase64 } from "../../vendor/yjs.vendor.js";
3
3
  import { NET } from "./layout.js";
4
4
  import { ORIGINS_ORIGIN } from "./origins.js";
5
5
  import { PART_KEY, BASE64, entriesOf, captureYValue } from "./parts.js";
6
+ import { containsUpdate } from "./recovery-proof.js";
6
7
 
7
8
  export { entriesOf } from "./parts.js";
8
9
 
@@ -10,8 +11,11 @@ export const enc = (u) => toBase64(u);
10
11
  export const dec = (s) => fromBase64(s);
11
12
 
12
13
  export const PART = 12 * 1024;
14
+
15
+ // The origin a replayed journal is applied through. It is offered to the room like any
16
+ // other local work and is never journaled again: a recovery is not this tab's typing.
17
+ export const RECOVERY_ORIGIN = "page-recovery";
13
18
  const baseOf = (k) => PART_KEY.exec(k)?.[1] ?? k;
14
- const sameBytes = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
15
19
 
16
20
  export function yValueOf(ydoc) {
17
21
  return { v: 1, g: 0, y: { seed: enc(Y.encodeStateAsUpdateV2(ydoc)) } };
@@ -75,10 +79,12 @@ export function docFromValue(value, { strict = false } = {}) {
75
79
  }
76
80
  }
77
81
 
82
+ const encoder = typeof TextEncoder === "function" ? new TextEncoder() : null;
83
+ const utf8Bytes = (text) => (encoder ? encoder.encode(text).length : text.length * 2);
78
84
  const sameDeletes = (a, b) => Y.equalDeleteSets(Y.createDeleteSetFromStructStore(a.store), Y.createDeleteSetFromStructStore(b.store));
79
85
  const svEqual = (a, b) => { if (a.size !== b.size) return false; for (const [c, n] of a) if (b.get(c) !== n) return false; return true; };
80
86
 
81
- export function createYProvider({ store, ydoc, compact: limits = {}, setTimer = setTimeout, clearTimer = clearTimeout, onStopped = () => {}, stats = { applied: 0, offered: 0, compactions: 0 } }) {
87
+ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer = setTimeout, clearTimer = clearTimeout, onStopped = () => {}, stats = { applied: 0, offered: 0, compactions: 0 }, journal = null }) {
82
88
  const initial = entriesOf(store.value.y);
83
89
  const known = new Set();
84
90
  // Set when this tab's page can no longer reach the room: "lineage" when the room holds
@@ -95,6 +101,9 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
95
101
  const me = ydoc.clientID.toString(36);
96
102
  const nextKey = (p) => `${p}${me}-${(counter++).toString(36)}`;
97
103
  let timer = null;
104
+ const generation = store.value.g;
105
+ // Keys this tab wrote itself: the updates and offers the room has not confirmed yet.
106
+ const own = new Set();
98
107
 
99
108
  const partsOf = (key, s) => {
100
109
  if (s.length <= PART) return [[key, s]];
@@ -104,13 +113,17 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
104
113
  const put = (key, bytes) => {
105
114
  canCompact = true;
106
115
  known.add(key);
116
+ own.add(key);
107
117
  const parts = partsOf(key, enc(bytes));
108
118
  store.handle.update((data) => { for (const [k, v] of parts) data.y[k] = v; });
109
119
  };
110
120
 
111
121
  for (const [k] of initial) known.add(k);
112
122
 
123
+ // Ahead of the stop guards, and of the room's own delivery: work this tab can no longer
124
+ // offer, and work a refused offer took back, is still work a crash must not lose.
113
125
  const onUpdate = (u, origin) => {
126
+ if (origin !== NET && origin !== ORIGINS_ORIGIN && origin !== RECOVERY_ORIGIN) journal?.local(u);
114
127
  if (stopped || origin === NET || origin === ORIGINS_ORIGIN) return;
115
128
  put(nextKey("u"), u);
116
129
  };
@@ -121,7 +134,11 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
121
134
  canCompact = true;
122
135
  known.add(k);
123
136
  stats.applied++;
124
- Y.applyUpdateV2(ydoc, dec(v), NET);
137
+ const update = dec(v);
138
+ Y.applyUpdateV2(ydoc, update, NET);
139
+ // A received update can be the prerequisite of a later local insert, so the journal
140
+ // keeps it beside the work it owns, and never counts it as this tab's own.
141
+ journal?.dependency(update);
125
142
  };
126
143
 
127
144
  let wantReconcile = false;
@@ -163,20 +180,70 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
163
180
  const compact = { keys: 32, room: 3840 * 1024, ...limits };
164
181
  let fold = null;
165
182
  let quietUntil = 0;
183
+ const COVER_MAX = 256;
166
184
  const coverCache = new Map();
167
185
  const drop = (keys) => store.handle.update((data) => { for (const k of keys) delete data.y[k]; });
168
186
  const keysOf = (auth, bases) => Object.keys(auth).filter((k) => bases.has(baseOf(k)));
187
+ // A key is retired only while what this tab shows under it is still what the proof was
188
+ // made over: a late write under the same key keeps its own entry.
189
+ const retire = (auth, keys) => { const visible = store.value.y ?? {}; drop(keys.filter((k) => k in visible && visible[k] === auth[k])); };
169
190
 
191
+ // `known` and the coverage memo keep live references only: the authoritative entries,
192
+ // this tab's own unconfirmed writes and the fold in progress. Pruned after a rebase, a
193
+ // fold and a refusal. A key that is forgotten can be decoded again; Yjs is idempotent.
194
+ function prune() {
195
+ const auth = store.authoritative.y ?? {};
196
+ const visible = store.value.y ?? {};
197
+ for (const k of own) if (!(k in visible)) own.delete(k);
198
+ const held = fold ? [...fold.parts.map(([k]) => k), ...fold.inputs.keys()] : [];
199
+ const retained = new Set([...Object.keys(auth), ...own, ...held]);
200
+ for (const key of known) if (!retained.has(key)) known.delete(key);
201
+ const values = new Map(entriesOf(auth));
202
+ for (const [key, proof] of coverCache) if (!retained.has(key) || values.get(key) !== proof.value) coverCache.delete(key);
203
+ }
204
+
205
+ // Whether a fold's merged bytes already carry this input: merging it in again changes
206
+ // neither an insertion nor a deletion. Equal state vectors say nothing about deletes.
207
+ function coveredBy(key, value, foldKey, base) {
208
+ let proof = coverCache.get(key);
209
+ if (proof && proof.value !== value) { coverCache.delete(key); proof = null; }
210
+ if (!proof) {
211
+ if (coverCache.size >= COVER_MAX) coverCache.delete(coverCache.keys().next().value);
212
+ proof = { value, folds: new Map() };
213
+ coverCache.set(key, proof);
214
+ }
215
+ if (!proof.folds.has(foldKey)) proof.folds.set(foldKey, containsUpdate(base, dec(value)));
216
+ return proof.folds.get(foldKey);
217
+ }
218
+
219
+ // An input the authority no longer has was retired already; one it holds differently
220
+ // was rewritten after the fold was built, and the fold no longer speaks for it.
221
+ const stillHeld = (auth, active) => { for (const [k, v] of active.held) if (auth[k] !== undefined && auth[k] !== v) return false; return true; };
222
+ const coversInputs = (active) => {
223
+ const base = dec(active.merged);
224
+ for (const [k, v] of active.inputs) if (v !== null && !coveredBy(k, v, active.key, base)) return false;
225
+ return true;
226
+ };
227
+
228
+ // Inputs retire only once the authority holds every proposed part with the identical
229
+ // value, still holds each input as it was captured, and the fold covers every one of
230
+ // them. Anything less keeps the originals; pending confirmation keeps them too.
170
231
  function finishFold(auth) {
171
- if (fold.parts.every((k) => k in auth)) {
172
- drop(keysOf(auth, fold.inputs));
232
+ const active = fold;
233
+ const whole = captureYValue({ v: 1, y: auth }).y;
234
+ const arrived = active.parts.filter(([k, v]) => auth[k] === v && whole[k] === v);
235
+ if (arrived.length !== active.parts.length) {
236
+ if (store.pending.length) return false;
173
237
  fold = null;
238
+ if (arrived.length) retire(auth, arrived.map(([k]) => k));
239
+ quietUntil = entriesOf(auth).length + compact.keys;
240
+ prune();
174
241
  return true;
175
242
  }
176
- if (store.pending.length) return false;
177
- drop(fold.parts.filter((k) => k in auth));
178
- quietUntil = entriesOf(auth).length + compact.keys;
243
+ if (store.pending.length || !stillHeld(auth, active) || !coversInputs(active)) return false;
179
244
  fold = null;
245
+ retire(auth, keysOf(auth, active.inputs));
246
+ prune();
180
247
  return true;
181
248
  }
182
249
 
@@ -185,11 +252,7 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
185
252
  const val = new Map(entries);
186
253
  const norm = new Map(folds);
187
254
  const normal = (k) => { const v = norm.get(k); if (typeof v === "string") norm.set(k, Y.mergeUpdatesV2([dec(v)])); return norm.get(k); };
188
- const covered = (k, v, mk) => {
189
- const c = `${k}|${mk}`;
190
- if (!coverCache.has(c)) { const b = normal(mk); coverCache.set(c, sameBytes(Y.mergeUpdatesV2([b, dec(v)]), b)); }
191
- return coverCache.get(c);
192
- };
255
+ const covered = (k, v, mk) => coveredBy(k, v, mk, normal(mk));
193
256
  const gone = new Set();
194
257
  for (const [k, v] of entries) {
195
258
  if (!crowded && k[0] !== "m") continue;
@@ -201,31 +264,36 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
201
264
  }
202
265
  }
203
266
  if (!gone.size) return false;
204
- drop(keysOf(auth, gone));
267
+ retire(auth, keysOf(auth, gone));
205
268
  return true;
206
269
  }
207
270
 
208
271
  function maybeCompact() {
209
- if (!canCompact) return false;
272
+ if (stopped || !canCompact || store.value.g !== generation) return false;
210
273
  const auth = store.authoritative.y;
211
274
  if (fold) return finishFold(auth);
212
275
  const entries = entriesOf(auth);
213
276
  const crowded = entries.length > Math.max(compact.keys, quietUntil);
214
277
  if (!crowded && entries.filter(([k]) => k[0] === "m").length < 2) return false;
215
- if (dropCovered(auth, entries, crowded)) return true;
278
+ if (dropCovered(auth, entries, crowded)) { prune(); return true; }
216
279
  if (!crowded) return false;
217
280
  const merged = enc(Y.mergeUpdatesV2(entries.map(([, v]) => dec(v))));
218
- const used = Object.values(auth).reduce((n, v) => n + v.length, 0);
219
- if (used + merged.length > compact.room) {
281
+ const key = nextKey("m");
282
+ const parts = partsOf(key, merged);
283
+ const candidate = { ...store.value, y: { ...store.value.y } };
284
+ for (const [k, v] of parts) candidate.y[k] = v;
285
+ if (utf8Bytes(JSON.stringify(candidate)) > compact.room) {
220
286
  quietUntil = entries.length + compact.keys;
221
287
  return false;
222
288
  }
223
- const key = nextKey("m");
224
- known.add(key);
225
- const parts = partsOf(key, merged);
226
289
  const captured = captureYValue({ v: 1, y: auth }).y;
227
290
  const incompleteFolds = Object.keys(auth).filter((k) => !(k in captured)).map(baseOf).filter((k) => k[0] === "m");
228
- fold = { key, parts: parts.map(([k]) => k), inputs: new Set([...entries.map(([k]) => k), ...incompleteFolds]) };
291
+ const inputs = new Map(entries);
292
+ for (const k of incompleteFolds) inputs.set(k, null);
293
+ const held = new Map();
294
+ for (const k of keysOf(auth, inputs)) if (inputs.get(baseOf(k)) !== null) held.set(k, captured[k] ?? auth[k]);
295
+ fold = { key, parts, merged, inputs, held };
296
+ known.add(key);
229
297
  store.handle.update((data) => { for (const [k, v] of parts) data.y[k] = v; });
230
298
  stats.compactions++;
231
299
  return true;
@@ -264,6 +332,7 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
264
332
  } else {
265
333
  for (const [k, v] of entriesOf(ev.next.y ?? {})) applyKey(k, v);
266
334
  }
335
+ prune();
267
336
  schedule(ev.source !== "remote" || !ops, ev.source !== "remote");
268
337
  });
269
338
 
@@ -272,14 +341,18 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
272
341
  // the refused one, so they are all taken back, with the parts of updates the room does not
273
342
  // hold whole, and the room's page stays one a checkpoint can render.
274
343
  const offDelivery = store.onDelivery((event) => {
275
- if (event.phase !== "refused" || event.code !== "too-large" || stopped) return;
344
+ if (event.phase !== "refused") return;
345
+ if (event.code !== "too-large" || stopped) { prune(); return; }
276
346
  stop("too-large");
277
347
  const auth = store.authoritative.y ?? {};
278
348
  const whole = captureYValue({ v: 1, y: auth }).y;
279
349
  const unconfirmed = Object.keys(store.value.y ?? {}).filter((k) => !(k in auth));
280
350
  const partial = Object.keys(auth).filter((k) => PART_KEY.test(k) && !(k in whole) && baseOf(k).slice(1).startsWith(`${me}-`));
281
351
  if (unconfirmed.length || partial.length) drop([...unconfirmed, ...partial]);
352
+ prune();
282
353
  });
283
354
 
284
- return { known, stats, settle, reconcile, get reconcilePending() { return reconcilePending; }, get stopped() { return stopped; }, compact: maybeCompact, dispose() { off(); offDelivery(); ydoc.off("updateV2", onUpdate); if (timer) clearTimer(timer); } };
355
+ return { known, stats, settle, reconcile, get reconcilePending() { return reconcilePending; }, get stopped() { return stopped; },
356
+ // Test-only snapshot of the counters and cache sizes; no mutable set leaves the provider.
357
+ _diagnostics: () => ({ known: known.size, cover: coverCache.size, fold: fold ? fold.parts.length : 0, generation }), compact: maybeCompact, dispose() { off(); offDelivery(); ydoc.off("updateV2", onUpdate); if (timer) clearTimer(timer); } };
285
358
  }