@jackuait/blok 0.6.0-beta.9 → 0.7.0-beta.1

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 (336) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-Bn6Q_o8h.mjs → blok-ob9Fwr1L.mjs} +3414 -2975
  3. package/dist/chunks/i18next-B47TKgbU.mjs +1303 -0
  4. package/dist/chunks/{i18next-loader-DjR4d8M7.mjs → i18next-loader-Bu3vFvye.mjs} +2 -2
  5. package/dist/chunks/{index-oe38cp86.mjs → index-CZmRzRIX.mjs} +12 -12
  6. package/dist/chunks/{inline-tool-convert-SRTkyaZn.mjs → inline-tool-convert-CvFW2iie.mjs} +1579 -961
  7. package/dist/chunks/{messages-BogRq8lt.mjs → messages-0AbcLMLm.mjs} +6 -0
  8. package/dist/chunks/{messages-DJDG55Vq.mjs → messages-0E0AkrNu.mjs} +6 -0
  9. package/dist/{messages-DnXLrlHh.mjs → chunks/messages-4v4MuVEc.mjs} +6 -0
  10. package/dist/chunks/{messages-DnIhyAJk.mjs → messages-62v-CLC-.mjs} +6 -0
  11. package/dist/chunks/{messages-Dzwxv9v1.mjs → messages-8DeO60Oo.mjs} +6 -0
  12. package/dist/chunks/{messages-B1Aww8q7.mjs → messages-8IPXkrDl.mjs} +6 -0
  13. package/dist/{messages-uKX8WBaD.mjs → chunks/messages-96kNZDll.mjs} +6 -0
  14. package/dist/chunks/{messages-BL0tXcDf.mjs → messages-B1FZ8lxU.mjs} +6 -0
  15. package/dist/{messages-DBn76jVV.mjs → chunks/messages-B217znr-.mjs} +8 -2
  16. package/dist/{messages-DT4dP5uK.mjs → chunks/messages-B8WNljW3.mjs} +6 -0
  17. package/dist/chunks/{messages-BdeLo0N9.mjs → messages-BC8IN4Bf.mjs} +6 -0
  18. package/dist/{messages-CZygwLwM.mjs → chunks/messages-BI43k_BD.mjs} +6 -0
  19. package/dist/{messages-CzTufCHu.mjs → chunks/messages-BJ6zrz2j.mjs} +6 -0
  20. package/dist/{messages-BoJc_p1r.mjs → chunks/messages-BUl_Rcnj.mjs} +6 -0
  21. package/dist/chunks/{messages-CnwibSvh.mjs → messages-BZlmVRwn.mjs} +6 -0
  22. package/dist/{messages-C2htQ_3F.mjs → chunks/messages-BcpCubnC.mjs} +6 -0
  23. package/dist/{messages-D5C3J9qr.mjs → chunks/messages-Bm-E4iRC.mjs} +6 -0
  24. package/dist/chunks/{messages-BELRf6DU.mjs → messages-C4jL-90N.mjs} +6 -0
  25. package/dist/chunks/{messages-1fC8IMyX.mjs → messages-CDBLbUOQ.mjs} +6 -0
  26. package/dist/chunks/{messages-7QoX8DkW.mjs → messages-CH4hrauY.mjs} +6 -0
  27. package/dist/{messages-Dz9L52ol.mjs → chunks/messages-CRJ_mchV.mjs} +6 -0
  28. package/dist/chunks/{messages-JELdtT6E.mjs → messages-CW4c4cRk.mjs} +6 -0
  29. package/dist/chunks/{messages-CKI54h6O.mjs → messages-C_4otP7U.mjs} +6 -0
  30. package/dist/{messages-R3hUSvr3.mjs → chunks/messages-CfiyT2Wi.mjs} +6 -0
  31. package/dist/{messages-CJdUsQ-c.mjs → chunks/messages-CgTq3QhU.mjs} +6 -0
  32. package/dist/chunks/{messages-D1Hv8XGo.mjs → messages-Chb7k3Rg.mjs} +6 -0
  33. package/dist/{messages-Q7AO_FLv.mjs → chunks/messages-Cjjo7yHR.mjs} +6 -0
  34. package/dist/{messages-C99mq906.mjs → chunks/messages-Cl6ayUaq.mjs} +6 -0
  35. package/dist/chunks/{messages-Diu6jAaR.mjs → messages-CmR9ftc_.mjs} +6 -0
  36. package/dist/chunks/{messages-LPVfA-8K.mjs → messages-Cr49Nt3U.mjs} +6 -0
  37. package/dist/chunks/{messages-DqM1LFg5.mjs → messages-Cr94GzbX.mjs} +6 -0
  38. package/dist/{messages-BWF-zUpY.mjs → chunks/messages-CrCYPCk3.mjs} +6 -0
  39. package/dist/{messages-D-ZtY5v0.mjs → chunks/messages-Cs8zmZ3L.mjs} +6 -0
  40. package/dist/{messages-DprmQg6V.mjs → chunks/messages-CzK0LEhb.mjs} +6 -0
  41. package/dist/chunks/{messages-BSbjsyHY.mjs → messages-D00x4S8o.mjs} +6 -0
  42. package/dist/chunks/{messages-Xq8UmkVs.mjs → messages-D1mn7Zd5.mjs} +6 -0
  43. package/dist/chunks/{messages-BC86qLvI.mjs → messages-D2NOpHn9.mjs} +6 -0
  44. package/dist/{messages-kep5wtm4.mjs → chunks/messages-D4qqwVgQ.mjs} +6 -0
  45. package/dist/chunks/{messages-7W4d0DwD.mjs → messages-D5S1Dnpm.mjs} +6 -0
  46. package/dist/{messages-CY8_RyFE.mjs → chunks/messages-D7u2bmP2.mjs} +6 -0
  47. package/dist/chunks/{messages-BFG6Wlgy.mjs → messages-D85FqxgY.mjs} +6 -0
  48. package/dist/{messages-DLfR5bMd.mjs → chunks/messages-D9ndgBnU.mjs} +6 -0
  49. package/dist/{messages-CVw84KdI.mjs → chunks/messages-DDTQgImT.mjs} +6 -0
  50. package/dist/{messages-_ErNTNhk.mjs → chunks/messages-DH_jBeED.mjs} +6 -0
  51. package/dist/chunks/{messages-CMkNSDTo.mjs → messages-DRXWF0PV.mjs} +6 -0
  52. package/dist/chunks/{messages-BYyy6Wqf.mjs → messages-DVQvl8Qj.mjs} +6 -0
  53. package/dist/chunks/{messages-CznZadDf.mjs → messages-DXktiao_.mjs} +6 -0
  54. package/dist/chunks/{messages-DhLKYm2j.mjs → messages-DdK-nFGm.mjs} +6 -0
  55. package/dist/chunks/{messages-BMXCuEKO.mjs → messages-DlJbPF2T.mjs} +6 -0
  56. package/dist/chunks/{messages-CvGLfqmV.mjs → messages-DnVlmiNT.mjs} +6 -0
  57. package/dist/{messages-Z9nEU2xK.mjs → chunks/messages-DviiFSv2.mjs} +6 -0
  58. package/dist/chunks/{messages-BB5z9Uba.mjs → messages-DzqM3Fel.mjs} +6 -0
  59. package/dist/{messages-w7v1GNaE.mjs → chunks/messages-Dzzn6XoD.mjs} +6 -0
  60. package/dist/{messages-CqWJcCbY.mjs → chunks/messages-GSByFygY.mjs} +6 -0
  61. package/dist/chunks/{messages-_ncGrKHh.mjs → messages-L_kl2Qvh.mjs} +6 -0
  62. package/dist/chunks/{messages-BrPFGbM-.mjs → messages-Phkd7XmE.mjs} +6 -0
  63. package/dist/{messages-BU2nlrLK.mjs → chunks/messages-RonBBCnh.mjs} +6 -0
  64. package/dist/{messages-Bmu_S7GM.mjs → chunks/messages-VDriF5Qy.mjs} +6 -0
  65. package/dist/{messages-CLhcMlTc.mjs → chunks/messages-ZjUAIWb1.mjs} +6 -0
  66. package/dist/{messages-9SihnaXQ.mjs → chunks/messages-b1EdvUm0.mjs} +6 -0
  67. package/dist/{messages-DvFLX36Q.mjs → chunks/messages-begYOTgC.mjs} +6 -0
  68. package/dist/{messages-BMv4xwIr.mjs → chunks/messages-jrncnb-H.mjs} +6 -0
  69. package/dist/{messages-D5iv1Kox.mjs → chunks/messages-nefz1S71.mjs} +6 -0
  70. package/dist/{messages-CQwpzUFp.mjs → chunks/messages-ucTVgS5G.mjs} +6 -0
  71. package/dist/chunks/{messages-DBRw-7Zc.mjs → messages-v3GipbFl.mjs} +6 -0
  72. package/dist/{messages-C9eaarcK.mjs → chunks/messages-wmi-iFkH.mjs} +6 -0
  73. package/dist/chunks/{messages-O5tQus_0.mjs → messages-yHcs38yI.mjs} +6 -0
  74. package/dist/full.mjs +30 -27
  75. package/dist/locales.mjs +90 -84
  76. package/dist/{messages-BogRq8lt.mjs → messages-0AbcLMLm.mjs} +6 -0
  77. package/dist/{messages-DJDG55Vq.mjs → messages-0E0AkrNu.mjs} +6 -0
  78. package/dist/{chunks/messages-DnXLrlHh.mjs → messages-4v4MuVEc.mjs} +6 -0
  79. package/dist/{messages-DnIhyAJk.mjs → messages-62v-CLC-.mjs} +6 -0
  80. package/dist/{messages-Dzwxv9v1.mjs → messages-8DeO60Oo.mjs} +6 -0
  81. package/dist/{messages-B1Aww8q7.mjs → messages-8IPXkrDl.mjs} +6 -0
  82. package/dist/{chunks/messages-uKX8WBaD.mjs → messages-96kNZDll.mjs} +6 -0
  83. package/dist/{messages-BL0tXcDf.mjs → messages-B1FZ8lxU.mjs} +6 -0
  84. package/dist/{chunks/messages-DBn76jVV.mjs → messages-B217znr-.mjs} +8 -2
  85. package/dist/{chunks/messages-DT4dP5uK.mjs → messages-B8WNljW3.mjs} +6 -0
  86. package/dist/{messages-BdeLo0N9.mjs → messages-BC8IN4Bf.mjs} +6 -0
  87. package/dist/{chunks/messages-CZygwLwM.mjs → messages-BI43k_BD.mjs} +6 -0
  88. package/dist/{chunks/messages-CzTufCHu.mjs → messages-BJ6zrz2j.mjs} +6 -0
  89. package/dist/{chunks/messages-BoJc_p1r.mjs → messages-BUl_Rcnj.mjs} +6 -0
  90. package/dist/{messages-CnwibSvh.mjs → messages-BZlmVRwn.mjs} +6 -0
  91. package/dist/{chunks/messages-C2htQ_3F.mjs → messages-BcpCubnC.mjs} +6 -0
  92. package/dist/{chunks/messages-D5C3J9qr.mjs → messages-Bm-E4iRC.mjs} +6 -0
  93. package/dist/{messages-BELRf6DU.mjs → messages-C4jL-90N.mjs} +6 -0
  94. package/dist/{messages-1fC8IMyX.mjs → messages-CDBLbUOQ.mjs} +6 -0
  95. package/dist/{messages-7QoX8DkW.mjs → messages-CH4hrauY.mjs} +6 -0
  96. package/dist/{chunks/messages-Dz9L52ol.mjs → messages-CRJ_mchV.mjs} +6 -0
  97. package/dist/{messages-JELdtT6E.mjs → messages-CW4c4cRk.mjs} +6 -0
  98. package/dist/{messages-CKI54h6O.mjs → messages-C_4otP7U.mjs} +6 -0
  99. package/dist/{chunks/messages-R3hUSvr3.mjs → messages-CfiyT2Wi.mjs} +6 -0
  100. package/dist/{chunks/messages-CJdUsQ-c.mjs → messages-CgTq3QhU.mjs} +6 -0
  101. package/dist/{messages-D1Hv8XGo.mjs → messages-Chb7k3Rg.mjs} +6 -0
  102. package/dist/{chunks/messages-Q7AO_FLv.mjs → messages-Cjjo7yHR.mjs} +6 -0
  103. package/dist/{chunks/messages-C99mq906.mjs → messages-Cl6ayUaq.mjs} +6 -0
  104. package/dist/{messages-Diu6jAaR.mjs → messages-CmR9ftc_.mjs} +6 -0
  105. package/dist/{messages-LPVfA-8K.mjs → messages-Cr49Nt3U.mjs} +6 -0
  106. package/dist/{messages-DqM1LFg5.mjs → messages-Cr94GzbX.mjs} +6 -0
  107. package/dist/{chunks/messages-BWF-zUpY.mjs → messages-CrCYPCk3.mjs} +6 -0
  108. package/dist/{chunks/messages-D-ZtY5v0.mjs → messages-Cs8zmZ3L.mjs} +6 -0
  109. package/dist/{chunks/messages-DprmQg6V.mjs → messages-CzK0LEhb.mjs} +6 -0
  110. package/dist/{messages-BSbjsyHY.mjs → messages-D00x4S8o.mjs} +6 -0
  111. package/dist/{messages-Xq8UmkVs.mjs → messages-D1mn7Zd5.mjs} +6 -0
  112. package/dist/{messages-BC86qLvI.mjs → messages-D2NOpHn9.mjs} +6 -0
  113. package/dist/{chunks/messages-kep5wtm4.mjs → messages-D4qqwVgQ.mjs} +6 -0
  114. package/dist/{messages-7W4d0DwD.mjs → messages-D5S1Dnpm.mjs} +6 -0
  115. package/dist/{chunks/messages-CY8_RyFE.mjs → messages-D7u2bmP2.mjs} +6 -0
  116. package/dist/{messages-BFG6Wlgy.mjs → messages-D85FqxgY.mjs} +6 -0
  117. package/dist/{chunks/messages-DLfR5bMd.mjs → messages-D9ndgBnU.mjs} +6 -0
  118. package/dist/{chunks/messages-CVw84KdI.mjs → messages-DDTQgImT.mjs} +6 -0
  119. package/dist/{chunks/messages-_ErNTNhk.mjs → messages-DH_jBeED.mjs} +6 -0
  120. package/dist/{messages-CMkNSDTo.mjs → messages-DRXWF0PV.mjs} +6 -0
  121. package/dist/{messages-BYyy6Wqf.mjs → messages-DVQvl8Qj.mjs} +6 -0
  122. package/dist/{messages-CznZadDf.mjs → messages-DXktiao_.mjs} +6 -0
  123. package/dist/{messages-DhLKYm2j.mjs → messages-DdK-nFGm.mjs} +6 -0
  124. package/dist/{messages-BMXCuEKO.mjs → messages-DlJbPF2T.mjs} +6 -0
  125. package/dist/{messages-CvGLfqmV.mjs → messages-DnVlmiNT.mjs} +6 -0
  126. package/dist/{chunks/messages-Z9nEU2xK.mjs → messages-DviiFSv2.mjs} +6 -0
  127. package/dist/{messages-BB5z9Uba.mjs → messages-DzqM3Fel.mjs} +6 -0
  128. package/dist/{chunks/messages-w7v1GNaE.mjs → messages-Dzzn6XoD.mjs} +6 -0
  129. package/dist/{chunks/messages-CqWJcCbY.mjs → messages-GSByFygY.mjs} +6 -0
  130. package/dist/{messages-_ncGrKHh.mjs → messages-L_kl2Qvh.mjs} +6 -0
  131. package/dist/{messages-BrPFGbM-.mjs → messages-Phkd7XmE.mjs} +6 -0
  132. package/dist/{chunks/messages-BU2nlrLK.mjs → messages-RonBBCnh.mjs} +6 -0
  133. package/dist/{chunks/messages-Bmu_S7GM.mjs → messages-VDriF5Qy.mjs} +6 -0
  134. package/dist/{chunks/messages-CLhcMlTc.mjs → messages-ZjUAIWb1.mjs} +6 -0
  135. package/dist/{chunks/messages-9SihnaXQ.mjs → messages-b1EdvUm0.mjs} +6 -0
  136. package/dist/{chunks/messages-DvFLX36Q.mjs → messages-begYOTgC.mjs} +6 -0
  137. package/dist/{chunks/messages-BMv4xwIr.mjs → messages-jrncnb-H.mjs} +6 -0
  138. package/dist/{chunks/messages-D5iv1Kox.mjs → messages-nefz1S71.mjs} +6 -0
  139. package/dist/{chunks/messages-CQwpzUFp.mjs → messages-ucTVgS5G.mjs} +6 -0
  140. package/dist/{messages-DBRw-7Zc.mjs → messages-v3GipbFl.mjs} +6 -0
  141. package/dist/{chunks/messages-C9eaarcK.mjs → messages-wmi-iFkH.mjs} +6 -0
  142. package/dist/{messages-O5tQus_0.mjs → messages-yHcs38yI.mjs} +6 -0
  143. package/dist/tools.mjs +3537 -1710
  144. package/dist/vendor.LICENSE.txt +109 -109
  145. package/package.json +43 -57
  146. package/src/blok.ts +12 -0
  147. package/src/components/__module.ts +21 -0
  148. package/src/components/block/api.ts +17 -0
  149. package/src/components/block/style-manager.ts +6 -2
  150. package/src/components/block/tool-renderer.ts +33 -30
  151. package/src/components/blocks.ts +132 -15
  152. package/src/components/constants/data-attributes.ts +7 -0
  153. package/src/components/i18n/locales/am/messages.json +6 -0
  154. package/src/components/i18n/locales/ar/messages.json +6 -0
  155. package/src/components/i18n/locales/az/messages.json +6 -0
  156. package/src/components/i18n/locales/bg/messages.json +6 -0
  157. package/src/components/i18n/locales/bn/messages.json +6 -0
  158. package/src/components/i18n/locales/bs/messages.json +6 -0
  159. package/src/components/i18n/locales/cs/messages.json +6 -0
  160. package/src/components/i18n/locales/da/messages.json +6 -0
  161. package/src/components/i18n/locales/de/messages.json +6 -0
  162. package/src/components/i18n/locales/dv/messages.json +6 -0
  163. package/src/components/i18n/locales/el/messages.json +6 -0
  164. package/src/components/i18n/locales/en/messages.json +6 -0
  165. package/src/components/i18n/locales/es/messages.json +6 -0
  166. package/src/components/i18n/locales/et/messages.json +6 -0
  167. package/src/components/i18n/locales/fa/messages.json +6 -0
  168. package/src/components/i18n/locales/fi/messages.json +6 -0
  169. package/src/components/i18n/locales/fil/messages.json +6 -0
  170. package/src/components/i18n/locales/fr/messages.json +6 -0
  171. package/src/components/i18n/locales/gu/messages.json +6 -0
  172. package/src/components/i18n/locales/he/messages.json +6 -0
  173. package/src/components/i18n/locales/hi/messages.json +6 -0
  174. package/src/components/i18n/locales/hr/messages.json +6 -0
  175. package/src/components/i18n/locales/hu/messages.json +6 -0
  176. package/src/components/i18n/locales/hy/messages.json +6 -0
  177. package/src/components/i18n/locales/id/messages.json +6 -0
  178. package/src/components/i18n/locales/it/messages.json +6 -0
  179. package/src/components/i18n/locales/ja/messages.json +6 -0
  180. package/src/components/i18n/locales/ka/messages.json +6 -0
  181. package/src/components/i18n/locales/km/messages.json +6 -0
  182. package/src/components/i18n/locales/kn/messages.json +6 -0
  183. package/src/components/i18n/locales/ko/messages.json +6 -0
  184. package/src/components/i18n/locales/ku/messages.json +6 -0
  185. package/src/components/i18n/locales/lo/messages.json +6 -0
  186. package/src/components/i18n/locales/lt/messages.json +6 -0
  187. package/src/components/i18n/locales/lv/messages.json +6 -0
  188. package/src/components/i18n/locales/mk/messages.json +6 -0
  189. package/src/components/i18n/locales/ml/messages.json +6 -0
  190. package/src/components/i18n/locales/mn/messages.json +6 -0
  191. package/src/components/i18n/locales/mr/messages.json +6 -0
  192. package/src/components/i18n/locales/ms/messages.json +6 -0
  193. package/src/components/i18n/locales/my/messages.json +6 -0
  194. package/src/components/i18n/locales/ne/messages.json +6 -0
  195. package/src/components/i18n/locales/nl/messages.json +6 -0
  196. package/src/components/i18n/locales/no/messages.json +6 -0
  197. package/src/components/i18n/locales/pa/messages.json +6 -0
  198. package/src/components/i18n/locales/pl/messages.json +6 -0
  199. package/src/components/i18n/locales/ps/messages.json +6 -0
  200. package/src/components/i18n/locales/pt/messages.json +6 -0
  201. package/src/components/i18n/locales/ro/messages.json +6 -0
  202. package/src/components/i18n/locales/ru/messages.json +6 -0
  203. package/src/components/i18n/locales/sd/messages.json +6 -0
  204. package/src/components/i18n/locales/si/messages.json +6 -0
  205. package/src/components/i18n/locales/sk/messages.json +6 -0
  206. package/src/components/i18n/locales/sl/messages.json +6 -0
  207. package/src/components/i18n/locales/sq/messages.json +6 -0
  208. package/src/components/i18n/locales/sr/messages.json +6 -0
  209. package/src/components/i18n/locales/sv/messages.json +6 -0
  210. package/src/components/i18n/locales/sw/messages.json +6 -0
  211. package/src/components/i18n/locales/ta/messages.json +6 -0
  212. package/src/components/i18n/locales/te/messages.json +6 -0
  213. package/src/components/i18n/locales/th/messages.json +6 -0
  214. package/src/components/i18n/locales/tr/messages.json +6 -0
  215. package/src/components/i18n/locales/ug/messages.json +6 -0
  216. package/src/components/i18n/locales/uk/messages.json +6 -0
  217. package/src/components/i18n/locales/ur/messages.json +6 -0
  218. package/src/components/i18n/locales/vi/messages.json +6 -0
  219. package/src/components/i18n/locales/yi/messages.json +6 -0
  220. package/src/components/i18n/locales/zh/messages.json +6 -0
  221. package/src/components/icons/index.ts +61 -7
  222. package/src/components/inline-tools/inline-tool-link.ts +1 -1
  223. package/src/components/inline-tools/inline-tool-marker.ts +737 -0
  224. package/src/components/inline-tools/utils/formatting-range-utils.ts +6 -3
  225. package/src/components/inline-tools/utils/marker-dom-utils.ts +17 -0
  226. package/src/components/modules/api/blocks.ts +34 -9
  227. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +75 -29
  228. package/src/components/modules/blockEvents/composers/markdownShortcuts.ts +54 -2
  229. package/src/components/modules/blockEvents/constants.ts +12 -0
  230. package/src/components/modules/blockEvents/index.ts +13 -5
  231. package/src/components/modules/blockManager/blockManager.ts +81 -2
  232. package/src/components/modules/blockManager/hierarchy.ts +20 -2
  233. package/src/components/modules/blockManager/operations.ts +70 -35
  234. package/src/components/modules/blockManager/repository.ts +22 -0
  235. package/src/components/modules/blockManager/types.ts +3 -1
  236. package/src/components/modules/blockManager/yjs-sync.ts +173 -39
  237. package/src/components/modules/blockSelection.ts +3 -0
  238. package/src/components/modules/crossBlockSelection.ts +11 -3
  239. package/src/components/modules/drag/preview/DragPreview.ts +10 -2
  240. package/src/components/modules/drag/target/DropTargetDetector.ts +100 -11
  241. package/src/components/modules/drag/utils/drag.constants.ts +1 -1
  242. package/src/components/modules/normalizeInlineImages.ts +263 -0
  243. package/src/components/modules/paste/google-docs-preprocessor.ts +197 -0
  244. package/src/components/modules/paste/handlers/base.ts +43 -2
  245. package/src/components/modules/paste/handlers/html-handler.ts +1 -1
  246. package/src/components/modules/paste/handlers/index.ts +1 -0
  247. package/src/components/modules/paste/handlers/table-cells-handler.ts +104 -0
  248. package/src/components/modules/paste/index.ts +20 -3
  249. package/src/components/modules/readonly.ts +8 -2
  250. package/src/components/modules/rectangleSelection.ts +5 -2
  251. package/src/components/modules/renderer.ts +35 -0
  252. package/src/components/modules/saver.ts +52 -2
  253. package/src/components/modules/toolbar/blockSettings.ts +52 -44
  254. package/src/components/modules/toolbar/index.ts +124 -17
  255. package/src/components/modules/toolbar/inline/index.ts +4 -4
  256. package/src/components/modules/toolbar/plus-button.ts +3 -3
  257. package/src/components/modules/toolbar/settings-toggler.ts +3 -3
  258. package/src/components/modules/toolbar/styles.ts +7 -7
  259. package/src/components/modules/ui.ts +6 -6
  260. package/src/components/modules/uiControllers/controllers/blockHover.ts +16 -2
  261. package/src/components/modules/uiControllers/handlers/touch.ts +83 -10
  262. package/src/components/modules/yjs/block-observer.ts +9 -3
  263. package/src/components/modules/yjs/document-store.ts +10 -7
  264. package/src/components/modules/yjs/types.ts +8 -6
  265. package/src/components/modules/yjs/undo-history.ts +90 -11
  266. package/src/components/selection/fake-background/shadows.ts +1 -1
  267. package/src/components/shared/color-picker.ts +211 -0
  268. package/src/components/shared/color-presets.ts +25 -0
  269. package/src/components/ui/toolbox.ts +27 -11
  270. package/src/components/utils/color-mapping.ts +241 -0
  271. package/src/components/utils/notifier/draw.ts +9 -9
  272. package/src/components/utils/placeholder.ts +24 -8
  273. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +3 -3
  274. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +15 -12
  275. package/src/components/utils/popover/components/search-input/search-input.const.ts +2 -2
  276. package/src/components/utils/popover/popover-abstract.ts +30 -5
  277. package/src/components/utils/popover/popover-desktop.ts +26 -3
  278. package/src/components/utils/popover/popover-inline.ts +14 -1
  279. package/src/components/utils/popover/popover-mobile.ts +4 -4
  280. package/src/components/utils/popover/popover.const.ts +3 -3
  281. package/src/components/utils/sanitizer.ts +24 -3
  282. package/src/components/utils/tw.ts +17 -5
  283. package/src/full.ts +4 -0
  284. package/src/stories/Header.stories.ts +106 -0
  285. package/src/stories/MarkerColors.stories.ts +730 -0
  286. package/src/stories/Placeholder.stories.ts +7 -2
  287. package/src/stories/Popover.stories.ts +1 -3
  288. package/src/stories/Table.stories.ts +1662 -0
  289. package/src/stories/helpers.ts +2 -0
  290. package/src/styles/main.css +217 -39
  291. package/src/tools/header/index.ts +204 -26
  292. package/src/tools/index.ts +5 -1
  293. package/src/tools/list/caret-manager.ts +28 -10
  294. package/src/tools/list/constants.ts +2 -2
  295. package/src/tools/list/dom-builder.ts +3 -3
  296. package/src/tools/list/static-configs.ts +0 -1
  297. package/src/tools/paragraph/index.ts +9 -5
  298. package/src/tools/table/core/table-commands.ts +99 -0
  299. package/src/tools/table/core/table-controller.ts +231 -0
  300. package/src/tools/table/core/table-events.ts +102 -0
  301. package/src/tools/table/index.ts +1070 -174
  302. package/src/tools/table/ownership/table-event-broker.ts +74 -0
  303. package/src/tools/table/ownership/table-ownership-registry.ts +126 -0
  304. package/src/tools/table/table-add-controls.ts +85 -15
  305. package/src/tools/table/table-cell-blocks.ts +336 -38
  306. package/src/tools/table/table-cell-clipboard.ts +415 -0
  307. package/src/tools/table/table-cell-color-picker.ts +34 -0
  308. package/src/tools/table/table-cell-selection.ts +264 -15
  309. package/src/tools/table/table-core.ts +3 -42
  310. package/src/tools/table/table-heading-toggle.ts +2 -2
  311. package/src/tools/table/table-model.ts +623 -0
  312. package/src/tools/table/table-operations.ts +59 -78
  313. package/src/tools/table/table-resize.ts +15 -11
  314. package/src/tools/table/table-restrictions.ts +69 -3
  315. package/src/tools/table/table-row-col-action-handler.ts +22 -7
  316. package/src/tools/table/table-row-col-controls.ts +129 -12
  317. package/src/tools/table/table-row-col-drag.ts +14 -0
  318. package/src/tools/table/table-scroll-haze.ts +152 -0
  319. package/src/tools/table/types.ts +22 -1
  320. package/src/tools/table/view/table-cell-blocks-adapter.ts +47 -0
  321. package/src/tools/toggle/block-operations.ts +110 -0
  322. package/src/tools/toggle/constants.ts +49 -0
  323. package/src/tools/toggle/dom-builder.ts +125 -0
  324. package/src/tools/toggle/index.ts +280 -0
  325. package/src/tools/toggle/toggle-keyboard.ts +139 -0
  326. package/src/tools/toggle/toggle-lifecycle.ts +80 -0
  327. package/src/tools/toggle/toggle-shortcuts.ts +107 -0
  328. package/src/tools/toggle/types.ts +21 -0
  329. package/src/variants/blok-minimum.ts +13 -0
  330. package/types/api/block.d.ts +13 -0
  331. package/types/api/blocks.d.ts +16 -0
  332. package/types/full.d.ts +2 -0
  333. package/types/tools/table.d.ts +2 -0
  334. package/types/tools-entry.d.ts +2 -1
  335. package/dist/chunks/i18next-CugVlwWp.mjs +0 -1292
  336. package/src/tools/table/data-normalizer.ts +0 -32
@@ -138,6 +138,20 @@ export class DropTargetDetector {
138
138
  return null;
139
139
  }
140
140
 
141
+ /**
142
+ * If the target block is inside a table cell and the source block is NOT
143
+ * in the same cell, redirect the drop target to the table block itself.
144
+ * This prevents blocks from being dropped into table cells via drag & drop.
145
+ */
146
+ const targetCellContainer = targetBlock.holder.closest('[data-blok-table-cell-blocks]');
147
+ const sourceCellContainer = sourceBlock.holder.closest('[data-blok-table-cell-blocks]');
148
+ const isTargetInCell = targetCellContainer !== null;
149
+ const isCrossCellDrop = sourceCellContainer !== targetCellContainer;
150
+
151
+ if (isTargetInCell && isCrossCellDrop) {
152
+ return this.redirectToTableBlock(targetCellContainer, clientY);
153
+ }
154
+
141
155
  // Determine edge (top or bottom half of block)
142
156
  const rect = blockHolder.getBoundingClientRect();
143
157
  const isTopHalf = clientY < rect.top + rect.height / 2;
@@ -151,24 +165,29 @@ export class DropTargetDetector {
151
165
  const canUsePreviousBlock = previousBlock && !this.sourceBlocks.includes(previousBlock);
152
166
 
153
167
  if (isTopHalf && targetIndex > 0 && canUsePreviousBlock) {
154
- const targetDepth = this.calculateTargetDepth(previousBlock, 'bottom');
168
+ const targetDepth = this.calculateTargetDepth(previousBlock, 'bottom', sourceBlock);
155
169
  return { block: previousBlock, edge: 'bottom', depth: targetDepth };
156
170
  }
157
171
 
158
172
  // First block top half, or any block bottom half
159
173
  const edge: 'top' | 'bottom' = isTopHalf ? 'top' : 'bottom';
160
- const targetDepth = this.calculateTargetDepth(targetBlock, edge);
174
+ const targetDepth = this.calculateTargetDepth(targetBlock, edge, sourceBlock);
161
175
 
162
176
  return { block: targetBlock, edge, depth: targetDepth };
163
177
  }
164
178
 
165
179
  /**
166
- * Calculates the target depth for list item nesting
180
+ * Calculates the target depth for list item nesting.
181
+ * When a sourceBlock is provided and is a list item, the depth is calculated
182
+ * to match what ListDepthValidator.getTargetDepthForMove will produce post-drop,
183
+ * ensuring the visual indicator accurately predicts the final depth.
184
+ *
167
185
  * @param targetBlock - Block being dropped onto
168
186
  * @param targetEdge - Edge of target ('top' or 'bottom')
187
+ * @param sourceBlock - Optional block being dragged (for accurate list item depth prediction)
169
188
  * @returns The target depth (0 for root level, 1+ for nested)
170
189
  */
171
- calculateTargetDepth(targetBlock: Block, targetEdge: 'top' | 'bottom'): number {
190
+ calculateTargetDepth(targetBlock: Block, targetEdge: 'top' | 'bottom', sourceBlock?: Block): number {
172
191
  const targetIndex = this.blockManager.getBlockIndex(targetBlock);
173
192
  const dropIndex = targetEdge === 'top' ? targetIndex : targetIndex + 1;
174
193
 
@@ -184,23 +203,93 @@ export class DropTargetDetector {
184
203
  return 0;
185
204
  }
186
205
 
187
- const previousDepth = this.listItemDepth.getDepth(previousBlock) ?? 0;
206
+ const previousDepth = this.listItemDepth.getDepth(previousBlock);
207
+ const previousIsListItem = previousDepth !== null;
208
+ const prevDepthValue = previousDepth ?? 0;
188
209
 
189
210
  // Get the block that will be immediately after the drop position
190
211
  const nextBlock = this.blockManager.getBlockByIndex(dropIndex);
191
- const nextDepth = nextBlock ? (this.listItemDepth.getDepth(nextBlock) ?? 0) : 0;
212
+ const nextDepth = nextBlock ? this.listItemDepth.getDepth(nextBlock) : null;
213
+ const nextIsListItem = nextDepth !== null;
214
+ const nextDepthValue = nextDepth ?? 0;
215
+
216
+ // When dragging a list item, predict the exact depth ListDepthValidator will
217
+ // compute post-drop — so the visual indicator matches the actual result.
218
+ const sourceDepth = sourceBlock ? this.listItemDepth.getDepth(sourceBlock) : null;
219
+
220
+ if (sourceDepth !== null) {
221
+ return this.predictListItemDepth(
222
+ sourceDepth, prevDepthValue, previousIsListItem, nextDepthValue, nextIsListItem
223
+ );
224
+ }
225
+
226
+ // For non-list blocks (or when sourceBlock not provided), use neighbor-based
227
+ // depth for cosmetic indicator positioning only.
228
+ if (nextDepthValue > 0 && nextDepthValue <= prevDepthValue + 1) {
229
+ return nextDepthValue;
230
+ }
231
+
232
+ if (prevDepthValue > 0) {
233
+ return prevDepthValue;
234
+ }
235
+
236
+ return 0;
237
+ }
238
+
239
+ /**
240
+ * Mirrors ListDepthValidator.getTargetDepthForMove to predict the exact
241
+ * post-drop depth for a list item. This ensures the visual indicator
242
+ * shows the same depth the item will actually have after being dropped.
243
+ */
244
+ private predictListItemDepth(
245
+ currentDepth: number,
246
+ previousDepth: number,
247
+ previousIsListItem: boolean,
248
+ nextDepth: number,
249
+ nextIsListItem: boolean
250
+ ): number {
251
+ const maxAllowedDepth = previousIsListItem ? previousDepth + 1 : 0;
252
+
253
+ // Cap current depth at max allowed
254
+ if (currentDepth > maxAllowedDepth) {
255
+ return maxAllowedDepth;
256
+ }
192
257
 
193
- // If next item is nested, match its depth (become sibling)
194
- if (nextDepth > 0 && nextDepth <= previousDepth + 1) {
258
+ // Match next depth if it's deeper than current and within bounds
259
+ if (nextIsListItem && nextDepth > currentDepth && nextDepth <= maxAllowedDepth) {
195
260
  return nextDepth;
196
261
  }
197
262
 
198
- // If previous item is nested, match its depth
199
- if (previousDepth > 0) {
263
+ // Match previous depth if deeper than current and no next list item
264
+ if (previousIsListItem && !nextIsListItem && previousDepth > currentDepth && previousDepth <= maxAllowedDepth) {
200
265
  return previousDepth;
201
266
  }
202
267
 
203
- return 0;
268
+ return currentDepth;
269
+ }
270
+
271
+ /**
272
+ * Redirect a drop target to the table block that contains the given cell container.
273
+ * Finds the outermost [data-blok-element] ancestor and returns it as the target.
274
+ *
275
+ * @param cellContainer - The [data-blok-table-cell-blocks] element containing the target
276
+ * @param clientY - Cursor Y position for edge calculation
277
+ * @returns Drop target pointing to the table block, or null if table block not found
278
+ */
279
+ private redirectToTableBlock(cellContainer: Element, clientY: number): DropTarget | null {
280
+ const tableHolder = cellContainer.closest(createSelector(DATA_ATTR.element));
281
+ const tableBlock = tableHolder
282
+ ? this.blockManager.blocks.find(b => b.holder === tableHolder)
283
+ : undefined;
284
+
285
+ if (!tableBlock) {
286
+ return null;
287
+ }
288
+
289
+ const tableRect = tableBlock.holder.getBoundingClientRect();
290
+ const isTopHalf = clientY < tableRect.top + tableRect.height / 2;
291
+
292
+ return { block: tableBlock, edge: isTopHalf ? 'top' : 'bottom', depth: 0 };
204
293
  }
205
294
 
206
295
  }
@@ -21,7 +21,7 @@ export const DRAG_CONFIG = {
21
21
  * Styles for the drag preview element
22
22
  */
23
23
  export const PREVIEW_STYLES = {
24
- base: 'fixed pointer-events-none z-[10000] opacity-80 transition-none',
24
+ base: 'fixed pointer-events-none z-10000 opacity-80 transition-none',
25
25
  content: 'relative mx-auto max-w-content',
26
26
  } as const;
27
27
 
@@ -0,0 +1,263 @@
1
+ import { generateBlockId } from '../utils/id-generator';
2
+
3
+ /**
4
+ * Minimal block shape expected by the normalizer.
5
+ * Matches the SaverValidatedData shape from the saver pipeline.
6
+ */
7
+ interface NormalizableBlock {
8
+ id?: string;
9
+ tool?: string;
10
+ data?: Record<string, unknown>;
11
+ isValid: boolean;
12
+ parentId?: string | null;
13
+ contentIds?: string[];
14
+ tunes?: Record<string, unknown>;
15
+ time?: number;
16
+ }
17
+
18
+ /**
19
+ * Shape of a table cell within the table's content data.
20
+ */
21
+ interface TableCell {
22
+ blocks: string[];
23
+ }
24
+
25
+ /**
26
+ * Tracking info for a paragraph whose inline images need extraction.
27
+ */
28
+ interface ExtractionInfo {
29
+ parentTableId: string;
30
+ imgSrcs: string[];
31
+ cleanedText: string;
32
+ }
33
+
34
+ /**
35
+ * Regex to match <img> tags and capture their src attribute.
36
+ * Handles both single and double quotes around src value.
37
+ */
38
+ const IMG_TAG_REGEX = /<img\s+[^>]*src=["']([^"']+)["'][^>]*>/g;
39
+
40
+ /**
41
+ * Normalizes inline images in table cell paragraphs by extracting `<img>` tags
42
+ * into standalone image blocks.
43
+ *
44
+ * For each paragraph block whose parent is a table:
45
+ * 1. Extracts all `<img>` tags from the paragraph's text
46
+ * 2. Creates a new image block for each extracted `<img>`
47
+ * 3. Removes the `<img>` tags from the paragraph text
48
+ * 4. Inserts the new image block IDs before the paragraph in the table cell's blocks array
49
+ * 5. Adds the new image block IDs to the table's contentIds
50
+ *
51
+ * @param blocks - array of saved block data
52
+ * @returns new array with inline images extracted into standalone blocks
53
+ */
54
+ export const normalizeInlineImages = <T extends NormalizableBlock>(blocks: T[]): T[] => {
55
+ /**
56
+ * Build a lookup of block id → block for quick parent resolution.
57
+ */
58
+ const blockById = new Map<string, T>();
59
+
60
+ for (const block of blocks) {
61
+ if (block.id !== undefined) {
62
+ blockById.set(block.id, block);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Check if there are any table blocks at all. If not, return input unchanged.
68
+ */
69
+ const hasTable = blocks.some((b) => b.tool === 'table');
70
+
71
+ if (!hasTable) {
72
+ return blocks;
73
+ }
74
+
75
+ const extractionMap = new Map<string, ExtractionInfo>();
76
+
77
+ for (const block of blocks) {
78
+ if (block.tool !== 'paragraph' || block.parentId === undefined || block.parentId === null) {
79
+ continue;
80
+ }
81
+
82
+ const parent = blockById.get(block.parentId);
83
+
84
+ if (parent === undefined || parent.tool !== 'table') {
85
+ continue;
86
+ }
87
+
88
+ const text = block.data?.text;
89
+
90
+ if (typeof text !== 'string') {
91
+ continue;
92
+ }
93
+
94
+ const imgSrcs = Array.from(text.matchAll(IMG_TAG_REGEX), (m) => m[1]);
95
+
96
+ if (imgSrcs.length === 0) {
97
+ continue;
98
+ }
99
+
100
+ /**
101
+ * Remove all <img> tags from text.
102
+ */
103
+ IMG_TAG_REGEX.lastIndex = 0;
104
+ const cleanedText = text.replace(IMG_TAG_REGEX, '');
105
+
106
+ if (block.id !== undefined) {
107
+ extractionMap.set(block.id, {
108
+ parentTableId: block.parentId,
109
+ imgSrcs,
110
+ cleanedText,
111
+ });
112
+ }
113
+ }
114
+
115
+ /**
116
+ * If no paragraphs need extraction, return input unchanged.
117
+ */
118
+ if (extractionMap.size === 0) {
119
+ return blocks;
120
+ }
121
+
122
+ /**
123
+ * Generate image block IDs and build new image blocks.
124
+ * Maps paragraph id → array of new image block entries.
125
+ */
126
+ const newImageBlocksPerParagraph = new Map<string, T[]>();
127
+
128
+ for (const [paragraphId, info] of extractionMap) {
129
+ const imageBlocks: T[] = [];
130
+
131
+ for (const src of info.imgSrcs) {
132
+ const imageBlock = {
133
+ id: generateBlockId(),
134
+ tool: 'image',
135
+ data: { url: src },
136
+ isValid: true,
137
+ parentId: info.parentTableId,
138
+ } as unknown as T;
139
+
140
+ imageBlocks.push(imageBlock);
141
+ }
142
+
143
+ newImageBlocksPerParagraph.set(paragraphId, imageBlocks);
144
+ }
145
+
146
+ /**
147
+ * Clone table blocks and update their content/contentIds with new image block references.
148
+ */
149
+ const updatedTableIds = new Set<string>();
150
+
151
+ for (const info of extractionMap.values()) {
152
+ updatedTableIds.add(info.parentTableId);
153
+ }
154
+
155
+ const clonedTables = new Map<string, T>();
156
+
157
+ for (const tableId of updatedTableIds) {
158
+ const original = blockById.get(tableId);
159
+
160
+ if (original === undefined) {
161
+ continue;
162
+ }
163
+
164
+ const cloned = { ...original };
165
+ const originalData = original.data as { content: TableCell[][] } | undefined;
166
+
167
+ if (originalData?.content !== undefined) {
168
+ /**
169
+ * Deep clone the content array so we can mutate cell blocks arrays.
170
+ */
171
+ const clonedContent: TableCell[][] = originalData.content.map(
172
+ (row) => row.map((cell) => ({ ...cell, blocks: [...cell.blocks] }))
173
+ );
174
+
175
+ cloned.data = { ...original.data, content: clonedContent };
176
+ }
177
+
178
+ cloned.contentIds = original.contentIds !== undefined ? [...original.contentIds] : [];
179
+ clonedTables.set(tableId, cloned);
180
+ }
181
+
182
+ /**
183
+ * Update cloned table cell blocks arrays and contentIds.
184
+ */
185
+ for (const [paragraphId, imageBlocks] of newImageBlocksPerParagraph) {
186
+ const info = extractionMap.get(paragraphId);
187
+
188
+ if (info === undefined) {
189
+ continue;
190
+ }
191
+
192
+ const clonedTable = clonedTables.get(info.parentTableId);
193
+
194
+ if (clonedTable === undefined) {
195
+ continue;
196
+ }
197
+
198
+ const tableData = clonedTable.data as { content: TableCell[][] } | undefined;
199
+
200
+ if (tableData?.content === undefined) {
201
+ continue;
202
+ }
203
+
204
+ const imageBlockIds = imageBlocks.map((b) => b.id ?? '');
205
+
206
+ /**
207
+ * Find the cell containing this paragraph and insert image IDs before the paragraph.
208
+ */
209
+ tableData.content.flat().forEach((cell) => {
210
+ const paragraphIndex = cell.blocks.indexOf(paragraphId);
211
+
212
+ if (paragraphIndex !== -1) {
213
+ cell.blocks.splice(paragraphIndex, 0, ...imageBlockIds);
214
+ }
215
+ });
216
+
217
+ /**
218
+ * Add image block IDs to the table's contentIds.
219
+ */
220
+ if (clonedTable.contentIds !== undefined) {
221
+ clonedTable.contentIds.push(...imageBlockIds);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Build the result array:
227
+ * - Replace table blocks with cloned versions
228
+ * - Replace paragraph blocks with cleaned text versions
229
+ * - Insert image blocks before their source paragraph
230
+ */
231
+ const result: T[] = [];
232
+
233
+ for (const block of blocks) {
234
+ /**
235
+ * If this is a table that was updated, use the cloned version.
236
+ */
237
+ if (block.id !== undefined && clonedTables.has(block.id)) {
238
+ result.push(clonedTables.get(block.id) as T);
239
+ continue;
240
+ }
241
+
242
+ /**
243
+ * If this is a paragraph with images to extract, insert image blocks before it
244
+ * and update its text.
245
+ */
246
+ if (block.id !== undefined && extractionMap.has(block.id)) {
247
+ result.push(...(newImageBlocksPerParagraph.get(block.id) ?? []));
248
+
249
+ const info = extractionMap.get(block.id);
250
+ const updatedParagraph = {
251
+ ...block,
252
+ data: { ...block.data, text: info?.cleanedText ?? '' },
253
+ };
254
+
255
+ result.push(updatedParagraph);
256
+ continue;
257
+ }
258
+
259
+ result.push(block);
260
+ }
261
+
262
+ return result;
263
+ };
@@ -0,0 +1,197 @@
1
+ import { mapToNearestPresetColor } from '../../utils/color-mapping';
2
+
3
+ /**
4
+ * Pre-process Google Docs clipboard HTML before sanitization.
5
+ *
6
+ * Google Docs wraps content in `<b id="docs-internal-guid-...">` and
7
+ * encodes formatting as inline styles on `<span>` elements rather than
8
+ * semantic tags. The sanitizer strips `<span>` (not in the allowed
9
+ * config), destroying formatting. This function converts style-based
10
+ * spans to `<b>`/`<i>`/`<mark>` BEFORE the sanitizer runs.
11
+ *
12
+ * @param html - raw clipboard HTML string
13
+ * @returns preprocessed HTML string
14
+ */
15
+ export function preprocessGoogleDocsHtml(html: string): string {
16
+ const wrapper = document.createElement('div');
17
+
18
+ wrapper.innerHTML = html;
19
+
20
+ unwrapGoogleDocsContent(wrapper);
21
+ convertGoogleDocsStyles(wrapper);
22
+ convertTableCellParagraphs(wrapper);
23
+
24
+ return wrapper.innerHTML;
25
+ }
26
+
27
+ /**
28
+ * Strip Google Docs wrapper elements to expose underlying content.
29
+ * Google Docs wraps clipboard HTML in `<b id="docs-internal-guid-...">`.
30
+ * Content may be split across multiple child `<div>` elements (e.g. one
31
+ * per table), so all children are moved out of the wrapper.
32
+ */
33
+ function unwrapGoogleDocsContent(wrapper: HTMLElement): void {
34
+ const googleDocsWrapper = wrapper.querySelector<HTMLElement>('b[id^="docs-internal-guid-"]');
35
+
36
+ if (!googleDocsWrapper) {
37
+ return;
38
+ }
39
+
40
+ const fragment = document.createDocumentFragment();
41
+
42
+ while (googleDocsWrapper.firstChild) {
43
+ fragment.appendChild(googleDocsWrapper.firstChild);
44
+ }
45
+
46
+ googleDocsWrapper.replaceWith(fragment);
47
+ }
48
+
49
+ /**
50
+ * Determine the background-color style declaration for a Google Docs element.
51
+ *
52
+ * When a background color is present, it is mapped to the nearest preset.
53
+ * When only a foreground color is present, an explicit `transparent` background
54
+ * is returned so the mark element doesn't inherit an unwanted background.
55
+ */
56
+ function resolveBackgroundStyle(hasBgColor: boolean, hasColor: boolean, mappedBg: string): string {
57
+ if (hasBgColor) {
58
+ return `background-color: ${mappedBg}`;
59
+ }
60
+
61
+ if (hasColor) {
62
+ return 'background-color: transparent';
63
+ }
64
+
65
+ return '';
66
+ }
67
+
68
+ /**
69
+ * Check whether a CSS color value is the default black text color.
70
+ * Google Docs uses different formats: `rgb(0, 0, 0)`, `rgb(0,0,0)`, or `#000000`.
71
+ * Spans with only this color should not be converted to `<mark>`.
72
+ */
73
+ function isDefaultBlack(color: string): boolean {
74
+ const normalized = color.replace(/\s/g, '');
75
+
76
+ return normalized === 'rgb(0,0,0)' || normalized === '#000000';
77
+ }
78
+
79
+ /**
80
+ * Convert Google Docs style-based `<span>` elements to semantic HTML tags.
81
+ *
82
+ * - `<span style="font-weight:700">` or `font-weight:bold` → `<b>`
83
+ * - `<span style="font-style:italic">` → `<i>`
84
+ * - `<span style="color:...">` → `<mark style="color: ...">`
85
+ * - `<span style="background-color:...">` → `<mark style="background-color: ...">`
86
+ *
87
+ * Color and bold/italic can combine: a bold red span becomes `<b><mark style="color: red;">text</mark></b>`.
88
+ */
89
+ function convertGoogleDocsStyles(wrapper: HTMLElement): void {
90
+ for (const span of Array.from(wrapper.querySelectorAll('span[style]'))) {
91
+ const style = span.getAttribute('style') ?? '';
92
+ const isBold = /font-weight\s*:\s*(700|bold)/i.test(style);
93
+ const isItalic = /font-style\s*:\s*italic/i.test(style);
94
+
95
+ const colorMatch = /(?<![a-z-])color\s*:\s*([^;]+)/i.exec(style);
96
+ const bgMatch = /background-color\s*:\s*([^;]+)/i.exec(style);
97
+
98
+ const color = colorMatch?.[1]?.trim();
99
+ const bgColor = bgMatch?.[1]?.trim();
100
+
101
+ const hasColor = color !== undefined && !isDefaultBlack(color);
102
+ const hasBgColor = bgColor !== undefined && bgColor !== 'transparent';
103
+
104
+ if (!isBold && !isItalic && !hasColor && !hasBgColor) {
105
+ continue;
106
+ }
107
+
108
+ const mappedColor = hasColor ? mapToNearestPresetColor(color, 'text') : '';
109
+ const mappedBg = hasBgColor ? mapToNearestPresetColor(bgColor, 'bg') : '';
110
+
111
+ const colorStyles = [
112
+ hasColor ? `color: ${mappedColor}` : '',
113
+ resolveBackgroundStyle(hasBgColor, hasColor, mappedBg),
114
+ ].filter(Boolean).join('; ');
115
+
116
+ const inner = colorStyles
117
+ ? `<mark style="${colorStyles};">${span.innerHTML}</mark>`
118
+ : span.innerHTML;
119
+
120
+ const italic = isItalic ? `<i>${inner}</i>` : inner;
121
+ const wrapped = isBold ? `<b>${italic}</b>` : italic;
122
+
123
+ span.replaceWith(document.createRange().createContextualFragment(wrapped));
124
+ }
125
+
126
+ convertAnchorColorStyles(wrapper);
127
+ }
128
+
129
+ /**
130
+ * Convert color/background-color styles on `<a>` elements to `<mark>` tags.
131
+ *
132
+ * Google Docs sometimes puts background-color directly on the `<a>` element.
133
+ * The sanitizer only allows `href`/`target`/`rel` on `<a>`, so inline styles
134
+ * are stripped — losing the background. This moves color styles into a
135
+ * `<mark>` wrapping the link content before sanitization runs.
136
+ */
137
+ function convertAnchorColorStyles(wrapper: HTMLElement): void {
138
+ for (const anchor of Array.from(wrapper.querySelectorAll('a[style]'))) {
139
+ const style = anchor.getAttribute('style') ?? '';
140
+
141
+ const colorMatch = /(?<![a-z-])color\s*:\s*([^;]+)/i.exec(style);
142
+ const bgMatch = /background-color\s*:\s*([^;]+)/i.exec(style);
143
+
144
+ const color = colorMatch?.[1]?.trim();
145
+ const bgColor = bgMatch?.[1]?.trim();
146
+
147
+ const hasColor = color !== undefined && !isDefaultBlack(color) && color !== 'inherit';
148
+ const hasBgColor = bgColor !== undefined && bgColor !== 'transparent' && bgColor !== 'inherit';
149
+
150
+ if (!hasColor && !hasBgColor) {
151
+ continue;
152
+ }
153
+
154
+ const mappedColor = hasColor ? mapToNearestPresetColor(color, 'text') : '';
155
+ const mappedBg = hasBgColor ? mapToNearestPresetColor(bgColor, 'bg') : '';
156
+
157
+ const colorStyles = [
158
+ hasColor ? `color: ${mappedColor}` : '',
159
+ resolveBackgroundStyle(hasBgColor, hasColor, mappedBg),
160
+ ].filter(Boolean).join('; ');
161
+
162
+ const el = anchor as HTMLElement;
163
+
164
+ el.innerHTML = `<mark style="${colorStyles};">${el.innerHTML}</mark>`;
165
+ el.style.removeProperty('color');
166
+ el.style.removeProperty('background-color');
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Convert `<p>` boundaries to `<br>` line breaks inside table cells.
172
+ *
173
+ * Google Docs wraps each line in a cell as a separate `<p>`. The sanitizer
174
+ * strips `<p>` (not in the allowed config), losing line breaks. Converting
175
+ * to `<br>` preserves them since `<br>` IS in the config (`{ br: {} }`).
176
+ *
177
+ * Only targets `<td>` and `<th>` elements — top-level `<p>` tags are left
178
+ * intact so the paste pipeline can split them into separate blocks.
179
+ */
180
+ function convertTableCellParagraphs(wrapper: HTMLElement): void {
181
+ for (const cell of Array.from(wrapper.querySelectorAll('td, th'))) {
182
+ const paragraphs = cell.querySelectorAll('p');
183
+
184
+ if (paragraphs.length === 0) {
185
+ continue;
186
+ }
187
+
188
+ for (const p of Array.from(paragraphs)) {
189
+ const fragment = document.createRange().createContextualFragment(p.innerHTML + '<br>');
190
+
191
+ p.replaceWith(fragment);
192
+ }
193
+
194
+ // Remove trailing <br> from the cell
195
+ cell.innerHTML = cell.innerHTML.replace(/(<br\s*\/?>|\s)+$/i, '');
196
+ }
197
+ }