@jackuait/blok 0.10.0 → 0.10.2
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-BfcBwAfE.mjs → blok-D-T1XZ92.mjs} +1494 -1491
- package/dist/chunks/{constants-QNVyXALL.mjs → constants-CaB-mlB5.mjs} +55 -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/{messages-96iaAUds2.mjs → chunks/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/chunks/{messages-Bphq_Bt3.mjs → messages-BKN3YVIj.mjs} +1 -0
- package/dist/{messages-DTh9a8mR.mjs → chunks/messages-BMD37y3q2.mjs} +1 -0
- package/dist/chunks/{messages-DlonA3wa.mjs → messages-BONyZroH.mjs} +1 -0
- package/dist/chunks/{messages-DMVXnAYj.mjs → messages-BRAoJpOu.mjs} +1 -0
- package/dist/chunks/{messages-C6Mpiacw.mjs → messages-BRoa9tGl.mjs} +1 -0
- package/dist/{messages-aWZH50vu2.mjs → chunks/messages-BbEW9bQz.mjs} +1 -0
- package/dist/{messages-BIHc0KHY.mjs → chunks/messages-BeGZqQwz.mjs} +1 -0
- package/dist/chunks/{messages-C7VGpihw.mjs → 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/chunks/{messages-Dr0Ekmbz.mjs → 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/{messages-BiiongNz2.mjs → chunks/messages-CIfUm1Oa.mjs} +1 -0
- package/dist/chunks/{messages-B9kmbUWV.mjs → messages-CPBN4zWc.mjs} +1 -0
- package/dist/chunks/{messages-DlLXpgWM2.mjs → 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/chunks/{messages-CVdpweyf2.mjs → messages-CZSlfnkO2.mjs} +1 -0
- package/dist/chunks/{messages-iS34FHFB.mjs → messages-ChK7v1PV.mjs} +1 -0
- package/dist/chunks/{messages-BjadX8jR2.mjs → messages-Clku7Cf-2.mjs} +1 -0
- package/dist/{messages-DIJlIqlQ2.mjs → chunks/messages-CszmHAvQ.mjs} +1 -0
- package/dist/{messages-D0aw5_0k2.mjs → chunks/messages-CvANwuht2.mjs} +1 -0
- package/dist/chunks/{messages-BQYvBqm2.mjs → messages-CxiURE2X.mjs} +1 -0
- package/dist/chunks/{messages-A_MkXDlG.mjs → messages-CxxyR4vY.mjs} +1 -0
- package/dist/chunks/{messages-BUVhHx0q2.mjs → 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/chunks/{messages-JyvWu4rf2.mjs → messages-DEBy3nuJ2.mjs} +1 -0
- package/dist/chunks/{messages-NEqrrYvE2.mjs → messages-DMoERagV2.mjs} +1 -0
- package/dist/{messages-Dy-Y_nEI.mjs → chunks/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/chunks/{messages-BE6lHKwf.mjs → messages-DUeiPraX.mjs} +1 -0
- package/dist/{messages-Ddnj2iTG2.mjs → chunks/messages-DUr9WAkD.mjs} +1 -0
- package/dist/chunks/{messages-Ck81cQkn2.mjs → messages-DVr1sqfI2.mjs} +1 -0
- package/dist/chunks/{messages-aZcy0JQq2.mjs → messages-DjvaiALg2.mjs} +1 -0
- package/dist/{messages-DkLU_rWm.mjs → chunks/messages-DrfRYiM32.mjs} +1 -0
- package/dist/{messages-Dw__BcTj.mjs → chunks/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/{messages-B1ZUQagA.mjs → chunks/messages-JSQjKQ8I.mjs} +1 -0
- package/dist/{messages-CWIXvnDf2.mjs → chunks/messages-Q7-4ZJLB2.mjs} +1 -0
- package/dist/{messages-BJeGJksD.mjs → chunks/messages-QMOmwcZb.mjs} +1 -0
- package/dist/chunks/{messages-xuqyb6Ff2.mjs → messages-QilfinOn2.mjs} +1 -0
- package/dist/{messages-BmAn22OX.mjs → chunks/messages-a07QVz8U.mjs} +1 -0
- package/dist/{messages-nlhESX9t.mjs → chunks/messages-eFd4YYzt.mjs} +1 -0
- package/dist/{messages-Dnp9N6RU2.mjs → chunks/messages-euM2m3wQ.mjs} +1 -0
- package/dist/chunks/{messages-BVjoM7P0.mjs → messages-kGmxkeFH.mjs} +1 -0
- package/dist/chunks/{messages-D9N2MvQx2.mjs → messages-oMc7qugU2.mjs} +1 -0
- package/dist/{messages-Dvn35ksS.mjs → chunks/messages-sDdNf8O9.mjs} +1 -0
- package/dist/{messages-Bm0Feca1.mjs → chunks/messages-wl8YrvGG.mjs} +1 -0
- package/dist/{messages-j7o5rT9s.mjs → chunks/messages-zt6zdYWh.mjs} +1 -0
- package/dist/chunks/{tools-DHtzbrxy.mjs → tools-BFK2MvVI.mjs} +1024 -895
- 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/{messages-CcF4y-E4.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/{messages-BwHs4cm1.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/{messages-Dl5Y2-Ia.mjs → messages-BesJaI6A.mjs} +1 -0
- package/dist/{chunks/messages-D3zojZ94.mjs → messages-BiTMwiKH.mjs} +1 -0
- package/dist/{chunks/messages-BhzzNkN-.mjs → messages-BmH2cQHQ.mjs} +1 -0
- package/dist/{chunks/messages-Bfnq1xv4.mjs → messages-BrOWqNCu2.mjs} +1 -0
- package/dist/{chunks/messages-BSghd0ez.mjs → messages-Brd5R-da2.mjs} +1 -0
- package/dist/{chunks/messages-DJoNVjqP.mjs → messages-C0GSBBCo2.mjs} +1 -0
- package/dist/{chunks/messages-B2N4fUi72.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/{messages-DPA-mMWC2.mjs → messages-CTTmWn4Y2.mjs} +1 -0
- package/dist/{chunks/messages-DWu1r4gc2.mjs → messages-CZbcxlZt2.mjs} +1 -0
- package/dist/{chunks/messages-Bp8qin1R.mjs → messages-C_Qn9SbQ.mjs} +1 -0
- package/dist/{chunks/messages-DGL1ySqb2.mjs → messages-CdEASHDp2.mjs} +1 -0
- package/dist/{chunks/messages-Bxvi1ebN.mjs → messages-CdduYw-q.mjs} +1 -0
- package/dist/{chunks/messages-Je5YvxiY.mjs → messages-Che99vKP.mjs} +1 -0
- package/dist/{chunks/messages-BYxLFj7y.mjs → messages-CisR4PNV.mjs} +1 -0
- package/dist/{messages-5jvKxQNu2.mjs → messages-ClGvlFcH2.mjs} +1 -0
- package/dist/{chunks/messages-FWfsxpBz.mjs → messages-CnuH-BZK2.mjs} +1 -0
- package/dist/{messages-BYmmMDrN2.mjs → messages-D0005ti32.mjs} +1 -0
- package/dist/{messages-B7ieAJBd2.mjs → messages-D05jqBIa2.mjs} +1 -0
- package/dist/{chunks/messages-BXM80fdr2.mjs → messages-D0lLw9KM.mjs} +1 -0
- package/dist/{chunks/messages-ihCjSFJI2.mjs → messages-D3rwCtKn.mjs} +1 -0
- package/dist/{messages-hWwSRF-2.mjs → messages-D6VIFnSW.mjs} +1 -0
- package/dist/{chunks/messages-KdawW5Na.mjs → messages-D81w6AmW.mjs} +1 -0
- package/dist/{messages-ynAe7ewZ.mjs → messages-DBhvm8NK.mjs} +1 -0
- package/dist/{messages-CmB406HW.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/{messages-BbYq1pk-.mjs → messages-DODrhcop.mjs} +1 -0
- package/dist/{messages-BsycN_JI2.mjs → messages-DOGbHYv-2.mjs} +1 -0
- package/dist/{messages-nUVjeh7K.mjs → messages-DQORja0D.mjs} +1 -0
- package/dist/{messages-DnG0ef8t2.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/{messages-DY4IqlhY.mjs → messages-Dnd5YSWv.mjs} +1 -0
- package/dist/{chunks/messages-DOuS1Qge.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/{chunks/messages-BAsb5CgZ.mjs → messages-Dqu4aX9s.mjs} +1 -0
- package/dist/{messages-BiUGXvYr2.mjs → messages-E8NjqzWq2.mjs} +1 -0
- package/dist/{chunks/messages-RInp1ytX.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/{messages-DMr62KiO2.mjs → messages-Q5sQeVap2.mjs} +1 -0
- package/dist/{messages-Ci0KqX-J.mjs → messages-Xc0KUbYl.mjs} +1 -0
- package/dist/{messages-B19o-Teb.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/{chunks/messages-D55HRx5O2.mjs → messages-neGD3WGq.mjs} +1 -0
- package/dist/{chunks/messages-G416eyjY.mjs → messages-qbKjjvgd2.mjs} +1 -0
- package/dist/{chunks/messages-BYlSMRkd.mjs → messages-qfvXgPpu2.mjs} +1 -0
- package/dist/{chunks/messages-DzTk8bJ5.mjs → messages-uwK7ktqk.mjs} +1 -0
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/components/block/index.ts +14 -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 +22 -27
- package/src/components/modules/toolbar/index.ts +21 -9
- package/src/components/modules/toolbar/positioning.ts +11 -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/tools/code/constants.ts +24 -12
- package/src/tools/code/dom-builder.ts +122 -89
- package/src/tools/code/index.ts +255 -104
- package/src/tools/code/language-detector.ts +118 -0
- package/types/tools/block-tool.d.ts +10 -0
- package/types/utils/popover/popover-item.d.ts +6 -0
package/src/tools/code/index.ts
CHANGED
|
@@ -10,32 +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
17
|
import { PopoverDesktop } from '../../components/utils/popover';
|
|
18
|
+
import { onHover as tooltipOnHover } from '../../components/utils/tooltip';
|
|
18
19
|
import type { PopoverItemParams } from '@/types/utils/popover/popover-item';
|
|
20
|
+
import { PopoverItemType } from '@/types/utils/popover/popover-item-type';
|
|
19
21
|
import {
|
|
20
22
|
DEFAULT_LANGUAGE,
|
|
21
23
|
LANGUAGES,
|
|
22
|
-
CODE_AREA_STYLES,
|
|
23
24
|
COPY_CODE_KEY,
|
|
24
|
-
WRAP_LINES_KEY,
|
|
25
|
-
LINE_NUMBERS_KEY,
|
|
26
25
|
COPIED_KEY,
|
|
27
26
|
LANGUAGE_KEY,
|
|
28
27
|
SEARCH_LANGUAGE_KEY,
|
|
29
28
|
COPIED_FEEDBACK_STYLES,
|
|
30
29
|
PREVIEWABLE_LANGUAGES,
|
|
31
|
-
|
|
30
|
+
CODE_TAB_KEY,
|
|
31
|
+
PREVIEW_TAB_KEY,
|
|
32
|
+
SIDE_BY_SIDE_KEY,
|
|
32
33
|
PREVIEW_AREA_STYLES,
|
|
33
34
|
GUTTER_LINE_STYLES,
|
|
35
|
+
SPLIT_CONTAINER_STYLES,
|
|
36
|
+
SPLIT_CONTAINER_SPLIT_STYLES,
|
|
34
37
|
} from './constants';
|
|
38
|
+
import type { CodeViewMode } from './constants';
|
|
35
39
|
import { renderLatex } from './katex-loader';
|
|
36
40
|
import { renderMermaid } from './mermaid-loader';
|
|
37
41
|
import { tokenizeCode, isHighlightable } from './shiki-loader';
|
|
38
42
|
import { applyHighlights, isHighlightingSupported } from './highlight-applier';
|
|
43
|
+
import { detectLanguage } from './language-detector';
|
|
39
44
|
|
|
40
45
|
const COPIED_FEEDBACK_DURATION = 1500;
|
|
41
46
|
|
|
@@ -44,13 +49,14 @@ export class CodeTool implements BlockTool {
|
|
|
44
49
|
private readOnly: boolean;
|
|
45
50
|
private _data: CodeData;
|
|
46
51
|
private _dom: CodeDOMRefs | null = null;
|
|
47
|
-
private _wrapping = true;
|
|
48
52
|
private _lineNumbers = true;
|
|
49
53
|
private _picker: PopoverDesktop | null = null;
|
|
50
|
-
private
|
|
54
|
+
private _viewMode: CodeViewMode = 'preview';
|
|
51
55
|
private _previewContainer: HTMLElement | null = null;
|
|
52
56
|
private _disposeHighlights: (() => void) | null = null;
|
|
53
57
|
private _highlightRafId: number | null = null;
|
|
58
|
+
private _detectedLanguage: string | null = null;
|
|
59
|
+
private _detectionTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
54
60
|
|
|
55
61
|
constructor({ data, api, readOnly }: BlockToolConstructorOptions<CodeData>) {
|
|
56
62
|
this.api = api;
|
|
@@ -71,20 +77,18 @@ export class CodeTool implements BlockTool {
|
|
|
71
77
|
languageName: this.getLanguageName(this._data.language),
|
|
72
78
|
readOnly: this.readOnly,
|
|
73
79
|
copyLabel: this.api.i18n.t(COPY_CODE_KEY),
|
|
74
|
-
wrapLabel: this.api.i18n.t(WRAP_LINES_KEY),
|
|
75
|
-
lineNumbersLabel: this.api.i18n.t(LINE_NUMBERS_KEY),
|
|
76
80
|
previewable: this.readOnly ? false : isPreviewable,
|
|
77
|
-
|
|
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
|
+
},
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
this._dom = dom;
|
|
81
89
|
|
|
82
90
|
// Line numbers gutter visibility
|
|
83
91
|
dom.gutterElement.hidden = !this._lineNumbers;
|
|
84
|
-
dom.lineNumbersButton.addEventListener('click', () => this.toggleLineNumbers());
|
|
85
|
-
|
|
86
|
-
// More menu toggle
|
|
87
|
-
dom.moreButton.addEventListener('click', () => this.toggleMoreMenu());
|
|
88
92
|
|
|
89
93
|
// Read-only + previewable: show preview only, hide code, no toggle
|
|
90
94
|
if (this.readOnly && isPreviewable) {
|
|
@@ -99,16 +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
|
+
}
|
|
110
115
|
|
|
111
|
-
|
|
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]'));
|
|
119
|
+
|
|
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
|
+
}
|
|
112
133
|
}
|
|
113
134
|
|
|
114
135
|
if (!this.readOnly) {
|
|
@@ -120,6 +141,7 @@ export class CodeTool implements BlockTool {
|
|
|
120
141
|
this.syncTrailingBr();
|
|
121
142
|
this.updateGutter();
|
|
122
143
|
this.scheduleHighlight();
|
|
144
|
+
this.scheduleDetection();
|
|
123
145
|
}
|
|
124
146
|
});
|
|
125
147
|
|
|
@@ -127,88 +149,99 @@ export class CodeTool implements BlockTool {
|
|
|
127
149
|
this.syncTrailingBr();
|
|
128
150
|
this.updateGutter();
|
|
129
151
|
this.scheduleHighlight();
|
|
152
|
+
this.scheduleDetection();
|
|
130
153
|
});
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
dom.copyButton.addEventListener('click', () => this.copyCode());
|
|
134
|
-
dom.
|
|
157
|
+
tooltipOnHover(dom.copyButton, this.api.i18n.t(COPY_CODE_KEY), { placement: 'bottom' });
|
|
135
158
|
|
|
136
159
|
if (!this.readOnly) {
|
|
137
|
-
|
|
138
|
-
title: lang.name,
|
|
139
|
-
name: lang.id,
|
|
140
|
-
toggle: 'language',
|
|
141
|
-
isActive: (): boolean => this._data.language === lang.id,
|
|
142
|
-
closeOnActivate: true,
|
|
143
|
-
onActivate: (): void => this.setLanguage(lang.id),
|
|
144
|
-
}));
|
|
145
|
-
|
|
146
|
-
this._picker = new PopoverDesktop({
|
|
147
|
-
items: languageItems,
|
|
148
|
-
trigger: dom.languageButton,
|
|
149
|
-
leftAlignElement: dom.wrapper,
|
|
150
|
-
searchable: true,
|
|
151
|
-
width: '200px',
|
|
152
|
-
messages: {
|
|
153
|
-
search: this.api.i18n.t(SEARCH_LANGUAGE_KEY),
|
|
154
|
-
},
|
|
155
|
-
});
|
|
160
|
+
this._picker = this.buildLanguagePicker(dom.languageButton, dom.wrapper);
|
|
156
161
|
|
|
157
162
|
dom.languageButton.addEventListener('click', () => {
|
|
158
|
-
this._picker?.
|
|
163
|
+
if (this._picker?.isShown) {
|
|
164
|
+
this._picker.hide();
|
|
165
|
+
} else {
|
|
166
|
+
this._picker?.show();
|
|
167
|
+
}
|
|
159
168
|
});
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
return dom.wrapper;
|
|
163
172
|
}
|
|
164
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
|
+
|
|
165
183
|
public rendered(): void {
|
|
166
184
|
void this.highlightCode();
|
|
167
185
|
}
|
|
168
186
|
|
|
169
|
-
private
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
} else {
|
|
173
|
-
this.showPreview();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
187
|
+
private setViewMode(mode: CodeViewMode): void {
|
|
188
|
+
this._viewMode = mode;
|
|
189
|
+
this.applyViewMode();
|
|
176
190
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return;
|
|
191
|
+
if (mode === 'preview' || mode === 'split') {
|
|
192
|
+
void this.renderPreview();
|
|
180
193
|
}
|
|
181
|
-
|
|
182
|
-
this._previewActive = false;
|
|
183
|
-
this._dom.preElement.hidden = false;
|
|
184
|
-
this._dom.gutterElement.hidden = !this._lineNumbers;
|
|
185
|
-
this._dom.previewElement.hidden = true;
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
private
|
|
189
|
-
if (!this._dom?.previewElement || !this._dom.
|
|
196
|
+
private applyViewMode(): void {
|
|
197
|
+
if (!this._dom?.previewElement || !this._dom.viewModeContainer || !this._dom.splitContainer) {
|
|
190
198
|
return;
|
|
191
199
|
}
|
|
192
200
|
|
|
193
|
-
|
|
194
|
-
this._dom.
|
|
195
|
-
this._dom.gutterElement.hidden = true;
|
|
196
|
-
this._dom.previewElement.hidden = false;
|
|
201
|
+
// Update segmented control active state
|
|
202
|
+
setActiveViewMode(this._dom.viewModeContainer, this._viewMode);
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
void this.renderPreview();
|
|
200
|
-
}
|
|
204
|
+
const codeBody = this._dom.preElement.parentElement?.parentElement;
|
|
201
205
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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;
|
|
205
236
|
}
|
|
206
|
-
|
|
207
|
-
this._dom.moreMenu.hidden = !this._dom.moreMenu.hidden;
|
|
208
237
|
}
|
|
209
238
|
|
|
210
239
|
private async renderPreview(): Promise<void> {
|
|
211
|
-
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) {
|
|
212
245
|
return;
|
|
213
246
|
}
|
|
214
247
|
|
|
@@ -217,7 +250,7 @@ export class CodeTool implements BlockTool {
|
|
|
217
250
|
? await renderMermaid(code)
|
|
218
251
|
: await renderLatex(code);
|
|
219
252
|
|
|
220
|
-
|
|
253
|
+
container.innerHTML = rendered;
|
|
221
254
|
}
|
|
222
255
|
|
|
223
256
|
public setReadOnly(state: boolean): void {
|
|
@@ -253,25 +286,48 @@ export class CodeTool implements BlockTool {
|
|
|
253
286
|
|
|
254
287
|
if (this._dom) {
|
|
255
288
|
this._dom.codeElement.textContent = this._data.code;
|
|
289
|
+
this.syncTrailingBr();
|
|
290
|
+
this.updateGutter();
|
|
256
291
|
}
|
|
257
292
|
|
|
258
293
|
void this.highlightCode();
|
|
259
294
|
}
|
|
260
295
|
|
|
261
296
|
public renderSettings(): MenuConfig {
|
|
297
|
+
const selectedId = this._data.language;
|
|
298
|
+
const detectedId = this._detectedLanguage;
|
|
299
|
+
const showDetected = detectedId !== null && detectedId !== selectedId;
|
|
300
|
+
|
|
301
|
+
const childItems: PopoverItemParams[] = [];
|
|
302
|
+
|
|
303
|
+
if (showDetected) {
|
|
304
|
+
const detectedLanguage = LANGUAGES.find((lang) => lang.id === detectedId);
|
|
305
|
+
if (detectedLanguage) {
|
|
306
|
+
childItems.push({
|
|
307
|
+
title: detectedLanguage.name,
|
|
308
|
+
secondaryLabel: 'auto',
|
|
309
|
+
icon: IconWand,
|
|
310
|
+
onActivate: (): void => this.setLanguage(detectedLanguage.id),
|
|
311
|
+
closeOnActivate: true,
|
|
312
|
+
isActive: (): boolean => this._data.language === detectedLanguage.id,
|
|
313
|
+
});
|
|
314
|
+
childItems.push({ type: PopoverItemType.Separator });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
childItems.push(...LANGUAGES.map((lang) => ({
|
|
319
|
+
title: lang.name,
|
|
320
|
+
trailingIcon: lang.id === selectedId ? IconCheck : undefined,
|
|
321
|
+
onActivate: (): void => this.setLanguage(lang.id),
|
|
322
|
+
closeOnActivate: true,
|
|
323
|
+
})));
|
|
324
|
+
|
|
262
325
|
return [
|
|
263
326
|
{
|
|
264
327
|
icon: IconCodeBlock,
|
|
265
328
|
title: this.api.i18n.t(LANGUAGE_KEY),
|
|
266
329
|
name: 'code-language',
|
|
267
|
-
children: {
|
|
268
|
-
items: LANGUAGES.map((lang) => ({
|
|
269
|
-
title: lang.name,
|
|
270
|
-
onActivate: (): void => this.setLanguage(lang.id),
|
|
271
|
-
closeOnActivate: true,
|
|
272
|
-
isActive: this._data.language === lang.id,
|
|
273
|
-
})),
|
|
274
|
-
},
|
|
330
|
+
children: { items: childItems },
|
|
275
331
|
},
|
|
276
332
|
];
|
|
277
333
|
}
|
|
@@ -294,6 +350,8 @@ export class CodeTool implements BlockTool {
|
|
|
294
350
|
|
|
295
351
|
if (this._dom) {
|
|
296
352
|
this._dom.codeElement.textContent = this._data.code;
|
|
353
|
+
this.syncTrailingBr();
|
|
354
|
+
this.updateGutter();
|
|
297
355
|
}
|
|
298
356
|
|
|
299
357
|
void this.highlightCode();
|
|
@@ -301,6 +359,7 @@ export class CodeTool implements BlockTool {
|
|
|
301
359
|
|
|
302
360
|
private setLanguage(id: string): void {
|
|
303
361
|
this._data.language = id;
|
|
362
|
+
const isPreviewable = PREVIEWABLE_LANGUAGES.has(id);
|
|
304
363
|
|
|
305
364
|
if (this._dom) {
|
|
306
365
|
// Update the text span inside the language button (first child)
|
|
@@ -309,11 +368,94 @@ export class CodeTool implements BlockTool {
|
|
|
309
368
|
if (textSpan) {
|
|
310
369
|
textSpan.textContent = this.getLanguageName(id);
|
|
311
370
|
}
|
|
371
|
+
|
|
372
|
+
// Show or hide the view mode segmented control based on previewability
|
|
373
|
+
if (this._dom.viewModeContainer) {
|
|
374
|
+
this._dom.viewModeContainer.hidden = !isPreviewable;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// When switching to a previewable language, activate preview mode
|
|
378
|
+
if (isPreviewable && this._dom.previewElement) {
|
|
379
|
+
this._previewContainer = this._dom.previewElement;
|
|
380
|
+
this._viewMode = 'preview';
|
|
381
|
+
this.applyViewMode();
|
|
382
|
+
void this.renderPreview();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// When switching away from a previewable language, reset to code mode
|
|
386
|
+
if (!isPreviewable) {
|
|
387
|
+
this._previewContainer = null;
|
|
388
|
+
this._viewMode = 'code';
|
|
389
|
+
this.applyViewMode();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Rebuild the language picker so the selected language check icon updates
|
|
393
|
+
if (this._picker) {
|
|
394
|
+
this._picker.destroy();
|
|
395
|
+
}
|
|
396
|
+
this._picker = this.buildLanguagePicker(this._dom.languageButton, this._dom.wrapper);
|
|
312
397
|
}
|
|
313
398
|
|
|
314
399
|
void this.highlightCode();
|
|
315
400
|
}
|
|
316
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Builds the language items array. When a detected language differs from the
|
|
404
|
+
* chosen one, it appears first with a wand icon and "auto" secondary label.
|
|
405
|
+
* The currently selected language is shown with a trailing check icon
|
|
406
|
+
* in its natural position in the full language list.
|
|
407
|
+
*/
|
|
408
|
+
private buildLanguagePickerItems(): PopoverItemParams[] {
|
|
409
|
+
const selectedId = this._data.language;
|
|
410
|
+
const detectedId = this._detectedLanguage;
|
|
411
|
+
const showDetected = detectedId !== null && detectedId !== selectedId;
|
|
412
|
+
|
|
413
|
+
const items: PopoverItemParams[] = [];
|
|
414
|
+
|
|
415
|
+
if (showDetected) {
|
|
416
|
+
const detectedLanguage = LANGUAGES.find((lang) => lang.id === detectedId);
|
|
417
|
+
if (detectedLanguage) {
|
|
418
|
+
items.push({
|
|
419
|
+
title: detectedLanguage.name,
|
|
420
|
+
name: detectedLanguage.id,
|
|
421
|
+
icon: IconWand,
|
|
422
|
+
toggle: 'language',
|
|
423
|
+
isActive: (): boolean => this._data.language === detectedLanguage.id,
|
|
424
|
+
closeOnActivate: true,
|
|
425
|
+
onActivate: (): void => this.setLanguage(detectedLanguage.id),
|
|
426
|
+
});
|
|
427
|
+
items.push({ type: PopoverItemType.Separator });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
items.push(...LANGUAGES.map((lang) => ({
|
|
432
|
+
title: lang.name,
|
|
433
|
+
name: lang.id,
|
|
434
|
+
trailingIcon: lang.id === selectedId ? IconCheck : undefined,
|
|
435
|
+
toggle: 'language',
|
|
436
|
+
closeOnActivate: true,
|
|
437
|
+
onActivate: (): void => this.setLanguage(lang.id),
|
|
438
|
+
})));
|
|
439
|
+
|
|
440
|
+
return items;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Creates a new PopoverDesktop instance for the language picker.
|
|
445
|
+
*/
|
|
446
|
+
private buildLanguagePicker(trigger: HTMLElement, leftAlignElement: HTMLElement): PopoverDesktop {
|
|
447
|
+
return new PopoverDesktop({
|
|
448
|
+
items: this.buildLanguagePickerItems(),
|
|
449
|
+
trigger,
|
|
450
|
+
leftAlignElement,
|
|
451
|
+
searchable: true,
|
|
452
|
+
width: '200px',
|
|
453
|
+
messages: {
|
|
454
|
+
search: this.api.i18n.t(SEARCH_LANGUAGE_KEY),
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
317
459
|
private getLanguageName(id: string): string {
|
|
318
460
|
const entry = LANGUAGES.find((lang) => lang.id === id);
|
|
319
461
|
|
|
@@ -339,28 +481,6 @@ export class CodeTool implements BlockTool {
|
|
|
339
481
|
}).catch(() => { /* clipboard unavailable */ });
|
|
340
482
|
}
|
|
341
483
|
|
|
342
|
-
private toggleWrap(): void {
|
|
343
|
-
this._wrapping = !this._wrapping;
|
|
344
|
-
|
|
345
|
-
if (!this._dom) {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (this._wrapping) {
|
|
350
|
-
this._dom.codeElement.className = CODE_AREA_STYLES;
|
|
351
|
-
} else {
|
|
352
|
-
this._dom.codeElement.className = CODE_AREA_STYLES.replace('whitespace-pre-wrap', 'whitespace-pre');
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private toggleLineNumbers(): void {
|
|
357
|
-
this._lineNumbers = !this._lineNumbers;
|
|
358
|
-
|
|
359
|
-
if (this._dom) {
|
|
360
|
-
this._dom.gutterElement.hidden = !this._lineNumbers;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
484
|
private updateGutter(): void {
|
|
365
485
|
if (!this._dom) {
|
|
366
486
|
return;
|
|
@@ -419,6 +539,30 @@ export class CodeTool implements BlockTool {
|
|
|
419
539
|
});
|
|
420
540
|
}
|
|
421
541
|
|
|
542
|
+
private scheduleDetection(): void {
|
|
543
|
+
if (this._detectionTimeoutId !== null) {
|
|
544
|
+
clearTimeout(this._detectionTimeoutId);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
this._detectionTimeoutId = setTimeout(() => {
|
|
548
|
+
this._detectionTimeoutId = null;
|
|
549
|
+
const code = this._dom?.codeElement.textContent ?? '';
|
|
550
|
+
void detectLanguage(code).then((detected) => {
|
|
551
|
+
if (detected === this._detectedLanguage) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this._detectedLanguage = detected;
|
|
555
|
+
// Rebuild picker so the detected language section updates
|
|
556
|
+
if (this._dom) {
|
|
557
|
+
if (this._picker) {
|
|
558
|
+
this._picker.destroy();
|
|
559
|
+
}
|
|
560
|
+
this._picker = this.buildLanguagePicker(this._dom.languageButton, this._dom.wrapper);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}, 600);
|
|
564
|
+
}
|
|
565
|
+
|
|
422
566
|
private async highlightCode(): Promise<void> {
|
|
423
567
|
if (!isHighlightingSupported() || !isHighlightable(this._data.language)) {
|
|
424
568
|
this._disposeHighlights?.();
|
|
@@ -460,10 +604,17 @@ export class CodeTool implements BlockTool {
|
|
|
460
604
|
this._highlightRafId = null;
|
|
461
605
|
}
|
|
462
606
|
|
|
607
|
+
if (this._detectionTimeoutId !== null) {
|
|
608
|
+
clearTimeout(this._detectionTimeoutId);
|
|
609
|
+
this._detectionTimeoutId = null;
|
|
610
|
+
}
|
|
611
|
+
|
|
463
612
|
if (this._picker) {
|
|
464
613
|
this._picker.destroy();
|
|
465
614
|
this._picker = null;
|
|
466
615
|
}
|
|
616
|
+
|
|
617
|
+
this._dom = null;
|
|
467
618
|
}
|
|
468
619
|
|
|
469
620
|
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
|
+
}
|
|
@@ -88,6 +88,16 @@ export interface BlockTool extends BaseTool {
|
|
|
88
88
|
*/
|
|
89
89
|
getContentOffset?(hoveredElement: Element): { left: number } | undefined;
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Returns the element that the toolbar should vertically center on.
|
|
93
|
+
* Used by tools whose editable area is deeply nested below non-editable UI
|
|
94
|
+
* (e.g., a header bar), where the default contenteditable-descendant search
|
|
95
|
+
* would position the toolbar too far down inside the block.
|
|
96
|
+
*
|
|
97
|
+
* Return undefined to use the default positioning logic.
|
|
98
|
+
*/
|
|
99
|
+
getToolbarAnchorElement?(): HTMLElement | undefined;
|
|
100
|
+
|
|
91
101
|
/**
|
|
92
102
|
* Called when read-only mode is toggled without re-rendering the block.
|
|
93
103
|
* Implementations should update the DOM in place: toggle contentEditable,
|
|
@@ -151,6 +151,12 @@ export interface PopoverItemDefaultBaseParams {
|
|
|
151
151
|
*/
|
|
152
152
|
icon?: string;
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Icon to be displayed on the trailing (right) side of the item.
|
|
156
|
+
* Rendered without a box background, suitable for indicators like a checkmark.
|
|
157
|
+
*/
|
|
158
|
+
trailingIcon?: string;
|
|
159
|
+
|
|
154
160
|
/**
|
|
155
161
|
* Additional displayed text
|
|
156
162
|
*/
|