@jackuait/blok 0.10.0-beta.9 → 0.10.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 (269) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-DDu252IK.mjs → blok-u_68bnlk.mjs} +1617 -1562
  3. package/dist/chunks/{constants-DMW9a31I.mjs → constants-VDhCUk4c.mjs} +56 -48
  4. package/dist/chunks/{i18next-loader-CwsYu0n6.mjs → i18next-loader-CDnSPae_.mjs} +1 -1
  5. package/dist/chunks/{lightweight-i18n-Cvv8CWh4.mjs → lightweight-i18n-DZmo8dAI.mjs} +1 -0
  6. package/dist/chunks/{messages-DG-4DPmP.mjs → messages-1_6UkKLS.mjs} +1 -0
  7. package/dist/{messages-CqXtJTpU.mjs → chunks/messages-4Ck88DYZ2.mjs} +1 -0
  8. package/dist/chunks/{messages-DGL1ySqb2.mjs → messages-8Ld7P_9j2.mjs} +1 -0
  9. package/dist/{messages-DLX_iBDJ.mjs → chunks/messages-BAlZjPcl.mjs} +1 -0
  10. package/dist/chunks/{messages-p1mbe__S.mjs → messages-BHMiK51R.mjs} +1 -0
  11. package/dist/chunks/{messages-Cdf0W9H02.mjs → messages-BJ-vT1SU2.mjs} +1 -0
  12. package/dist/{messages-Smt4GBbj.mjs → chunks/messages-BK8Cp2d0.mjs} +1 -0
  13. package/dist/{messages-Ci0KqX-J.mjs → chunks/messages-BKN3YVIj.mjs} +1 -0
  14. package/dist/chunks/{messages-BXM80fdr2.mjs → messages-BMD37y3q2.mjs} +1 -0
  15. package/dist/{messages-B19o-Teb.mjs → chunks/messages-BONyZroH.mjs} +1 -0
  16. package/dist/{messages-BwHs4cm1.mjs → chunks/messages-BRAoJpOu.mjs} +1 -0
  17. package/dist/{messages-DY4IqlhY.mjs → chunks/messages-BRoa9tGl.mjs} +1 -0
  18. package/dist/chunks/{messages-RInp1ytX.mjs → messages-BbEW9bQz.mjs} +1 -0
  19. package/dist/{messages-BIHc0KHY.mjs → chunks/messages-BeGZqQwz.mjs} +1 -0
  20. package/dist/{messages-CmB406HW.mjs → chunks/messages-BfAcUavP.mjs} +1 -0
  21. package/dist/chunks/{messages-Cu-Wevxs2.mjs → messages-BgM91Lxm2.mjs} +1 -0
  22. package/dist/{messages-BYNcD6uR.mjs → chunks/messages-BlxwW7M6.mjs} +1 -0
  23. package/dist/chunks/{messages-rCd0Rrw6.mjs → messages-Bz0-KNEB.mjs} +1 -0
  24. package/dist/{messages-7QD-X6XT2.mjs → chunks/messages-C0IFfhnp.mjs} +1 -0
  25. package/dist/{messages-Dl5Y2-Ia.mjs → chunks/messages-C15z2r5U.mjs} +1 -0
  26. package/dist/chunks/{messages-MxpWO1db.mjs → messages-C1S9ztpF.mjs} +1 -0
  27. package/dist/chunks/{messages-8IHf7ZP3.mjs → messages-CC_noR8y.mjs} +1 -0
  28. package/dist/chunks/{messages-COO5xmcA.mjs → messages-CD_MnBln.mjs} +1 -0
  29. package/dist/chunks/{messages-BYlSMRkd.mjs → messages-CIfUm1Oa.mjs} +1 -0
  30. package/dist/{messages-BbYq1pk-.mjs → chunks/messages-CPBN4zWc.mjs} +1 -0
  31. package/dist/{messages-DPA-mMWC2.mjs → chunks/messages-CQBo3lmL2.mjs} +1 -0
  32. package/dist/{messages-DnGJD4TL.mjs → chunks/messages-CRF7nNrO.mjs} +1 -0
  33. package/dist/{messages-D8FQWulF2.mjs → chunks/messages-CTCe595D2.mjs} +1 -0
  34. package/dist/{messages-BRZX964b2.mjs → chunks/messages-CW35K1pq.mjs} +1 -0
  35. package/dist/{messages-DnG0ef8t2.mjs → chunks/messages-CZSlfnkO2.mjs} +1 -0
  36. package/dist/chunks/{messages-iS34FHFB.mjs → messages-ChK7v1PV.mjs} +1 -0
  37. package/dist/{messages-BiUGXvYr2.mjs → chunks/messages-Clku7Cf-2.mjs} +1 -0
  38. package/dist/{messages-DIJlIqlQ2.mjs → chunks/messages-CszmHAvQ.mjs} +1 -0
  39. package/dist/chunks/{messages-DWu1r4gc2.mjs → messages-CvANwuht2.mjs} +1 -0
  40. package/dist/{messages-nUVjeh7K.mjs → chunks/messages-CxiURE2X.mjs} +1 -0
  41. package/dist/chunks/{messages-A_MkXDlG.mjs → messages-CxxyR4vY.mjs} +1 -0
  42. package/dist/{messages-ynAe7ewZ.mjs → chunks/messages-D22e9h7V2.mjs} +1 -0
  43. package/dist/{messages-DYTTu0O12.mjs → chunks/messages-D7dx_6k8.mjs} +1 -0
  44. package/dist/chunks/{messages-BA8Iv99Y2.mjs → messages-DBMaLL8b2.mjs} +1 -0
  45. package/dist/{messages-DbySKTKt2.mjs → chunks/messages-DB_-5Xln.mjs} +1 -0
  46. package/dist/{messages-CcF4y-E4.mjs → chunks/messages-DEBy3nuJ2.mjs} +1 -0
  47. package/dist/chunks/{messages-NEqrrYvE2.mjs → messages-DMoERagV2.mjs} +1 -0
  48. package/dist/chunks/{messages-Bxvi1ebN.mjs → messages-DPzHD51Y.mjs} +1 -0
  49. package/dist/chunks/{messages-jfVpL9c-2.mjs → messages-DSrdy9Nw2.mjs} +1 -0
  50. package/dist/chunks/{messages-Cmf6NhSC.mjs → messages-DTN1XGll.mjs} +1 -0
  51. package/dist/{messages-5jvKxQNu2.mjs → chunks/messages-DUeiPraX.mjs} +1 -0
  52. package/dist/chunks/{messages-G416eyjY.mjs → messages-DUr9WAkD.mjs} +1 -0
  53. package/dist/chunks/{messages-Ck81cQkn2.mjs → messages-DVr1sqfI2.mjs} +1 -0
  54. package/dist/{messages-BYmmMDrN2.mjs → chunks/messages-DjvaiALg2.mjs} +1 -0
  55. package/dist/chunks/{messages-D55HRx5O2.mjs → messages-DrfRYiM32.mjs} +1 -0
  56. package/dist/chunks/{messages-B2N4fUi72.mjs → messages-DtoId_bw2.mjs} +1 -0
  57. package/dist/{messages-Bq3F2Tp_.mjs → chunks/messages-Du2BffA7.mjs} +1 -0
  58. package/dist/{messages-CjbnogEC.mjs → chunks/messages-DxHh0O8j2.mjs} +1 -0
  59. package/dist/{messages-BECMxmfX.mjs → chunks/messages-EDMC5ukV.mjs} +1 -0
  60. package/dist/{messages-BTQPpoM42.mjs → chunks/messages-ElIGUi0O2.mjs} +1 -0
  61. package/dist/chunks/{messages-BhzzNkN-.mjs → messages-JSQjKQ8I.mjs} +1 -0
  62. package/dist/{messages-CWIXvnDf2.mjs → chunks/messages-Q7-4ZJLB2.mjs} +1 -0
  63. package/dist/chunks/{messages-DOuS1Qge.mjs → messages-QMOmwcZb.mjs} +1 -0
  64. package/dist/{messages-hWwSRF-2.mjs → chunks/messages-QilfinOn2.mjs} +1 -0
  65. package/dist/{messages-BmAn22OX.mjs → chunks/messages-a07QVz8U.mjs} +1 -0
  66. package/dist/chunks/{messages-BYxLFj7y.mjs → messages-eFd4YYzt.mjs} +1 -0
  67. package/dist/chunks/{messages-BSghd0ez.mjs → messages-euM2m3wQ.mjs} +1 -0
  68. package/dist/chunks/{messages-BVjoM7P0.mjs → messages-kGmxkeFH.mjs} +1 -0
  69. package/dist/{messages-DMr62KiO2.mjs → chunks/messages-oMc7qugU2.mjs} +1 -0
  70. package/dist/chunks/{messages-DzTk8bJ5.mjs → messages-sDdNf8O9.mjs} +1 -0
  71. package/dist/{messages-Bm0Feca1.mjs → chunks/messages-wl8YrvGG.mjs} +1 -0
  72. package/dist/chunks/{messages-BAsb5CgZ.mjs → messages-zt6zdYWh.mjs} +1 -0
  73. package/dist/chunks/{tools-XmzH2rgQ.mjs → tools-1ZFajlGN.mjs} +1619 -1307
  74. package/dist/full.mjs +3 -3
  75. package/dist/locales.mjs +68 -67
  76. package/dist/{messages-F2xRoY1w.mjs → messages-2ZWBTerL.mjs} +1 -0
  77. package/dist/{messages-Dl3Sv6Rq2.mjs → messages-53w0fPZS2.mjs} +1 -0
  78. package/dist/{chunks/messages-BDZA10kl2.mjs → messages-98nQiC7t2.mjs} +1 -0
  79. package/dist/{chunks/messages-JyvWu4rf2.mjs → messages-A96tMxeU.mjs} +1 -0
  80. package/dist/{messages-Ce6KVEbT.mjs → messages-BE_z-zrb.mjs} +1 -0
  81. package/dist/{chunks/messages-CSJ_zb3a2.mjs → messages-BK_LsgY4.mjs} +1 -0
  82. package/dist/{messages-CJTy6JZt.mjs → messages-BbJ7ZXY8.mjs} +1 -0
  83. package/dist/{chunks/messages-DMVXnAYj.mjs → messages-BcVB3osF.mjs} +1 -0
  84. package/dist/{chunks/messages-CSL-6xfb2.mjs → messages-BckDk9aq2.mjs} +1 -0
  85. package/dist/{chunks/messages-C0HvoMPb.mjs → messages-Be_2RHZD.mjs} +1 -0
  86. package/dist/{chunks/messages-Dr0Ekmbz.mjs → messages-BesJaI6A.mjs} +1 -0
  87. package/dist/{chunks/messages-D3zojZ94.mjs → messages-BiTMwiKH.mjs} +1 -0
  88. package/dist/{messages-B1ZUQagA.mjs → messages-BmH2cQHQ.mjs} +1 -0
  89. package/dist/{chunks/messages-Bfnq1xv4.mjs → messages-BrOWqNCu2.mjs} +1 -0
  90. package/dist/{messages-Dnp9N6RU2.mjs → messages-Brd5R-da2.mjs} +1 -0
  91. package/dist/{chunks/messages-DJoNVjqP.mjs → messages-C0GSBBCo2.mjs} +1 -0
  92. package/dist/{messages-Dw__BcTj.mjs → messages-C1vc5584.mjs} +1 -0
  93. package/dist/{messages-aMXpHt5X2.mjs → messages-C6ONf71u2.mjs} +1 -0
  94. package/dist/{chunks/messages-BeFqtIrc2.mjs → messages-C7lJg8fy2.mjs} +1 -0
  95. package/dist/{messages-CSUHBs4c2.mjs → messages-CRNogopy2.mjs} +1 -0
  96. package/dist/{messages-DLlc9QPw.mjs → messages-CT-Kdas6.mjs} +1 -0
  97. package/dist/{chunks/messages-DlLXpgWM2.mjs → messages-CTTmWn4Y2.mjs} +1 -0
  98. package/dist/{messages-D0aw5_0k2.mjs → messages-CZbcxlZt2.mjs} +1 -0
  99. package/dist/{chunks/messages-Bp8qin1R.mjs → messages-C_Qn9SbQ.mjs} +1 -0
  100. package/dist/{messages-96iaAUds2.mjs → messages-CdEASHDp2.mjs} +1 -0
  101. package/dist/{messages-Dy-Y_nEI.mjs → messages-CdduYw-q.mjs} +1 -0
  102. package/dist/{chunks/messages-Je5YvxiY.mjs → messages-Che99vKP.mjs} +1 -0
  103. package/dist/{messages-nlhESX9t.mjs → messages-CisR4PNV.mjs} +1 -0
  104. package/dist/{chunks/messages-BE6lHKwf.mjs → messages-ClGvlFcH2.mjs} +1 -0
  105. package/dist/{chunks/messages-FWfsxpBz.mjs → messages-CnuH-BZK2.mjs} +1 -0
  106. package/dist/{chunks/messages-aZcy0JQq2.mjs → messages-D0005ti32.mjs} +1 -0
  107. package/dist/{messages-B7ieAJBd2.mjs → messages-D05jqBIa2.mjs} +1 -0
  108. package/dist/{messages-DTh9a8mR.mjs → messages-D0lLw9KM.mjs} +1 -0
  109. package/dist/{chunks/messages-ihCjSFJI2.mjs → messages-D3rwCtKn.mjs} +1 -0
  110. package/dist/{chunks/messages-xuqyb6Ff2.mjs → messages-D6VIFnSW.mjs} +1 -0
  111. package/dist/{chunks/messages-KdawW5Na.mjs → messages-D81w6AmW.mjs} +1 -0
  112. package/dist/{chunks/messages-BUVhHx0q2.mjs → messages-DBhvm8NK.mjs} +1 -0
  113. package/dist/{chunks/messages-C7VGpihw.mjs → messages-DK6dA0O2.mjs} +1 -0
  114. package/dist/{messages-rk-A1Wa42.mjs → messages-DKHbt-7l2.mjs} +1 -0
  115. package/dist/{messages-BIoeoik5.mjs → messages-DM4Gjc9h.mjs} +1 -0
  116. package/dist/{chunks/messages-B9kmbUWV.mjs → messages-DODrhcop.mjs} +1 -0
  117. package/dist/{messages-BsycN_JI2.mjs → messages-DOGbHYv-2.mjs} +1 -0
  118. package/dist/{chunks/messages-BQYvBqm2.mjs → messages-DQORja0D.mjs} +1 -0
  119. package/dist/{chunks/messages-CVdpweyf2.mjs → messages-DSmxJWju2.mjs} +1 -0
  120. package/dist/{messages-CR_L_UtK.mjs → messages-DVL0KZE5.mjs} +1 -0
  121. package/dist/{chunks/messages-Cs81Z_Bh.mjs → messages-DYuD5-rO.mjs} +1 -0
  122. package/dist/{chunks/messages-CKBhDGI3.mjs → messages-Ddq3Ce3E2.mjs} +1 -0
  123. package/dist/{messages-xh2eOLvs.mjs → messages-DfFZ6Yj5.mjs} +1 -0
  124. package/dist/{chunks/messages-C6Mpiacw.mjs → messages-Dnd5YSWv.mjs} +1 -0
  125. package/dist/{messages-BJeGJksD.mjs → messages-Do7Xjy0n.mjs} +1 -0
  126. package/dist/{chunks/messages-C3aX3q0H.mjs → messages-DopaMHC42.mjs} +1 -0
  127. package/dist/{messages-dv19AkyJ.mjs → messages-DpJGbx3q.mjs} +1 -0
  128. package/dist/{messages-DBiVgUs2.mjs → messages-DpwMKDV0.mjs} +1 -0
  129. package/dist/{messages-j7o5rT9s.mjs → messages-Dqu4aX9s.mjs} +1 -0
  130. package/dist/{chunks/messages-BjadX8jR2.mjs → messages-E8NjqzWq2.mjs} +1 -0
  131. package/dist/{messages-aWZH50vu2.mjs → messages-JNrYldAa2.mjs} +1 -0
  132. package/dist/{chunks/messages-B4UMuyjT.mjs → messages-LMaR2_bE.mjs} +1 -0
  133. package/dist/{messages-E_ZuzGDt.mjs → messages-LYJbLq_F.mjs} +1 -0
  134. package/dist/{chunks/messages-D9N2MvQx2.mjs → messages-Q5sQeVap2.mjs} +1 -0
  135. package/dist/{chunks/messages-Bphq_Bt3.mjs → messages-Xc0KUbYl.mjs} +1 -0
  136. package/dist/{chunks/messages-DlonA3wa.mjs → messages-_PLyRfVw.mjs} +1 -0
  137. package/dist/{chunks/messages-TRUuyiFB.mjs → messages-apA6BStA.mjs} +1 -0
  138. package/dist/{chunks/messages-B0vPBsWq.mjs → messages-bkGniiaz.mjs} +1 -0
  139. package/dist/{messages-DkLU_rWm.mjs → messages-neGD3WGq.mjs} +1 -0
  140. package/dist/{messages-Ddnj2iTG2.mjs → messages-qbKjjvgd2.mjs} +1 -0
  141. package/dist/{messages-BiiongNz2.mjs → messages-qfvXgPpu2.mjs} +1 -0
  142. package/dist/{messages-Dvn35ksS.mjs → messages-uwK7ktqk.mjs} +1 -0
  143. package/dist/react.mjs +2 -2
  144. package/dist/tools.mjs +2 -2
  145. package/package.json +3 -5
  146. package/src/cli/commands/convert-gdocs/index.ts +26 -0
  147. package/src/cli/commands/convert-html/block-builder.ts +392 -0
  148. package/src/cli/commands/convert-html/id-generator.ts +11 -0
  149. package/src/cli/commands/convert-html/index.ts +23 -0
  150. package/src/cli/commands/convert-html/preprocessor.ts +422 -0
  151. package/src/cli/commands/convert-html/sanitizer.ts +93 -0
  152. package/src/cli/commands/convert-html/types.ts +15 -0
  153. package/src/cli/index.ts +56 -5
  154. package/src/components/block/index.ts +58 -10
  155. package/src/components/constants/data-attributes.ts +10 -0
  156. package/src/components/i18n/locales/am/messages.json +1 -0
  157. package/src/components/i18n/locales/ar/messages.json +1 -0
  158. package/src/components/i18n/locales/az/messages.json +1 -0
  159. package/src/components/i18n/locales/bg/messages.json +1 -0
  160. package/src/components/i18n/locales/bn/messages.json +1 -0
  161. package/src/components/i18n/locales/bs/messages.json +1 -0
  162. package/src/components/i18n/locales/cs/messages.json +1 -0
  163. package/src/components/i18n/locales/da/messages.json +1 -0
  164. package/src/components/i18n/locales/de/messages.json +1 -0
  165. package/src/components/i18n/locales/dv/messages.json +1 -0
  166. package/src/components/i18n/locales/el/messages.json +1 -0
  167. package/src/components/i18n/locales/en/messages.json +1 -0
  168. package/src/components/i18n/locales/es/messages.json +1 -0
  169. package/src/components/i18n/locales/et/messages.json +1 -0
  170. package/src/components/i18n/locales/fa/messages.json +1 -0
  171. package/src/components/i18n/locales/fi/messages.json +1 -0
  172. package/src/components/i18n/locales/fil/messages.json +1 -0
  173. package/src/components/i18n/locales/fr/messages.json +1 -0
  174. package/src/components/i18n/locales/gu/messages.json +1 -0
  175. package/src/components/i18n/locales/he/messages.json +1 -0
  176. package/src/components/i18n/locales/hi/messages.json +1 -0
  177. package/src/components/i18n/locales/hr/messages.json +1 -0
  178. package/src/components/i18n/locales/hu/messages.json +1 -0
  179. package/src/components/i18n/locales/hy/messages.json +1 -0
  180. package/src/components/i18n/locales/id/messages.json +1 -0
  181. package/src/components/i18n/locales/it/messages.json +1 -0
  182. package/src/components/i18n/locales/ja/messages.json +1 -0
  183. package/src/components/i18n/locales/ka/messages.json +1 -0
  184. package/src/components/i18n/locales/km/messages.json +1 -0
  185. package/src/components/i18n/locales/kn/messages.json +1 -0
  186. package/src/components/i18n/locales/ko/messages.json +1 -0
  187. package/src/components/i18n/locales/ku/messages.json +1 -0
  188. package/src/components/i18n/locales/lo/messages.json +1 -0
  189. package/src/components/i18n/locales/lt/messages.json +1 -0
  190. package/src/components/i18n/locales/lv/messages.json +1 -0
  191. package/src/components/i18n/locales/mk/messages.json +1 -0
  192. package/src/components/i18n/locales/ml/messages.json +1 -0
  193. package/src/components/i18n/locales/mn/messages.json +1 -0
  194. package/src/components/i18n/locales/mr/messages.json +1 -0
  195. package/src/components/i18n/locales/ms/messages.json +1 -0
  196. package/src/components/i18n/locales/my/messages.json +1 -0
  197. package/src/components/i18n/locales/ne/messages.json +1 -0
  198. package/src/components/i18n/locales/nl/messages.json +1 -0
  199. package/src/components/i18n/locales/no/messages.json +1 -0
  200. package/src/components/i18n/locales/pa/messages.json +1 -0
  201. package/src/components/i18n/locales/pl/messages.json +1 -0
  202. package/src/components/i18n/locales/ps/messages.json +1 -0
  203. package/src/components/i18n/locales/pt/messages.json +1 -0
  204. package/src/components/i18n/locales/ro/messages.json +1 -0
  205. package/src/components/i18n/locales/ru/messages.json +1 -0
  206. package/src/components/i18n/locales/sd/messages.json +1 -0
  207. package/src/components/i18n/locales/si/messages.json +1 -0
  208. package/src/components/i18n/locales/sk/messages.json +1 -0
  209. package/src/components/i18n/locales/sl/messages.json +1 -0
  210. package/src/components/i18n/locales/sq/messages.json +1 -0
  211. package/src/components/i18n/locales/sr/messages.json +1 -0
  212. package/src/components/i18n/locales/sv/messages.json +1 -0
  213. package/src/components/i18n/locales/sw/messages.json +1 -0
  214. package/src/components/i18n/locales/ta/messages.json +1 -0
  215. package/src/components/i18n/locales/te/messages.json +1 -0
  216. package/src/components/i18n/locales/th/messages.json +1 -0
  217. package/src/components/i18n/locales/tr/messages.json +1 -0
  218. package/src/components/i18n/locales/ug/messages.json +1 -0
  219. package/src/components/i18n/locales/uk/messages.json +1 -0
  220. package/src/components/i18n/locales/ur/messages.json +1 -0
  221. package/src/components/i18n/locales/vi/messages.json +1 -0
  222. package/src/components/i18n/locales/yi/messages.json +1 -0
  223. package/src/components/i18n/locales/zh/messages.json +1 -0
  224. package/src/components/icons/index.ts +29 -18
  225. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +18 -0
  226. package/src/components/modules/blockManager/hierarchy.ts +4 -1
  227. package/src/components/modules/readonly.ts +46 -0
  228. package/src/components/modules/rectangleSelection.ts +25 -5
  229. package/src/components/modules/toolbar/index.ts +96 -19
  230. package/src/components/modules/toolbar/positioning.ts +11 -2
  231. package/src/components/modules/toolbar/styles.ts +0 -2
  232. package/src/components/modules/uiControllers/controllers/blockHover.ts +44 -1
  233. package/src/components/tools/block.ts +10 -0
  234. package/src/components/utils/placeholder.ts +9 -2
  235. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +11 -0
  236. package/src/components/utils/popover/popover-abstract.ts +7 -0
  237. package/src/styles/main.css +16 -0
  238. package/src/tools/callout/constants.ts +2 -1
  239. package/src/tools/callout/dom-builder.ts +13 -1
  240. package/src/tools/callout/index.ts +21 -7
  241. package/src/tools/code/constants.ts +28 -8
  242. package/src/tools/code/dom-builder.ts +133 -64
  243. package/src/tools/code/index.ts +280 -91
  244. package/src/tools/code/language-detector.ts +118 -0
  245. package/src/tools/divider/index.ts +5 -0
  246. package/src/tools/header/index.ts +47 -1
  247. package/src/tools/list/dom-builder.ts +3 -1
  248. package/src/tools/list/index.ts +55 -3
  249. package/src/tools/list/list-helpers.ts +2 -2
  250. package/src/tools/nested-blocks.ts +25 -0
  251. package/src/tools/paragraph/index.ts +47 -6
  252. package/src/tools/quote/index.ts +43 -8
  253. package/src/tools/stub/index.ts +10 -0
  254. package/src/tools/table/index.ts +238 -6
  255. package/src/tools/table/table-add-controls.ts +37 -5
  256. package/src/tools/table/table-cell-blocks.ts +57 -18
  257. package/src/tools/table/table-core.ts +2 -0
  258. package/src/tools/table/table-corner-drag.ts +247 -0
  259. package/src/tools/table/table-operations.ts +41 -14
  260. package/src/tools/toggle/dom-builder.ts +1 -0
  261. package/src/tools/toggle/index.ts +25 -0
  262. package/src/tools/toggle/toggle-lifecycle.ts +5 -4
  263. package/src/types-internal/jsdom.d.ts +9 -0
  264. package/types/tools/adapters/block-tool-adapter.d.ts +6 -0
  265. package/types/tools/block-tool.d.ts +20 -0
  266. package/types/utils/popover/popover-item.d.ts +6 -0
  267. package/bin/blok.mjs +0 -10
  268. package/dist/cli.mjs +0 -37
  269. package/src/tools/code/language-picker.ts +0 -241
@@ -10,34 +10,37 @@ import type {
10
10
  } from '../../../types';
11
11
  import type { MenuConfig } from '../../../types/tools/menu-config';
12
12
  import type { CodeData } from '../../../types/tools/code';
13
- import { IconCodeBlock } from '../../components/icons';
14
- import { buildCodeDOM } from './dom-builder';
13
+ import { IconCodeBlock, IconCheck, IconWand } from '../../components/icons';
14
+ import { buildCodeDOM, setActiveViewMode } from './dom-builder';
15
15
  import type { CodeDOMRefs } from './dom-builder';
16
16
  import { handleCodeKeydown } from './code-keyboard';
17
- import { LanguagePicker } from './language-picker';
17
+ import { PopoverDesktop } from '../../components/utils/popover';
18
+ import { onHover as tooltipOnHover } from '../../components/utils/tooltip';
19
+ import type { PopoverItemParams } from '@/types/utils/popover/popover-item';
20
+ import { PopoverItemType } from '@/types/utils/popover/popover-item-type';
18
21
  import {
19
22
  DEFAULT_LANGUAGE,
20
23
  LANGUAGES,
21
- CODE_AREA_STYLES,
22
24
  COPY_CODE_KEY,
23
- WRAP_LINES_KEY,
24
- LINE_NUMBERS_KEY,
25
25
  COPIED_KEY,
26
26
  LANGUAGE_KEY,
27
+ SEARCH_LANGUAGE_KEY,
27
28
  COPIED_FEEDBACK_STYLES,
28
29
  PREVIEWABLE_LANGUAGES,
29
30
  CODE_TAB_KEY,
30
31
  PREVIEW_TAB_KEY,
31
- TAB_STYLES,
32
- TAB_ACTIVE_STYLES,
33
- TAB_INACTIVE_STYLES,
32
+ SIDE_BY_SIDE_KEY,
34
33
  PREVIEW_AREA_STYLES,
35
34
  GUTTER_LINE_STYLES,
35
+ SPLIT_CONTAINER_STYLES,
36
+ SPLIT_CONTAINER_SPLIT_STYLES,
36
37
  } from './constants';
38
+ import type { CodeViewMode } from './constants';
37
39
  import { renderLatex } from './katex-loader';
38
40
  import { renderMermaid } from './mermaid-loader';
39
41
  import { tokenizeCode, isHighlightable } from './shiki-loader';
40
42
  import { applyHighlights, isHighlightingSupported } from './highlight-applier';
43
+ import { detectLanguage } from './language-detector';
41
44
 
42
45
  const COPIED_FEEDBACK_DURATION = 1500;
43
46
 
@@ -46,13 +49,14 @@ export class CodeTool implements BlockTool {
46
49
  private readOnly: boolean;
47
50
  private _data: CodeData;
48
51
  private _dom: CodeDOMRefs | null = null;
49
- private _wrapping = true;
50
52
  private _lineNumbers = true;
51
- private _picker: LanguagePicker | null = null;
52
- private _previewActive = false;
53
+ private _picker: PopoverDesktop | null = null;
54
+ private _viewMode: CodeViewMode = 'preview';
53
55
  private _previewContainer: HTMLElement | null = null;
54
56
  private _disposeHighlights: (() => void) | null = null;
55
57
  private _highlightRafId: number | null = null;
58
+ private _detectedLanguage: string | null = null;
59
+ private _detectionTimeoutId: ReturnType<typeof setTimeout> | null = null;
56
60
 
57
61
  constructor({ data, api, readOnly }: BlockToolConstructorOptions<CodeData>) {
58
62
  this.api = api;
@@ -73,20 +77,20 @@ export class CodeTool implements BlockTool {
73
77
  languageName: this.getLanguageName(this._data.language),
74
78
  readOnly: this.readOnly,
75
79
  copyLabel: this.api.i18n.t(COPY_CODE_KEY),
76
- wrapLabel: this.api.i18n.t(WRAP_LINES_KEY),
77
- lineNumbersLabel: this.api.i18n.t(LINE_NUMBERS_KEY),
78
80
  previewable: this.readOnly ? false : isPreviewable,
79
- codeTabLabel: this.api.i18n.t(CODE_TAB_KEY),
80
- previewTabLabel: this.api.i18n.t(PREVIEW_TAB_KEY),
81
+ viewModeLabels: this.readOnly ? undefined : {
82
+ code: this.api.i18n.t(CODE_TAB_KEY),
83
+ preview: this.api.i18n.t(PREVIEW_TAB_KEY),
84
+ split: this.api.i18n.t(SIDE_BY_SIDE_KEY),
85
+ },
81
86
  });
82
87
 
83
88
  this._dom = dom;
84
89
 
85
90
  // Line numbers gutter visibility
86
91
  dom.gutterElement.hidden = !this._lineNumbers;
87
- dom.lineNumbersButton.addEventListener('click', () => this.toggleLineNumbers());
88
92
 
89
- // Read-only + previewable: show preview only, hide code, no tabs
93
+ // Read-only + previewable: show preview only, hide code, no toggle
90
94
  if (this.readOnly && isPreviewable) {
91
95
  const previewEl = document.createElement('div');
92
96
 
@@ -99,17 +103,33 @@ export class CodeTool implements BlockTool {
99
103
  void this.renderPreview();
100
104
  }
101
105
 
102
- // Edit mode + previewable: show tabs, default to preview
103
- if (!this.readOnly && isPreviewable && dom.codeTab && dom.previewTab && dom.previewElement) {
104
- this._previewActive = true;
105
- dom.preElement.hidden = true;
106
- dom.gutterElement.hidden = true;
107
- dom.previewElement.hidden = false;
106
+ // Edit mode + previewable: default to preview mode and render
107
+ if (!this.readOnly && isPreviewable && dom.previewElement) {
108
+ this._viewMode = 'preview';
108
109
  this._previewContainer = dom.previewElement;
110
+
111
+ // Apply initial state: preview mode
112
+ this.applyViewMode();
109
113
  void this.renderPreview();
114
+ }
115
+
116
+ // Edit mode: wire view mode button listeners (buttons always present in edit mode)
117
+ if (!this.readOnly && dom.viewModeContainer) {
118
+ const modeButtons = Array.from(dom.viewModeContainer.querySelectorAll<HTMLButtonElement>('[data-mode]'));
110
119
 
111
- dom.codeTab.addEventListener('click', () => this.showCode());
112
- dom.previewTab.addEventListener('click', () => this.showPreview());
120
+ for (const btn of modeButtons) {
121
+ const label = btn.getAttribute('aria-label') ?? '';
122
+
123
+ tooltipOnHover(btn, label, { placement: 'bottom' });
124
+
125
+ btn.addEventListener('click', () => {
126
+ const mode = btn.getAttribute('data-mode') as CodeViewMode;
127
+
128
+ if (mode && mode !== this._viewMode) {
129
+ this.setViewMode(mode);
130
+ }
131
+ });
132
+ }
113
133
  }
114
134
 
115
135
  if (!this.readOnly) {
@@ -121,6 +141,7 @@ export class CodeTool implements BlockTool {
121
141
  this.syncTrailingBr();
122
142
  this.updateGutter();
123
143
  this.scheduleHighlight();
144
+ this.scheduleDetection();
124
145
  }
125
146
  });
126
147
 
@@ -128,65 +149,99 @@ export class CodeTool implements BlockTool {
128
149
  this.syncTrailingBr();
129
150
  this.updateGutter();
130
151
  this.scheduleHighlight();
152
+ this.scheduleDetection();
131
153
  });
132
154
  }
133
155
 
134
156
  dom.copyButton.addEventListener('click', () => this.copyCode());
135
- dom.wrapButton.addEventListener('click', () => this.toggleWrap());
157
+ tooltipOnHover(dom.copyButton, this.api.i18n.t(COPY_CODE_KEY), { placement: 'bottom' });
136
158
 
137
159
  if (!this.readOnly) {
138
- this._picker = new LanguagePicker({
139
- languages: LANGUAGES,
140
- onSelect: (id: string) => this.setLanguage(id),
141
- i18n: this.api.i18n,
142
- activeLanguageId: this._data.language,
143
- });
144
-
145
- document.body.appendChild(this._picker.getElement());
160
+ this._picker = this.buildLanguagePicker(dom.languageButton, dom.wrapper);
146
161
 
147
162
  dom.languageButton.addEventListener('click', () => {
148
- this._picker?.open(dom.languageButton);
163
+ if (this._picker?.isShown) {
164
+ this._picker.hide();
165
+ } else {
166
+ this._picker?.show();
167
+ }
149
168
  });
150
169
  }
151
170
 
152
171
  return dom.wrapper;
153
172
  }
154
173
 
174
+ /**
175
+ * Returns the wrapper element as the toolbar anchor so the toolbar
176
+ * centers on the block's visual top, not on the deeply nested
177
+ * contenteditable code element below the header bar.
178
+ */
179
+ public getToolbarAnchorElement(): HTMLElement | undefined {
180
+ return this._dom?.wrapper;
181
+ }
182
+
155
183
  public rendered(): void {
156
184
  void this.highlightCode();
157
185
  }
158
186
 
159
- private showCode(): void {
160
- if (!this._dom?.previewElement || !this._dom.codeTab || !this._dom.previewTab) {
161
- return;
162
- }
187
+ private setViewMode(mode: CodeViewMode): void {
188
+ this._viewMode = mode;
189
+ this.applyViewMode();
163
190
 
164
- this._previewActive = false;
165
- this._dom.preElement.hidden = false;
166
- this._dom.gutterElement.hidden = !this._lineNumbers;
167
- this._dom.previewElement.hidden = true;
168
- this._dom.codeTab.className = `${TAB_STYLES} ${TAB_ACTIVE_STYLES}`;
169
- this._dom.previewTab.className = `${TAB_STYLES} ${TAB_INACTIVE_STYLES}`;
191
+ if (mode === 'preview' || mode === 'split') {
192
+ void this.renderPreview();
193
+ }
170
194
  }
171
195
 
172
- private showPreview(): void {
173
- if (!this._dom?.previewElement || !this._dom.codeTab || !this._dom.previewTab) {
196
+ private applyViewMode(): void {
197
+ if (!this._dom?.previewElement || !this._dom.viewModeContainer || !this._dom.splitContainer) {
174
198
  return;
175
199
  }
176
200
 
177
- this._previewActive = true;
178
- this._dom.preElement.hidden = true;
179
- this._dom.gutterElement.hidden = true;
180
- this._dom.previewElement.hidden = false;
181
- this._dom.codeTab.className = `${TAB_STYLES} ${TAB_INACTIVE_STYLES}`;
182
- this._dom.previewTab.className = `${TAB_STYLES} ${TAB_ACTIVE_STYLES}`;
201
+ // Update segmented control active state
202
+ setActiveViewMode(this._dom.viewModeContainer, this._viewMode);
203
+
204
+ const codeBody = this._dom.preElement.parentElement?.parentElement;
183
205
 
184
- // Re-render preview with current code content
185
- void this.renderPreview();
206
+ switch (this._viewMode) {
207
+ case 'code':
208
+ this._dom.preElement.hidden = false;
209
+ this._dom.gutterElement.hidden = !this._lineNumbers;
210
+ this._dom.previewElement.hidden = true;
211
+ if (codeBody) {
212
+ codeBody.hidden = false;
213
+ }
214
+ this._dom.splitContainer.className = SPLIT_CONTAINER_STYLES;
215
+ break;
216
+
217
+ case 'preview':
218
+ this._dom.preElement.hidden = true;
219
+ this._dom.gutterElement.hidden = true;
220
+ this._dom.previewElement.hidden = false;
221
+ if (codeBody) {
222
+ codeBody.hidden = true;
223
+ }
224
+ this._dom.splitContainer.className = SPLIT_CONTAINER_STYLES;
225
+ break;
226
+
227
+ case 'split':
228
+ this._dom.preElement.hidden = false;
229
+ this._dom.gutterElement.hidden = !this._lineNumbers;
230
+ this._dom.previewElement.hidden = false;
231
+ if (codeBody) {
232
+ codeBody.hidden = false;
233
+ }
234
+ this._dom.splitContainer.className = SPLIT_CONTAINER_SPLIT_STYLES;
235
+ break;
236
+ }
186
237
  }
187
238
 
188
239
  private async renderPreview(): Promise<void> {
189
- if (!this._previewContainer) {
240
+ // Capture the container reference before the async gap so that if the language
241
+ // changes mid-flight (nulling _previewContainer), we don't write to null.
242
+ const container = this._previewContainer;
243
+
244
+ if (!container) {
190
245
  return;
191
246
  }
192
247
 
@@ -195,7 +250,23 @@ export class CodeTool implements BlockTool {
195
250
  ? await renderMermaid(code)
196
251
  : await renderLatex(code);
197
252
 
198
- this._previewContainer.innerHTML = rendered;
253
+ container.innerHTML = rendered;
254
+ }
255
+
256
+ public setReadOnly(state: boolean): void {
257
+ this.readOnly = state;
258
+
259
+ if (!this._dom) {
260
+ return;
261
+ }
262
+
263
+ if (state) {
264
+ this._dom.codeElement.setAttribute('contenteditable', 'false');
265
+ this._dom.codeElement.removeAttribute('spellcheck');
266
+ } else {
267
+ this._dom.codeElement.setAttribute('contenteditable', 'plaintext-only');
268
+ this._dom.codeElement.setAttribute('spellcheck', 'false');
269
+ }
199
270
  }
200
271
 
201
272
  public save(_blockContent: HTMLElement): CodeData {
@@ -221,19 +292,40 @@ export class CodeTool implements BlockTool {
221
292
  }
222
293
 
223
294
  public renderSettings(): MenuConfig {
295
+ const selectedId = this._data.language;
296
+ const detectedId = this._detectedLanguage;
297
+ const showDetected = detectedId !== null && detectedId !== selectedId;
298
+
299
+ const childItems: PopoverItemParams[] = [];
300
+
301
+ if (showDetected) {
302
+ const detectedLanguage = LANGUAGES.find((lang) => lang.id === detectedId);
303
+ if (detectedLanguage) {
304
+ childItems.push({
305
+ title: detectedLanguage.name,
306
+ secondaryLabel: 'auto',
307
+ icon: IconWand,
308
+ onActivate: (): void => this.setLanguage(detectedLanguage.id),
309
+ closeOnActivate: true,
310
+ isActive: (): boolean => this._data.language === detectedLanguage.id,
311
+ });
312
+ childItems.push({ type: PopoverItemType.Separator });
313
+ }
314
+ }
315
+
316
+ childItems.push(...LANGUAGES.map((lang) => ({
317
+ title: lang.name,
318
+ trailingIcon: lang.id === selectedId ? IconCheck : undefined,
319
+ onActivate: (): void => this.setLanguage(lang.id),
320
+ closeOnActivate: true,
321
+ })));
322
+
224
323
  return [
225
324
  {
226
325
  icon: IconCodeBlock,
227
326
  title: this.api.i18n.t(LANGUAGE_KEY),
228
327
  name: 'code-language',
229
- children: {
230
- items: LANGUAGES.map((lang) => ({
231
- title: lang.name,
232
- onActivate: (): void => this.setLanguage(lang.id),
233
- closeOnActivate: true,
234
- isActive: this._data.language === lang.id,
235
- })),
236
- },
328
+ children: { items: childItems },
237
329
  },
238
330
  ];
239
331
  }
@@ -263,15 +355,103 @@ export class CodeTool implements BlockTool {
263
355
 
264
356
  private setLanguage(id: string): void {
265
357
  this._data.language = id;
358
+ const isPreviewable = PREVIEWABLE_LANGUAGES.has(id);
266
359
 
267
360
  if (this._dom) {
268
- this._dom.languageButton.textContent = this.getLanguageName(id);
361
+ // Update the text span inside the language button (first child)
362
+ const textSpan = this._dom.languageButton.querySelector('span');
363
+
364
+ if (textSpan) {
365
+ textSpan.textContent = this.getLanguageName(id);
366
+ }
367
+
368
+ // Show or hide the view mode segmented control based on previewability
369
+ if (this._dom.viewModeContainer) {
370
+ this._dom.viewModeContainer.hidden = !isPreviewable;
371
+ }
372
+
373
+ // When switching to a previewable language, activate preview mode
374
+ if (isPreviewable && this._dom.previewElement) {
375
+ this._previewContainer = this._dom.previewElement;
376
+ this._viewMode = 'preview';
377
+ this.applyViewMode();
378
+ void this.renderPreview();
379
+ }
380
+
381
+ // When switching away from a previewable language, reset to code mode
382
+ if (!isPreviewable) {
383
+ this._previewContainer = null;
384
+ this._viewMode = 'code';
385
+ this.applyViewMode();
386
+ }
387
+
388
+ // Rebuild the language picker so the selected language check icon updates
389
+ if (this._picker) {
390
+ this._picker.destroy();
391
+ }
392
+ this._picker = this.buildLanguagePicker(this._dom.languageButton, this._dom.wrapper);
269
393
  }
270
394
 
271
- this._picker?.setActiveLanguage(id);
272
395
  void this.highlightCode();
273
396
  }
274
397
 
398
+ /**
399
+ * Builds the language items array. When a detected language differs from the
400
+ * chosen one, it appears first with a wand icon and "auto" secondary label.
401
+ * The currently selected language is shown with a trailing check icon
402
+ * in its natural position in the full language list.
403
+ */
404
+ private buildLanguagePickerItems(): PopoverItemParams[] {
405
+ const selectedId = this._data.language;
406
+ const detectedId = this._detectedLanguage;
407
+ const showDetected = detectedId !== null && detectedId !== selectedId;
408
+
409
+ const items: PopoverItemParams[] = [];
410
+
411
+ if (showDetected) {
412
+ const detectedLanguage = LANGUAGES.find((lang) => lang.id === detectedId);
413
+ if (detectedLanguage) {
414
+ items.push({
415
+ title: detectedLanguage.name,
416
+ name: detectedLanguage.id,
417
+ icon: IconWand,
418
+ toggle: 'language',
419
+ isActive: (): boolean => this._data.language === detectedLanguage.id,
420
+ closeOnActivate: true,
421
+ onActivate: (): void => this.setLanguage(detectedLanguage.id),
422
+ });
423
+ items.push({ type: PopoverItemType.Separator });
424
+ }
425
+ }
426
+
427
+ items.push(...LANGUAGES.map((lang) => ({
428
+ title: lang.name,
429
+ name: lang.id,
430
+ trailingIcon: lang.id === selectedId ? IconCheck : undefined,
431
+ toggle: 'language',
432
+ closeOnActivate: true,
433
+ onActivate: (): void => this.setLanguage(lang.id),
434
+ })));
435
+
436
+ return items;
437
+ }
438
+
439
+ /**
440
+ * Creates a new PopoverDesktop instance for the language picker.
441
+ */
442
+ private buildLanguagePicker(trigger: HTMLElement, leftAlignElement: HTMLElement): PopoverDesktop {
443
+ return new PopoverDesktop({
444
+ items: this.buildLanguagePickerItems(),
445
+ trigger,
446
+ leftAlignElement,
447
+ searchable: true,
448
+ width: '200px',
449
+ messages: {
450
+ search: this.api.i18n.t(SEARCH_LANGUAGE_KEY),
451
+ },
452
+ });
453
+ }
454
+
275
455
  private getLanguageName(id: string): string {
276
456
  const entry = LANGUAGES.find((lang) => lang.id === id);
277
457
 
@@ -297,28 +477,6 @@ export class CodeTool implements BlockTool {
297
477
  }).catch(() => { /* clipboard unavailable */ });
298
478
  }
299
479
 
300
- private toggleWrap(): void {
301
- this._wrapping = !this._wrapping;
302
-
303
- if (!this._dom) {
304
- return;
305
- }
306
-
307
- if (this._wrapping) {
308
- this._dom.codeElement.className = CODE_AREA_STYLES;
309
- } else {
310
- this._dom.codeElement.className = CODE_AREA_STYLES.replace('whitespace-pre-wrap', 'whitespace-pre');
311
- }
312
- }
313
-
314
- private toggleLineNumbers(): void {
315
- this._lineNumbers = !this._lineNumbers;
316
-
317
- if (this._dom) {
318
- this._dom.gutterElement.hidden = !this._lineNumbers;
319
- }
320
- }
321
-
322
480
  private updateGutter(): void {
323
481
  if (!this._dom) {
324
482
  return;
@@ -377,6 +535,30 @@ export class CodeTool implements BlockTool {
377
535
  });
378
536
  }
379
537
 
538
+ private scheduleDetection(): void {
539
+ if (this._detectionTimeoutId !== null) {
540
+ clearTimeout(this._detectionTimeoutId);
541
+ }
542
+
543
+ this._detectionTimeoutId = setTimeout(() => {
544
+ this._detectionTimeoutId = null;
545
+ const code = this._dom?.codeElement.textContent ?? '';
546
+ void detectLanguage(code).then((detected) => {
547
+ if (detected === this._detectedLanguage) {
548
+ return;
549
+ }
550
+ this._detectedLanguage = detected;
551
+ // Rebuild picker so the detected language section updates
552
+ if (this._dom) {
553
+ if (this._picker) {
554
+ this._picker.destroy();
555
+ }
556
+ this._picker = this.buildLanguagePicker(this._dom.languageButton, this._dom.wrapper);
557
+ }
558
+ });
559
+ }, 600);
560
+ }
561
+
380
562
  private async highlightCode(): Promise<void> {
381
563
  if (!isHighlightingSupported() || !isHighlightable(this._data.language)) {
382
564
  this._disposeHighlights?.();
@@ -418,10 +600,17 @@ export class CodeTool implements BlockTool {
418
600
  this._highlightRafId = null;
419
601
  }
420
602
 
603
+ if (this._detectionTimeoutId !== null) {
604
+ clearTimeout(this._detectionTimeoutId);
605
+ this._detectionTimeoutId = null;
606
+ }
607
+
421
608
  if (this._picker) {
422
- this._picker.getElement().remove();
609
+ this._picker.destroy();
423
610
  this._picker = null;
424
611
  }
612
+
613
+ this._dom = null;
425
614
  }
426
615
 
427
616
  public static get toolbox(): ToolboxConfig {
@@ -0,0 +1,118 @@
1
+ import { tokenizeCode } from './shiki-loader';
2
+
3
+ /**
4
+ * Languages considered for auto-detection.
5
+ * Kept to ~15 common ones to keep detection fast.
6
+ */
7
+ export const DETECTION_CANDIDATE_LANGUAGES = [
8
+ 'javascript',
9
+ 'typescript',
10
+ 'python',
11
+ 'java',
12
+ 'html',
13
+ 'css',
14
+ 'json',
15
+ 'bash',
16
+ 'sql',
17
+ 'rust',
18
+ 'go',
19
+ 'cpp',
20
+ 'yaml',
21
+ 'markdown',
22
+ 'php',
23
+ ] as const;
24
+
25
+ /** Minimum code length (characters) before attempting detection. */
26
+ const MIN_CODE_LENGTH = 20;
27
+
28
+ /**
29
+ * Threshold: if the winning language's fg-token ratio is above this value,
30
+ * no meaningful detection was possible and we return null.
31
+ * A ratio of 1.0 means all tokens are unrecognized (plain fg color).
32
+ * A ratio of 0.75 means 75% of characters are unrecognized.
33
+ */
34
+ const MAX_ACCEPTABLE_FG_RATIO = 0.75;
35
+
36
+ /**
37
+ * Minimum number of distinct non-fg colors a tokenization must produce
38
+ * to be considered a genuine match. Languages that colorize everything
39
+ * with a single non-fg color (e.g. YAML treating code as a string block)
40
+ * produce a deceptively low fg-ratio without actually recognizing the syntax.
41
+ */
42
+ const MIN_DISTINCT_COLORS = 2;
43
+
44
+ /**
45
+ * Scores how well shiki tokenized the code in a given language.
46
+ * Returns a value in [0, 1] — lower is better (fewer unrecognized tokens),
47
+ * or 1 if the tokenization didn't use enough distinct colors to be a real match.
48
+ *
49
+ * Strategy: compute the ratio of characters colored with the theme's
50
+ * foreground color (= unrecognized/plain text) to total characters.
51
+ * A well-matched language has many distinctly-colored tokens; a poorly-matched
52
+ * language produces mostly fg-colored (unrecognized) tokens.
53
+ *
54
+ * Guard: if fewer than MIN_DISTINCT_COLORS non-fg colors appear, the grammar
55
+ * is treating everything as the same token type (e.g. YAML string), which is
56
+ * a false positive. Return 1 in that case.
57
+ */
58
+ function scoreTokens(tokens: Array<Array<{ content: string; color: string }>>, fg: string): number {
59
+ const allTokens = tokens.flat();
60
+ const totalChars = allTokens.reduce((sum, token) => sum + token.content.length, 0);
61
+
62
+ if (totalChars === 0) return 1;
63
+
64
+ const { fgChars, nonFgColors } = allTokens.reduce(
65
+ (acc, token) => ({
66
+ fgChars: acc.fgChars + (token.color === fg ? token.content.length : 0),
67
+ nonFgColors: token.color === fg ? acc.nonFgColors : acc.nonFgColors.add(token.color),
68
+ }),
69
+ { fgChars: 0, nonFgColors: new Set<string>() }
70
+ );
71
+
72
+ // Reject tokenizations that use fewer than MIN_DISTINCT_COLORS non-fg colors —
73
+ // these are grammars that "colorize" everything as a single token type
74
+ // (e.g. YAML interpreting code as block scalars), which is a false positive.
75
+ if (nonFgColors.size < MIN_DISTINCT_COLORS) {
76
+ return 1;
77
+ }
78
+
79
+ return fgChars / totalChars;
80
+ }
81
+
82
+ /**
83
+ * Detects the most likely programming language for the given code.
84
+ * Returns a language ID from DETECTION_CANDIDATE_LANGUAGES, or null if:
85
+ * - code is too short
86
+ * - shiki isn't loaded yet
87
+ * - no language scores clearly better than plain text
88
+ */
89
+ export async function detectLanguage(code: string): Promise<string | null> {
90
+ if (code.length < MIN_CODE_LENGTH) {
91
+ return null;
92
+ }
93
+
94
+ // Tokenize all candidate languages concurrently
95
+ const results = await Promise.all(
96
+ DETECTION_CANDIDATE_LANGUAGES.map(async (lang) => {
97
+ const tokens = await tokenizeCode(code, lang);
98
+ return { lang, tokens };
99
+ })
100
+ );
101
+
102
+ const best = results.reduce<{ lang: string | null; score: number }>(
103
+ (acc, { lang, tokens }) => {
104
+ if (!tokens) return acc;
105
+
106
+ const score = scoreTokens(tokens.light.tokens, tokens.light.fg);
107
+
108
+ return score < acc.score ? { lang, score } : acc;
109
+ },
110
+ { lang: null, score: Infinity }
111
+ );
112
+
113
+ if (best.lang === null || best.score >= MAX_ACCEPTABLE_FG_RATIO) {
114
+ return null;
115
+ }
116
+
117
+ return best.lang;
118
+ }
@@ -96,6 +96,11 @@ export class DividerTool implements BlockTool {
96
96
  */
97
97
  public onPaste(_event: PasteEvent): void {}
98
98
 
99
+ /**
100
+ * Toggle read-only mode in place. Divider is purely presentational — no-op.
101
+ */
102
+ public setReadOnly(_state: boolean): void {}
103
+
99
104
  /**
100
105
  * Nothing to sanitize — no HTML content
101
106
  */