@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.
- package/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-DDu252IK.mjs → blok-u_68bnlk.mjs} +1617 -1562
- package/dist/chunks/{constants-DMW9a31I.mjs → constants-VDhCUk4c.mjs} +56 -48
- package/dist/chunks/{i18next-loader-CwsYu0n6.mjs → i18next-loader-CDnSPae_.mjs} +1 -1
- package/dist/chunks/{lightweight-i18n-Cvv8CWh4.mjs → lightweight-i18n-DZmo8dAI.mjs} +1 -0
- package/dist/chunks/{messages-DG-4DPmP.mjs → messages-1_6UkKLS.mjs} +1 -0
- package/dist/{messages-CqXtJTpU.mjs → chunks/messages-4Ck88DYZ2.mjs} +1 -0
- package/dist/chunks/{messages-DGL1ySqb2.mjs → messages-8Ld7P_9j2.mjs} +1 -0
- package/dist/{messages-DLX_iBDJ.mjs → chunks/messages-BAlZjPcl.mjs} +1 -0
- package/dist/chunks/{messages-p1mbe__S.mjs → messages-BHMiK51R.mjs} +1 -0
- package/dist/chunks/{messages-Cdf0W9H02.mjs → messages-BJ-vT1SU2.mjs} +1 -0
- package/dist/{messages-Smt4GBbj.mjs → chunks/messages-BK8Cp2d0.mjs} +1 -0
- package/dist/{messages-Ci0KqX-J.mjs → chunks/messages-BKN3YVIj.mjs} +1 -0
- package/dist/chunks/{messages-BXM80fdr2.mjs → messages-BMD37y3q2.mjs} +1 -0
- package/dist/{messages-B19o-Teb.mjs → chunks/messages-BONyZroH.mjs} +1 -0
- package/dist/{messages-BwHs4cm1.mjs → chunks/messages-BRAoJpOu.mjs} +1 -0
- package/dist/{messages-DY4IqlhY.mjs → chunks/messages-BRoa9tGl.mjs} +1 -0
- package/dist/chunks/{messages-RInp1ytX.mjs → messages-BbEW9bQz.mjs} +1 -0
- package/dist/{messages-BIHc0KHY.mjs → chunks/messages-BeGZqQwz.mjs} +1 -0
- package/dist/{messages-CmB406HW.mjs → chunks/messages-BfAcUavP.mjs} +1 -0
- package/dist/chunks/{messages-Cu-Wevxs2.mjs → messages-BgM91Lxm2.mjs} +1 -0
- package/dist/{messages-BYNcD6uR.mjs → chunks/messages-BlxwW7M6.mjs} +1 -0
- package/dist/chunks/{messages-rCd0Rrw6.mjs → messages-Bz0-KNEB.mjs} +1 -0
- package/dist/{messages-7QD-X6XT2.mjs → chunks/messages-C0IFfhnp.mjs} +1 -0
- package/dist/{messages-Dl5Y2-Ia.mjs → chunks/messages-C15z2r5U.mjs} +1 -0
- package/dist/chunks/{messages-MxpWO1db.mjs → messages-C1S9ztpF.mjs} +1 -0
- package/dist/chunks/{messages-8IHf7ZP3.mjs → messages-CC_noR8y.mjs} +1 -0
- package/dist/chunks/{messages-COO5xmcA.mjs → messages-CD_MnBln.mjs} +1 -0
- package/dist/chunks/{messages-BYlSMRkd.mjs → messages-CIfUm1Oa.mjs} +1 -0
- package/dist/{messages-BbYq1pk-.mjs → chunks/messages-CPBN4zWc.mjs} +1 -0
- package/dist/{messages-DPA-mMWC2.mjs → chunks/messages-CQBo3lmL2.mjs} +1 -0
- package/dist/{messages-DnGJD4TL.mjs → chunks/messages-CRF7nNrO.mjs} +1 -0
- package/dist/{messages-D8FQWulF2.mjs → chunks/messages-CTCe595D2.mjs} +1 -0
- package/dist/{messages-BRZX964b2.mjs → chunks/messages-CW35K1pq.mjs} +1 -0
- package/dist/{messages-DnG0ef8t2.mjs → chunks/messages-CZSlfnkO2.mjs} +1 -0
- package/dist/chunks/{messages-iS34FHFB.mjs → messages-ChK7v1PV.mjs} +1 -0
- package/dist/{messages-BiUGXvYr2.mjs → chunks/messages-Clku7Cf-2.mjs} +1 -0
- package/dist/{messages-DIJlIqlQ2.mjs → chunks/messages-CszmHAvQ.mjs} +1 -0
- package/dist/chunks/{messages-DWu1r4gc2.mjs → messages-CvANwuht2.mjs} +1 -0
- package/dist/{messages-nUVjeh7K.mjs → chunks/messages-CxiURE2X.mjs} +1 -0
- package/dist/chunks/{messages-A_MkXDlG.mjs → messages-CxxyR4vY.mjs} +1 -0
- package/dist/{messages-ynAe7ewZ.mjs → chunks/messages-D22e9h7V2.mjs} +1 -0
- package/dist/{messages-DYTTu0O12.mjs → chunks/messages-D7dx_6k8.mjs} +1 -0
- package/dist/chunks/{messages-BA8Iv99Y2.mjs → messages-DBMaLL8b2.mjs} +1 -0
- package/dist/{messages-DbySKTKt2.mjs → chunks/messages-DB_-5Xln.mjs} +1 -0
- package/dist/{messages-CcF4y-E4.mjs → chunks/messages-DEBy3nuJ2.mjs} +1 -0
- package/dist/chunks/{messages-NEqrrYvE2.mjs → messages-DMoERagV2.mjs} +1 -0
- package/dist/chunks/{messages-Bxvi1ebN.mjs → messages-DPzHD51Y.mjs} +1 -0
- package/dist/chunks/{messages-jfVpL9c-2.mjs → messages-DSrdy9Nw2.mjs} +1 -0
- package/dist/chunks/{messages-Cmf6NhSC.mjs → messages-DTN1XGll.mjs} +1 -0
- package/dist/{messages-5jvKxQNu2.mjs → chunks/messages-DUeiPraX.mjs} +1 -0
- package/dist/chunks/{messages-G416eyjY.mjs → messages-DUr9WAkD.mjs} +1 -0
- package/dist/chunks/{messages-Ck81cQkn2.mjs → messages-DVr1sqfI2.mjs} +1 -0
- package/dist/{messages-BYmmMDrN2.mjs → chunks/messages-DjvaiALg2.mjs} +1 -0
- package/dist/chunks/{messages-D55HRx5O2.mjs → messages-DrfRYiM32.mjs} +1 -0
- package/dist/chunks/{messages-B2N4fUi72.mjs → messages-DtoId_bw2.mjs} +1 -0
- package/dist/{messages-Bq3F2Tp_.mjs → chunks/messages-Du2BffA7.mjs} +1 -0
- package/dist/{messages-CjbnogEC.mjs → chunks/messages-DxHh0O8j2.mjs} +1 -0
- package/dist/{messages-BECMxmfX.mjs → chunks/messages-EDMC5ukV.mjs} +1 -0
- package/dist/{messages-BTQPpoM42.mjs → chunks/messages-ElIGUi0O2.mjs} +1 -0
- package/dist/chunks/{messages-BhzzNkN-.mjs → messages-JSQjKQ8I.mjs} +1 -0
- package/dist/{messages-CWIXvnDf2.mjs → chunks/messages-Q7-4ZJLB2.mjs} +1 -0
- package/dist/chunks/{messages-DOuS1Qge.mjs → messages-QMOmwcZb.mjs} +1 -0
- package/dist/{messages-hWwSRF-2.mjs → chunks/messages-QilfinOn2.mjs} +1 -0
- package/dist/{messages-BmAn22OX.mjs → chunks/messages-a07QVz8U.mjs} +1 -0
- package/dist/chunks/{messages-BYxLFj7y.mjs → messages-eFd4YYzt.mjs} +1 -0
- package/dist/chunks/{messages-BSghd0ez.mjs → messages-euM2m3wQ.mjs} +1 -0
- package/dist/chunks/{messages-BVjoM7P0.mjs → messages-kGmxkeFH.mjs} +1 -0
- package/dist/{messages-DMr62KiO2.mjs → chunks/messages-oMc7qugU2.mjs} +1 -0
- package/dist/chunks/{messages-DzTk8bJ5.mjs → messages-sDdNf8O9.mjs} +1 -0
- package/dist/{messages-Bm0Feca1.mjs → chunks/messages-wl8YrvGG.mjs} +1 -0
- package/dist/chunks/{messages-BAsb5CgZ.mjs → messages-zt6zdYWh.mjs} +1 -0
- package/dist/chunks/{tools-XmzH2rgQ.mjs → tools-1ZFajlGN.mjs} +1619 -1307
- package/dist/full.mjs +3 -3
- package/dist/locales.mjs +68 -67
- package/dist/{messages-F2xRoY1w.mjs → messages-2ZWBTerL.mjs} +1 -0
- package/dist/{messages-Dl3Sv6Rq2.mjs → messages-53w0fPZS2.mjs} +1 -0
- package/dist/{chunks/messages-BDZA10kl2.mjs → messages-98nQiC7t2.mjs} +1 -0
- package/dist/{chunks/messages-JyvWu4rf2.mjs → messages-A96tMxeU.mjs} +1 -0
- package/dist/{messages-Ce6KVEbT.mjs → messages-BE_z-zrb.mjs} +1 -0
- package/dist/{chunks/messages-CSJ_zb3a2.mjs → messages-BK_LsgY4.mjs} +1 -0
- package/dist/{messages-CJTy6JZt.mjs → messages-BbJ7ZXY8.mjs} +1 -0
- package/dist/{chunks/messages-DMVXnAYj.mjs → messages-BcVB3osF.mjs} +1 -0
- package/dist/{chunks/messages-CSL-6xfb2.mjs → messages-BckDk9aq2.mjs} +1 -0
- package/dist/{chunks/messages-C0HvoMPb.mjs → messages-Be_2RHZD.mjs} +1 -0
- package/dist/{chunks/messages-Dr0Ekmbz.mjs → messages-BesJaI6A.mjs} +1 -0
- package/dist/{chunks/messages-D3zojZ94.mjs → messages-BiTMwiKH.mjs} +1 -0
- package/dist/{messages-B1ZUQagA.mjs → messages-BmH2cQHQ.mjs} +1 -0
- package/dist/{chunks/messages-Bfnq1xv4.mjs → messages-BrOWqNCu2.mjs} +1 -0
- package/dist/{messages-Dnp9N6RU2.mjs → messages-Brd5R-da2.mjs} +1 -0
- package/dist/{chunks/messages-DJoNVjqP.mjs → messages-C0GSBBCo2.mjs} +1 -0
- package/dist/{messages-Dw__BcTj.mjs → messages-C1vc5584.mjs} +1 -0
- package/dist/{messages-aMXpHt5X2.mjs → messages-C6ONf71u2.mjs} +1 -0
- package/dist/{chunks/messages-BeFqtIrc2.mjs → messages-C7lJg8fy2.mjs} +1 -0
- package/dist/{messages-CSUHBs4c2.mjs → messages-CRNogopy2.mjs} +1 -0
- package/dist/{messages-DLlc9QPw.mjs → messages-CT-Kdas6.mjs} +1 -0
- package/dist/{chunks/messages-DlLXpgWM2.mjs → messages-CTTmWn4Y2.mjs} +1 -0
- package/dist/{messages-D0aw5_0k2.mjs → messages-CZbcxlZt2.mjs} +1 -0
- package/dist/{chunks/messages-Bp8qin1R.mjs → messages-C_Qn9SbQ.mjs} +1 -0
- package/dist/{messages-96iaAUds2.mjs → messages-CdEASHDp2.mjs} +1 -0
- package/dist/{messages-Dy-Y_nEI.mjs → messages-CdduYw-q.mjs} +1 -0
- package/dist/{chunks/messages-Je5YvxiY.mjs → messages-Che99vKP.mjs} +1 -0
- package/dist/{messages-nlhESX9t.mjs → messages-CisR4PNV.mjs} +1 -0
- package/dist/{chunks/messages-BE6lHKwf.mjs → messages-ClGvlFcH2.mjs} +1 -0
- package/dist/{chunks/messages-FWfsxpBz.mjs → messages-CnuH-BZK2.mjs} +1 -0
- package/dist/{chunks/messages-aZcy0JQq2.mjs → messages-D0005ti32.mjs} +1 -0
- package/dist/{messages-B7ieAJBd2.mjs → messages-D05jqBIa2.mjs} +1 -0
- package/dist/{messages-DTh9a8mR.mjs → messages-D0lLw9KM.mjs} +1 -0
- package/dist/{chunks/messages-ihCjSFJI2.mjs → messages-D3rwCtKn.mjs} +1 -0
- package/dist/{chunks/messages-xuqyb6Ff2.mjs → messages-D6VIFnSW.mjs} +1 -0
- package/dist/{chunks/messages-KdawW5Na.mjs → messages-D81w6AmW.mjs} +1 -0
- package/dist/{chunks/messages-BUVhHx0q2.mjs → messages-DBhvm8NK.mjs} +1 -0
- package/dist/{chunks/messages-C7VGpihw.mjs → messages-DK6dA0O2.mjs} +1 -0
- package/dist/{messages-rk-A1Wa42.mjs → messages-DKHbt-7l2.mjs} +1 -0
- package/dist/{messages-BIoeoik5.mjs → messages-DM4Gjc9h.mjs} +1 -0
- package/dist/{chunks/messages-B9kmbUWV.mjs → messages-DODrhcop.mjs} +1 -0
- package/dist/{messages-BsycN_JI2.mjs → messages-DOGbHYv-2.mjs} +1 -0
- package/dist/{chunks/messages-BQYvBqm2.mjs → messages-DQORja0D.mjs} +1 -0
- package/dist/{chunks/messages-CVdpweyf2.mjs → messages-DSmxJWju2.mjs} +1 -0
- package/dist/{messages-CR_L_UtK.mjs → messages-DVL0KZE5.mjs} +1 -0
- package/dist/{chunks/messages-Cs81Z_Bh.mjs → messages-DYuD5-rO.mjs} +1 -0
- package/dist/{chunks/messages-CKBhDGI3.mjs → messages-Ddq3Ce3E2.mjs} +1 -0
- package/dist/{messages-xh2eOLvs.mjs → messages-DfFZ6Yj5.mjs} +1 -0
- package/dist/{chunks/messages-C6Mpiacw.mjs → messages-Dnd5YSWv.mjs} +1 -0
- package/dist/{messages-BJeGJksD.mjs → messages-Do7Xjy0n.mjs} +1 -0
- package/dist/{chunks/messages-C3aX3q0H.mjs → messages-DopaMHC42.mjs} +1 -0
- package/dist/{messages-dv19AkyJ.mjs → messages-DpJGbx3q.mjs} +1 -0
- package/dist/{messages-DBiVgUs2.mjs → messages-DpwMKDV0.mjs} +1 -0
- package/dist/{messages-j7o5rT9s.mjs → messages-Dqu4aX9s.mjs} +1 -0
- package/dist/{chunks/messages-BjadX8jR2.mjs → messages-E8NjqzWq2.mjs} +1 -0
- package/dist/{messages-aWZH50vu2.mjs → messages-JNrYldAa2.mjs} +1 -0
- package/dist/{chunks/messages-B4UMuyjT.mjs → messages-LMaR2_bE.mjs} +1 -0
- package/dist/{messages-E_ZuzGDt.mjs → messages-LYJbLq_F.mjs} +1 -0
- package/dist/{chunks/messages-D9N2MvQx2.mjs → messages-Q5sQeVap2.mjs} +1 -0
- package/dist/{chunks/messages-Bphq_Bt3.mjs → messages-Xc0KUbYl.mjs} +1 -0
- package/dist/{chunks/messages-DlonA3wa.mjs → messages-_PLyRfVw.mjs} +1 -0
- package/dist/{chunks/messages-TRUuyiFB.mjs → messages-apA6BStA.mjs} +1 -0
- package/dist/{chunks/messages-B0vPBsWq.mjs → messages-bkGniiaz.mjs} +1 -0
- package/dist/{messages-DkLU_rWm.mjs → messages-neGD3WGq.mjs} +1 -0
- package/dist/{messages-Ddnj2iTG2.mjs → messages-qbKjjvgd2.mjs} +1 -0
- package/dist/{messages-BiiongNz2.mjs → messages-qfvXgPpu2.mjs} +1 -0
- package/dist/{messages-Dvn35ksS.mjs → messages-uwK7ktqk.mjs} +1 -0
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +3 -5
- package/src/cli/commands/convert-gdocs/index.ts +26 -0
- package/src/cli/commands/convert-html/block-builder.ts +392 -0
- package/src/cli/commands/convert-html/id-generator.ts +11 -0
- package/src/cli/commands/convert-html/index.ts +23 -0
- package/src/cli/commands/convert-html/preprocessor.ts +422 -0
- package/src/cli/commands/convert-html/sanitizer.ts +93 -0
- package/src/cli/commands/convert-html/types.ts +15 -0
- package/src/cli/index.ts +56 -5
- package/src/components/block/index.ts +58 -10
- package/src/components/constants/data-attributes.ts +10 -0
- package/src/components/i18n/locales/am/messages.json +1 -0
- package/src/components/i18n/locales/ar/messages.json +1 -0
- package/src/components/i18n/locales/az/messages.json +1 -0
- package/src/components/i18n/locales/bg/messages.json +1 -0
- package/src/components/i18n/locales/bn/messages.json +1 -0
- package/src/components/i18n/locales/bs/messages.json +1 -0
- package/src/components/i18n/locales/cs/messages.json +1 -0
- package/src/components/i18n/locales/da/messages.json +1 -0
- package/src/components/i18n/locales/de/messages.json +1 -0
- package/src/components/i18n/locales/dv/messages.json +1 -0
- package/src/components/i18n/locales/el/messages.json +1 -0
- package/src/components/i18n/locales/en/messages.json +1 -0
- package/src/components/i18n/locales/es/messages.json +1 -0
- package/src/components/i18n/locales/et/messages.json +1 -0
- package/src/components/i18n/locales/fa/messages.json +1 -0
- package/src/components/i18n/locales/fi/messages.json +1 -0
- package/src/components/i18n/locales/fil/messages.json +1 -0
- package/src/components/i18n/locales/fr/messages.json +1 -0
- package/src/components/i18n/locales/gu/messages.json +1 -0
- package/src/components/i18n/locales/he/messages.json +1 -0
- package/src/components/i18n/locales/hi/messages.json +1 -0
- package/src/components/i18n/locales/hr/messages.json +1 -0
- package/src/components/i18n/locales/hu/messages.json +1 -0
- package/src/components/i18n/locales/hy/messages.json +1 -0
- package/src/components/i18n/locales/id/messages.json +1 -0
- package/src/components/i18n/locales/it/messages.json +1 -0
- package/src/components/i18n/locales/ja/messages.json +1 -0
- package/src/components/i18n/locales/ka/messages.json +1 -0
- package/src/components/i18n/locales/km/messages.json +1 -0
- package/src/components/i18n/locales/kn/messages.json +1 -0
- package/src/components/i18n/locales/ko/messages.json +1 -0
- package/src/components/i18n/locales/ku/messages.json +1 -0
- package/src/components/i18n/locales/lo/messages.json +1 -0
- package/src/components/i18n/locales/lt/messages.json +1 -0
- package/src/components/i18n/locales/lv/messages.json +1 -0
- package/src/components/i18n/locales/mk/messages.json +1 -0
- package/src/components/i18n/locales/ml/messages.json +1 -0
- package/src/components/i18n/locales/mn/messages.json +1 -0
- package/src/components/i18n/locales/mr/messages.json +1 -0
- package/src/components/i18n/locales/ms/messages.json +1 -0
- package/src/components/i18n/locales/my/messages.json +1 -0
- package/src/components/i18n/locales/ne/messages.json +1 -0
- package/src/components/i18n/locales/nl/messages.json +1 -0
- package/src/components/i18n/locales/no/messages.json +1 -0
- package/src/components/i18n/locales/pa/messages.json +1 -0
- package/src/components/i18n/locales/pl/messages.json +1 -0
- package/src/components/i18n/locales/ps/messages.json +1 -0
- package/src/components/i18n/locales/pt/messages.json +1 -0
- package/src/components/i18n/locales/ro/messages.json +1 -0
- package/src/components/i18n/locales/ru/messages.json +1 -0
- package/src/components/i18n/locales/sd/messages.json +1 -0
- package/src/components/i18n/locales/si/messages.json +1 -0
- package/src/components/i18n/locales/sk/messages.json +1 -0
- package/src/components/i18n/locales/sl/messages.json +1 -0
- package/src/components/i18n/locales/sq/messages.json +1 -0
- package/src/components/i18n/locales/sr/messages.json +1 -0
- package/src/components/i18n/locales/sv/messages.json +1 -0
- package/src/components/i18n/locales/sw/messages.json +1 -0
- package/src/components/i18n/locales/ta/messages.json +1 -0
- package/src/components/i18n/locales/te/messages.json +1 -0
- package/src/components/i18n/locales/th/messages.json +1 -0
- package/src/components/i18n/locales/tr/messages.json +1 -0
- package/src/components/i18n/locales/ug/messages.json +1 -0
- package/src/components/i18n/locales/uk/messages.json +1 -0
- package/src/components/i18n/locales/ur/messages.json +1 -0
- package/src/components/i18n/locales/vi/messages.json +1 -0
- package/src/components/i18n/locales/yi/messages.json +1 -0
- package/src/components/i18n/locales/zh/messages.json +1 -0
- package/src/components/icons/index.ts +29 -18
- package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +18 -0
- package/src/components/modules/blockManager/hierarchy.ts +4 -1
- package/src/components/modules/readonly.ts +46 -0
- package/src/components/modules/rectangleSelection.ts +25 -5
- package/src/components/modules/toolbar/index.ts +96 -19
- package/src/components/modules/toolbar/positioning.ts +11 -2
- package/src/components/modules/toolbar/styles.ts +0 -2
- package/src/components/modules/uiControllers/controllers/blockHover.ts +44 -1
- package/src/components/tools/block.ts +10 -0
- package/src/components/utils/placeholder.ts +9 -2
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +11 -0
- package/src/components/utils/popover/popover-abstract.ts +7 -0
- package/src/styles/main.css +16 -0
- package/src/tools/callout/constants.ts +2 -1
- package/src/tools/callout/dom-builder.ts +13 -1
- package/src/tools/callout/index.ts +21 -7
- package/src/tools/code/constants.ts +28 -8
- package/src/tools/code/dom-builder.ts +133 -64
- package/src/tools/code/index.ts +280 -91
- package/src/tools/code/language-detector.ts +118 -0
- package/src/tools/divider/index.ts +5 -0
- package/src/tools/header/index.ts +47 -1
- package/src/tools/list/dom-builder.ts +3 -1
- package/src/tools/list/index.ts +55 -3
- package/src/tools/list/list-helpers.ts +2 -2
- package/src/tools/nested-blocks.ts +25 -0
- package/src/tools/paragraph/index.ts +47 -6
- package/src/tools/quote/index.ts +43 -8
- package/src/tools/stub/index.ts +10 -0
- package/src/tools/table/index.ts +238 -6
- package/src/tools/table/table-add-controls.ts +37 -5
- package/src/tools/table/table-cell-blocks.ts +57 -18
- package/src/tools/table/table-core.ts +2 -0
- package/src/tools/table/table-corner-drag.ts +247 -0
- package/src/tools/table/table-operations.ts +41 -14
- package/src/tools/toggle/dom-builder.ts +1 -0
- package/src/tools/toggle/index.ts +25 -0
- package/src/tools/toggle/toggle-lifecycle.ts +5 -4
- package/src/types-internal/jsdom.d.ts +9 -0
- package/types/tools/adapters/block-tool-adapter.d.ts +6 -0
- package/types/tools/block-tool.d.ts +20 -0
- package/types/utils/popover/popover-item.d.ts +6 -0
- package/bin/blok.mjs +0 -10
- package/dist/cli.mjs +0 -37
- package/src/tools/code/language-picker.ts +0 -241
package/src/tools/code/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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:
|
|
52
|
-
private
|
|
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
|
-
|
|
80
|
-
|
|
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
|
|
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:
|
|
103
|
-
if (!this.readOnly && isPreviewable && dom.
|
|
104
|
-
this.
|
|
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
|
-
|
|
112
|
-
|
|
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.
|
|
157
|
+
tooltipOnHover(dom.copyButton, this.api.i18n.t(COPY_CODE_KEY), { placement: 'bottom' });
|
|
136
158
|
|
|
137
159
|
if (!this.readOnly) {
|
|
138
|
-
this._picker =
|
|
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?.
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
187
|
+
private setViewMode(mode: CodeViewMode): void {
|
|
188
|
+
this._viewMode = mode;
|
|
189
|
+
this.applyViewMode();
|
|
163
190
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
173
|
-
if (!this._dom?.previewElement || !this._dom.
|
|
196
|
+
private applyViewMode(): void {
|
|
197
|
+
if (!this._dom?.previewElement || !this._dom.viewModeContainer || !this._dom.splitContainer) {
|
|
174
198
|
return;
|
|
175
199
|
}
|
|
176
200
|
|
|
177
|
-
|
|
178
|
-
this._dom.
|
|
179
|
-
|
|
180
|
-
this._dom.
|
|
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
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
*/
|