@panphora/clayjs 1.15.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (506) hide show
  1. package/README.md +4 -5
  2. package/THIRD-PARTY-NOTICES.md +16 -0
  3. package/compat/highlight-11.9.0.min.js +1213 -0
  4. package/compat/highlight-LICENSE +29 -0
  5. package/compat/journal-assets.lock.json +22 -0
  6. package/compat/overtype-2.6.1.min.js +116 -0
  7. package/compat/overtype-LICENSE +21 -0
  8. package/dist/all.js +90 -0
  9. package/dist/clay-dom.js +84 -0
  10. package/dist/clay-events.js +103 -0
  11. package/dist/clay-internals.js +85 -0
  12. package/dist/clay-options.js +93 -0
  13. package/dist/clay-router.js +98 -0
  14. package/dist/clay-ui.js +95 -0
  15. package/dist/clay-utils.js +84 -0
  16. package/dist/clay.js +146 -0
  17. package/dist/clay.standalone.js +83090 -58706
  18. package/dist/m/attrs/add-from-T5LJXFFI.js +1 -0
  19. package/dist/m/attrs/onaftersave-OKSWEJQF.js +1 -0
  20. package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +1 -0
  21. package/dist/m/attrs/save-freeze-FUP4Q4X3.js +1 -0
  22. package/dist/m/c-2AZDZWCK.js +1 -0
  23. package/dist/m/c-2JL6E75N.js +1 -0
  24. package/dist/m/c-2M73252O.js +1 -0
  25. package/dist/m/c-2SPVQSHC.js +1 -0
  26. package/dist/m/c-33HUYEDI.js +1 -0
  27. package/dist/m/c-33JJ6CBX.js +2 -0
  28. package/dist/m/c-35NBGMZB.js +1 -0
  29. package/dist/m/c-3GJ4SDE2.js +1 -0
  30. package/dist/m/c-3IQRECYS.js +1 -0
  31. package/dist/m/c-3OWDMNJR.js +2 -0
  32. package/dist/m/c-46XYM55L.js +1 -0
  33. package/dist/m/c-4B6H4G5V.js +1 -0
  34. package/dist/m/c-4DRMS4QG.js +9 -0
  35. package/dist/m/c-4FFHZM32.js +1 -0
  36. package/dist/m/c-4JZDZFF6.js +1 -0
  37. package/dist/m/c-4L3BTFRK.js +1 -0
  38. package/dist/m/c-4Q2XTQ2N.js +2 -0
  39. package/dist/m/c-4WKFKEKF.js +1 -0
  40. package/dist/m/c-4YSCRICO.js +1 -0
  41. package/dist/m/c-54RCDCBU.js +1 -0
  42. package/dist/m/c-5M4YEKCX.js +4 -0
  43. package/dist/m/c-5R4PTRG2.js +1 -0
  44. package/dist/m/c-5V3MOVJT.js +1 -0
  45. package/dist/m/c-5VYOWDTI.js +1 -0
  46. package/dist/m/c-5Y6TNX5W.js +34 -0
  47. package/dist/m/c-5YIHJ3TJ.js +1 -0
  48. package/dist/m/c-66O32MV3.js +28 -0
  49. package/dist/m/c-6HMB24EV.js +1 -0
  50. package/dist/m/c-7DJDKPGL.js +1 -0
  51. package/dist/m/c-7O6AFR65.js +1 -0
  52. package/dist/m/c-7SWMYKAI.js +1 -0
  53. package/dist/m/c-7VGZ7RU5.js +1 -0
  54. package/dist/m/c-A7LS5UZW.js +1 -0
  55. package/dist/m/c-ANQTJ3B5.js +2 -0
  56. package/dist/m/c-AQLFEATB.js +1 -0
  57. package/dist/m/c-AYVOREBE.js +1 -0
  58. package/dist/m/c-B2ZXPOS3.js +3 -0
  59. package/dist/m/c-BBVGTQG7.js +1 -0
  60. package/dist/m/c-BMIZKMPV.js +1 -0
  61. package/dist/m/c-BTDI4Y23.js +1 -0
  62. package/dist/m/c-BWFVSBDF.js +1 -0
  63. package/dist/m/c-BYTKL6UX.js +1 -0
  64. package/dist/m/c-CFHW5AMY.js +6 -0
  65. package/dist/m/c-CJZSLMVQ.js +1 -0
  66. package/dist/m/c-CYWLPPUS.js +1 -0
  67. package/dist/m/c-CZUSOB7V.js +1 -0
  68. package/dist/m/c-DBE6ORSI.js +1 -0
  69. package/dist/m/c-DCFSEJZG.js +1 -0
  70. package/dist/m/c-DNZ5IFM6.js +1 -0
  71. package/dist/m/c-EDHUR4MX.js +1 -0
  72. package/dist/m/c-EJTGYSLN.js +1 -0
  73. package/dist/m/c-EY6UOM54.js +1 -0
  74. package/dist/m/c-F3CKJTJS.js +1 -0
  75. package/dist/m/c-F7Y6FLPY.js +1 -0
  76. package/dist/m/c-FA4DZVTC.js +1 -0
  77. package/dist/m/c-FILLTJA7.js +1 -0
  78. package/dist/m/c-G2BUZITR.js +1 -0
  79. package/dist/m/c-GB3JTV6E.js +1 -0
  80. package/dist/m/c-GBGWXTX4.js +1 -0
  81. package/dist/m/c-GHBCCVG4.js +1 -0
  82. package/dist/m/c-GJAEGGXY.js +1 -0
  83. package/dist/m/c-GOWBDM6J.js +1 -0
  84. package/dist/m/c-GQ6F6IDK.js +1 -0
  85. package/dist/m/c-H4LG442Q.js +1 -0
  86. package/dist/m/c-HA6INT6J.js +1 -0
  87. package/dist/m/c-HPURYVZS.js +2 -0
  88. package/dist/m/c-HRDCD3WN.js +67 -0
  89. package/dist/m/c-HT33EQRN.js +1 -0
  90. package/dist/m/c-HVYXIS72.js +1 -0
  91. package/dist/m/c-IB52R237.js +1 -0
  92. package/dist/m/c-IJUK2GPW.js +1 -0
  93. package/dist/m/c-IKWNE4R4.js +1 -0
  94. package/dist/m/c-IU3IYEU6.js +1 -0
  95. package/dist/m/c-IZ2EBRAJ.js +1 -0
  96. package/dist/m/c-J7CA54IP.js +1 -0
  97. package/dist/m/c-JAX2667S.js +6 -0
  98. package/dist/m/c-KI7M4AY3.js +1 -0
  99. package/dist/m/c-KIPQJX4B.js +1 -0
  100. package/dist/m/c-KKWLDOPM.js +1 -0
  101. package/dist/m/c-KR4UW4KW.js +1 -0
  102. package/dist/m/c-KY3VJGUA.js +8 -0
  103. package/dist/m/c-L4WQSC27.js +1 -0
  104. package/dist/m/c-LFPHCXY3.js +1 -0
  105. package/dist/m/c-LR4CGEPT.js +91 -0
  106. package/dist/m/c-LRPUO2MX.js +1 -0
  107. package/dist/m/c-LTGMXXKO.js +1 -0
  108. package/dist/m/c-LXENEBV7.js +1 -0
  109. package/dist/m/c-LZJPNR2W.js +1 -0
  110. package/dist/m/c-MMUIUUZG.js +2 -0
  111. package/dist/m/c-MTQHWQC3.js +1 -0
  112. package/dist/m/c-N326R2JD.js +1 -0
  113. package/dist/m/c-N7JPUPGC.js +1 -0
  114. package/dist/m/c-NMJRXKPC.js +8 -0
  115. package/dist/m/c-NRPJEVJS.js +1 -0
  116. package/dist/m/c-NUHP2XDB.js +1 -0
  117. package/dist/m/c-NZMNXVF6.js +1 -0
  118. package/dist/m/c-OCSZ4QGN.js +1 -0
  119. package/dist/m/c-OIBUTDXI.js +1 -0
  120. package/dist/m/c-OIDRZKAF.js +1 -0
  121. package/dist/m/c-OYTFP5QN.js +1 -0
  122. package/dist/m/c-P7KNDG34.js +1 -0
  123. package/dist/m/c-PPOSCAQD.js +1 -0
  124. package/dist/m/c-PQ5LK6QE.js +993 -0
  125. package/dist/m/c-PZ4XEPUP.js +2 -0
  126. package/dist/m/c-QHIWXHRO.js +1 -0
  127. package/dist/m/c-QHWJ5ZIJ.js +1 -0
  128. package/dist/m/c-QIX5BJYN.js +1 -0
  129. package/dist/m/c-QNKREOL6.js +1 -0
  130. package/dist/m/c-QQHEX6HA.js +1 -0
  131. package/dist/m/c-R7RMANJI.js +9 -0
  132. package/dist/m/c-RI4NT75Y.js +1 -0
  133. package/dist/m/c-RM7J7BV2.js +1 -0
  134. package/dist/m/c-RUJKSUQW.js +1 -0
  135. package/dist/m/c-RVMWGXEJ.js +1 -0
  136. package/dist/m/c-S7H2DRCB.js +1 -0
  137. package/dist/m/c-S7LK4KUE.js +1 -0
  138. package/dist/m/c-SGCCC4FW.js +25 -0
  139. package/dist/m/c-SGIA4IEQ.js +188 -0
  140. package/dist/m/c-SWRJFCNN.js +1 -0
  141. package/dist/m/c-T4VO4OCB.js +1 -0
  142. package/dist/m/c-TCCA5BZN.js +2 -0
  143. package/dist/m/c-TQEMVJCS.js +1 -0
  144. package/dist/m/c-TQZ2JYHD.js +1 -0
  145. package/dist/m/c-TR6RMDQ7.js +3 -0
  146. package/dist/m/c-TRS6TM6O.js +1 -0
  147. package/dist/m/c-UGHH4MB3.js +1 -0
  148. package/dist/m/c-UKHTLWUW.js +1 -0
  149. package/dist/m/c-UL2YUY52.js +1 -0
  150. package/dist/m/c-UNN7LWCT.js +1 -0
  151. package/dist/m/c-UXBMRDZB.js +235 -0
  152. package/dist/m/c-V7PZZRG7.js +1 -0
  153. package/dist/m/c-VAK7SG37.js +1 -0
  154. package/dist/m/c-VDSS4K3Z.js +1 -0
  155. package/dist/m/c-VFX45IZQ.js +1 -0
  156. package/dist/m/c-VHNSBD2M.js +1 -0
  157. package/dist/m/c-VKEDWYDF.js +1 -0
  158. package/dist/m/c-VL52ADDR.js +1 -0
  159. package/dist/m/c-WN5AY3OZ.js +1 -0
  160. package/dist/m/c-WNYBLIOZ.js +1 -0
  161. package/dist/m/c-WRBKEJD4.js +1 -0
  162. package/dist/m/c-WWDBYXLK.js +1 -0
  163. package/dist/m/c-WYFDLITL.js +1 -0
  164. package/dist/m/c-XN6MBKI6.js +67 -0
  165. package/dist/m/c-XNNW2CWK.js +1 -0
  166. package/dist/m/c-XPELB6E5.js +1 -0
  167. package/dist/m/c-XQXZAO7N.js +1 -0
  168. package/dist/m/c-Y6USGTS5.js +5 -0
  169. package/dist/m/c-YDH7Q3NY.js +1 -0
  170. package/dist/m/c-YG2NJYS6.js +135 -0
  171. package/dist/m/c-Z36J63PP.js +1 -0
  172. package/dist/m/c-Z4VRC7GU.js +7 -0
  173. package/dist/m/c-Z6K52OLP.js +19 -0
  174. package/dist/m/c-ZDY2MDAA.js +21 -0
  175. package/dist/m/c-ZE46QDIH.js +1 -0
  176. package/dist/m/c-ZUCOKOHT.js +1 -0
  177. package/dist/m/core/admin-attrs-VGNLAUDK.js +1 -0
  178. package/dist/m/core/autosave-E4PEFPZ6.js +1 -0
  179. package/dist/m/core/conflict-notice-VQ6Y64PH.js +1 -0
  180. package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
  181. package/dist/m/core/host-permission-XN3K2IW3.js +1 -0
  182. package/dist/m/core/is-edit-mode-JXHDVGUL.js +1 -0
  183. package/dist/m/core/persist-HKHA66IY.js +3 -0
  184. package/dist/m/core/recovery-TBBSICE2.js +1 -0
  185. package/dist/m/core/save-WUCHTQBE.js +1 -0
  186. package/dist/m/core/save-core-FJVHT5M3.js +1 -0
  187. package/dist/m/core/snapshot-F3SWDZMM.js +1 -0
  188. package/dist/m/core/unsaved-warning-STKBHE6G.js +1 -0
  189. package/dist/m/dom/all-APS5ZSKP.js +1 -0
  190. package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
  191. package/dist/m/events/index-23DPTUT4.js +1 -0
  192. package/dist/m/internals/index-WOVAP3RU.js +1 -0
  193. package/dist/m/lib/cache-bust-CGWT6ZJI.js +1 -0
  194. package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
  195. package/dist/m/loader-HXX6YNH5.js +1 -0
  196. package/dist/m/manifest-F22DEDAC.js +1 -0
  197. package/dist/m/manifest-edit-C3C7376B.js +1 -0
  198. package/dist/m/manifest.json +3491 -0
  199. package/dist/m/options/options-2LH76JQ3.js +1 -0
  200. package/dist/m/page/editor-XUTB2HB2.js +189 -0
  201. package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +6 -0
  202. package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
  203. package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
  204. package/dist/m/plugins/demo-ZIBFAA3R.js +1 -0
  205. package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
  206. package/dist/m/plugins/indicator-PV4YW2ID.js +1 -0
  207. package/dist/m/plugins/live-NUTTXSBS.js +1 -0
  208. package/dist/m/plugins/menu-HCKO3DSP.js +138 -0
  209. package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
  210. package/dist/m/plugins/page-KSUH7G6J.js +2 -0
  211. package/dist/m/plugins/page-view-W73OW7WI.js +1 -0
  212. package/dist/m/plugins/people-AJJ32YCG.js +1 -0
  213. package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
  214. package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
  215. package/dist/m/plugins/richclay-RC3L2F6X.js +517 -0
  216. package/dist/m/plugins/richclay-RC3L2F6X.js.LEGAL.txt +1 -0
  217. package/dist/m/plugins/sortable-YRMQX3ZU.js +1 -0
  218. package/dist/m/plugins/source-KHK5UUSK.js +1 -0
  219. package/dist/m/plugins/undo-OF2L3CXU.js +1 -0
  220. package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
  221. package/dist/m/plugins/upgrade-ZNYODP5Y.js +1 -0
  222. package/dist/m/plugins/upload-XSGNILUY.js +1 -0
  223. package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +21 -0
  224. package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +1 -0
  225. package/dist/m/plugins/wire-ZWVWKF76.js +1 -0
  226. package/dist/m/reader-core-ILWLXVMP.js +68 -0
  227. package/dist/m/route/route-KOCZSEGN.js +1 -0
  228. package/dist/m/sync/live-sync-4AUVXVJU.js +1 -0
  229. package/dist/m/ui/index-EVHKMKBL.js +1 -0
  230. package/dist/m/utils/index-4TJCVLT7.js +1 -0
  231. package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
  232. package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
  233. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
  234. package/dist/offline-manifest.json +3800 -0
  235. package/dist/page/themes/light-1.css +406 -0
  236. package/dist/server/core.cjs +23662 -22860
  237. package/dist/server/offline-runtime.cjs +4954 -0
  238. package/dist/server/offline-runtime.mjs +4923 -0
  239. package/dist/server/rooms.mjs +9 -0
  240. package/docs/guide.md +1078 -669
  241. package/entries/all.js +2 -0
  242. package/entries/clay-data.js +1 -1
  243. package/entries/clay-dom.js +2 -0
  244. package/entries/clay-events.js +2 -0
  245. package/entries/clay-internals.js +2 -0
  246. package/entries/clay-options.js +2 -0
  247. package/entries/clay-router.js +2 -0
  248. package/entries/clay-ui.js +2 -0
  249. package/entries/clay-utils.js +2 -0
  250. package/entries/clay.js +2 -0
  251. package/entries/sap.js +6 -6
  252. package/fonts/ibm-plex-mono-LICENSE +93 -0
  253. package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  254. package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  255. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  256. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  257. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  258. package/fonts/ibm-plex-sans-LICENSE +93 -0
  259. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  260. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  261. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  262. package/fonts/ibm-plex.css +71 -0
  263. package/fonts/inter-LICENSE +93 -0
  264. package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  265. package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
  266. package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
  267. package/fonts/inter-greek-wght-normal.woff2 +0 -0
  268. package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  269. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  270. package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
  271. package/fonts/inter.css +62 -0
  272. package/fonts/newsreader-LICENSE +93 -0
  273. package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
  274. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  275. package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
  276. package/fonts/newsreader.css +26 -0
  277. package/fonts/work-sans-LICENSE +93 -0
  278. package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
  279. package/fonts/work-sans.css +8 -0
  280. package/package.json +20 -3
  281. package/packed-contract.json +1 -0
  282. package/src/attrs/add-from.js +27 -4
  283. package/src/attrs/save-freeze.js +7 -52
  284. package/src/core/admin-attrs.js +119 -12
  285. package/src/core/admin-contenteditable.js +12 -10
  286. package/src/core/admin-inputs.js +14 -15
  287. package/src/core/admin-onclick.js +2 -0
  288. package/src/core/autosave.js +74 -21
  289. package/src/core/conflict-download.js +22 -0
  290. package/src/core/conflict-notice-ui.js +1173 -0
  291. package/src/core/conflict-notice.js +140 -697
  292. package/src/core/edit-mode.js +147 -14
  293. package/src/core/etag.js +30 -12
  294. package/src/core/host-meta.js +72 -8
  295. package/src/core/host-permission.js +29 -0
  296. package/src/core/is-edit-mode.js +145 -12
  297. package/src/core/mode-key.js +125 -0
  298. package/src/core/mode-lifecycle.js +544 -0
  299. package/src/core/mode-receipt.js +30 -0
  300. package/src/core/mode-refusal.js +13 -0
  301. package/src/core/mode-resources.js +52 -0
  302. package/src/core/mode-support.js +82 -0
  303. package/src/core/mode-switch.js +259 -0
  304. package/src/core/mode-transition.js +112 -0
  305. package/src/core/page-data-write.js +305 -0
  306. package/src/core/page-data.js +26 -227
  307. package/src/core/page-format-wire.js +4 -0
  308. package/src/core/page-format.js +58 -0
  309. package/src/core/persist.js +37 -9
  310. package/src/core/preserve-authored.js +114 -0
  311. package/src/core/recovery.js +772 -0
  312. package/src/core/runtime-url.js +105 -0
  313. package/src/core/save-core.js +109 -12
  314. package/src/core/save-replace.js +72 -0
  315. package/src/core/save-size-notice.js +134 -0
  316. package/src/core/save-size.js +140 -0
  317. package/src/core/save.js +805 -118
  318. package/src/core/snapshot.js +121 -11
  319. package/src/core/source-map.js +157 -9
  320. package/src/core/unsaved-warning.js +14 -4
  321. package/src/core/upgrade-run.js +408 -0
  322. package/src/core/version.js +18 -0
  323. package/src/lib/assets.js +30 -0
  324. package/src/lib/autosave-debug-gate.js +50 -0
  325. package/src/lib/autosave-scheduler.js +141 -0
  326. package/src/lib/boot-imports.js +8 -0
  327. package/src/lib/derived.js +3 -1
  328. package/src/lib/dirty-gate.js +15 -0
  329. package/src/lib/doc-storage.js +33 -0
  330. package/src/lib/effect.js +9 -4
  331. package/src/lib/mutation.js +4 -2
  332. package/src/lib/offline-runtime.js +226 -0
  333. package/src/lib/preload.js +48 -0
  334. package/src/lib/root-attrs.js +1 -1
  335. package/src/lib/trash.js +18 -0
  336. package/src/lib/undo/capture-move.js +152 -0
  337. package/src/live/local-channel.js +61 -0
  338. package/src/live/local-lane.js +60 -9
  339. package/src/live/server-lane.js +6 -3
  340. package/src/loader-logic.js +76 -15
  341. package/src/loader.js +218 -30
  342. package/src/page/adopt-boot.js +76 -22
  343. package/src/page/adopt.js +25 -4
  344. package/src/page/awareness.js +230 -0
  345. package/src/page/blocks.js +79 -5
  346. package/src/page/blocksel.js +20 -4
  347. package/src/page/board-face.js +13 -0
  348. package/src/page/board-panel.js +5 -17
  349. package/src/page/board-style.js +2 -13
  350. package/src/page/board-view.js +1 -11
  351. package/src/page/board-views.js +17 -0
  352. package/src/page/board.js +2 -2
  353. package/src/page/boot-pack.js +26 -0
  354. package/src/page/boot.js +237 -0
  355. package/src/page/checklist.js +1 -1
  356. package/src/page/chrome.js +411 -61
  357. package/src/page/csv-import.js +17 -2
  358. package/src/page/document-title-dom.js +40 -0
  359. package/src/page/document-title-runtime.js +76 -0
  360. package/src/page/document-title.js +9 -0
  361. package/src/page/drag.js +48 -13
  362. package/src/page/drop.js +2 -1
  363. package/src/page/editor.js +330 -86
  364. package/src/page/feature-loader.js +72 -0
  365. package/src/page/features/board.js +12 -0
  366. package/src/page/features/code.js +13 -0
  367. package/src/page/features/media.js +290 -0
  368. package/src/page/features/registry.js +23 -0
  369. package/src/page/features/table.js +383 -0
  370. package/src/page/{tool-view.js → features/tool.js} +33 -2
  371. package/src/page/format-codec.js +215 -0
  372. package/src/page/format-gate.js +179 -0
  373. package/src/page/format-level.js +157 -0
  374. package/src/page/highlight-slot.js +23 -0
  375. package/src/page/highlight.js +30 -16
  376. package/src/page/history-intent.js +78 -0
  377. package/src/page/inline.js +18 -25
  378. package/src/page/items.js +52 -11
  379. package/src/page/link.js +1 -0
  380. package/src/page/markdown.js +86 -1
  381. package/src/page/media-views.js +3 -0
  382. package/src/page/media.js +33 -245
  383. package/src/page/outline-cache.js +11 -0
  384. package/src/page/placeholder.js +27 -7
  385. package/src/page/projection.js +196 -50
  386. package/src/page/reader-patch.js +306 -0
  387. package/src/page/recover.js +1 -1
  388. package/src/page/rich/page-tool.js +1 -4
  389. package/src/page/runtime-owner.js +27 -0
  390. package/src/page/save-projection.js +7 -4
  391. package/src/page/schema.js +14 -0
  392. package/src/page/table-rect-key.js +3 -0
  393. package/src/page/table-select.js +4 -3
  394. package/src/page/table-style.js +1 -1
  395. package/src/page/table-views.js +3 -0
  396. package/src/page/table.js +8 -293
  397. package/src/page/themes/light-1.css +139 -2
  398. package/src/page/themes/light-1.source.css +269 -0
  399. package/src/page/tool-capability.js +3 -0
  400. package/src/page/tool-views.js +3 -0
  401. package/src/page/view.js +390 -107
  402. package/src/page/ybind/awareness.js +172 -0
  403. package/src/page/ybind/binding.js +9 -1
  404. package/src/page/ybind/bridge.js +50 -47
  405. package/src/page/ybind/decode.js +52 -19
  406. package/src/page/ybind/doc.js +27 -2
  407. package/src/page/ybind/group.js +12 -3
  408. package/src/page/ybind/index.js +2 -0
  409. package/src/page/ybind/lineage.js +71 -0
  410. package/src/page/ybind/marks.js +25 -1
  411. package/src/page/ybind/migrate.js +48 -13
  412. package/src/page/ybind/moves.js +32 -21
  413. package/src/page/ybind/positions.js +50 -0
  414. package/src/page/ybind/provider.js +84 -22
  415. package/src/page/ybind/recovery-proof.js +191 -0
  416. package/src/page/ybind/recovery-replay.js +130 -0
  417. package/src/page/ybind/tokens.js +15 -6
  418. package/src/page/ybind/walk.js +10 -3
  419. package/src/page/ybind/writer.js +48 -4
  420. package/src/plugins/ai-edit.js +167 -32
  421. package/src/plugins/bind-text.js +218 -0
  422. package/src/plugins/cms.js +93 -0
  423. package/src/plugins/indicator.js +57 -5
  424. package/src/plugins/live.js +282 -34
  425. package/src/plugins/menu/claygoo.svg.js +1 -0
  426. package/src/plugins/menu/seo-head.js +323 -0
  427. package/src/plugins/menu/seo.js +849 -0
  428. package/src/plugins/menu.js +425 -0
  429. package/src/plugins/page-view.js +131 -0
  430. package/src/plugins/page.js +1022 -275
  431. package/src/plugins/people.js +158 -31
  432. package/src/plugins/richclay.js +119 -9
  433. package/src/plugins/sortable.js +370 -60
  434. package/src/plugins/source.js +189 -366
  435. package/src/plugins/undo.js +289 -19
  436. package/src/plugins/unsent.js +132 -0
  437. package/src/plugins/upgrade-run.js +1 -0
  438. package/src/plugins/upgrade.js +210 -0
  439. package/src/plugins/upload-task.js +571 -0
  440. package/src/plugins/upload-ui.js +265 -0
  441. package/src/plugins/upload.js +34 -153
  442. package/src/plugins/visitor-drafts/inert.js +32 -0
  443. package/src/plugins/visitor-drafts/owner-chip.js +3 -0
  444. package/src/plugins/visitor-drafts/prompt.js +2 -2
  445. package/src/plugins/visitor-drafts-owner.js +85 -0
  446. package/src/plugins/visitor-drafts.js +31 -42
  447. package/src/plugins/wire.js +82 -1
  448. package/src/reader-core.js +26 -0
  449. package/src/recovery/conflicts.js +287 -0
  450. package/src/recovery/identity.js +154 -0
  451. package/src/recovery/store.js +792 -0
  452. package/src/recovery/undo.js +257 -0
  453. package/src/server/checkpoint/index.js +68 -4
  454. package/src/server/offline-runtime/index.js +214 -0
  455. package/src/server/rooms/index.js +2 -1
  456. package/src/server/rooms/live-data-rooms.js +2 -2
  457. package/src/server/rooms/page-admission.js +33 -0
  458. package/src/server/rooms/page-lineage.js +56 -0
  459. package/src/server/rooms/page-rebase.js +11 -5
  460. package/src/server/rooms/page-state.js +2 -0
  461. package/src/server/rooms/page-write-admission.js +9 -3
  462. package/src/source/session.js +705 -0
  463. package/src/sync/conflict-revert.js +5 -4
  464. package/src/sync/conflicts-edit.js +119 -0
  465. package/src/sync/conflicts-facade.js +49 -0
  466. package/src/sync/conflicts.js +99 -1
  467. package/src/sync/editing-marker.js +37 -6
  468. package/src/sync/editing-sender.js +8 -1
  469. package/src/sync/live-sync-edit.js +63 -0
  470. package/src/sync/live-sync.js +364 -119
  471. package/src/sync/local-presence.js +187 -0
  472. package/src/sync/merge-declarations.js +10 -0
  473. package/src/sync/merge-engine.js +12 -0
  474. package/src/sync/presence-store.js +412 -0
  475. package/src/sync/presence-target.js +243 -0
  476. package/src/sync/presence.js +72 -41
  477. package/src/sync/saved-handoff.js +200 -0
  478. package/src/sync/saved-stream.js +535 -0
  479. package/src/sync/version-guard.js +92 -0
  480. package/src/sync/wire-profile.js +81 -0
  481. package/src/ui/bevel-controls.js +2 -8
  482. package/src/ui/page-scheme.js +7 -0
  483. package/src/ui/skins/cms.js +1 -1
  484. package/src/ui/theme-config.js +150 -0
  485. package/src/ui/theme.js +6 -138
  486. package/src/ui/toast-lazy.js +11 -0
  487. package/src/vendor/LICENSES-optional.md +253 -0
  488. package/src/vendor/highlight.vendor.js +3 -0
  489. package/src/vendor/hljs-bash.vendor.js +2 -0
  490. package/src/vendor/hljs-css.vendor.js +2 -0
  491. package/src/vendor/hljs-javascript.vendor.js +2 -0
  492. package/src/vendor/hljs-json.vendor.js +2 -0
  493. package/src/vendor/hljs-python.vendor.js +2 -0
  494. package/src/vendor/hljs-xml.vendor.js +2 -0
  495. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  496. package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
  497. package/src/vendor/hyper-morph-identity.vendor.js +2 -0
  498. package/src/vendor/hyper-morph-json.vendor.js +3 -0
  499. package/src/vendor/hyper-morph.vendor.js +6 -6
  500. package/src/vendor/hyper-undo.vendor.js +1 -1
  501. package/src/vendor/hypercms.vendor.js +169 -84
  502. package/src/vendor/marked.vendor.js +68 -0
  503. package/src/vendor/overtype.vendor.js +994 -0
  504. package/src/vendor/richclay.vendor.js +17 -17
  505. package/src/vendor/sap-core.vendor.js +5 -5
  506. package/src/vendor/sap-ownership.vendor.js +11 -7
@@ -74,6 +74,65 @@ 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 artifact this capture's renderer attached, or null. */
109
+ export function captureArtifact(result) {
110
+ return result ? captureArtifacts.get(result) || null : null;
111
+ }
112
+
113
+ /** Forget a capture's artifact: it was skipped, refused, or has now settled. */
114
+ export function releaseCaptureArtifact(result) {
115
+ if (result) captureArtifacts.delete(result);
116
+ }
117
+
118
+ /**
119
+ * Does this clone hold an authored save handler, template contents included?
120
+ *
121
+ * Those handlers are page JavaScript that can change the clone after the capture, so
122
+ * the accepted bytes may no longer describe the live page node for node; a pairing
123
+ * rebuilt by walking the two together would be describing the handler's tree. The
124
+ * inspection path stays for those pages.
125
+ */
126
+ function hasAuthoredSaveHandlers(root) {
127
+ const visit = (el) => {
128
+ if (el.hasAttribute('onbeforesave') || el.hasAttribute('onbeforesnapshot')) return true;
129
+ const kids = el.localName === 'template' && el.content ? el.content : el;
130
+ for (const child of kids.children) if (visit(child)) return true;
131
+ return false;
132
+ };
133
+ return root.nodeType === 1 ? visit(root) : false;
134
+ }
135
+
77
136
  function stripSnapshotRegions(clone) {
78
137
  for (const el of clone.querySelectorAll(SNAPSHOT_REMOVE_SELECTOR)) el.remove();
79
138
  }
@@ -103,6 +162,19 @@ function runAuthoredHandlers(clone, attr) {
103
162
  }
104
163
  }
105
164
 
165
+ let cloneCanonicalizer = null;
166
+
167
+ export function setCloneCanonicalizer(canonicalize) {
168
+ cloneCanonicalizer = canonicalize;
169
+ }
170
+
171
+ // A document parsed from received bytes gets the same canonical URLs a capture has, so a merge
172
+ // compares like with like. A no-op until save-core registers the canonicalizer.
173
+ export function canonicalizeReceived(doc) {
174
+ cloneCanonicalizer?.(doc);
175
+ return doc;
176
+ }
177
+
106
178
  /**
107
179
  * Register a hook that runs on EVERY snapshot (save AND sync).
108
180
  * Use for: syncing form values to the clone.
@@ -190,14 +262,18 @@ function serializeClone(clone) {
190
262
  * document reprinted; this one is the guarantee that no bug in an optional plugin can
191
263
  * cost somebody a save.
192
264
  */
193
- function serializeSaveClone(clone) {
265
+ function serializeSaveClone(clone, context = null) {
194
266
  const today = serializeClone(clone);
195
267
  if (!saveRenderer) return today;
268
+ const previous = activeCapture;
269
+ activeCapture = context;
196
270
  try {
197
271
  return saveRenderer(clone, today);
198
272
  } catch (err) {
199
273
  console.error('clayjs: save renderer failed, sending the full serialization', err);
200
274
  return today;
275
+ } finally {
276
+ activeCapture = previous;
201
277
  }
202
278
  }
203
279
 
@@ -236,7 +312,7 @@ function clonePreventingOnclone(node, deep = true) {
236
312
  finally { window.__preventOnclone = prev; }
237
313
  }
238
314
 
239
- export function captureSnapshot({ flushUndo = true, authored = true, forSave = false, requestRefresh = false } = {}) {
315
+ export function captureSnapshot({ flushUndo = true, authored = true, forSave = false, requestRefresh = false, context = null } = {}) {
240
316
  // Force-close any pending undo idle batch BEFORE cloning the DOM, so the
241
317
  // snapshot reflects a clean undo boundary. Without this, a save that fires
242
318
  // mid-typing would leave the idle batch open across the save boundary, and
@@ -265,7 +341,14 @@ export function captureSnapshot({ flushUndo = true, authored = true, forSave = f
265
341
  // authored handler sees the same URLs the file will.
266
342
  restoreAuthoredUrls(clone);
267
343
 
344
+ cloneCanonicalizer?.(clone);
345
+
268
346
  stripSnapshotRegions(clone);
347
+ // Whether the page authors its own save handlers, read here because this is the
348
+ // snapshot BEFORE either of them runs: a handler can add the other one, and it is the
349
+ // page as captured that decides whether the save can be paired without an inspection.
350
+ if (context) context.authoredHandlers = hasAuthoredSaveHandlers(clone);
351
+
269
352
  // authored: false is for a capture that is not a save and not a frame the page will
270
353
  // see. The attributes hold page-author JavaScript, so running them is a side effect
271
354
  // the author asked for once per save, not once per capture.
@@ -479,12 +562,22 @@ export function captureForComparisonAndDirty({ flushUndo = true } = {}) {
479
562
  *
480
563
  * @param {Object} options
481
564
  * @param {boolean} options.emitForSync - Whether to emit snapshot-ready event (default: true)
565
+ * @param {boolean} options.forRequest - These bytes go to a host, so the renderer may
566
+ * attach the verified model they came from, for the accepted save to adopt
482
567
  * @returns {{ forSave: string, forComparison: string, forDirty: string, captureId: number }}
483
568
  */
484
- export function captureForSaveAndComparison({ emitForSync = true, flushUndo = true, requestRefresh = false } = {}) {
569
+ export function captureForSaveAndComparison({ emitForSync = true, flushUndo = true, requestRefresh = false, forRequest = false } = {}) {
485
570
  if (!Number.isSafeInteger(captureCounter + 1)) throw new Error('Save capture identity exhausted');
486
571
  const captureId = ++captureCounter;
487
- const clone = captureSnapshot({ flushUndo, forSave: emitForSync, requestRefresh });
572
+ // The context this capture's renderer may attach a private artifact to, and the
573
+ // record of whether the page has its own save handlers. It never reaches the object
574
+ // this function returns: that keeps its four public fields.
575
+ //
576
+ // Only for a capture whose bytes go to a host. A capture taken for anything else (a
577
+ // live-sync disk base, a download, an inspection) has no request to adopt it, so it
578
+ // pays neither the retained parse nor the pairing.
579
+ const context = forRequest ? { captureId, authoredHandlers: false, artifact: null } : null;
580
+ const clone = captureSnapshot({ flushUndo, forSave: emitForSync, requestRefresh, context });
488
581
 
489
582
  // Emit for live-sync before any stripping.
490
583
  //
@@ -525,7 +618,7 @@ export function captureForSaveAndComparison({ emitForSync = true, flushUndo = tr
525
618
  el.remove();
526
619
  }
527
620
  stripDiagnosticBeacons(clone);
528
- const forSave = serializeSaveClone(clone);
621
+ const forSave = serializeSaveClone(clone, context);
529
622
 
530
623
  // Compare clone: strip every autosave-off region, then run hooks
531
624
  for (const el of compareClone.querySelectorAll(STRIP_FROM_COMPARISON)) {
@@ -552,7 +645,9 @@ export function captureForSaveAndComparison({ emitForSync = true, flushUndo = tr
552
645
  forDirty = serializeForCompare(dirtyClone);
553
646
  }
554
647
 
555
- return { forSave, forComparison, forDirty, captureId };
648
+ const result = { forSave, forComparison, forDirty, captureId };
649
+ if (context && context.artifact) captureArtifacts.set(result, context.artifact);
650
+ return result;
556
651
  }
557
652
 
558
653
  /**
@@ -704,13 +799,28 @@ export function captureBodyForSync() {
704
799
  }
705
800
 
706
801
  /**
707
- * Get page contents for change detection.
708
- * Does NOT emit snapshot-ready event (safe for comparison).
802
+ * The bytes a save would send, without saving anything.
803
+ *
804
+ * Emits no snapshot-ready event (safe for comparison), and with no options it is
805
+ * exactly `captureForSave({ emitForSync: false })`.
709
806
  *
710
- * Returns full HTML via snapshot pipeline.
807
+ * `transform(root)` edits the detached clone and nothing else, and runs before
808
+ * prepareCloneForSave so the authored hooks, the transforms and the [no-save] strip
809
+ * still have their say. It must be synchronous: a preview that finished later would
810
+ * describe a page that has moved on, so a thenable is refused rather than awaited.
811
+ * There is no global registration, so no other save ever sees these edits, and the
812
+ * renderer runs without a capture context, so nothing can adopt these bytes as a model.
711
813
  */
712
- export function getPageContents() {
713
- return captureForSave({ emitForSync: false });
814
+ export function getPageContents({ transform = null, flushUndo = true } = {}) {
815
+ if (!transform && flushUndo) return captureForSave({ emitForSync: false });
816
+ const clone = captureSnapshot({ flushUndo });
817
+ if (transform) {
818
+ const pending = transform(clone);
819
+ if (pending && typeof pending.then === 'function') {
820
+ throw new TypeError('getHTML: the transform must be synchronous');
821
+ }
822
+ }
823
+ return serializeSaveClone(prepareCloneForSave(clone));
714
824
  }
715
825
 
716
826
  /**
@@ -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;