@panphora/clayjs 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +7917 -5621
  11. package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
  12. package/dist/m/attrs/save-freeze-MYNVXREN.js +1 -0
  13. package/dist/m/c-2YYDMLNZ.js +1 -0
  14. package/dist/m/c-336SGU5W.js +1 -0
  15. package/dist/m/c-3H6BZDNJ.js +6 -0
  16. package/dist/m/{c-LR4CGEPT.js → c-3RUU4TPX.js} +1 -1
  17. package/dist/m/c-3XIRD4NU.js +1 -0
  18. package/dist/m/c-4DFEJ2PO.js +3 -0
  19. package/dist/m/c-56QRZTZH.js +1 -0
  20. package/dist/m/c-5J4E7O4Y.js +4 -0
  21. package/dist/m/c-5LKDWQTL.js +19 -0
  22. package/dist/m/c-5QWQ7A5L.js +1 -0
  23. package/dist/m/c-5RJFTLSF.js +1 -0
  24. package/dist/m/c-6AZOGAPE.js +1 -0
  25. package/dist/m/{c-Y6USGTS5.js → c-6CRUUXOX.js} +5 -5
  26. package/dist/m/{c-54RCDCBU.js → c-6KEVEQ7L.js} +1 -1
  27. package/dist/m/{c-LTGMXXKO.js → c-6NC5I2TV.js} +1 -1
  28. package/dist/m/{c-YDH7Q3NY.js → c-74MGYWZF.js} +1 -1
  29. package/dist/m/{c-Z36J63PP.js → c-7OLLJPWP.js} +1 -1
  30. package/dist/m/c-A3E3NIMC.js +1 -0
  31. package/dist/m/{c-5R4PTRG2.js → c-AMSNPOCX.js} +1 -1
  32. package/dist/m/c-B3JM5ZXC.js +1 -0
  33. package/dist/m/c-BPF2NSPV.js +1 -0
  34. package/dist/m/c-BQ6LOBEE.js +1 -0
  35. package/dist/m/c-CJ3A7ZKE.js +1 -0
  36. package/dist/m/c-CQHPGKX2.js +1 -0
  37. package/dist/m/c-DCXOXBF3.js +1 -0
  38. package/dist/m/c-DFS4XZJV.js +2 -0
  39. package/dist/m/{c-F3CKJTJS.js → c-DNENWY4O.js} +1 -1
  40. package/dist/m/c-E4LIR5X5.js +1 -0
  41. package/dist/m/c-EVCQ3BKA.js +1 -0
  42. package/dist/m/{c-5Y6TNX5W.js → c-EZU56NQV.js} +2 -2
  43. package/dist/m/c-FDNHHZY5.js +1 -0
  44. package/dist/m/{c-SWRJFCNN.js → c-FRLHSY7E.js} +1 -1
  45. package/dist/m/c-FROJUZMV.js +1 -0
  46. package/dist/m/c-G2JPKCTP.js +1 -0
  47. package/dist/m/c-GB6TU7WV.js +1 -0
  48. package/dist/m/{c-GB3JTV6E.js → c-GFJJEBTU.js} +1 -1
  49. package/dist/m/{c-J7CA54IP.js → c-GSFIGKOP.js} +1 -1
  50. package/dist/m/{c-OYTFP5QN.js → c-GY422N3Q.js} +1 -1
  51. package/dist/m/c-HAWFNBYG.js +1 -0
  52. package/dist/m/{c-33HUYEDI.js → c-HDDEEL77.js} +1 -1
  53. package/dist/m/c-HFKUVACH.js +1 -0
  54. package/dist/m/c-HHDFCNHV.js +1 -0
  55. package/dist/m/c-I2ZATQCD.js +2 -0
  56. package/dist/m/c-IEM4NTCS.js +1 -0
  57. package/dist/m/c-IW6GGOU2.js +1 -0
  58. package/dist/m/{c-SGCCC4FW.js → c-JDCX26UP.js} +2 -2
  59. package/dist/m/c-JRKY44DK.js +1 -0
  60. package/dist/m/c-JU3NK25H.js +1 -0
  61. package/dist/m/{c-VFX45IZQ.js → c-KAU6E43D.js} +1 -1
  62. package/dist/m/c-KBS3UV6B.js +1 -0
  63. package/dist/m/c-KGVGROZT.js +1 -0
  64. package/dist/m/{c-YG2NJYS6.js → c-KQ6TYJAP.js} +2 -2
  65. package/dist/m/{c-2JL6E75N.js → c-KU3ZFADW.js} +1 -1
  66. package/dist/m/c-LWLBHLVC.js +3 -0
  67. package/dist/m/c-M2L47S2X.js +7 -0
  68. package/dist/m/c-MFP2ZWD4.js +1 -0
  69. package/dist/m/{c-4YSCRICO.js → c-MKSQ4Y57.js} +1 -1
  70. package/dist/m/{c-2M73252O.js → c-NUFFJVVJ.js} +1 -1
  71. package/dist/m/c-NUHSPT5P.js +1 -0
  72. package/dist/m/c-O72HVYSA.js +1 -0
  73. package/dist/m/c-OW5QIGG3.js +1 -0
  74. package/dist/m/c-PGRCK73F.js +1 -0
  75. package/dist/m/{c-4DRMS4QG.js → c-PS5ARVWR.js} +1 -1
  76. package/dist/m/c-Q622NS2Y.js +1 -0
  77. package/dist/m/c-Q7NQD65V.js +1 -0
  78. package/dist/m/c-QH63LQOS.js +1 -0
  79. package/dist/m/{c-TQZ2JYHD.js → c-QICP2YR5.js} +1 -1
  80. package/dist/m/{c-7VGZ7RU5.js → c-QJ4OM3G6.js} +1 -1
  81. package/dist/m/{c-MMUIUUZG.js → c-R3L7ZJGB.js} +1 -1
  82. package/dist/m/c-R7IWVINQ.js +1 -0
  83. package/dist/m/{c-HVYXIS72.js → c-RSMOODDU.js} +1 -1
  84. package/dist/m/{c-KKWLDOPM.js → c-RYUXUUBX.js} +1 -1
  85. package/dist/m/c-S4ZEH46B.js +9 -0
  86. package/dist/m/c-SDAXDM2D.js +1 -0
  87. package/dist/m/c-SMBZG5X4.js +1 -0
  88. package/dist/m/{c-UXBMRDZB.js → c-SMGU7RT3.js} +1 -1
  89. package/dist/m/c-SPIORRYS.js +1 -0
  90. package/dist/m/c-SWLRT66R.js +1 -0
  91. package/dist/m/c-SZGYW5HT.js +1 -0
  92. package/dist/m/c-TCJTVAFN.js +1 -0
  93. package/dist/m/{c-HRDCD3WN.js → c-TJ6WOJL3.js} +1 -1
  94. package/dist/m/c-TYLXX4NF.js +21 -0
  95. package/dist/m/c-U62OB4BT.js +1 -0
  96. package/dist/m/{c-NMJRXKPC.js → c-U6YWUAR3.js} +1 -1
  97. package/dist/m/c-UDCSU5G5.js +2 -0
  98. package/dist/m/{c-NZMNXVF6.js → c-UDUUJPFZ.js} +1 -1
  99. package/dist/m/c-VE6ZMYMC.js +1 -0
  100. package/dist/m/c-VG5XIN4T.js +1 -0
  101. package/dist/m/c-VHM5XQHK.js +1 -0
  102. package/dist/m/c-VI5RMSVY.js +1 -0
  103. package/dist/m/c-VRZN56XY.js +1 -0
  104. package/dist/m/c-W5MCHEGA.js +1 -0
  105. package/dist/m/c-WDC5GVKU.js +1 -0
  106. package/dist/m/c-WWXIDH4O.js +28 -0
  107. package/dist/m/{c-QIX5BJYN.js → c-XCNTK76U.js} +1 -1
  108. package/dist/m/c-XMS6DPBR.js +1 -0
  109. package/dist/m/c-Y5HBBIQ4.js +1 -0
  110. package/dist/m/{c-5M4YEKCX.js → c-ZVPVXTJF.js} +1 -1
  111. package/dist/m/c-ZVRKXKM7.js +1 -0
  112. package/dist/m/{c-HT33EQRN.js → c-ZWIOJGMT.js} +1 -1
  113. package/dist/m/core/admin-attrs-QHOKPJ7W.js +1 -0
  114. package/dist/m/core/autosave-HRYGUWV6.js +1 -0
  115. package/dist/m/core/conflict-notice-6MROJN2L.js +1 -0
  116. package/dist/m/core/host-permission-FNXVEFNE.js +1 -0
  117. package/dist/m/core/is-edit-mode-KTWA3O5N.js +1 -0
  118. package/dist/m/core/{persist-HKHA66IY.js → persist-JFYRHMRJ.js} +1 -1
  119. package/dist/m/core/recovery-FLTCC2OC.js +1 -0
  120. package/dist/m/core/save-HQZ4S667.js +1 -0
  121. package/dist/m/core/save-core-KGQWFUNH.js +1 -0
  122. package/dist/m/core/snapshot-EGQZNT45.js +1 -0
  123. package/dist/m/core/unsaved-warning-IDJLD66H.js +1 -0
  124. package/dist/m/events/index-RN2JNGBV.js +1 -0
  125. package/dist/m/internals/index-PVXGPUDA.js +1 -0
  126. package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
  127. package/dist/m/loader-S4I7GKXR.js +1 -0
  128. package/dist/m/manifest-4C419D67.js +1 -0
  129. package/dist/m/manifest-edit-B077F288.js +1 -0
  130. package/dist/m/manifest.json +1555 -1058
  131. package/dist/m/options/options-EJMS3K24.js +1 -0
  132. package/dist/m/page/{editor-XUTB2HB2.js → editor-NNJA4OKD.js} +21 -8
  133. package/dist/m/plugins/{ai-edit-ZQ4CGWIP.js → ai-edit-QN4PMMNL.js} +1 -1
  134. package/dist/m/plugins/demo-RM5IR6AO.js +1 -0
  135. package/dist/m/plugins/indicator-URDBBOQO.js +1 -0
  136. package/dist/m/plugins/live-HFWD4OIH.js +1 -0
  137. package/dist/m/plugins/{menu-HCKO3DSP.js → menu-3HIEQSI6.js} +2 -2
  138. package/dist/m/plugins/page-JOKB6XPR.js +2 -0
  139. package/dist/m/plugins/page-view-NXOJHNW6.js +1 -0
  140. package/dist/m/plugins/people-SGPJKANK.js +1 -0
  141. package/dist/m/plugins/{richclay-RC3L2F6X.js → richclay-NRQDSWX6.js} +2 -2
  142. package/dist/m/plugins/sortable-GWT4PWDV.js +1 -0
  143. package/dist/m/plugins/source-4ZRSAIYM.js +1 -0
  144. package/dist/m/plugins/undo-VDA5P7FH.js +1 -0
  145. package/dist/m/plugins/{upgrade-ZNYODP5Y.js → upgrade-W7FG5VFG.js} +1 -1
  146. package/dist/m/plugins/upload-WLYKK6TL.js +1 -0
  147. package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +21 -0
  148. package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +1 -0
  149. package/dist/m/plugins/wire-4N6GYAF6.js +1 -0
  150. package/dist/m/{reader-core-ILWLXVMP.js → reader-core-DXYCURFF.js} +2 -2
  151. package/dist/m/sync/live-sync-J7VMYTKS.js +1 -0
  152. package/dist/m/ui/{index-EVHKMKBL.js → index-3JLVI27O.js} +1 -1
  153. package/dist/offline-manifest.json +857 -623
  154. package/dist/page/themes/light-1.css +25 -1
  155. package/dist/server/core.cjs +115 -24
  156. package/package.json +1 -1
  157. package/src/core/boot-phase.js +2 -1
  158. package/src/core/conflict-download.js +3 -29
  159. package/src/core/download.js +32 -0
  160. package/src/core/host-meta.js +18 -0
  161. package/src/core/save-core.js +8 -4
  162. package/src/core/save.js +4 -4
  163. package/src/core/snapshot.js +39 -1
  164. package/src/lib/autosave-scheduler.js +3 -4
  165. package/src/page/blocks.js +34 -1
  166. package/src/page/board-views.js +2 -16
  167. package/src/page/chart-tables.js +11 -0
  168. package/src/page/chart.js +6 -10
  169. package/src/page/checkpoint-proof.js +289 -0
  170. package/src/page/chrome.js +24 -6
  171. package/src/page/document-title-dom.js +12 -0
  172. package/src/page/document-title-runtime.js +15 -1
  173. package/src/page/document-title.js +4 -1
  174. package/src/page/editor.js +77 -12
  175. package/src/page/emoji-data.js +162 -0
  176. package/src/page/features/board.js +12 -3
  177. package/src/page/features/chart.js +11 -0
  178. package/src/page/features/dates.js +180 -0
  179. package/src/page/features/emoji.js +187 -0
  180. package/src/page/features/mentions.js +188 -0
  181. package/src/page/features/registry.js +6 -1
  182. package/src/page/features/table.js +2 -2
  183. package/src/page/format-command.js +9 -0
  184. package/src/page/format-host.js +54 -0
  185. package/src/page/format-native-commands.js +142 -0
  186. package/src/page/format-native.js +42 -0
  187. package/src/page/inline.js +32 -8
  188. package/src/page/items.js +15 -3
  189. package/src/page/markdown.js +26 -2
  190. package/src/page/mention-picker.js +99 -0
  191. package/src/page/merge.js +1 -1
  192. package/src/page/projection.js +58 -15
  193. package/src/page/save-projection.js +39 -0
  194. package/src/page/save-states.js +1 -1
  195. package/src/page/schema.js +33 -0
  196. package/src/page/themes/content.css +25 -1
  197. package/src/page/themes/light-1.css +25 -1
  198. package/src/page/view-hooks.js +32 -0
  199. package/src/page/ybind/binding.js +31 -4
  200. package/src/page/ybind/marks.js +2 -1
  201. package/src/page/ybind/provider.js +13 -2
  202. package/src/page/ybind/recovery-replay.js +362 -4
  203. package/src/plugins/live.js +37 -1
  204. package/src/plugins/page.js +117 -3
  205. package/src/plugins/visitor-drafts/intro.js +2 -1
  206. package/src/server/checkpoint/index.js +2 -2
  207. package/src/sync/saved-stream.js +17 -8
  208. package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +0 -1
  209. package/dist/m/attrs/save-freeze-FUP4Q4X3.js +0 -1
  210. package/dist/m/c-35NBGMZB.js +0 -1
  211. package/dist/m/c-3IQRECYS.js +0 -1
  212. package/dist/m/c-4B6H4G5V.js +0 -1
  213. package/dist/m/c-4FFHZM32.js +0 -1
  214. package/dist/m/c-66O32MV3.js +0 -28
  215. package/dist/m/c-7O6AFR65.js +0 -1
  216. package/dist/m/c-ANQTJ3B5.js +0 -2
  217. package/dist/m/c-AYVOREBE.js +0 -1
  218. package/dist/m/c-B2ZXPOS3.js +0 -3
  219. package/dist/m/c-BMIZKMPV.js +0 -1
  220. package/dist/m/c-CJZSLMVQ.js +0 -1
  221. package/dist/m/c-DBE6ORSI.js +0 -1
  222. package/dist/m/c-EDHUR4MX.js +0 -1
  223. package/dist/m/c-EY6UOM54.js +0 -1
  224. package/dist/m/c-F7Y6FLPY.js +0 -1
  225. package/dist/m/c-FA4DZVTC.js +0 -1
  226. package/dist/m/c-G2BUZITR.js +0 -1
  227. package/dist/m/c-GBGWXTX4.js +0 -1
  228. package/dist/m/c-GHBCCVG4.js +0 -1
  229. package/dist/m/c-IB52R237.js +0 -1
  230. package/dist/m/c-IKWNE4R4.js +0 -1
  231. package/dist/m/c-IU3IYEU6.js +0 -1
  232. package/dist/m/c-IZ2EBRAJ.js +0 -1
  233. package/dist/m/c-KR4UW4KW.js +0 -1
  234. package/dist/m/c-KY3VJGUA.js +0 -8
  235. package/dist/m/c-LXENEBV7.js +0 -1
  236. package/dist/m/c-PPOSCAQD.js +0 -1
  237. package/dist/m/c-QQHEX6HA.js +0 -1
  238. package/dist/m/c-R7RMANJI.js +0 -9
  239. package/dist/m/c-RI4NT75Y.js +0 -1
  240. package/dist/m/c-RUJKSUQW.js +0 -1
  241. package/dist/m/c-T4VO4OCB.js +0 -1
  242. package/dist/m/c-TR6RMDQ7.js +0 -3
  243. package/dist/m/c-UKHTLWUW.js +0 -1
  244. package/dist/m/c-UNN7LWCT.js +0 -1
  245. package/dist/m/c-V7PZZRG7.js +0 -1
  246. package/dist/m/c-VL52ADDR.js +0 -1
  247. package/dist/m/c-WWDBYXLK.js +0 -1
  248. package/dist/m/c-XNNW2CWK.js +0 -1
  249. package/dist/m/c-Z4VRC7GU.js +0 -7
  250. package/dist/m/c-Z6K52OLP.js +0 -19
  251. package/dist/m/c-ZDY2MDAA.js +0 -21
  252. package/dist/m/core/admin-attrs-VGNLAUDK.js +0 -1
  253. package/dist/m/core/autosave-E4PEFPZ6.js +0 -1
  254. package/dist/m/core/conflict-notice-VQ6Y64PH.js +0 -1
  255. package/dist/m/core/host-permission-XN3K2IW3.js +0 -1
  256. package/dist/m/core/is-edit-mode-JXHDVGUL.js +0 -1
  257. package/dist/m/core/recovery-TBBSICE2.js +0 -1
  258. package/dist/m/core/save-WUCHTQBE.js +0 -1
  259. package/dist/m/core/save-core-FJVHT5M3.js +0 -1
  260. package/dist/m/core/snapshot-F3SWDZMM.js +0 -1
  261. package/dist/m/core/unsaved-warning-STKBHE6G.js +0 -1
  262. package/dist/m/events/index-23DPTUT4.js +0 -1
  263. package/dist/m/internals/index-WOVAP3RU.js +0 -1
  264. package/dist/m/lib/cache-bust-CGWT6ZJI.js +0 -1
  265. package/dist/m/loader-HXX6YNH5.js +0 -1
  266. package/dist/m/manifest-F22DEDAC.js +0 -1
  267. package/dist/m/manifest-edit-C3C7376B.js +0 -1
  268. package/dist/m/options/options-2LH76JQ3.js +0 -1
  269. package/dist/m/plugins/demo-ZIBFAA3R.js +0 -1
  270. package/dist/m/plugins/indicator-PV4YW2ID.js +0 -1
  271. package/dist/m/plugins/live-NUTTXSBS.js +0 -1
  272. package/dist/m/plugins/page-KSUH7G6J.js +0 -2
  273. package/dist/m/plugins/page-view-W73OW7WI.js +0 -1
  274. package/dist/m/plugins/people-AJJ32YCG.js +0 -1
  275. package/dist/m/plugins/sortable-YRMQX3ZU.js +0 -1
  276. package/dist/m/plugins/source-KHK5UUSK.js +0 -1
  277. package/dist/m/plugins/undo-OF2L3CXU.js +0 -1
  278. package/dist/m/plugins/upload-XSGNILUY.js +0 -1
  279. package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +0 -21
  280. package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +0 -1
  281. package/dist/m/plugins/wire-ZWVWKF76.js +0 -1
  282. package/dist/m/sync/live-sync-4AUVXVJU.js +0 -1
  283. /package/dist/m/plugins/{richclay-RC3L2F6X.js.LEGAL.txt → richclay-NRQDSWX6.js.LEGAL.txt} +0 -0
@@ -14,28 +14,41 @@
14
14
  // encodeForMerge (replay.js) gives each one a code of its own.
15
15
  // A mark this schema has no type for keeps its token or named descriptor
16
16
  // instead of throwing (format-codec.js): the unit's key is written back exactly
17
- // as it was read.
17
+ // as it was read. A mark of this schema with no compact code of its own is
18
+ // written as a named descriptor too, x[name, attrs], so a new mark never takes a
19
+ // letter an older writer already reads.
18
20
  import { model } from "../vendor/prosemirror.vendor.js";
19
21
  import { descriptors, entriesOf, isOpaque, keyOf, opaqueMark } from "./format-codec.js";
22
+ import { nativeAttrs } from "./format-native.js";
20
23
 
21
24
  export const ATOM = "";
22
25
  export const CODES = { strong: "b", em: "i", strike: "s", code: "c", link: "a" };
26
+ const NATIVE = new Set(["underline", "text_color", "highlight"]);
23
27
  const NAMES = Object.fromEntries(Object.entries(CODES).map(([name, code]) => [code, name]));
24
28
 
25
29
  export function markKey(marks) {
26
30
  let key = "";
31
+ const named = [];
27
32
  for (const mark of marks) {
28
33
  if (isOpaque(mark)) {
29
34
  key += keyOf(mark.attrs.entries);
30
35
  continue;
31
36
  }
32
37
  const code = CODES[mark.type.name];
33
- if (!code) throw new TypeError(`page: the mark ${mark.type.name} has no key code`);
38
+ if (!code) {
39
+ // A mark with no compact letter travels as a named descriptor, after the codes
40
+ // and in the codec's order: one of the four formats only with attrs it writes,
41
+ // any other (a mention, a date) with its own attrs.
42
+ const native = nativeAttrs(mark.type.name, mark.attrs);
43
+ if (!native && NATIVE.has(mark.type.name)) throw new TypeError(`page: the mark ${mark.type.name} has no key code`);
44
+ named.push({ name: mark.type.name, attrs: native ?? mark.attrs });
45
+ continue;
46
+ }
34
47
  key += code;
35
48
  const names = Object.keys(mark.type.attrs);
36
49
  if (names.length) key += JSON.stringify(names.map((name) => mark.attrs[name]));
37
50
  }
38
- return key;
51
+ return named.length ? key + keyOf(named) : key;
39
52
  }
40
53
 
41
54
  const parsed = new WeakMap();
@@ -49,15 +62,26 @@ export function marksOf(schema, key) {
49
62
  marks = [];
50
63
  const opaque = [];
51
64
  for (const desc of descriptors(key)) {
52
- const name = desc.code === undefined ? null : NAMES[desc.code];
65
+ // A named descriptor this schema does have a type for is that mark, not an opaque
66
+ // entry: one of the four formats only with attrs it writes, any other mark only
67
+ // with attributes its type declares. Anything else stays opaque, so nothing is lost.
68
+ if (desc.code === undefined) {
69
+ const native = nativeAttrs(desc.name, desc.attrs);
70
+ const type = schema.marks[desc.name];
71
+ if (native) marks = type.create(native).addToSet(marks);
72
+ else if (type && !NATIVE.has(desc.name) && Object.keys(desc.attrs ?? {}).every((field) => Object.hasOwn(type.attrs, field))) marks = type.create(desc.attrs).addToSet(marks);
73
+ else opaque.push(desc);
74
+ continue;
75
+ }
76
+ const name = NAMES[desc.code];
53
77
  if (!name) {
54
78
  opaque.push(desc);
55
79
  continue;
56
80
  }
57
- const type = schema.marks[name];
58
- if (!type) throw new TypeError(`page: unknown mark code ${desc.code} in ${key}`);
59
- const attrs = desc.attrs ? Object.fromEntries(Object.keys(type.attrs).map((field, j) => [field, desc.attrs[j]])) : null;
60
- marks = type.create(attrs).addToSet(marks);
81
+ const coded = schema.marks[name];
82
+ if (!coded) throw new TypeError(`page: unknown mark code ${desc.code} in ${key}`);
83
+ const attrs = desc.attrs ? Object.fromEntries(Object.keys(coded.attrs).map((field, j) => [field, desc.attrs[j]])) : null;
84
+ marks = coded.create(attrs).addToSet(marks);
61
85
  }
62
86
  if (opaque.length) marks = opaqueMark(entriesOf(opaque)).addToSet(marks);
63
87
  cache.set(key, marks);
package/src/page/items.js CHANGED
@@ -12,7 +12,8 @@ import { pickFiles, hostPlace } from "./media.js";
12
12
  import { makeTable, insertTableNode } from "./table.js";
13
13
  import { pickCsv } from "./csv-import.js";
14
14
  import { makeBoard, insertBoardNode } from "./board.js";
15
- import { chartFromSlash, chartTables } from "./chart.js";
15
+ import { chartTables } from "./chart-tables.js";
16
+ import { viewFeatures } from "./view-hooks.js";
16
17
 
17
18
  const { TextSelection } = state;
18
19
  const N = schema.nodes;
@@ -27,6 +28,7 @@ export const BLOCK_TYPES = [
27
28
  { key: "check", label: "Checklist", description: "Tasks to tick off", icon: "☐", type: "list_item", attrs: { kind: "check" }, hint: "[]", aliases: ["todo", "to-do", "task", "check", "checkbox"] },
28
29
  { key: "toggle", label: "Toggle", description: "Hide details under a line", icon: "▸", type: "toggle", attrs: { open: true }, hint: ">", aliases: ["collapse", "details", "fold"] },
29
30
  { key: "callout", label: "Callout", description: "Make a note stand out", icon: "!", type: "callout", aliases: ["note", "aside", "info"] },
31
+ { key: "quote", label: "Quote", description: "Set text apart as a quotation", icon: "\u201c", type: "quote", hint: "\"", aliases: ["blockquote", "quotation", "cite"] },
30
32
  { key: "code", label: "Code", description: "Code with highlighting", icon: "</>", type: "code_block", hint: "```", aliases: ["pre", "snippet"] },
31
33
  ];
32
34
 
@@ -41,6 +43,10 @@ export const INSERT_ITEMS = [
41
43
 
42
44
  export const CHART_ITEM = { key: "chart", label: "Chart", description: "A bar or line chart from a table", icon: "📊", aliases: ["graph", "plot", "bar", "line"], insert: "chart" };
43
45
 
46
+ // The chart module and its view part load together, before the block draws: a
47
+ // document that never takes a chart never fetches either.
48
+ export const loadChartPart = (view) => (viewFeatures.get(view)?.ensure("chart") ?? Promise.resolve()).catch(() => {}).then(() => import("./chart.js"));
49
+
44
50
  export const MENU_ITEMS = [...BLOCK_TYPES, ...INSERT_ITEMS];
45
51
 
46
52
  export const insertItemsFor = (st, items = MENU_ITEMS) => (chartTables(st.doc).length ? [...items, CHART_ITEM] : items);
@@ -176,14 +182,20 @@ export function rankItems(items, query) {
176
182
 
177
183
  // What a pick does in the insert menu for the block at `index`. A divider and
178
184
  // the file pickers have their own placement; a block type retypes an empty line
179
- // and otherwise goes into a new block below the line's whole subtree.
185
+ // and otherwise goes into a new block below the line's whole subtree. A block
186
+ // type from a later format asks the format gate before it writes anything.
180
187
  export function runItem(view, item, index, remove = null) {
188
+ if (item.type !== "quote") return insertItem(view, item, index, remove);
189
+ return import("./format-native-commands.js").then(({ runFormat }) => runFormat(view, () => insertItem(view, item, index, remove)));
190
+ }
191
+
192
+ function insertItem(view, item, index, remove = null) {
181
193
  const st = view.state;
182
194
  if (item.insert === "image" || item.insert === "file") return pickFiles(view, item.insert === "image" ? "image/*" : "", hostPlace(st, index), remove);
183
195
  if (item.insert === "csv") return pickCsv(view, remove);
184
196
  if (item.insert === "table") return insertTableNode(st, view.dispatch, makeTable(), remove, null, index);
185
197
  if (item.insert === "board") return insertBoardNode(st, view.dispatch, makeBoard(), remove, index);
186
- if (item.insert === "chart") return chartFromSlash(view, index, remove);
198
+ if (item.insert === "chart") return loadChartPart(view).then(({ chartFromSlash }) => chartFromSlash(view, index, remove));
187
199
  let tr = st.tr;
188
200
  if (remove) tr.delete(remove.from, remove.to);
189
201
  const node = tr.doc.child(index);
@@ -124,7 +124,31 @@ export const MARKDOWN_RULES = [
124
124
  ...INLINE_RULES,
125
125
  ];
126
126
 
127
- export const markdownPlugin = () => inputrules.inputRules({ rules: MARKDOWN_RULES });
127
+ // The shortest shortcut that quotes a line: `" ` typed at the start of a
128
+ // paragraph. It writes a floor-1 block, so it asks the format gate first and
129
+ // converts only when the gate answered applied; the transaction it dispatches is
130
+ // the input-rule one, recorded on this plugin so Backspace puts the literal `" `
131
+ // back and undo keeps the conversion whole. The typed space is never swallowed:
132
+ // when the format cannot be written, the space is inserted as itself.
133
+ function quoteShortcut(plugin, view, from, to, text) {
134
+ if (text !== " " || from !== to) return false;
135
+ const st = view.state;
136
+ const $from = st.doc.resolve(from);
137
+ const node = $from.parent;
138
+ if ($from.depth !== 1 || node.type !== N.paragraph || $from.parentOffset !== 1 || node.textContent !== '"') return false;
139
+ if (!view.editable || typeof view.runFormatCommand !== "function") return false;
140
+ const plain = () => { if (!view.isDestroyed) view.dispatch(st.tr.insertText(" ", from, to)); };
141
+ const at = $from.before(1);
142
+ import("./format-native-commands.js").then(({ quoteFromSpace }) => quoteFromSpace(plugin, view, { at, from, to }, plain), plain);
143
+ return true;
144
+ }
145
+
146
+ export const markdownPlugin = () => {
147
+ const plugin = inputrules.inputRules({ rules: MARKDOWN_RULES });
148
+ const rules = plugin.props.handleTextInput;
149
+ plugin.props.handleTextInput = (view, from, to, text) => quoteShortcut(plugin, view, from, to, text) || rules(view, from, to, text);
150
+ return plugin;
151
+ };
128
152
 
129
153
  // "| Name | Owner |" then Enter at the end of a paragraph: the paragraph becomes
130
154
  // a table with those column names and one empty row, the caret in its first cell.
@@ -150,7 +174,7 @@ export function backspaceToParagraph(st, dispatch) {
150
174
  const { $from, empty } = st.selection;
151
175
  if (!empty || $from.depth !== 1 || $from.parentOffset !== 0) return false;
152
176
  const n = $from.parent;
153
- if (!(n.type === N.heading || n.type === N.callout || (n.type === N.code_block && n.content.size === 0))) return false;
177
+ if (!(n.type === N.heading || n.type === N.callout || n.type === N.quote || (n.type === N.code_block && n.content.size === 0))) return false;
154
178
  if (dispatch) dispatch(asStep(st.tr.setNodeMarkup($from.before(), N.paragraph, { id: n.attrs.id, pos: n.attrs.pos, parent: n.attrs.parent })));
155
179
  return true;
156
180
  }
@@ -0,0 +1,99 @@
1
+ // The people picker the @ command draws: one listbox at the caret, outside the saved
2
+ // article and never in it. It is built from the editor's own menu markup, so it needs no
3
+ // stylesheet of its own, and it takes no focus: the caret and the typed query stay in the
4
+ // document, and the active row is announced through the editor's aria-activedescendant as
5
+ // the slash menu does. A name is only ever text.
6
+ export const PICKER = "page-mention-picker";
7
+ export const OPTION = "page-menu-item";
8
+ const EMPTY = "No people to mention";
9
+
10
+ let count = 0;
11
+
12
+ // The people a picker may offer: the ones the document already names, with the host's live
13
+ // list laid over them by id, so a renamed person is shown under the name the host answers
14
+ // with. Nothing is invented when the host has no list.
15
+ export function peopleFrom(saved = [], allowed = null) {
16
+ const byId = new Map();
17
+ for (const person of saved ?? []) if (person && typeof person.id === "string") byId.set(person.id, person);
18
+ for (const person of Array.isArray(allowed) ? allowed : []) if (person && typeof person.id === "string") byId.set(person.id, person);
19
+ return [...byId.values()];
20
+ }
21
+
22
+ export const filterPeople = (people, query) => {
23
+ const q = String(query ?? "").trim().toLowerCase();
24
+ if (!q) return people;
25
+ return people.filter((person) => String(person.name ?? "").toLowerCase().includes(q));
26
+ };
27
+
28
+ // The shell takes the words a command shows, so a date command reuses the same listbox:
29
+ // same placement, same rows, same keyboard, and its own name for what it lists.
30
+ export function createMentionPicker({ document: doc = globalThis.document, onPick = () => {}, label = "Mention a person", empty = EMPTY } = {}) {
31
+ const id = `page-mentions-${++count}`;
32
+ const el = doc.createElement("div");
33
+ el.className = `page-menu ${PICKER}`;
34
+ el.id = id;
35
+ el.setAttribute("role", "listbox");
36
+ el.setAttribute("aria-label", label);
37
+ el.hidden = true;
38
+ let people = [];
39
+ let active = 0;
40
+
41
+ const draw = () => {
42
+ if (!people.length) {
43
+ const none = doc.createElement("div");
44
+ none.className = "page-menu-title";
45
+ none.textContent = empty;
46
+ el.replaceChildren(none);
47
+ return;
48
+ }
49
+ el.replaceChildren(...people.map((person, i) => {
50
+ const row = doc.createElement("button");
51
+ row.type = "button";
52
+ row.className = `${OPTION}${i === active ? " is-active" : ""}`;
53
+ row.id = `${id}-${i}`;
54
+ row.dataset.index = String(i);
55
+ row.setAttribute("role", "option");
56
+ row.setAttribute("aria-selected", String(i === active));
57
+ row.textContent = String(person.name ?? "");
58
+ row.addEventListener("mousedown", (event) => event.preventDefault());
59
+ row.addEventListener("click", () => onPick(i));
60
+ return row;
61
+ }));
62
+ };
63
+
64
+ // The caret the list hangs from, in the page's own placement variables: the same ones
65
+ // the block menu writes, so the existing menu stylesheet paints and lifts it.
66
+ const place = (at) => {
67
+ el.style.setProperty("--page-pop-x", `${at.x}px`);
68
+ el.style.setProperty("--page-pop-y", `${at.y}px`);
69
+ };
70
+
71
+ return {
72
+ el,
73
+ show: (next, at = null) => {
74
+ people = next;
75
+ active = 0;
76
+ draw();
77
+ if (at) place(at);
78
+ el.hidden = false;
79
+ },
80
+ move: (step) => {
81
+ if (!people.length) return;
82
+ active = (active + step + people.length) % people.length;
83
+ draw();
84
+ },
85
+ activeId: () => (people.length ? `${id}-${active}` : null),
86
+ index: () => active,
87
+ personAt: (i) => people[i] ?? null,
88
+ hide: () => {
89
+ el.hidden = true;
90
+ people = [];
91
+ active = 0;
92
+ el.replaceChildren();
93
+ },
94
+ destroy() {
95
+ this.hide();
96
+ el.remove();
97
+ },
98
+ };
99
+ }
package/src/page/merge.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { canon, renderModel, parseModel, fragmentCleaner } from "./projection.js";
2
2
 
3
- const TEXT = new Set(["paragraph", "heading", "list_item", "callout", "code_block", "toggle"]);
3
+ const TEXT = new Set(["paragraph", "heading", "list_item", "callout", "quote", "code_block", "toggle"]);
4
4
  const same = (a, b) => canon(a) === canon(b);
5
5
 
6
6
  function inlineOf(item, doc) {
@@ -11,6 +11,7 @@
11
11
  import { chartModel, renderChartSvg } from "./rich/chart.js";
12
12
  import { cellText } from "./rich/cell.js";
13
13
  import { bounded, descriptors, entriesOf, keyOf, normalizeEntries } from "./format-codec.js";
14
+ import { nativeAttrs } from "./format-native.js";
14
15
 
15
16
  export const ATOM = "";
16
17
  export const BREAK = '@{"type":"hard_break"}';
@@ -26,6 +27,13 @@ const KNOWN = new Set(CODES);
26
27
  export const OPAQUE = "data-clay-page-opaque";
27
28
  export const VALUE = "data-clay-page-value";
28
29
  export const MARKS = "data-clay-page-marks";
30
+ // A person mention's span states its id plainly beside the envelope that carries it:
31
+ // the label is the text, and a reader without the editor shows a name, never a link.
32
+ export const MENTION = "data-page-mention";
33
+ // A date mention reads as a `time` element: the text is the ISO date and `datetime`
34
+ // repeats it, so a reader without the editor shows the day and no zone or reminder.
35
+ export const DATE = "data-page-date";
36
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
29
37
  const ENVELOPE = "1";
30
38
  const STRUCTURAL = new Set(["id", "t", "parent", "c", "m"]);
31
39
  const knownType = (t) => Object.hasOwn(DEFAULTS, t);
@@ -36,6 +44,7 @@ export const DEFAULTS = {
36
44
  heading: { parent: null, level: 2 },
37
45
  list_item: { parent: null, kind: "bullet", indent: 0, checked: false, owner: null, due: null },
38
46
  callout: { parent: null },
47
+ quote: { parent: null },
39
48
  code_block: { parent: null, lang: "" },
40
49
  toggle: { parent: null, open: false },
41
50
  divider: { parent: null },
@@ -47,7 +56,7 @@ export const DEFAULTS = {
47
56
  tool: { parent: null, artifact: null, rev: null, state: {} },
48
57
  preserved: { parent: null, html: "" },
49
58
  };
50
- const TEXT = new Set(["paragraph", "heading", "list_item", "callout", "code_block", "toggle"]);
59
+ const TEXT = new Set(["paragraph", "heading", "list_item", "callout", "quote", "code_block", "toggle"]);
51
60
 
52
61
  const same = (a, b) => canon(a) === canon(b);
53
62
 
@@ -146,18 +155,25 @@ export function runsOf(units) {
146
155
  return out;
147
156
  }
148
157
 
149
- // A mark key as { marks: { b: true, a: ["href"] ... }, entries: [...] }, the
150
- // entries being the descriptors of every mark this runtime has no code for, in
151
- // the codec's canonical order.
158
+ // A mark key as { marks: { b: true, a: ["href"] ... }, native: [...], entries: [...] }:
159
+ // the codes this runtime knows, the named descriptors of the four formats it
160
+ // writes (format-native.js), and the entries of every mark it has no code for,
161
+ // each in the codec's canonical order.
152
162
  function marksOfKey(key) {
153
- const marks = {};
154
- if (!key || key[0] === "@") return { marks, entries: [] };
163
+ const marks = {}, native = [];
164
+ if (!key || key[0] === "@") return { marks, native: [], entries: [] };
155
165
  const opaque = [];
156
166
  for (const desc of descriptors(key)) {
157
- if (desc.name === undefined && KNOWN.has(desc.code)) marks[desc.code] = desc.attrs ?? true;
167
+ if (desc.name === undefined) {
168
+ if (KNOWN.has(desc.code)) marks[desc.code] = desc.attrs ?? true;
169
+ else opaque.push(desc);
170
+ continue;
171
+ }
172
+ const attrs = nativeAttrs(desc.name, desc.attrs);
173
+ if (attrs) native.push({ name: desc.name, attrs });
158
174
  else opaque.push(desc);
159
175
  }
160
- return { marks, entries: opaque.length ? entriesOf(opaque) : [] };
176
+ return { marks, native, entries: opaque.length ? entriesOf(opaque) : [] };
161
177
  }
162
178
 
163
179
  // One unit's key, as the record keeps it: the codes this runtime knows, then
@@ -165,8 +181,8 @@ function marksOfKey(key) {
165
181
  const unitKey = (key, text, i) => {
166
182
  if (!key) return "";
167
183
  if (key[0] === "@") return key === BREAK && text[i] === ATOM ? key : "";
168
- const { marks, entries } = marksOfKey(key);
169
- return keyOfMarks(safeMarks(marks), entries);
184
+ const { marks, native, entries } = marksOfKey(key);
185
+ return keyOfMarks(safeMarks(marks), native.length ? [...entries, ...entriesOf(native)] : entries);
170
186
  };
171
187
 
172
188
  // A link to a script URL is never kept: it would run on the page's origin when a
@@ -410,6 +426,10 @@ const escText = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>
410
426
  const escAttr = (s) => escText(String(s)).replace(/"/g, "&quot;");
411
427
  const attr = (name, value) => (value === null || value === undefined ? "" : ` ${name}="${escAttr(value)}"`);
412
428
 
429
+ // The element each of the four formats is read as in the saved article; the
430
+ // text color and the highlight carry their palette key as data-color.
431
+ const NATIVE_TAGS = { underline: "u", text_color: "span", highlight: "mark" };
432
+
413
433
  export function renderInline(c, m, marksEnvelopes = true) {
414
434
  const units = unitsOf(m, c.length) || [];
415
435
  let html = "";
@@ -425,13 +445,22 @@ export function renderInline(c, m, marksEnvelopes = true) {
425
445
  }
426
446
  let j = i + 1;
427
447
  while (j < c.length && (units[j] || "") === key && !(c[j] === ATOM && key[0] === "@")) j++;
428
- const { marks, entries } = marksOfKey(key);
448
+ const { marks, native, entries } = marksOfKey(key);
429
449
  const want = NEST.filter((code) => marks[code]).map((code) => ({ code, sig: code + JSON.stringify(marks[code]), tag: code === "a" ? "a" : TAGS[code], href: code === "a" ? marks.a[0] : null }));
430
- if (entries.length && marksEnvelopes) want.push({ sig: `marks${canon(entries)}`, tag: "span", entries });
450
+ for (const n of native) want.push({ sig: `native${n.name}${canon(n.attrs)}`, tag: NATIVE_TAGS[n.name], color: n.attrs.color ?? null });
451
+ const mention = entries.find((entry) => entry.name === "mention");
452
+ const date = entries.find((entry) => entry.name === "date");
453
+ const day = date && DAY.test(String(date.attrs.date)) ? String(date.attrs.date) : null;
454
+ if (entries.length && marksEnvelopes) want.push({ sig: `marks${canon(entries)}`, tag: day ? "time" : "span", entries, mention: mention?.attrs.id, day });
431
455
  let keep = 0;
432
456
  while (keep < open.length && keep < want.length && open[keep].sig === want[keep].sig) keep++;
433
457
  closeTo(keep);
434
- for (const w of want.slice(keep)) { html += w.entries ? `<span${marksEnvelope(w.entries)}>` : w.tag === "a" ? `<a${attr("href", w.href)}>` : `<${w.tag}>`; open.push(w); }
458
+ for (const w of want.slice(keep)) {
459
+ if (w.entries) html += w.day ? `<time${attr("datetime", w.day)}${attr(DATE, "")}${marksEnvelope(w.entries)}>` : `<span${marksEnvelope(w.entries)}${attr(MENTION, w.mention)}>`;
460
+ else if (w.tag === "a") html += `<a${attr("href", w.href)}>`;
461
+ else html += w.color ? `<${w.tag}${attr("data-color", w.color)}>` : `<${w.tag}>`;
462
+ open.push(w);
463
+ }
435
464
  html += escText(c.slice(i, j));
436
465
  i = j;
437
466
  }
@@ -454,6 +483,7 @@ function renderLeaf(item, clean, tables) {
454
483
  case "paragraph": return `<p${at}>${inlineOf(item)}</p>`;
455
484
  case "heading": { const h = `h${item.level ?? 2}`; return `<${h}${at}>${inlineOf(item)}</${h}>`; }
456
485
  case "callout": return `<aside${at} class="page-callout">${inlineOf(item)}</aside>`;
486
+ case "quote": return `<blockquote${at}>${inlineOf(item)}</blockquote>`;
457
487
  case "code_block": return `<pre${at}${attr("data-lang", item.lang || null)}><code>${escText(item.c ?? "")}</code></pre>`;
458
488
  case "divider": return `<hr${at}>`;
459
489
  case "image": return `<figure${at} data-type="image"${attr("data-align", item.align ?? null)}><img${attr("src", item.src ?? "")}${attr("alt", item.alt ?? "")}${attr("width", item.width ?? null)}>${item.caption ? `<figcaption>${escText(item.caption)}</figcaption>` : ""}</figure>`;
@@ -561,6 +591,17 @@ export function renderBlocks(blocks, opts) {
561
591
  const DROP_TAGS = new Set(["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT"]);
562
592
  const INLINE_TAGS = new Set(["A", "ABBR", "B", "BDI", "BDO", "BR", "CITE", "CODE", "DATA", "DEL", "DFN", "EM", "FONT", "I", "INS", "KBD", "LABEL", "MARK", "Q", "S", "SAMP", "SMALL", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "TIME", "U", "VAR", "WBR"]);
563
593
 
594
+ // The named descriptor an inline element stands for, when it is one of the four
595
+ // formats: <u>, <mark> and a <span data-color> with a palette key. Anything else
596
+ // is not a format this runtime reads natively.
597
+ function nativeOfEl(tag, el) {
598
+ if (tag === "u") return { name: "underline", attrs: {} };
599
+ if (tag !== "mark" && tag !== "span") return null;
600
+ if (tag === "span" && !el.hasAttribute("data-color")) return null;
601
+ const name = tag === "mark" ? "highlight" : "text_color";
602
+ return { name, attrs: { color: el.getAttribute("data-color") } };
603
+ }
604
+
564
605
  // Inline content of el as { c, units }, minus the children skip(child) names.
565
606
  function inlineOfEl(el, skip = () => false) {
566
607
  let c = "";
@@ -579,7 +620,7 @@ function inlineOfEl(el, skip = () => false) {
579
620
  if (tag === "br") { c += ATOM; units.push(BREAK); continue; }
580
621
  if (tag === "span" && child.hasAttribute("data-atom")) { c += ATOM; units.push(child.getAttribute("data-atom")); continue; }
581
622
  const next = { ...marks };
582
- if (tag === "span" && child.hasAttribute(MARKS)) {
623
+ if ((tag === "span" || tag === "time") && child.hasAttribute(MARKS)) {
583
624
  walk(child, next, normalizeEntries([...entries, ...entriesOfEnvelope(child.getAttribute(MARKS))]));
584
625
  continue;
585
626
  }
@@ -588,7 +629,8 @@ function inlineOfEl(el, skip = () => false) {
588
629
  else if (tag === "s" || tag === "del" || tag === "strike") next.s = true;
589
630
  else if (tag === "code") next.c = true;
590
631
  else if (tag === "a" && child.hasAttribute("href")) next.a = [child.getAttribute("href")];
591
- walk(child, next, entries);
632
+ const named = nativeOfEl(tag, child);
633
+ walk(child, next, named ? [...entries, named] : entries);
592
634
  }
593
635
  };
594
636
  walk(el, {}, []);
@@ -672,6 +714,7 @@ export function parseArticle(article) {
672
714
  if (tag === "P" && !onlyImage) return push(el, "paragraph", parent, { ...omitDefaults("paragraph", P), ...tf(el) });
673
715
  if (/^H[1-6]$/.test(tag)) return push(el, "heading", parent, { ...omitDefaults("heading", { ...P, level: +tag[1] }), ...tf(el) });
674
716
  if (tag === "ASIDE") return push(el, "callout", parent, { ...omitDefaults("callout", P), ...tf(el) });
717
+ if (tag === "BLOCKQUOTE") return push(el, "quote", parent, { ...omitDefaults("quote", P), ...tf(el) });
675
718
  if (tag === "PRE") {
676
719
  const code = el.querySelector(":scope > code");
677
720
  return push(el, "code_block", parent, { ...omitDefaults("code_block", { ...P, lang: el.getAttribute("data-lang") || "" }), c: (code || el).textContent });
@@ -53,4 +53,43 @@ export function projectInto(clone) {
53
53
  if (!customTitle(clone)) writeTitle(clone, title);
54
54
  }
55
55
 
56
+ /**
57
+ * The page state a final save clone carries, or null.
58
+ *
59
+ * One page tag is one page state: a Yjs page never has two, a tag holding plain block
60
+ * data is a page's own content rather than one, and a tag that does not parse is not one
61
+ * either. The value is copied, because the store keeps writing to its own after the
62
+ * capture and a checkpoint must prove the bytes this capture sent.
63
+ *
64
+ * Registered with core/snapshot only by the recovery integration, so a page that
65
+ * journals nothing never reads it.
66
+ *
67
+ * @param {HTMLElement} root - the save clone, after every transform
68
+ * @returns {?Object}
69
+ */
70
+ export function pageStateOfClone(root) {
71
+ const tags = pageTags(root);
72
+ if (tags.length !== 1) return null;
73
+ let value;
74
+ try { value = parseJsonRelaxed(tags[0].textContent); }
75
+ catch { return null; }
76
+ const { v, g, y } = value ?? {};
77
+ if (v !== 1 || !y || typeof y !== "object" || Array.isArray(y)) return null;
78
+ return { v, g, y: { ...y } };
79
+ }
80
+
81
+ /**
82
+ * The canonical article one page state projects to.
83
+ *
84
+ * The same projection the save writes into the capture's article, run over the state a
85
+ * capture pinned or a host handed back. A state whose projection is not the article
86
+ * that was accepted is not a proof of those bytes, whichever transform changed it.
87
+ *
88
+ * @param {Object|string} state - a page value, or the page tag text holding one
89
+ * @returns {string}
90
+ */
91
+ export function articleOfPageState(state) {
92
+ return renderFromTags([typeof state === "string" ? state : JSON.stringify(state)]).html;
93
+ }
94
+
56
95
  addDocumentTransform(projectInto);
@@ -1,5 +1,5 @@
1
1
  import { captureForSaveAndComparison } from "../core/snapshot.js";
2
- import { downloadHtml, documentName } from "../core/conflict-download.js";
2
+ import { downloadHtml, documentName } from "../core/download.js";
3
3
 
4
4
  // One plain state per save refusal code, so the indicator, the toast and the
5
5
  // page plugin all read the same answer. `saved` is the only happy one; every
@@ -50,6 +50,12 @@ export const schema = new model.Schema({
50
50
  "data-checked": n.attrs.checked ? "true" : "false",
51
51
  }), 0],
52
52
  },
53
+ quote: {
54
+ group: "block",
55
+ content: "inline*",
56
+ attrs: block,
57
+ toDOM: (n) => ["blockquote", ref(n), 0],
58
+ },
53
59
  callout: {
54
60
  group: "block",
55
61
  content: "inline*",
@@ -174,5 +180,32 @@ export const schema = new model.Schema({
174
180
  inclusive: false,
175
181
  toDOM: (m) => ["a", { href: safeHref(m.attrs.href) }, 0],
176
182
  },
183
+ // A person mention is ordinary text with this mark over it: the name is the label
184
+ // and `id` is the people registry's reference. It links nowhere and notifies nobody.
185
+ mention: {
186
+ attrs: { id: {} },
187
+ inclusive: false,
188
+ excludes: "mention date link",
189
+ toDOM: (m) => ["span", { "data-page-mention": m.attrs.id }, 0],
190
+ },
191
+ // A date mention is the calendar date as ordinary ISO text with this mark over it:
192
+ // `date` is YYYY-MM-DD, and no time, zone, reminder or relative word is stored.
193
+ date: {
194
+ attrs: { date: {} },
195
+ inclusive: false,
196
+ excludes: "mention date link",
197
+ toDOM: (m) => ["time", { datetime: m.attrs.date, "data-page-date": "" }, 0],
198
+ },
199
+ // The floor-1 formats (format-native.js): written as named descriptors,
200
+ // drawn as the elements the saved article keeps for them.
201
+ underline: { toDOM: () => ["u", 0] },
202
+ text_color: {
203
+ attrs: { color: {} },
204
+ toDOM: (m) => ["span", { "data-color": m.attrs.color }, 0],
205
+ },
206
+ highlight: {
207
+ attrs: { color: {} },
208
+ toDOM: (m) => ["mark", { "data-color": m.attrs.color }, 0],
209
+ },
177
210
  },
178
211
  });
@@ -2,6 +2,11 @@
2
2
  article[data-page] {
3
3
  --page-link: #164583;
4
4
  --page-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
5
+ --page-color-gray: #5B5953; --page-color-brown: #8A5A1E; --page-color-orange: #B4531A; --page-color-yellow: #7A6210; --page-color-green: #3E6A28;
6
+ --page-color-blue: #1F5A8A; --page-color-purple: #5E4596; --page-color-pink: #A23E6E; --page-color-red: #9E3B2F;
7
+ --page-mark-bg-gray: rgba(35, 34, 31, 0.12); --page-mark-bg-brown: rgba(138, 90, 30, 0.20); --page-mark-bg-orange: rgba(180, 83, 26, 0.20);
8
+ --page-mark-bg-yellow: rgba(122, 98, 16, 0.22); --page-mark-bg-green: rgba(62, 106, 40, 0.20); --page-mark-bg-blue: rgba(31, 90, 138, 0.18);
9
+ --page-mark-bg-purple: rgba(94, 69, 150, 0.18); --page-mark-bg-pink: rgba(162, 62, 110, 0.18); --page-mark-bg-red: rgba(158, 59, 47, 0.18);
5
10
  min-height: 100vh;
6
11
  box-sizing: border-box;
7
12
  padding: var(--page-pad-top) 16px 30vh;
@@ -42,5 +47,24 @@
42
47
  article[data-page] th, article[data-page] td { min-width: 120px; padding: 6px 10px; border-inline-end: 1px solid var(--page-rule); border-block-end: 1px solid var(--page-rule); text-align: start; white-space: pre-wrap; }
43
48
  article[data-page] th { background: var(--page-code-bg); color: var(--page-ink-soft); font-weight: 600; }
44
49
  article[data-page] th[data-kind="number"], article[data-page] td[data-kind="number"] { text-align: end; }
45
- article[data-page] blockquote { color: var(--page-ink-soft); border-inline-start: 1px solid var(--page-rule); padding-inline-start: var(--page-indent); }
50
+ article[data-page] blockquote { color: var(--page-ink-soft); padding-inline-start: var(--page-indent); }
51
+ article[data-page] span[data-color="gray"] { color: var(--page-color-gray); }
52
+ article[data-page] span[data-color="brown"] { color: var(--page-color-brown); }
53
+ article[data-page] span[data-color="orange"] { color: var(--page-color-orange); }
54
+ article[data-page] span[data-color="yellow"] { color: var(--page-color-yellow); }
55
+ article[data-page] span[data-color="green"] { color: var(--page-color-green); }
56
+ article[data-page] span[data-color="blue"] { color: var(--page-color-blue); }
57
+ article[data-page] span[data-color="purple"] { color: var(--page-color-purple); }
58
+ article[data-page] span[data-color="pink"] { color: var(--page-color-pink); }
59
+ article[data-page] span[data-color="red"] { color: var(--page-color-red); }
60
+ article[data-page] mark[data-color="gray"] { background: var(--page-mark-bg-gray); }
61
+ article[data-page] mark[data-color="brown"] { background: var(--page-mark-bg-brown); }
62
+ article[data-page] mark[data-color="orange"] { background: var(--page-mark-bg-orange); }
63
+ article[data-page] mark[data-color="yellow"] { background: var(--page-mark-bg-yellow); }
64
+ article[data-page] mark[data-color="green"] { background: var(--page-mark-bg-green); }
65
+ article[data-page] mark[data-color="blue"] { background: var(--page-mark-bg-blue); }
66
+ article[data-page] mark[data-color="purple"] { background: var(--page-mark-bg-purple); }
67
+ article[data-page] mark[data-color="pink"] { background: var(--page-mark-bg-pink); }
68
+ article[data-page] mark[data-color="red"] { background: var(--page-mark-bg-red); }
69
+ article[data-page] mark { color: inherit; }
46
70
  }
@@ -95,6 +95,11 @@
95
95
  article[data-page] {
96
96
  --page-link: #164583;
97
97
  --page-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
98
+ --page-color-gray: #5B5953; --page-color-brown: #8A5A1E; --page-color-orange: #B4531A; --page-color-yellow: #7A6210; --page-color-green: #3E6A28;
99
+ --page-color-blue: #1F5A8A; --page-color-purple: #5E4596; --page-color-pink: #A23E6E; --page-color-red: #9E3B2F;
100
+ --page-mark-bg-gray: rgba(35, 34, 31, 0.12); --page-mark-bg-brown: rgba(138, 90, 30, 0.20); --page-mark-bg-orange: rgba(180, 83, 26, 0.20);
101
+ --page-mark-bg-yellow: rgba(122, 98, 16, 0.22); --page-mark-bg-green: rgba(62, 106, 40, 0.20); --page-mark-bg-blue: rgba(31, 90, 138, 0.18);
102
+ --page-mark-bg-purple: rgba(94, 69, 150, 0.18); --page-mark-bg-pink: rgba(162, 62, 110, 0.18); --page-mark-bg-red: rgba(158, 59, 47, 0.18);
98
103
  min-height: 100vh;
99
104
  box-sizing: border-box;
100
105
  padding: var(--page-pad-top) 16px 30vh;
@@ -135,7 +140,26 @@
135
140
  article[data-page] th, article[data-page] td { min-width: 120px; padding: 6px 10px; border-inline-end: 1px solid var(--page-rule); border-block-end: 1px solid var(--page-rule); text-align: start; white-space: pre-wrap; }
136
141
  article[data-page] th { background: var(--page-code-bg); color: var(--page-ink-soft); font-weight: 600; }
137
142
  article[data-page] th[data-kind="number"], article[data-page] td[data-kind="number"] { text-align: end; }
138
- article[data-page] blockquote { color: var(--page-ink-soft); border-inline-start: 1px solid var(--page-rule); padding-inline-start: var(--page-indent); }
143
+ article[data-page] blockquote { color: var(--page-ink-soft); padding-inline-start: var(--page-indent); }
144
+ article[data-page] span[data-color="gray"] { color: var(--page-color-gray); }
145
+ article[data-page] span[data-color="brown"] { color: var(--page-color-brown); }
146
+ article[data-page] span[data-color="orange"] { color: var(--page-color-orange); }
147
+ article[data-page] span[data-color="yellow"] { color: var(--page-color-yellow); }
148
+ article[data-page] span[data-color="green"] { color: var(--page-color-green); }
149
+ article[data-page] span[data-color="blue"] { color: var(--page-color-blue); }
150
+ article[data-page] span[data-color="purple"] { color: var(--page-color-purple); }
151
+ article[data-page] span[data-color="pink"] { color: var(--page-color-pink); }
152
+ article[data-page] span[data-color="red"] { color: var(--page-color-red); }
153
+ article[data-page] mark[data-color="gray"] { background: var(--page-mark-bg-gray); }
154
+ article[data-page] mark[data-color="brown"] { background: var(--page-mark-bg-brown); }
155
+ article[data-page] mark[data-color="orange"] { background: var(--page-mark-bg-orange); }
156
+ article[data-page] mark[data-color="yellow"] { background: var(--page-mark-bg-yellow); }
157
+ article[data-page] mark[data-color="green"] { background: var(--page-mark-bg-green); }
158
+ article[data-page] mark[data-color="blue"] { background: var(--page-mark-bg-blue); }
159
+ article[data-page] mark[data-color="purple"] { background: var(--page-mark-bg-purple); }
160
+ article[data-page] mark[data-color="pink"] { background: var(--page-mark-bg-pink); }
161
+ article[data-page] mark[data-color="red"] { background: var(--page-mark-bg-red); }
162
+ article[data-page] mark { color: inherit; }
139
163
  }
140
164
 
141
165