@panphora/clayjs 1.15.2 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (506) hide show
  1. package/README.md +4 -5
  2. package/THIRD-PARTY-NOTICES.md +16 -0
  3. package/compat/highlight-11.9.0.min.js +1213 -0
  4. package/compat/highlight-LICENSE +29 -0
  5. package/compat/journal-assets.lock.json +22 -0
  6. package/compat/overtype-2.6.1.min.js +116 -0
  7. package/compat/overtype-LICENSE +21 -0
  8. package/dist/all.js +90 -0
  9. package/dist/clay-dom.js +84 -0
  10. package/dist/clay-events.js +103 -0
  11. package/dist/clay-internals.js +85 -0
  12. package/dist/clay-options.js +93 -0
  13. package/dist/clay-router.js +98 -0
  14. package/dist/clay-ui.js +95 -0
  15. package/dist/clay-utils.js +84 -0
  16. package/dist/clay.js +146 -0
  17. package/dist/clay.standalone.js +83090 -58706
  18. package/dist/m/attrs/add-from-T5LJXFFI.js +1 -0
  19. package/dist/m/attrs/onaftersave-OKSWEJQF.js +1 -0
  20. package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +1 -0
  21. package/dist/m/attrs/save-freeze-FUP4Q4X3.js +1 -0
  22. package/dist/m/c-2AZDZWCK.js +1 -0
  23. package/dist/m/c-2JL6E75N.js +1 -0
  24. package/dist/m/c-2M73252O.js +1 -0
  25. package/dist/m/c-2SPVQSHC.js +1 -0
  26. package/dist/m/c-33HUYEDI.js +1 -0
  27. package/dist/m/c-33JJ6CBX.js +2 -0
  28. package/dist/m/c-35NBGMZB.js +1 -0
  29. package/dist/m/c-3GJ4SDE2.js +1 -0
  30. package/dist/m/c-3IQRECYS.js +1 -0
  31. package/dist/m/c-3OWDMNJR.js +2 -0
  32. package/dist/m/c-46XYM55L.js +1 -0
  33. package/dist/m/c-4B6H4G5V.js +1 -0
  34. package/dist/m/c-4DRMS4QG.js +9 -0
  35. package/dist/m/c-4FFHZM32.js +1 -0
  36. package/dist/m/c-4JZDZFF6.js +1 -0
  37. package/dist/m/c-4L3BTFRK.js +1 -0
  38. package/dist/m/c-4Q2XTQ2N.js +2 -0
  39. package/dist/m/c-4WKFKEKF.js +1 -0
  40. package/dist/m/c-4YSCRICO.js +1 -0
  41. package/dist/m/c-54RCDCBU.js +1 -0
  42. package/dist/m/c-5M4YEKCX.js +4 -0
  43. package/dist/m/c-5R4PTRG2.js +1 -0
  44. package/dist/m/c-5V3MOVJT.js +1 -0
  45. package/dist/m/c-5VYOWDTI.js +1 -0
  46. package/dist/m/c-5Y6TNX5W.js +34 -0
  47. package/dist/m/c-5YIHJ3TJ.js +1 -0
  48. package/dist/m/c-66O32MV3.js +28 -0
  49. package/dist/m/c-6HMB24EV.js +1 -0
  50. package/dist/m/c-7DJDKPGL.js +1 -0
  51. package/dist/m/c-7O6AFR65.js +1 -0
  52. package/dist/m/c-7SWMYKAI.js +1 -0
  53. package/dist/m/c-7VGZ7RU5.js +1 -0
  54. package/dist/m/c-A7LS5UZW.js +1 -0
  55. package/dist/m/c-ANQTJ3B5.js +2 -0
  56. package/dist/m/c-AQLFEATB.js +1 -0
  57. package/dist/m/c-AYVOREBE.js +1 -0
  58. package/dist/m/c-B2ZXPOS3.js +3 -0
  59. package/dist/m/c-BBVGTQG7.js +1 -0
  60. package/dist/m/c-BMIZKMPV.js +1 -0
  61. package/dist/m/c-BTDI4Y23.js +1 -0
  62. package/dist/m/c-BWFVSBDF.js +1 -0
  63. package/dist/m/c-BYTKL6UX.js +1 -0
  64. package/dist/m/c-CFHW5AMY.js +6 -0
  65. package/dist/m/c-CJZSLMVQ.js +1 -0
  66. package/dist/m/c-CYWLPPUS.js +1 -0
  67. package/dist/m/c-CZUSOB7V.js +1 -0
  68. package/dist/m/c-DBE6ORSI.js +1 -0
  69. package/dist/m/c-DCFSEJZG.js +1 -0
  70. package/dist/m/c-DNZ5IFM6.js +1 -0
  71. package/dist/m/c-EDHUR4MX.js +1 -0
  72. package/dist/m/c-EJTGYSLN.js +1 -0
  73. package/dist/m/c-EY6UOM54.js +1 -0
  74. package/dist/m/c-F3CKJTJS.js +1 -0
  75. package/dist/m/c-F7Y6FLPY.js +1 -0
  76. package/dist/m/c-FA4DZVTC.js +1 -0
  77. package/dist/m/c-FILLTJA7.js +1 -0
  78. package/dist/m/c-G2BUZITR.js +1 -0
  79. package/dist/m/c-GB3JTV6E.js +1 -0
  80. package/dist/m/c-GBGWXTX4.js +1 -0
  81. package/dist/m/c-GHBCCVG4.js +1 -0
  82. package/dist/m/c-GJAEGGXY.js +1 -0
  83. package/dist/m/c-GOWBDM6J.js +1 -0
  84. package/dist/m/c-GQ6F6IDK.js +1 -0
  85. package/dist/m/c-H4LG442Q.js +1 -0
  86. package/dist/m/c-HA6INT6J.js +1 -0
  87. package/dist/m/c-HPURYVZS.js +2 -0
  88. package/dist/m/c-HRDCD3WN.js +67 -0
  89. package/dist/m/c-HT33EQRN.js +1 -0
  90. package/dist/m/c-HVYXIS72.js +1 -0
  91. package/dist/m/c-IB52R237.js +1 -0
  92. package/dist/m/c-IJUK2GPW.js +1 -0
  93. package/dist/m/c-IKWNE4R4.js +1 -0
  94. package/dist/m/c-IU3IYEU6.js +1 -0
  95. package/dist/m/c-IZ2EBRAJ.js +1 -0
  96. package/dist/m/c-J7CA54IP.js +1 -0
  97. package/dist/m/c-JAX2667S.js +6 -0
  98. package/dist/m/c-KI7M4AY3.js +1 -0
  99. package/dist/m/c-KIPQJX4B.js +1 -0
  100. package/dist/m/c-KKWLDOPM.js +1 -0
  101. package/dist/m/c-KR4UW4KW.js +1 -0
  102. package/dist/m/c-KY3VJGUA.js +8 -0
  103. package/dist/m/c-L4WQSC27.js +1 -0
  104. package/dist/m/c-LFPHCXY3.js +1 -0
  105. package/dist/m/c-LR4CGEPT.js +91 -0
  106. package/dist/m/c-LRPUO2MX.js +1 -0
  107. package/dist/m/c-LTGMXXKO.js +1 -0
  108. package/dist/m/c-LXENEBV7.js +1 -0
  109. package/dist/m/c-LZJPNR2W.js +1 -0
  110. package/dist/m/c-MMUIUUZG.js +2 -0
  111. package/dist/m/c-MTQHWQC3.js +1 -0
  112. package/dist/m/c-N326R2JD.js +1 -0
  113. package/dist/m/c-N7JPUPGC.js +1 -0
  114. package/dist/m/c-NMJRXKPC.js +8 -0
  115. package/dist/m/c-NRPJEVJS.js +1 -0
  116. package/dist/m/c-NUHP2XDB.js +1 -0
  117. package/dist/m/c-NZMNXVF6.js +1 -0
  118. package/dist/m/c-OCSZ4QGN.js +1 -0
  119. package/dist/m/c-OIBUTDXI.js +1 -0
  120. package/dist/m/c-OIDRZKAF.js +1 -0
  121. package/dist/m/c-OYTFP5QN.js +1 -0
  122. package/dist/m/c-P7KNDG34.js +1 -0
  123. package/dist/m/c-PPOSCAQD.js +1 -0
  124. package/dist/m/c-PQ5LK6QE.js +993 -0
  125. package/dist/m/c-PZ4XEPUP.js +2 -0
  126. package/dist/m/c-QHIWXHRO.js +1 -0
  127. package/dist/m/c-QHWJ5ZIJ.js +1 -0
  128. package/dist/m/c-QIX5BJYN.js +1 -0
  129. package/dist/m/c-QNKREOL6.js +1 -0
  130. package/dist/m/c-QQHEX6HA.js +1 -0
  131. package/dist/m/c-R7RMANJI.js +9 -0
  132. package/dist/m/c-RI4NT75Y.js +1 -0
  133. package/dist/m/c-RM7J7BV2.js +1 -0
  134. package/dist/m/c-RUJKSUQW.js +1 -0
  135. package/dist/m/c-RVMWGXEJ.js +1 -0
  136. package/dist/m/c-S7H2DRCB.js +1 -0
  137. package/dist/m/c-S7LK4KUE.js +1 -0
  138. package/dist/m/c-SGCCC4FW.js +25 -0
  139. package/dist/m/c-SGIA4IEQ.js +188 -0
  140. package/dist/m/c-SWRJFCNN.js +1 -0
  141. package/dist/m/c-T4VO4OCB.js +1 -0
  142. package/dist/m/c-TCCA5BZN.js +2 -0
  143. package/dist/m/c-TQEMVJCS.js +1 -0
  144. package/dist/m/c-TQZ2JYHD.js +1 -0
  145. package/dist/m/c-TR6RMDQ7.js +3 -0
  146. package/dist/m/c-TRS6TM6O.js +1 -0
  147. package/dist/m/c-UGHH4MB3.js +1 -0
  148. package/dist/m/c-UKHTLWUW.js +1 -0
  149. package/dist/m/c-UL2YUY52.js +1 -0
  150. package/dist/m/c-UNN7LWCT.js +1 -0
  151. package/dist/m/c-UXBMRDZB.js +235 -0
  152. package/dist/m/c-V7PZZRG7.js +1 -0
  153. package/dist/m/c-VAK7SG37.js +1 -0
  154. package/dist/m/c-VDSS4K3Z.js +1 -0
  155. package/dist/m/c-VFX45IZQ.js +1 -0
  156. package/dist/m/c-VHNSBD2M.js +1 -0
  157. package/dist/m/c-VKEDWYDF.js +1 -0
  158. package/dist/m/c-VL52ADDR.js +1 -0
  159. package/dist/m/c-WN5AY3OZ.js +1 -0
  160. package/dist/m/c-WNYBLIOZ.js +1 -0
  161. package/dist/m/c-WRBKEJD4.js +1 -0
  162. package/dist/m/c-WWDBYXLK.js +1 -0
  163. package/dist/m/c-WYFDLITL.js +1 -0
  164. package/dist/m/c-XN6MBKI6.js +67 -0
  165. package/dist/m/c-XNNW2CWK.js +1 -0
  166. package/dist/m/c-XPELB6E5.js +1 -0
  167. package/dist/m/c-XQXZAO7N.js +1 -0
  168. package/dist/m/c-Y6USGTS5.js +5 -0
  169. package/dist/m/c-YDH7Q3NY.js +1 -0
  170. package/dist/m/c-YG2NJYS6.js +135 -0
  171. package/dist/m/c-Z36J63PP.js +1 -0
  172. package/dist/m/c-Z4VRC7GU.js +7 -0
  173. package/dist/m/c-Z6K52OLP.js +19 -0
  174. package/dist/m/c-ZDY2MDAA.js +21 -0
  175. package/dist/m/c-ZE46QDIH.js +1 -0
  176. package/dist/m/c-ZUCOKOHT.js +1 -0
  177. package/dist/m/core/admin-attrs-VGNLAUDK.js +1 -0
  178. package/dist/m/core/autosave-E4PEFPZ6.js +1 -0
  179. package/dist/m/core/conflict-notice-VQ6Y64PH.js +1 -0
  180. package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
  181. package/dist/m/core/host-permission-XN3K2IW3.js +1 -0
  182. package/dist/m/core/is-edit-mode-JXHDVGUL.js +1 -0
  183. package/dist/m/core/persist-HKHA66IY.js +3 -0
  184. package/dist/m/core/recovery-TBBSICE2.js +1 -0
  185. package/dist/m/core/save-WUCHTQBE.js +1 -0
  186. package/dist/m/core/save-core-FJVHT5M3.js +1 -0
  187. package/dist/m/core/snapshot-F3SWDZMM.js +1 -0
  188. package/dist/m/core/unsaved-warning-STKBHE6G.js +1 -0
  189. package/dist/m/dom/all-APS5ZSKP.js +1 -0
  190. package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
  191. package/dist/m/events/index-23DPTUT4.js +1 -0
  192. package/dist/m/internals/index-WOVAP3RU.js +1 -0
  193. package/dist/m/lib/cache-bust-CGWT6ZJI.js +1 -0
  194. package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
  195. package/dist/m/loader-HXX6YNH5.js +1 -0
  196. package/dist/m/manifest-F22DEDAC.js +1 -0
  197. package/dist/m/manifest-edit-C3C7376B.js +1 -0
  198. package/dist/m/manifest.json +3491 -0
  199. package/dist/m/options/options-2LH76JQ3.js +1 -0
  200. package/dist/m/page/editor-XUTB2HB2.js +189 -0
  201. package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +6 -0
  202. package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
  203. package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
  204. package/dist/m/plugins/demo-ZIBFAA3R.js +1 -0
  205. package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
  206. package/dist/m/plugins/indicator-PV4YW2ID.js +1 -0
  207. package/dist/m/plugins/live-NUTTXSBS.js +1 -0
  208. package/dist/m/plugins/menu-HCKO3DSP.js +138 -0
  209. package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
  210. package/dist/m/plugins/page-KSUH7G6J.js +2 -0
  211. package/dist/m/plugins/page-view-W73OW7WI.js +1 -0
  212. package/dist/m/plugins/people-AJJ32YCG.js +1 -0
  213. package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
  214. package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
  215. package/dist/m/plugins/richclay-RC3L2F6X.js +517 -0
  216. package/dist/m/plugins/richclay-RC3L2F6X.js.LEGAL.txt +1 -0
  217. package/dist/m/plugins/sortable-YRMQX3ZU.js +1 -0
  218. package/dist/m/plugins/source-KHK5UUSK.js +1 -0
  219. package/dist/m/plugins/undo-OF2L3CXU.js +1 -0
  220. package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
  221. package/dist/m/plugins/upgrade-ZNYODP5Y.js +1 -0
  222. package/dist/m/plugins/upload-XSGNILUY.js +1 -0
  223. package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +21 -0
  224. package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +1 -0
  225. package/dist/m/plugins/wire-ZWVWKF76.js +1 -0
  226. package/dist/m/reader-core-ILWLXVMP.js +68 -0
  227. package/dist/m/route/route-KOCZSEGN.js +1 -0
  228. package/dist/m/sync/live-sync-4AUVXVJU.js +1 -0
  229. package/dist/m/ui/index-EVHKMKBL.js +1 -0
  230. package/dist/m/utils/index-4TJCVLT7.js +1 -0
  231. package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
  232. package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
  233. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
  234. package/dist/offline-manifest.json +3800 -0
  235. package/dist/page/themes/light-1.css +406 -0
  236. package/dist/server/core.cjs +23662 -22860
  237. package/dist/server/offline-runtime.cjs +4954 -0
  238. package/dist/server/offline-runtime.mjs +4923 -0
  239. package/dist/server/rooms.mjs +9 -0
  240. package/docs/guide.md +1078 -669
  241. package/entries/all.js +2 -0
  242. package/entries/clay-data.js +1 -1
  243. package/entries/clay-dom.js +2 -0
  244. package/entries/clay-events.js +2 -0
  245. package/entries/clay-internals.js +2 -0
  246. package/entries/clay-options.js +2 -0
  247. package/entries/clay-router.js +2 -0
  248. package/entries/clay-ui.js +2 -0
  249. package/entries/clay-utils.js +2 -0
  250. package/entries/clay.js +2 -0
  251. package/entries/sap.js +6 -6
  252. package/fonts/ibm-plex-mono-LICENSE +93 -0
  253. package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  254. package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  255. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  256. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  257. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  258. package/fonts/ibm-plex-sans-LICENSE +93 -0
  259. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  260. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  261. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  262. package/fonts/ibm-plex.css +71 -0
  263. package/fonts/inter-LICENSE +93 -0
  264. package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  265. package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
  266. package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
  267. package/fonts/inter-greek-wght-normal.woff2 +0 -0
  268. package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  269. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  270. package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
  271. package/fonts/inter.css +62 -0
  272. package/fonts/newsreader-LICENSE +93 -0
  273. package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
  274. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  275. package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
  276. package/fonts/newsreader.css +26 -0
  277. package/fonts/work-sans-LICENSE +93 -0
  278. package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
  279. package/fonts/work-sans.css +8 -0
  280. package/package.json +20 -3
  281. package/packed-contract.json +1 -0
  282. package/src/attrs/add-from.js +27 -4
  283. package/src/attrs/save-freeze.js +7 -52
  284. package/src/core/admin-attrs.js +119 -12
  285. package/src/core/admin-contenteditable.js +12 -10
  286. package/src/core/admin-inputs.js +14 -15
  287. package/src/core/admin-onclick.js +2 -0
  288. package/src/core/autosave.js +74 -21
  289. package/src/core/conflict-download.js +22 -0
  290. package/src/core/conflict-notice-ui.js +1173 -0
  291. package/src/core/conflict-notice.js +140 -697
  292. package/src/core/edit-mode.js +147 -14
  293. package/src/core/etag.js +30 -12
  294. package/src/core/host-meta.js +72 -8
  295. package/src/core/host-permission.js +29 -0
  296. package/src/core/is-edit-mode.js +145 -12
  297. package/src/core/mode-key.js +125 -0
  298. package/src/core/mode-lifecycle.js +544 -0
  299. package/src/core/mode-receipt.js +30 -0
  300. package/src/core/mode-refusal.js +13 -0
  301. package/src/core/mode-resources.js +52 -0
  302. package/src/core/mode-support.js +82 -0
  303. package/src/core/mode-switch.js +259 -0
  304. package/src/core/mode-transition.js +112 -0
  305. package/src/core/page-data-write.js +305 -0
  306. package/src/core/page-data.js +26 -227
  307. package/src/core/page-format-wire.js +4 -0
  308. package/src/core/page-format.js +58 -0
  309. package/src/core/persist.js +37 -9
  310. package/src/core/preserve-authored.js +114 -0
  311. package/src/core/recovery.js +772 -0
  312. package/src/core/runtime-url.js +105 -0
  313. package/src/core/save-core.js +109 -12
  314. package/src/core/save-replace.js +72 -0
  315. package/src/core/save-size-notice.js +134 -0
  316. package/src/core/save-size.js +140 -0
  317. package/src/core/save.js +805 -118
  318. package/src/core/snapshot.js +121 -11
  319. package/src/core/source-map.js +157 -9
  320. package/src/core/unsaved-warning.js +14 -4
  321. package/src/core/upgrade-run.js +408 -0
  322. package/src/core/version.js +18 -0
  323. package/src/lib/assets.js +30 -0
  324. package/src/lib/autosave-debug-gate.js +50 -0
  325. package/src/lib/autosave-scheduler.js +141 -0
  326. package/src/lib/boot-imports.js +8 -0
  327. package/src/lib/derived.js +3 -1
  328. package/src/lib/dirty-gate.js +15 -0
  329. package/src/lib/doc-storage.js +33 -0
  330. package/src/lib/effect.js +9 -4
  331. package/src/lib/mutation.js +4 -2
  332. package/src/lib/offline-runtime.js +226 -0
  333. package/src/lib/preload.js +48 -0
  334. package/src/lib/root-attrs.js +1 -1
  335. package/src/lib/trash.js +18 -0
  336. package/src/lib/undo/capture-move.js +152 -0
  337. package/src/live/local-channel.js +61 -0
  338. package/src/live/local-lane.js +60 -9
  339. package/src/live/server-lane.js +6 -3
  340. package/src/loader-logic.js +76 -15
  341. package/src/loader.js +218 -30
  342. package/src/page/adopt-boot.js +76 -22
  343. package/src/page/adopt.js +25 -4
  344. package/src/page/awareness.js +230 -0
  345. package/src/page/blocks.js +79 -5
  346. package/src/page/blocksel.js +20 -4
  347. package/src/page/board-face.js +13 -0
  348. package/src/page/board-panel.js +5 -17
  349. package/src/page/board-style.js +2 -13
  350. package/src/page/board-view.js +1 -11
  351. package/src/page/board-views.js +17 -0
  352. package/src/page/board.js +2 -2
  353. package/src/page/boot-pack.js +26 -0
  354. package/src/page/boot.js +237 -0
  355. package/src/page/checklist.js +1 -1
  356. package/src/page/chrome.js +411 -61
  357. package/src/page/csv-import.js +17 -2
  358. package/src/page/document-title-dom.js +40 -0
  359. package/src/page/document-title-runtime.js +76 -0
  360. package/src/page/document-title.js +9 -0
  361. package/src/page/drag.js +48 -13
  362. package/src/page/drop.js +2 -1
  363. package/src/page/editor.js +330 -86
  364. package/src/page/feature-loader.js +72 -0
  365. package/src/page/features/board.js +12 -0
  366. package/src/page/features/code.js +13 -0
  367. package/src/page/features/media.js +290 -0
  368. package/src/page/features/registry.js +23 -0
  369. package/src/page/features/table.js +383 -0
  370. package/src/page/{tool-view.js → features/tool.js} +33 -2
  371. package/src/page/format-codec.js +215 -0
  372. package/src/page/format-gate.js +179 -0
  373. package/src/page/format-level.js +157 -0
  374. package/src/page/highlight-slot.js +23 -0
  375. package/src/page/highlight.js +30 -16
  376. package/src/page/history-intent.js +78 -0
  377. package/src/page/inline.js +18 -25
  378. package/src/page/items.js +52 -11
  379. package/src/page/link.js +1 -0
  380. package/src/page/markdown.js +86 -1
  381. package/src/page/media-views.js +3 -0
  382. package/src/page/media.js +33 -245
  383. package/src/page/outline-cache.js +11 -0
  384. package/src/page/placeholder.js +27 -7
  385. package/src/page/projection.js +196 -50
  386. package/src/page/reader-patch.js +306 -0
  387. package/src/page/recover.js +1 -1
  388. package/src/page/rich/page-tool.js +1 -4
  389. package/src/page/runtime-owner.js +27 -0
  390. package/src/page/save-projection.js +7 -4
  391. package/src/page/schema.js +14 -0
  392. package/src/page/table-rect-key.js +3 -0
  393. package/src/page/table-select.js +4 -3
  394. package/src/page/table-style.js +1 -1
  395. package/src/page/table-views.js +3 -0
  396. package/src/page/table.js +8 -293
  397. package/src/page/themes/light-1.css +139 -2
  398. package/src/page/themes/light-1.source.css +269 -0
  399. package/src/page/tool-capability.js +3 -0
  400. package/src/page/tool-views.js +3 -0
  401. package/src/page/view.js +390 -107
  402. package/src/page/ybind/awareness.js +172 -0
  403. package/src/page/ybind/binding.js +9 -1
  404. package/src/page/ybind/bridge.js +50 -47
  405. package/src/page/ybind/decode.js +52 -19
  406. package/src/page/ybind/doc.js +27 -2
  407. package/src/page/ybind/group.js +12 -3
  408. package/src/page/ybind/index.js +2 -0
  409. package/src/page/ybind/lineage.js +71 -0
  410. package/src/page/ybind/marks.js +25 -1
  411. package/src/page/ybind/migrate.js +48 -13
  412. package/src/page/ybind/moves.js +32 -21
  413. package/src/page/ybind/positions.js +50 -0
  414. package/src/page/ybind/provider.js +84 -22
  415. package/src/page/ybind/recovery-proof.js +191 -0
  416. package/src/page/ybind/recovery-replay.js +130 -0
  417. package/src/page/ybind/tokens.js +15 -6
  418. package/src/page/ybind/walk.js +10 -3
  419. package/src/page/ybind/writer.js +48 -4
  420. package/src/plugins/ai-edit.js +167 -32
  421. package/src/plugins/bind-text.js +218 -0
  422. package/src/plugins/cms.js +93 -0
  423. package/src/plugins/indicator.js +57 -5
  424. package/src/plugins/live.js +282 -34
  425. package/src/plugins/menu/claygoo.svg.js +1 -0
  426. package/src/plugins/menu/seo-head.js +323 -0
  427. package/src/plugins/menu/seo.js +849 -0
  428. package/src/plugins/menu.js +425 -0
  429. package/src/plugins/page-view.js +131 -0
  430. package/src/plugins/page.js +1022 -275
  431. package/src/plugins/people.js +158 -31
  432. package/src/plugins/richclay.js +119 -9
  433. package/src/plugins/sortable.js +370 -60
  434. package/src/plugins/source.js +189 -366
  435. package/src/plugins/undo.js +289 -19
  436. package/src/plugins/unsent.js +132 -0
  437. package/src/plugins/upgrade-run.js +1 -0
  438. package/src/plugins/upgrade.js +210 -0
  439. package/src/plugins/upload-task.js +571 -0
  440. package/src/plugins/upload-ui.js +265 -0
  441. package/src/plugins/upload.js +34 -153
  442. package/src/plugins/visitor-drafts/inert.js +32 -0
  443. package/src/plugins/visitor-drafts/owner-chip.js +3 -0
  444. package/src/plugins/visitor-drafts/prompt.js +2 -2
  445. package/src/plugins/visitor-drafts-owner.js +85 -0
  446. package/src/plugins/visitor-drafts.js +31 -42
  447. package/src/plugins/wire.js +82 -1
  448. package/src/reader-core.js +26 -0
  449. package/src/recovery/conflicts.js +287 -0
  450. package/src/recovery/identity.js +154 -0
  451. package/src/recovery/store.js +792 -0
  452. package/src/recovery/undo.js +257 -0
  453. package/src/server/checkpoint/index.js +68 -4
  454. package/src/server/offline-runtime/index.js +214 -0
  455. package/src/server/rooms/index.js +2 -1
  456. package/src/server/rooms/live-data-rooms.js +2 -2
  457. package/src/server/rooms/page-admission.js +33 -0
  458. package/src/server/rooms/page-lineage.js +56 -0
  459. package/src/server/rooms/page-rebase.js +11 -5
  460. package/src/server/rooms/page-state.js +2 -0
  461. package/src/server/rooms/page-write-admission.js +9 -3
  462. package/src/source/session.js +705 -0
  463. package/src/sync/conflict-revert.js +5 -4
  464. package/src/sync/conflicts-edit.js +119 -0
  465. package/src/sync/conflicts-facade.js +49 -0
  466. package/src/sync/conflicts.js +99 -1
  467. package/src/sync/editing-marker.js +37 -6
  468. package/src/sync/editing-sender.js +8 -1
  469. package/src/sync/live-sync-edit.js +63 -0
  470. package/src/sync/live-sync.js +364 -119
  471. package/src/sync/local-presence.js +187 -0
  472. package/src/sync/merge-declarations.js +10 -0
  473. package/src/sync/merge-engine.js +12 -0
  474. package/src/sync/presence-store.js +412 -0
  475. package/src/sync/presence-target.js +243 -0
  476. package/src/sync/presence.js +72 -41
  477. package/src/sync/saved-handoff.js +200 -0
  478. package/src/sync/saved-stream.js +535 -0
  479. package/src/sync/version-guard.js +92 -0
  480. package/src/sync/wire-profile.js +81 -0
  481. package/src/ui/bevel-controls.js +2 -8
  482. package/src/ui/page-scheme.js +7 -0
  483. package/src/ui/skins/cms.js +1 -1
  484. package/src/ui/theme-config.js +150 -0
  485. package/src/ui/theme.js +6 -138
  486. package/src/ui/toast-lazy.js +11 -0
  487. package/src/vendor/LICENSES-optional.md +253 -0
  488. package/src/vendor/highlight.vendor.js +3 -0
  489. package/src/vendor/hljs-bash.vendor.js +2 -0
  490. package/src/vendor/hljs-css.vendor.js +2 -0
  491. package/src/vendor/hljs-javascript.vendor.js +2 -0
  492. package/src/vendor/hljs-json.vendor.js +2 -0
  493. package/src/vendor/hljs-python.vendor.js +2 -0
  494. package/src/vendor/hljs-xml.vendor.js +2 -0
  495. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  496. package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
  497. package/src/vendor/hyper-morph-identity.vendor.js +2 -0
  498. package/src/vendor/hyper-morph-json.vendor.js +3 -0
  499. package/src/vendor/hyper-morph.vendor.js +6 -6
  500. package/src/vendor/hyper-undo.vendor.js +1 -1
  501. package/src/vendor/hypercms.vendor.js +169 -84
  502. package/src/vendor/marked.vendor.js +68 -0
  503. package/src/vendor/overtype.vendor.js +994 -0
  504. package/src/vendor/richclay.vendor.js +17 -17
  505. package/src/vendor/sap-core.vendor.js +5 -5
  506. package/src/vendor/sap-ownership.vendor.js +11 -7
@@ -0,0 +1,257 @@
1
+ /**
2
+ * undo.js — the durable half of a refused undo step.
3
+ *
4
+ * The undo stack belongs to this tab and this editing session: a reload starts with an
5
+ * empty history, and the entry a person tried to take back is live DOM nodes and a
6
+ * compiled replay plan, neither of which a reload can use. What a reload can keep is
7
+ * the losing side as plain data — the label, the direction, why it was refused, the
8
+ * targets with the page's own name for each, and every step's before and after.
9
+ *
10
+ * One internal channel, `clay.undo`, on the shared recovery service: registered the
11
+ * first time a step is refused or the page keeps one of its own, never at boot and
12
+ * never by a reader. A refused record is never the kind a save can cover, and this
13
+ * channel registers nothing a save could read back, so a normal save dismisses
14
+ * nothing here that a person has not dealt with. A record is still only a recovery
15
+ * copy: it is not a save and the page never renders from it.
16
+ */
17
+
18
+ import { representedEtag } from '../core/etag.js';
19
+ import { storage } from '../lib/doc-storage.js';
20
+ import { plainCopy, storedContext } from './conflicts.js';
21
+
22
+ export const UNDO_CHANNEL = 'clay.undo';
23
+ export const UNDO_VERSION = 1;
24
+ export const UNDO_KIND = 'conflict';
25
+ export const UNDO_TEXT = 'text';
26
+
27
+ // The same cheap read the conflicts channel keeps for the notice's shell: one small
28
+ // count in this document's own storage, so the next edit session learns that copies
29
+ // are waiting without opening the database. It is written here, on stage and on
30
+ // dismissal, and never read from the reader path, which loads neither module.
31
+ const HINT_KEY = 'undo';
32
+
33
+ /**
34
+ * How many copies this document's store is holding for review, without opening the
35
+ * database. Zero means a boot has nothing to show.
36
+ *
37
+ * @returns {number}
38
+ */
39
+ export function pendingUndo() {
40
+ const stored = storage.local.get(HINT_KEY);
41
+ return Number.isFinite(stored?.count) && stored.count > 0 ? stored.count : 0;
42
+ }
43
+
44
+ function noteHint(count) {
45
+ if (count > 0) storage.local.set(HINT_KEY, { count, at: Date.now() });
46
+ else storage.local.remove(HINT_KEY);
47
+ }
48
+
49
+ function recoveryService() {
50
+ const clay = globalThis.window?.clay ?? globalThis.clay;
51
+ return clay?.recovery ?? null;
52
+ }
53
+
54
+ let registered = null;
55
+
56
+ function channel() {
57
+ const service = recoveryService();
58
+ if (!service || typeof service.register !== 'function') return null;
59
+ if (registered?.service === service) return registered.channel;
60
+ try {
61
+ registered = {
62
+ service,
63
+ channel: service.register({ channel: UNDO_CHANNEL, version: UNDO_VERSION }),
64
+ };
65
+ } catch (_) {
66
+ return null;
67
+ }
68
+ return registered.channel;
69
+ }
70
+
71
+ // One refused step and one direction are one record: the key is what a repeated click
72
+ // lands on, and what lets the older copy go.
73
+ const keyOf = (entryId, direction) => `refused:${entryId}:${direction}`;
74
+
75
+ function elementFor(id) {
76
+ if (id == null || typeof document === 'undefined') return null;
77
+ const escaped = globalThis.CSS?.escape ? globalThis.CSS.escape(String(id)) : String(id).replace(/["\\]/g, '\\$&');
78
+ try {
79
+ return document.querySelector(`[data-id="${escaped}"]`);
80
+ } catch (_) {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ // The registry itself stays with the ledger: `clay.conflicts` is where a page registered
86
+ // its resolver, and asking it here costs no module the reader carries and no download.
87
+ function pageRegistry() {
88
+ const clay = globalThis.window?.clay ?? globalThis.clay;
89
+ const conflicts = clay?.conflicts;
90
+ return typeof conflicts?.contextFor === 'function' ? conflicts : null;
91
+ }
92
+
93
+ /**
94
+ * The page's name for one refused target, asked of the same resolver registry the
95
+ * conflict notice uses — the ledger's own `contextFor`, so the order, the throw and the
96
+ * fields kept are one behaviour and not two.
97
+ *
98
+ * The subject is the target's id and the live element that id finds, which a resolver
99
+ * written for the conflict ledger answers nothing for: it reads the record's own nodes,
100
+ * and a refused step has none. Such a page gets the generic shape until it names this
101
+ * subject too.
102
+ *
103
+ * @param {{id: ?string}} target
104
+ * @returns {Object}
105
+ */
106
+ export function contextForTarget({ id = null } = {}) {
107
+ const target = id == null ? null : String(id);
108
+ const registry = pageRegistry();
109
+ const found = registry ? registry.contextFor({ source: 'undo', id: target, element: elementFor(target) }, null) : null;
110
+ return found ?? { kind: null, id: target, label: null, field: null };
111
+ }
112
+
113
+ // One context per refused target, in the targets' own order.
114
+ function contextsOf(targets) {
115
+ return targets.map((target) => storedContext(contextForTarget(target)));
116
+ }
117
+
118
+ // A step refused twice is the same losing material. The older record goes only once
119
+ // the newer one is durable, and only at the revision this tab can see, so a copy
120
+ // another tab replaced or a person dismissed is never removed by a guess.
121
+ async function supersede(ch, key, id) {
122
+ let listed;
123
+ try {
124
+ listed = await ch.list();
125
+ } catch (_) {
126
+ return;
127
+ }
128
+ if (!listed?.ok) return;
129
+ for (const record of listed.records) {
130
+ if (record.key !== key || String(record.id) === String(id)) continue;
131
+ try {
132
+ await ch.dismiss(record.id, { revision: record.revision });
133
+ } catch (_) {}
134
+ }
135
+ }
136
+
137
+ // The whole stage, flush and supersede of one refusal, one call at a time: two refusals
138
+ // in the same tick would otherwise each delete the other's record as the older copy.
139
+ let keeping = Promise.resolve();
140
+
141
+ /**
142
+ * Keep what a refused undo or redo would have restored, before anything in this tab
143
+ * can be lost to a reload.
144
+ *
145
+ * The engine's own `describe(entryId)` is the source: it is plain data already, with
146
+ * the refusal on it and every step's endpoints, so nothing here reads a DOM node or a
147
+ * replay plan. A store that refuses the write leaves the copy in memory — the service
148
+ * keeps it — and answers with the failure.
149
+ *
150
+ * @param {Object} engine - the started undo scope (`clay.undo`)
151
+ * @param {{entryId: string, direction: string, code?: string}} receipt
152
+ * @returns {Promise<{ok: boolean, code?: string}>}
153
+ */
154
+ export function keepRefusal(engine, receipt) {
155
+ const run = keeping.then(() => keepRefusalNow(engine, receipt));
156
+ keeping = run.catch(() => {});
157
+ return run;
158
+ }
159
+
160
+ async function keepRefusalNow(engine, receipt) {
161
+ const ch = channel();
162
+ if (!ch) return { ok: false, code: 'unavailable' };
163
+ let described = null;
164
+ try {
165
+ described = engine?.describe?.(receipt?.entryId) ?? null;
166
+ } catch (_) {
167
+ described = null;
168
+ }
169
+ if (!described) return { ok: false, code: 'unknown-entry' };
170
+ const refused = described.refused && typeof described.refused === 'object' ? described.refused : {};
171
+ const direction = receipt?.direction ?? refused.direction ?? null;
172
+ const targets = (Array.isArray(refused.targets) ? refused.targets : []).map((target) => ({
173
+ id: target?.id == null ? null : String(target.id),
174
+ kind: target?.kind == null ? null : String(target.kind),
175
+ }));
176
+ const key = keyOf(described.id ?? receipt?.entryId, direction);
177
+ const payload = plainCopy({
178
+ label: described.label ?? null,
179
+ direction,
180
+ code: receipt?.code ?? refused.code ?? null,
181
+ targets,
182
+ context: contextsOf(targets),
183
+ steps: described.steps ?? [],
184
+ }, [], '');
185
+ let staged;
186
+ try {
187
+ staged = ch.stage({ key, kind: UNDO_KIND, baseEtag: representedEtag() ?? null, base: null, payload });
188
+ } catch (err) {
189
+ console.error('clayjs: the refused step could not be kept for a later reload; it is still in this tab\'s history', err);
190
+ return { ok: false, code: 'error' };
191
+ }
192
+ const written = await ch.flush();
193
+ if (written?.ok) {
194
+ await supersede(ch, key, staged.id);
195
+ await kept();
196
+ }
197
+ return written?.ok ? written : { ok: false, code: written?.code || 'error' };
198
+ }
199
+
200
+ /**
201
+ * Keep one of the page's own records on the same channel: an app's unsent text or its
202
+ * own unreviewed conflict material, staged under `app:<key>`.
203
+ *
204
+ * @param {{key: string, label?: ?string, context?: ?Object, payload?: ?Object}} spec
205
+ * @returns {Promise<{ok: boolean, code?: string}>}
206
+ */
207
+ export async function keep({ key, label = null, context = null, payload = null } = {}) {
208
+ const ch = channel();
209
+ if (!ch) return { ok: false, code: 'unavailable' };
210
+ const name = key == null ? '' : String(key);
211
+ if (!name) return { ok: false, code: 'invalid' };
212
+ const body = payload && typeof payload === 'object' ? payload : {};
213
+ const record = plainCopy({ ...body, label, context: storedContext(context) }, [], '');
214
+ try {
215
+ ch.stage({ key: `app:${name}`, kind: typeof body.text === 'string' ? UNDO_TEXT : UNDO_KIND, payload: record });
216
+ } catch (err) {
217
+ console.error('clayjs: the app\'s recovery copy could not be kept; it is still in this tab only', err);
218
+ return { ok: false, code: 'error' };
219
+ }
220
+ const written = await ch.flush();
221
+ if (written?.ok) await kept();
222
+ return written;
223
+ }
224
+
225
+ /**
226
+ * What this document's store holds for review, from every writer: a record belongs to
227
+ * the document, not to the tab that made it.
228
+ *
229
+ * @returns {Promise<{ok: boolean, code?: string, records: Object[], status?: Object}>}
230
+ */
231
+ export async function kept() {
232
+ const ch = channel();
233
+ if (!ch) return { ok: true, records: [] };
234
+ const listed = await ch.list();
235
+ if (listed?.ok) noteHint(Array.isArray(listed.records) ? listed.records.length : 0);
236
+ return listed;
237
+ }
238
+
239
+ /**
240
+ * Remove one kept record, at the revision the reviewer was looking at. A record that
241
+ * moved since then is left exactly as it is.
242
+ *
243
+ * @param {*} id
244
+ * @param {{revision?: number}} [options]
245
+ * @returns {Promise<{ok: boolean, code?: string}>}
246
+ */
247
+ export async function dismissKept(id, { revision } = {}) {
248
+ const ch = channel();
249
+ if (!ch) return { ok: false, code: 'unavailable' };
250
+ const receipt = await ch.dismiss(id, { revision });
251
+ if (receipt?.ok) {
252
+ try {
253
+ await kept();
254
+ } catch (_) {}
255
+ }
256
+ return receipt;
257
+ }
@@ -3,12 +3,14 @@ import { parseJsonRelaxed } from 'hyper-morph/json-parse';
3
3
  import { cloneJson, formatBy, serializeLive, stampOf, textHash, unionRanges } from '../rooms/index.js';
4
4
  import { blocksFromYValue } from '../../page/ybind/decode.js';
5
5
  import { YDecodeError } from '../../page/ybind/provider.js';
6
- import { canon, fragmentCleaner, renderBlocks } from '../../page/projection.js';
6
+ import { MARKS, OPAQUE, canon, fragmentCleaner, renderBlocks } from '../../page/projection.js';
7
7
  import { classifyFile, readBack } from '../../page/adopt.js';
8
8
  import { merge3, mergeJson } from 'hyper-morph';
9
9
  import { mergePageModels } from '../../page/merge.js';
10
+ import { documentTitle } from '../../page/document-title.js';
10
11
 
11
12
  const PAGE = /^page(\.[0-9]+)?$/;
13
+ const TITLE_MODE = 'clay-seo-title-mode';
12
14
  const NAME = /^[A-Za-z0-9_.:-]{1,64}$/;
13
15
  const attr = (node, name) => node.attrs.find((item) => item.name === name)?.value;
14
16
  const escapeAttr = (value) => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
@@ -36,9 +38,19 @@ function inspect(html) {
36
38
  const tags = new Map();
37
39
  const duplicates = new Set();
38
40
  const articles = [];
41
+ const titleModes = [];
42
+ let head = null;
43
+ let body = null;
44
+ let title = null;
39
45
  function visit(node, inert = false) {
40
46
  if (!node.tagName) { for (const child of node.childNodes ?? []) visit(child, inert); return; }
41
47
  const blocked = inert || node.tagName === 'template' || node.tagName === 'noscript';
48
+ if (!blocked && node.tagName === 'head') head = node;
49
+ if (!blocked && node.tagName === 'body') body = node;
50
+ if (!blocked && node.parentNode === head && node.tagName === 'title' && !title) title = node;
51
+ if (!blocked && node.parentNode === head && node.tagName === 'meta' && (attr(node, 'name') ?? '').trim().toLowerCase() === TITLE_MODE) {
52
+ titleModes.push(attr(node, 'content'));
53
+ }
42
54
  if (!blocked && node.tagName === 'article' && attr(node, 'data-page') !== undefined) articles.push(node);
43
55
  if (!blocked && node.tagName === 'script' && attr(node, 'src') === undefined &&
44
56
  (attr(node, 'clay') ?? '').split(/\s+/).includes('live')) {
@@ -54,7 +66,7 @@ function inspect(html) {
54
66
  }
55
67
  visit(root);
56
68
  for (const name of duplicates) tags.delete(name);
57
- return { article: articles.length === 1 ? articles[0] : null, tags };
69
+ return { article: articles.length === 1 ? articles[0] : null, tags, head, body, title, titleModes };
58
70
  }
59
71
 
60
72
  function startTag(html, node, store) {
@@ -87,7 +99,7 @@ function projection(capture) {
87
99
  }
88
100
 
89
101
  function placements(html, capture) {
90
- const { article, tags } = inspect(html);
102
+ const { article, tags, head, body, title, titleModes } = inspect(html);
91
103
  if (!article?.sourceCodeLocation?.startTag || !article.sourceCodeLocation.endTag ||
92
104
  capture.stores.some((store) => !tags.get(store.name)?.sourceCodeLocation?.endTag ||
93
105
  (attr(tags.get(store.name), 'clay') ?? '').split(/\s+/).includes('ephemeral'))) return null;
@@ -97,7 +109,7 @@ function placements(html, capture) {
97
109
  const tag = tags.get(store.name).sourceCodeLocation;
98
110
  return tag.startOffset < articleEnd && tag.endOffset > articleStart;
99
111
  })) return null;
100
- return { article, tags };
112
+ return { article, tags, head, body, title, titleModes };
101
113
  }
102
114
 
103
115
  function replace(html, edits) {
@@ -105,6 +117,46 @@ function replace(html, edits) {
105
117
  out.slice(0, edit.start) + edit.text + out.slice(edit.end), html);
106
118
  }
107
119
 
120
+ const customTitleOf = (titleModes) =>
121
+ titleModes.some((content) => String(content ?? '').trim().toLowerCase() === 'custom');
122
+
123
+ const escapeTitle = (value) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
124
+
125
+ function titleTextOf(node) {
126
+ let out = '';
127
+ const walk = (current) => {
128
+ for (const child of current.childNodes ?? []) {
129
+ if (child.nodeName === '#text') out += child.value;
130
+ else walk(child);
131
+ }
132
+ };
133
+ if (node) walk(node);
134
+ return out;
135
+ }
136
+
137
+ // A title with no element of its own goes after the head's start tag, else before its
138
+ // first child, else before the body starts.
139
+ function titleAt(placement) {
140
+ const head = placement.head?.sourceCodeLocation;
141
+ if (head?.startTag) return head.startTag.endOffset;
142
+ const inside = (placement.head?.childNodes ?? []).find((node) => node.sourceCodeLocation);
143
+ if (inside) return inside.sourceCodeLocation.startOffset;
144
+ const body = placement.body?.sourceCodeLocation;
145
+ if (body?.startTag) return body.startTag.startOffset;
146
+ const first = (placement.body?.childNodes ?? []).find((node) => node.sourceCodeLocation);
147
+ return first ? first.sourceCodeLocation.startOffset : null;
148
+ }
149
+
150
+ function titleEdit(placement, title) {
151
+ if (placement.title) {
152
+ const loc = placement.title.sourceCodeLocation;
153
+ if (!loc?.startTag || !loc.endTag) return null;
154
+ return { start: loc.startTag.endOffset, end: loc.endTag.startOffset, text: escapeTitle(title) };
155
+ }
156
+ const at = titleAt(placement);
157
+ return at === null ? null : { start: at, end: at, text: `<title>${escapeTitle(title)}</title>` };
158
+ }
159
+
108
160
  // Whether the room's blocks already carry everything the hand-edited article
109
161
  // changed since its tag was written: merging those changes in changes nothing.
110
162
  function holdsEdit(found, blocks, doc) {
@@ -151,6 +203,11 @@ function adoptionNeeded(html, createDocument) {
151
203
  }
152
204
  }
153
205
 
206
+ // An article that carries a semantic envelope holds content this renderer cannot
207
+ // project from the room's blocks. Rewriting it would drop that content, so the
208
+ // checkpoint refuses instead, leaving the file as it stands.
209
+ const holdsEnvelope = (html) => html.includes(`${OPAQUE}="`) || html.includes(`${MARKS}="`);
210
+
154
211
  // recovery: render the room's blocks even over an article it has not merged, for
155
212
  // a recovered copy of its edits (the file itself keeps the outside edit).
156
213
  export function renderCheckpoint(currentHtml, capture, { createDocument, recovery = false }) {
@@ -175,7 +232,9 @@ export function renderCheckpoint(currentHtml, capture, { createDocument, recover
175
232
  // A tab that merged this article says so on the frame that carried the
176
233
  // merge; otherwise the blocks must show the edit.
177
234
  if (!recovery && found.kind === 'adopt' && capture.adopted !== found.A && !holdsEdit(found, decoded.blocks, doc)) return { kind: 'refused', reason: 'needs-adoption' };
235
+ const current = currentHtml.slice(articleLoc.startTag.endOffset, articleLoc.endTag.startOffset);
178
236
  rendered = renderBlocks(decoded.blocks, { clean: fragmentCleaner(doc) });
237
+ if (current !== rendered.html && holdsEnvelope(current)) return { kind: 'refused', reason: 'unknown-page-content' };
179
238
  }
180
239
  catch (error) {
181
240
  if (error instanceof YDecodeError || error instanceof SyntaxError) return { kind: 'refused', reason: 'invalid-document' };
@@ -196,6 +255,10 @@ export function renderCheckpoint(currentHtml, capture, { createDocument, recover
196
255
  articleOpen.slice(revLoc.endOffset - articleLoc.startTag.startOffset) : articleOpen;
197
256
  edits.push({ start: articleLoc.startTag.startOffset, end: articleLoc.startTag.endOffset, text: articleStart.slice(0, -1) + rev + '>' });
198
257
  edits.push({ start: articleLoc.startTag.endOffset, end: articleLoc.endTag.startOffset, text: rendered.html });
258
+ if (!customTitleOf(placement.titleModes)) {
259
+ const edit = titleEdit(placement, documentTitle(rendered.model));
260
+ if (edit) edits.push(edit);
261
+ }
199
262
  return { kind: 'ok', html: replace(currentHtml, edits), revision: rendered.rev };
200
263
  }
201
264
 
@@ -208,6 +271,7 @@ export function isCheckpointCurrent(currentHtml, capture) {
208
271
  const articleLoc = placement.article.sourceCodeLocation;
209
272
  if (attr(placement.article, 'data-rev') !== rendered.rev ||
210
273
  currentHtml.slice(articleLoc.startTag.endOffset, articleLoc.endTag.startOffset) !== rendered.html) return false;
274
+ if (!customTitleOf(placement.titleModes) && titleTextOf(placement.title) !== documentTitle(rendered.model)) return false;
211
275
  return capture.stores.every((store) => {
212
276
  const node = placement.tags.get(store.name);
213
277
  const loc = node.sourceCodeLocation;
@@ -0,0 +1,214 @@
1
+ import { parse } from "../../vendor/parse5.vendor.js";
2
+ import {
3
+ canonicalizeRuntimeURL,
4
+ parseRuntimeURL,
5
+ resolveRuntimeURL,
6
+ } from "../../lib/offline-runtime.js";
7
+
8
+ export { canonicalizeRuntimeURL, parseRuntimeURL, resolveRuntimeURL };
9
+
10
+ export const DESCRIPTOR_ID = "clay-runtime-map";
11
+
12
+ const RUNTIME_REL = new Set(["stylesheet", "preload", "modulepreload"]);
13
+
14
+ function attributeOf(node, name) {
15
+ for (const attr of node.attrs ?? []) {
16
+ if (attr.name === name) return attr;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function isCandidate(node) {
22
+ if (node.tagName === "script") return Boolean(attributeOf(node, "src"));
23
+ if (node.tagName !== "link") return false;
24
+ if (!attributeOf(node, "href")) return false;
25
+ const rel = String(attributeOf(node, "rel")?.value ?? "").toLowerCase();
26
+ return rel.split(/\s+/).some((token) => RUNTIME_REL.has(token));
27
+ }
28
+
29
+ function valueBounds(raw) {
30
+ const eq = raw.indexOf("=");
31
+ if (eq < 0) return null;
32
+ let index = eq + 1;
33
+ while (index < raw.length && /\s/.test(raw[index])) index += 1;
34
+ const quote = raw[index] === '"' || raw[index] === "'" ? raw[index] : "";
35
+ if (!quote) return { start: index, end: raw.length, quote };
36
+ const close = raw.lastIndexOf(quote);
37
+ return { start: index + 1, end: close > index ? close : raw.length, quote };
38
+ }
39
+
40
+ function elements(doc) {
41
+ const found = [];
42
+ const walk = (node) => {
43
+ if (node.tagName) found.push(node);
44
+ for (const child of node.childNodes ?? []) walk(child);
45
+ if (node.content) walk(node.content);
46
+ };
47
+ walk(doc);
48
+ return found;
49
+ }
50
+
51
+ function candidates(doc, html) {
52
+ const found = [];
53
+ for (const node of elements(doc)) {
54
+ if (!isCandidate(node)) continue;
55
+ const name = node.tagName === "script" ? "src" : "href";
56
+ const location = node.sourceCodeLocation?.attrs?.[name];
57
+ if (!location) continue;
58
+ const bounds = valueBounds(html.slice(location.startOffset, location.endOffset));
59
+ if (!bounds) continue;
60
+ found.push({
61
+ node,
62
+ value: attributeOf(node, name).value,
63
+ raw: html.slice(location.startOffset + bounds.start, location.startOffset + bounds.end),
64
+ valueStart: location.startOffset + bounds.start,
65
+ valueEnd: location.startOffset + bounds.end,
66
+ startOffset: node.sourceCodeLocation.startTag?.startOffset ?? node.sourceCodeLocation.startOffset,
67
+ quote: bounds.quote,
68
+ });
69
+ }
70
+ return found;
71
+ }
72
+
73
+ function applySplices(html, splices) {
74
+ const ordered = [...splices].sort((a, b) => b.start - a.start);
75
+ let out = html;
76
+ for (const splice of ordered) {
77
+ out = out.slice(0, splice.start) + splice.text + out.slice(splice.end);
78
+ }
79
+ return out;
80
+ }
81
+
82
+ function attributeText(value, quote) {
83
+ const text = value.replace(/&/g, "&amp;");
84
+ if (quote === "'") return text.replace(/'/g, "&#39;");
85
+ if (quote === '"') return text.replace(/"/g, "&quot;");
86
+ return /[\s"'<>=`]/.test(text) ? `"${text.replace(/"/g, "&quot;")}"` : text;
87
+ }
88
+
89
+ // The attribute text for `replacement`, keeping the file's own spelling of the tail it shares with
90
+ // `value`, so a query written with a bare `&` keeps it. `raw` is how `value` is written in the file.
91
+ // A shared head that holds `&` could split a character reference, so that case escapes everything.
92
+ function respelled(raw, value, replacement, quote) {
93
+ let shared = 0;
94
+ while (
95
+ shared < value.length &&
96
+ shared < replacement.length &&
97
+ value[value.length - 1 - shared] === replacement[replacement.length - 1 - shared]
98
+ ) shared += 1;
99
+ const head = value.slice(0, value.length - shared);
100
+ const newHead = replacement.slice(0, replacement.length - shared);
101
+ if (head.includes("&") || !raw.startsWith(head)) return attributeText(replacement, quote);
102
+ if (!quote && /[\s"'<>=`]/.test(newHead)) return attributeText(replacement, quote);
103
+ return attributeText(newHead, quote) + raw.slice(head.length);
104
+ }
105
+
106
+ function descriptorTag(version, entries) {
107
+ const descriptor = { schema: 1, version, entries };
108
+ const json = JSON.stringify(descriptor).replace(/</g, "\\u003c");
109
+ return `<script type="application/json" id="${DESCRIPTOR_ID}" clay="no-save no-watch no-snapshot">${json}</script>`;
110
+ }
111
+
112
+ export function projectRuntimeHTML(html, { releases, selectedVersion, documentURL, descriptor = true } = {}) {
113
+ const nodes = candidates(parse(html, { sourceCodeLocationInfo: true }), html);
114
+ const resolved = nodes.map((node) => ({
115
+ node,
116
+ result: resolveRuntimeURL(node.value, { releases, selectedVersion, documentURL }),
117
+ }));
118
+
119
+ const versions = new Set();
120
+ let missing = false;
121
+ let absent = false;
122
+ for (const { node, result } of resolved) {
123
+ if (!result) {
124
+ const parsed = parseRuntimeURL(node.value, { documentURL });
125
+ if (parsed && (parsed.kind === "rolling" || parsed.kind === "exact")) absent = true;
126
+ continue;
127
+ }
128
+ if (result.available === false) missing = true;
129
+ versions.add(result.version);
130
+ }
131
+ if (missing || absent || versions.size > 1) {
132
+ return {
133
+ html,
134
+ mappings: [],
135
+ skipped: [],
136
+ unavailable: {
137
+ reason: missing ? "release-missing" : absent ? "path-missing" : "mixed-versions",
138
+ versions: [...versions].sort(),
139
+ },
140
+ };
141
+ }
142
+
143
+ const mappings = [];
144
+ const splices = [];
145
+ const version = versions.size === 1 ? [...versions][0] : null;
146
+ let insertAt = null;
147
+
148
+ for (const { node, result } of resolved) {
149
+ if (!result) continue;
150
+ splices.push({
151
+ start: node.valueStart,
152
+ end: node.valueEnd,
153
+ text: respelled(node.raw, node.value, result.localURL, node.quote),
154
+ });
155
+ mappings.push({ original: node.value, local: result.localURL });
156
+ if (insertAt === null) insertAt = node.startOffset;
157
+ }
158
+
159
+ if (!mappings.length) return { html, mappings, skipped: [], unavailable: null };
160
+
161
+ if (descriptor) splices.push({ start: insertAt, end: insertAt, text: descriptorTag(version, mappings) });
162
+ return { html: applySplices(html, splices), mappings, skipped: [], unavailable: null };
163
+ }
164
+
165
+ function descriptorSpan(node) {
166
+ const location = node.sourceCodeLocation;
167
+ if (!location) return null;
168
+ const start = location.startTag?.startOffset ?? location.startOffset;
169
+ const end = location.endTag ? location.endTag.endOffset : location.endOffset;
170
+ return { start, end };
171
+ }
172
+
173
+ function descriptorText(html, node) {
174
+ const location = node.sourceCodeLocation;
175
+ if (!location) return "";
176
+ const start = location.startTag?.endOffset ?? location.startOffset;
177
+ const end = location.endTag ? location.endTag.startOffset : location.endOffset;
178
+ return html.slice(start, end);
179
+ }
180
+
181
+ export function canonicalizeRuntimeHTML(html, { releases, documentURL } = {}) {
182
+ const doc = parse(html, { sourceCodeLocationInfo: true });
183
+ const nodes = candidates(doc, html);
184
+ const splices = [];
185
+ let descriptor = null;
186
+ let descriptorRemoved = false;
187
+
188
+ for (const node of elements(doc)) {
189
+ if (node.tagName !== "script" || attributeOf(node, "id")?.value !== DESCRIPTOR_ID) continue;
190
+ const span = descriptorSpan(node);
191
+ if (!span) continue;
192
+ if (!descriptorRemoved) {
193
+ try {
194
+ descriptor = JSON.parse(descriptorText(html, node));
195
+ } catch {
196
+ descriptor = null;
197
+ }
198
+ }
199
+ descriptorRemoved = true;
200
+ splices.push({ start: span.start, end: span.end, text: "" });
201
+ }
202
+
203
+ let changed = 0;
204
+ for (const node of nodes) {
205
+ const parsed = parseRuntimeURL(node.value, { documentURL });
206
+ if (!parsed || parsed.kind !== "local") continue;
207
+ const replacement = canonicalizeRuntimeURL(node.value, { descriptor, releases, documentURL });
208
+ if (replacement === node.value) continue;
209
+ splices.push({ start: node.valueStart, end: node.valueEnd, text: respelled(node.raw, node.value, replacement, node.quote) });
210
+ changed += 1;
211
+ }
212
+
213
+ return { html: applySplices(html, splices), changed, descriptorRemoved };
214
+ }
@@ -40,7 +40,8 @@ export {
40
40
  postReceipt,
41
41
  } from './live-provenance.js';
42
42
 
43
- export { isHybridPageValue, isYPageValue, inspectPage, pageBaseline } from './page-admission.js';
43
+ export { isHybridPageValue, isYPageValue, inspectPage, pageBaseline, captureFormat, checkPageFormatAdmission } from './page-admission.js';
44
+ export { PAGE_FORMAT, PAGE_FORMAT_HEADER, PAGE_FORMAT_EXTENSION, PAGE_FORMAT_UPGRADE, formatCapability, pageFormatFloor, withPageFormatFloor } from '../../core/page-format.js';
44
45
 
45
46
  export { splitPage, composePage, pageRev } from './page-state.js';
46
47
 
@@ -422,9 +422,9 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
422
422
  return reservePageTarget(key, nodeId, room, html, { source, base, epoch, names, format: 'yjs' });
423
423
  },
424
424
 
425
- reservePageInitial(key, { nodeId, currentHtml, html, ifMatch }) {
425
+ reservePageInitial(key, { nodeId, currentHtml, html, ifMatch, pageWrite = null, createDocument }) {
426
426
  const room = pageSource(key, nodeId);
427
- checkPageInitial({ currentHtml, html, ifMatch, room });
427
+ checkPageInitial({ currentHtml, html, ifMatch, room, pageWrite, createDocument });
428
428
  return reservePageTarget(key, nodeId, room, html, { source: 'initial', names: [], format: 'yjs' });
429
429
  },
430
430
 
@@ -3,6 +3,9 @@ import { parseJsonRelaxed } from 'hyper-morph/json-parse';
3
3
  import { cloneJson } from './live-data-ops.js';
4
4
  import { liveTags } from './live-data-tags.js';
5
5
  import { recent } from './recent.js';
6
+ import { project } from '../../page/projection.js';
7
+ import { blocksFromPageValues } from '../../page/ybind/decode.js';
8
+ import { pageFormatDecision, pageFormatFloor, pageFormatRefusal, requiredPageFormat, withPageFormatFloor } from '../../page/format-level.js';
6
9
 
7
10
  const PAGE = /^page(\.[0-9]+)?$/;
8
11
  const PART = /^(.+)\.(\d+)\.(\d+)$/;
@@ -77,3 +80,33 @@ export function pageBaseline(tags) {
77
80
  });
78
81
  return createHash('sha256').update(canon(source), 'utf8').digest('hex');
79
82
  }
83
+
84
+ // The floor a prospective page value needs: 1 when the model the capture would
85
+ // store holds a newer runtime's mark, block or field, 0 when it holds only what
86
+ // the frozen floor-0 registry knows. Invalid content refuses here rather than
87
+ // answering with a level, so a malformed capture cannot buy admission.
88
+ export function captureFormat(html) {
89
+ const answer = inspectPage(html ?? '');
90
+ if (answer.kind !== 'yjs') return 0;
91
+ return requiredPageFormat(project(blocksFromPageValues([answer.tags[0][1]])));
92
+ }
93
+
94
+ // The floor decision for one page write, taken before anything is mutated.
95
+ // `floor` is the document's established floor, `required` what the captured
96
+ // content needs (its own marker included) and `capability` what the writer
97
+ // declares in X-Clay-Page-Format. `admitted` lists the capabilities of the page
98
+ // editors the room has admitted, so a floor rise is refused while an editor that
99
+ // could not read the result is connected. On success the returned body carries
100
+ // the marker the room must persist, so a stale save that omitted it cannot lower
101
+ // the floor.
102
+ export function checkPageFormatAdmission({ currentHtml, html, capability = 0, admitted = [] }) {
103
+ const established = pageFormatFloor(currentHtml ?? '');
104
+ const decision = pageFormatDecision({
105
+ floor: established,
106
+ required: Math.max(captureFormat(html), pageFormatFloor(html ?? '')),
107
+ capability,
108
+ admitted,
109
+ });
110
+ if (!decision.ok) throw pageFormatRefusal(decision.minimum);
111
+ return { ...decision, html: withPageFormatFloor(html, decision.floor) };
112
+ }