@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
@@ -74,6 +74,98 @@ export function originalSnapshotNode(cloneNode) {
74
74
  return snapshotProvenance.get(cloneNode) || null;
75
75
  }
76
76
 
77
+ // The private artifact a save renderer may hand back for one capture: the model of
78
+ // the exact bytes it returned, paired with the tree it was given, so an accepted save
79
+ // can adopt it instead of parsing and pairing the accepted bytes again. It is keyed by
80
+ // the capture result object the caller gets, so it lives exactly as long as the save
81
+ // that produced it and there is no registry to grow.
82
+ const captureArtifacts = new WeakMap();
83
+ // The capture being serialized right now. Only a save capture has one; an inspection
84
+ // capture and a direct `captureForSave` have no request behind them to adopt anything.
85
+ let activeCapture = null;
86
+
87
+ /**
88
+ * The capture the save renderer is producing bytes for, or null.
89
+ *
90
+ * Read by the renderer to find out whose bytes these are, and to attach the artifact
91
+ * with attachCaptureArtifact. Per call, not global state: it is set for exactly the
92
+ * synchronous serialization of one save clone.
93
+ */
94
+ export function currentCaptureContext() {
95
+ return activeCapture;
96
+ }
97
+
98
+ /**
99
+ * Attach the artifact to the capture being serialized.
100
+ *
101
+ * Ignored outside a save capture, because an artifact nobody can accept is a parse and
102
+ * a pairing paid for nothing.
103
+ */
104
+ export function attachCaptureArtifact(artifact) {
105
+ if (activeCapture && artifact) activeCapture.artifact = artifact;
106
+ }
107
+
108
+ // The page state of one save capture: the exact value its page tag carried when its
109
+ // bytes were serialized, copied so the edits that follow the capture can never reach it.
110
+ // Keyed by the capture id every accepted event carries, and dropped when the capture
111
+ // settles, so an accepted save reads the state it was captured from rather than the
112
+ // store's value at the moment the host answered.
113
+ let capturePageStateReader = null;
114
+ const capturePageStates = new Map();
115
+
116
+ /**
117
+ * Register the reader that finds a page state in a final save clone.
118
+ *
119
+ * A page's own code registers it (page/save-projection.js), so a document without a
120
+ * page pays one null check per capture and nothing else. Read from the clone rather
121
+ * than from a store because that tag is what the file receives, after every transform
122
+ * and every authored save handler has run.
123
+ */
124
+ export function setCapturePageState(reader) {
125
+ capturePageStateReader = reader;
126
+ }
127
+
128
+ /**
129
+ * The page state pinned to one capture, or null.
130
+ *
131
+ * @param {?number} captureId
132
+ * @returns {?Object}
133
+ */
134
+ export function capturePageState(captureId) {
135
+ if (captureId === null || captureId === undefined) return null;
136
+ return capturePageStates.get(captureId) ?? null;
137
+ }
138
+
139
+ /** The artifact this capture's renderer attached, or null. */
140
+ export function captureArtifact(result) {
141
+ return result ? captureArtifacts.get(result) || null : null;
142
+ }
143
+
144
+ /** Forget a capture's artifact: it was skipped, refused, or has now settled. */
145
+ export function releaseCaptureArtifact(result) {
146
+ if (!result) return;
147
+ captureArtifacts.delete(result);
148
+ capturePageStates.delete(result.captureId);
149
+ }
150
+
151
+ /**
152
+ * Does this clone hold an authored save handler, template contents included?
153
+ *
154
+ * Those handlers are page JavaScript that can change the clone after the capture, so
155
+ * the accepted bytes may no longer describe the live page node for node; a pairing
156
+ * rebuilt by walking the two together would be describing the handler's tree. The
157
+ * inspection path stays for those pages.
158
+ */
159
+ function hasAuthoredSaveHandlers(root) {
160
+ const visit = (el) => {
161
+ if (el.hasAttribute('onbeforesave') || el.hasAttribute('onbeforesnapshot')) return true;
162
+ const kids = el.localName === 'template' && el.content ? el.content : el;
163
+ for (const child of kids.children) if (visit(child)) return true;
164
+ return false;
165
+ };
166
+ return root.nodeType === 1 ? visit(root) : false;
167
+ }
168
+
77
169
  function stripSnapshotRegions(clone) {
78
170
  for (const el of clone.querySelectorAll(SNAPSHOT_REMOVE_SELECTOR)) el.remove();
79
171
  }
@@ -103,6 +195,19 @@ function runAuthoredHandlers(clone, attr) {
103
195
  }
104
196
  }
105
197
 
198
+ let cloneCanonicalizer = null;
199
+
200
+ export function setCloneCanonicalizer(canonicalize) {
201
+ cloneCanonicalizer = canonicalize;
202
+ }
203
+
204
+ // A document parsed from received bytes gets the same canonical URLs a capture has, so a merge
205
+ // compares like with like. A no-op until save-core registers the canonicalizer.
206
+ export function canonicalizeReceived(doc) {
207
+ cloneCanonicalizer?.(doc);
208
+ return doc;
209
+ }
210
+
106
211
  /**
107
212
  * Register a hook that runs on EVERY snapshot (save AND sync).
108
213
  * Use for: syncing form values to the clone.
@@ -190,14 +295,23 @@ function serializeClone(clone) {
190
295
  * document reprinted; this one is the guarantee that no bug in an optional plugin can
191
296
  * cost somebody a save.
192
297
  */
193
- function serializeSaveClone(clone) {
298
+ function serializeSaveClone(clone, context = null) {
299
+ if (context && capturePageStateReader) {
300
+ let state = null;
301
+ try { state = capturePageStateReader(clone); } catch (_) {}
302
+ if (state) capturePageStates.set(context.captureId, state);
303
+ }
194
304
  const today = serializeClone(clone);
195
305
  if (!saveRenderer) return today;
306
+ const previous = activeCapture;
307
+ activeCapture = context;
196
308
  try {
197
309
  return saveRenderer(clone, today);
198
310
  } catch (err) {
199
311
  console.error('clayjs: save renderer failed, sending the full serialization', err);
200
312
  return today;
313
+ } finally {
314
+ activeCapture = previous;
201
315
  }
202
316
  }
203
317
 
@@ -236,7 +350,7 @@ function clonePreventingOnclone(node, deep = true) {
236
350
  finally { window.__preventOnclone = prev; }
237
351
  }
238
352
 
239
- export function captureSnapshot({ flushUndo = true, authored = true, forSave = false, requestRefresh = false } = {}) {
353
+ export function captureSnapshot({ flushUndo = true, authored = true, forSave = false, requestRefresh = false, context = null } = {}) {
240
354
  // Force-close any pending undo idle batch BEFORE cloning the DOM, so the
241
355
  // snapshot reflects a clean undo boundary. Without this, a save that fires
242
356
  // mid-typing would leave the idle batch open across the save boundary, and
@@ -265,7 +379,14 @@ export function captureSnapshot({ flushUndo = true, authored = true, forSave = f
265
379
  // authored handler sees the same URLs the file will.
266
380
  restoreAuthoredUrls(clone);
267
381
 
382
+ cloneCanonicalizer?.(clone);
383
+
268
384
  stripSnapshotRegions(clone);
385
+ // Whether the page authors its own save handlers, read here because this is the
386
+ // snapshot BEFORE either of them runs: a handler can add the other one, and it is the
387
+ // page as captured that decides whether the save can be paired without an inspection.
388
+ if (context) context.authoredHandlers = hasAuthoredSaveHandlers(clone);
389
+
269
390
  // authored: false is for a capture that is not a save and not a frame the page will
270
391
  // see. The attributes hold page-author JavaScript, so running them is a side effect
271
392
  // the author asked for once per save, not once per capture.
@@ -479,12 +600,22 @@ export function captureForComparisonAndDirty({ flushUndo = true } = {}) {
479
600
  *
480
601
  * @param {Object} options
481
602
  * @param {boolean} options.emitForSync - Whether to emit snapshot-ready event (default: true)
603
+ * @param {boolean} options.forRequest - These bytes go to a host, so the renderer may
604
+ * attach the verified model they came from, for the accepted save to adopt
482
605
  * @returns {{ forSave: string, forComparison: string, forDirty: string, captureId: number }}
483
606
  */
484
- export function captureForSaveAndComparison({ emitForSync = true, flushUndo = true, requestRefresh = false } = {}) {
607
+ export function captureForSaveAndComparison({ emitForSync = true, flushUndo = true, requestRefresh = false, forRequest = false } = {}) {
485
608
  if (!Number.isSafeInteger(captureCounter + 1)) throw new Error('Save capture identity exhausted');
486
609
  const captureId = ++captureCounter;
487
- const clone = captureSnapshot({ flushUndo, forSave: emitForSync, requestRefresh });
610
+ // The context this capture's renderer may attach a private artifact to, and the
611
+ // record of whether the page has its own save handlers. It never reaches the object
612
+ // this function returns: that keeps its four public fields.
613
+ //
614
+ // Only for a capture whose bytes go to a host. A capture taken for anything else (a
615
+ // live-sync disk base, a download, an inspection) has no request to adopt it, so it
616
+ // pays neither the retained parse nor the pairing.
617
+ const context = forRequest ? { captureId, authoredHandlers: false, artifact: null } : null;
618
+ const clone = captureSnapshot({ flushUndo, forSave: emitForSync, requestRefresh, context });
488
619
 
489
620
  // Emit for live-sync before any stripping.
490
621
  //
@@ -525,7 +656,7 @@ export function captureForSaveAndComparison({ emitForSync = true, flushUndo = tr
525
656
  el.remove();
526
657
  }
527
658
  stripDiagnosticBeacons(clone);
528
- const forSave = serializeSaveClone(clone);
659
+ const forSave = serializeSaveClone(clone, context);
529
660
 
530
661
  // Compare clone: strip every autosave-off region, then run hooks
531
662
  for (const el of compareClone.querySelectorAll(STRIP_FROM_COMPARISON)) {
@@ -552,7 +683,9 @@ export function captureForSaveAndComparison({ emitForSync = true, flushUndo = tr
552
683
  forDirty = serializeForCompare(dirtyClone);
553
684
  }
554
685
 
555
- return { forSave, forComparison, forDirty, captureId };
686
+ const result = { forSave, forComparison, forDirty, captureId };
687
+ if (context && context.artifact) captureArtifacts.set(result, context.artifact);
688
+ return result;
556
689
  }
557
690
 
558
691
  /**
@@ -704,13 +837,28 @@ export function captureBodyForSync() {
704
837
  }
705
838
 
706
839
  /**
707
- * Get page contents for change detection.
708
- * Does NOT emit snapshot-ready event (safe for comparison).
840
+ * The bytes a save would send, without saving anything.
709
841
  *
710
- * Returns full HTML via snapshot pipeline.
842
+ * Emits no snapshot-ready event (safe for comparison), and with no options it is
843
+ * exactly `captureForSave({ emitForSync: false })`.
844
+ *
845
+ * `transform(root)` edits the detached clone and nothing else, and runs before
846
+ * prepareCloneForSave so the authored hooks, the transforms and the [no-save] strip
847
+ * still have their say. It must be synchronous: a preview that finished later would
848
+ * describe a page that has moved on, so a thenable is refused rather than awaited.
849
+ * There is no global registration, so no other save ever sees these edits, and the
850
+ * renderer runs without a capture context, so nothing can adopt these bytes as a model.
711
851
  */
712
- export function getPageContents() {
713
- return captureForSave({ emitForSync: false });
852
+ export function getPageContents({ transform = null, flushUndo = true } = {}) {
853
+ if (!transform && flushUndo) return captureForSave({ emitForSync: false });
854
+ const clone = captureSnapshot({ flushUndo });
855
+ if (transform) {
856
+ const pending = transform(clone);
857
+ if (pending && typeof pending.then === 'function') {
858
+ throw new TypeError('getHTML: the transform must be synchronous');
859
+ }
860
+ }
861
+ return serializeSaveClone(prepareCloneForSave(clone));
714
862
  }
715
863
 
716
864
  /**
@@ -63,6 +63,22 @@ export function model(src) {
63
63
  sourceCodeLocationInfo: true,
64
64
  onParseError: (e) => parseErrors.set(e.code, (parseErrors.get(e.code) || 0) + 1),
65
65
  });
66
+ return modelFromParsed(src, doc, parseErrors);
67
+ }
68
+
69
+ /**
70
+ * The model for bytes that have already been parsed, by this module or by the verifier.
71
+ *
72
+ * The split exists so a verified render costs one parse instead of two: the verifier
73
+ * parses the bytes with locations to count the errors the render introduced, and that
74
+ * tree is the model of exactly the bytes it verified. Everything below the parse is
75
+ * here verbatim; nothing here assumes who called the parser.
76
+ *
77
+ * @param {string} src - the bytes the tree was parsed from, which the ranges index
78
+ * @param {Object} doc - parse5 document, parsed with sourceCodeLocationInfo
79
+ * @param {Map} parseErrors - that parse's error counts, by code
80
+ */
81
+ function modelFromParsed(src, doc, parseErrors) {
66
82
  const html = doc.childNodes.find((n) => n.nodeName === 'html');
67
83
  const outside = doc.childNodes.filter((n) => n !== html).map((n) => n.nodeName === '#documentType'
68
84
  ? { kind: 'doctype', name: n.name || '', publicId: n.publicId || '', systemId: n.systemId || '' }
@@ -296,11 +312,20 @@ function outsideOf(doc) {
296
312
 
297
313
  const UNIT_SEP = '\u0000';
298
314
 
315
+ // The two steps pairCaptured exists to skip. Its whole claim is that a structurally
316
+ // identical capture needs neither a signature tree nor an alignment, and a test that
317
+ // only checks the resulting map cannot see the difference: hashing and aligning the same
318
+ // two identical trees produces the same map. Read by tests and by the save meter; the
319
+ // save path itself never reads it.
320
+ export const pairingCounts = { align: 0, hashKey: 0 };
321
+ export function resetPairingCounts() { pairingCounts.align = 0; pairingCounts.hashKey = 0; }
322
+
299
323
  function mix(h, s) {
300
324
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
301
325
  return h >>> 0;
302
326
  }
303
327
  function hashKey(parts) {
328
+ pairingCounts.hashKey++;
304
329
  let a = 2166136261, b = 5381;
305
330
  for (const p of parts) { a = mix(a, p); b = (Math.imul(b, 33) ^ mix(a ^ 0x9e3779b9, p)) >>> 0; }
306
331
  return a.toString(36) + '.' + b.toString(36);
@@ -356,6 +381,7 @@ const DP_CELLS = 4096;
356
381
  * 4.7 ms, and 32,000 siblings in 15.5 ms.
357
382
  */
358
383
  export function align(LK, SK) {
384
+ pairingCounts.align++;
359
385
  const pairs = [];
360
386
  const LI = LK.map((_, i) => i), SI = SK.map((_, j) => j);
361
387
  stage(LI, SI, 0);
@@ -500,6 +526,84 @@ function bind(node, loc, map, keyMap, stats, path, resolve, gen) {
500
526
  for (let j = 0; j < S.length; j++) if (!usedS.has(j)) stats.unmatchedSource.push({ path, node: describeLoc(S[j]) });
501
527
  }
502
528
 
529
+ /**
530
+ * Pair a capture against the model built from the bytes that capture verified as.
531
+ *
532
+ * The map has to exist either way: it is what says where the live page is in the file.
533
+ * What this skips is the SEARCH for it. `pair` computes four signature keys per node and
534
+ * aligns child lists by them, because it is handed a page and a file that may have
535
+ * diverged arbitrarily. A verified capture has not diverged: the verifier compared the
536
+ * captured tree against today's bytes, node for node, so the model of those bytes has the
537
+ * captured tree, in order, with the same children. Walking the two together is the whole
538
+ * pairing.
539
+ *
540
+ * Refusing matters more than succeeding. Anything that differs returns null and the caller
541
+ * takes `pair`; a walk that guessed instead would pair one tree's locations to another
542
+ * tree's nodes and print the wrong bytes into the file. There is no partial result here.
543
+ *
544
+ * @param {Node} root - the prepared save clone's document element
545
+ * @param {Object} m - the model of the bytes this clone was verified to render as
546
+ * @param {Function} [provenance] - clone node -> the identity to key the map by
547
+ * @returns {?{map: WeakMap, stats: Object}} null when the trees are not the same shape
548
+ */
549
+ export function pairCaptured(root, m, provenance = (n) => n) {
550
+ if (!m.root) return null;
551
+ const found = [];
552
+ const unresolved = matchCaptured(root, m.root, provenance, found);
553
+ if (unresolved < 0) return null;
554
+ const map = new WeakMap();
555
+ // One token for this pass, set on the model and on every bound loc, exactly as `pair`
556
+ // does it: render slices a loc only when the two still agree.
557
+ const gen = {};
558
+ m.gen = gen;
559
+ let paired = 0;
560
+ // A clone node a transform created has no live original and stays unpaired, so it is
561
+ // printed: the same thing `pair` does with it.
562
+ for (const [key, loc] of found) if (key) { map.set(key, loc); loc.gen = gen; paired++; }
563
+ return {
564
+ map,
565
+ stats: { paired, unmatchedLive: [], unmatchedSource: [], attrDiffs: [], textDiffs: 0, unresolved },
566
+ };
567
+ }
568
+
569
+ /** @returns {number} unresolved nodes below this pair, or -1 when the two differ at all */
570
+ function matchCaptured(node, loc, provenance, found) {
571
+ if (loc.kind === 'text') {
572
+ if (node.nodeType !== 3 || node.data !== loc.value) return -1;
573
+ } else if (loc.kind === 'comment') {
574
+ if (node.nodeType !== 8 || node.data !== loc.value) return -1;
575
+ } else {
576
+ if (node.nodeType !== 1 || node.localName !== loc.tag || node.namespaceURI !== loc.node.namespaceURI) return -1;
577
+ if (capturedAttrs(node) !== capturedLocAttrs(loc.node)) return -1;
578
+ }
579
+ const key = provenance(node);
580
+ found.push([key, loc]);
581
+ let unresolved = key ? 0 : 1;
582
+ if (loc.kind !== 'element') return unresolved;
583
+ const L = capturedKids(node), S = loc.children;
584
+ if (L.length !== S.length) return -1;
585
+ for (let i = 0; i < L.length; i++) {
586
+ const n = matchCaptured(L[i], S[i], provenance, found);
587
+ if (n < 0) return -1;
588
+ unresolved += n;
589
+ }
590
+ return unresolved;
591
+ }
592
+
593
+ const attrKey = (a) => JSON.stringify(a);
594
+ /** Qualified name, namespace and value, sorted: two attribute sets are equal as sets. */
595
+ const capturedAttrs = (el) => Array.from(el.attributes, (a) => attrKey([a.namespaceURI || '', a.name, a.value])).sort().join(UNIT_SEP);
596
+ // parse5 adjusts a foreign attribute in the tree (`xlink:href` is `{ prefix: 'xlink', name: 'href' }`)
597
+ // and keeps the location map keyed by the name as written, so the qualified name is rebuilt here.
598
+ const capturedLocAttrs = (node) => node.attrs.map((a) => attrKey([a.namespace || '', a.prefix ? a.prefix + ':' + a.name : a.name, a.value])).sort().join(UNIT_SEP);
599
+
600
+ function capturedKids(el) {
601
+ const list = el.nodeType === 1 && el.tagName === 'TEMPLATE' && el.content ? el.content.childNodes : el.childNodes;
602
+ const out = [];
603
+ for (const n of list) if (n.nodeType === 1 || n.nodeType === 3 || n.nodeType === 8) out.push(n);
604
+ return out;
605
+ }
606
+
503
607
  // =============================================================================
504
608
  // RENDER
505
609
  // =============================================================================
@@ -923,6 +1027,27 @@ function lis(vals) {
923
1027
  // the right one.
924
1028
 
925
1029
  export function verify(rendered, today, liveDocument, clone = null, sourceErrors = null) {
1030
+ return verifyRendered(rendered, today, liveDocument, clone, sourceErrors, false);
1031
+ }
1032
+
1033
+ /**
1034
+ * verify, keeping the parse it already paid for.
1035
+ *
1036
+ * The parse-error check below is a parse5 parse of the rendered bytes, and it is thrown
1037
+ * away today: an accepted save then parses the accepted bytes a second time to model them.
1038
+ * This returns the same verdict with that tree as `model`, so the save that verified can
1039
+ * be modelled from the tree that verified it. `model` is present only when the verdict is
1040
+ * ok AND the candidate is still this document (checkSource), which is what a caller needs
1041
+ * before the candidate may be adopted; a fallback keeps today's path.
1042
+ *
1043
+ * A renderer must not verify its own output, so this is not a second verifier: it is the
1044
+ * same walk, told to retain what it parsed.
1045
+ */
1046
+ export function verifyForSave(rendered, today, liveDocument, clone = null, sourceErrors = null) {
1047
+ return verifyRendered(rendered, today, liveDocument, clone, sourceErrors, true);
1048
+ }
1049
+
1050
+ function verifyRendered(rendered, today, liveDocument, clone, sourceErrors, retain) {
926
1051
  const P = new DOMParser();
927
1052
  const A = P.parseFromString(rendered, 'text/html');
928
1053
  const outsideA = vOutside(A), outsideLive = vOutside(liveDocument);
@@ -937,14 +1062,20 @@ export function verify(rendered, today, liveDocument, clone = null, sourceErrors
937
1062
  at = [];
938
1063
  cloneDiff = vDiff(A.documentElement, clone, 'html', at);
939
1064
  if (!cloneDiff) {
940
- const w = vParseErrors(rendered, sourceErrors);
941
- return w ? { ok: false, diff: w } : { ok: true, diff: null, oracle: 'clone' };
1065
+ const w = vParseErrors(rendered, sourceErrors, retain);
1066
+ if (w.diff) return { ok: false, diff: w.diff };
1067
+ return retain ? withCandidate({ ok: true, diff: null, oracle: 'clone' }, w, rendered, liveDocument)
1068
+ : { ok: true, diff: null, oracle: 'clone' };
942
1069
  }
943
1070
  }
944
1071
  const B = P.parseFromString(today, 'text/html');
945
1072
  const diff = vDiff(A.documentElement, B.documentElement, 'html');
946
- const worse = diff ? null : vParseErrors(rendered, sourceErrors);
947
- if (!diff) return worse ? { ok: false, diff: worse } : { ok: true, diff: null, oracle: 'today' };
1073
+ const worse = diff ? null : vParseErrors(rendered, sourceErrors, retain);
1074
+ if (!diff) {
1075
+ if (worse.diff) return { ok: false, diff: worse.diff };
1076
+ return retain ? withCandidate({ ok: true, diff: null, oracle: 'today' }, worse, rendered, liveDocument)
1077
+ : { ok: true, diff: null, oracle: 'today' };
1078
+ }
948
1079
  // `at` is the index path, in the clone, of the node where the render first differs,
949
1080
  // so a caller can print that element and try again instead of discarding the whole
950
1081
  // render. It is valid in the clone because every earlier sibling at every level on
@@ -983,15 +1114,32 @@ export function nodeAt(root, at) {
983
1114
  * represent, such as a duplicated element, change the tree, and the comparison above
984
1115
  * is what catches those.
985
1116
  */
986
- function vParseErrors(rendered, sourceErrors) {
987
- if (!sourceErrors) return null;
1117
+ function vParseErrors(rendered, sourceErrors, retain) {
1118
+ if (!sourceErrors) return { diff: null };
988
1119
  const seen = new Map();
989
- parse(rendered, { onParseError: (e) => seen.set(e.code, (seen.get(e.code) || 0) + 1) });
1120
+ const doc = parse(rendered, retain
1121
+ ? { sourceCodeLocationInfo: true, onParseError: (e) => seen.set(e.code, (seen.get(e.code) || 0) + 1) }
1122
+ : { onParseError: (e) => seen.set(e.code, (seen.get(e.code) || 0) + 1) });
990
1123
  for (const [code, n] of seen) {
991
1124
  const was = sourceErrors.get(code) || 0;
992
- if (n > was) return `the render introduced parse errors the source does not have: ${code} ${was} -> ${n}`;
1125
+ if (n > was) return { diff: `the render introduced parse errors the source does not have: ${code} ${was} -> ${n}` };
993
1126
  }
994
- return null;
1127
+ return retain ? { diff: null, doc, parseErrors: seen } : { diff: null };
1128
+ }
1129
+
1130
+ /**
1131
+ * Attach the model of the bytes just verified, or leave the verdict as it is.
1132
+ *
1133
+ * checkSource first: the tree comparison cannot see anything outside <html>, and a model
1134
+ * that fails it is not this document no matter what the render verified as. A failed check
1135
+ * is not a refusal of the save; the caller keeps the fallback path it has today.
1136
+ */
1137
+ function withCandidate(verdict, errors, rendered, liveDocument) {
1138
+ if (!errors.doc) return verdict;
1139
+ const candidate = modelFromParsed(rendered, errors.doc, errors.parseErrors);
1140
+ if (checkSource(candidate, liveDocument)) return verdict;
1141
+ verdict.model = candidate;
1142
+ return verdict;
995
1143
  }
996
1144
  function vOutside(doc) {
997
1145
  const out = [];
@@ -18,8 +18,8 @@
18
18
  import { isEditMode, isDraftMode } from "./is-edit-mode.js";
19
19
  import { draftStoreFailed, flushDraft } from "./draft-mode.js";
20
20
  import { captureForDirtyCheck } from "./snapshot.js";
21
- import { getLastSavedDirty } from "./save.js";
22
- import { logUnloadDiffSync, preloadIfEnabled } from "../lib/autosave-debug.js";
21
+ import { getLastSavedDirty, sessionParked, styleAdoptionPending } from "./save.js";
22
+ import { logUnloadDiffSync, preloadIfEnabled } from "../lib/autosave-debug-gate.js";
23
23
  import { hasUnsavedState } from "../lib/unsaved-state.js";
24
24
 
25
25
  // One beforeunload let through, for the reload a person confirmed with two presses
@@ -48,6 +48,10 @@ preloadIfEnabled();
48
48
  // The same predicate is exposed as `clay.hasUnsavedChanges`, which a host asks
49
49
  // synchronously before deciding whether a save is worth asking for.
50
50
  export function hasUnsavedChanges() {
51
+ // Bytes a save session parked before its exit were never written, and whatever mode the
52
+ // page reports by then, they are still only here. Asked first, because the mode is a
53
+ // claim about the page and this is a claim about the host.
54
+ if (sessionParked()) return true;
51
55
  if (!isEditMode) return false;
52
56
 
53
57
  // Work outside the DOM first: it needs no capture, and a capture that throws
@@ -72,14 +76,20 @@ export function hasUnsavedChanges() {
72
76
  export function bytesUnsaved() {
73
77
  if (!isEditMode) return false;
74
78
  try {
75
- return captureForDirtyCheck() !== getLastSavedDirty();
79
+ const current = captureForDirtyCheck();
80
+ // A style adoption is not work: the page is only waiting for the stripper that
81
+ // takes its own owned tag out of the stored bytes. The capture above still runs,
82
+ // so a throw is still an unsaved page, and any edit since the adoption was
83
+ // queued turns this off.
84
+ if (styleAdoptionPending(current)) return false;
85
+ return current !== getLastSavedDirty();
76
86
  } catch {
77
87
  return true;
78
88
  }
79
89
  }
80
90
 
81
91
  window.addEventListener('beforeunload', (event) => {
82
- if (!isEditMode) return;
92
+ if (!isEditMode && !sessionParked()) return;
83
93
 
84
94
  if (discardPermit) {
85
95
  discardPermit = false;