@jackuait/blok 0.19.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blok.cjs +1 -1
- package/dist/blok.iife.js +4 -4
- package/dist/blok.mjs +2 -2
- package/dist/blok.umd.js +41 -41
- package/dist/chunks/{blok-C8_pY9Bg.cjs → blok-Cz71HTuP.cjs} +13 -13
- package/dist/chunks/{blok-DCc23rzA.mjs → blok-x8D2aON2.mjs} +2645 -2116
- package/dist/chunks/constants-BK6kLyZB.cjs +606 -0
- package/dist/chunks/{constants-C3-Lh6Fs.mjs → constants-CLzXh7Tb.mjs} +392 -267
- package/dist/chunks/{i18next-loader-BBPMegxc.cjs → i18next-loader-DVakgVCy.cjs} +1 -1
- package/dist/chunks/{i18next-loader-K-ir8UKk.mjs → i18next-loader-DfIonnVO.mjs} +1 -1
- package/dist/chunks/{lightweight-i18n-LFW8S8u-.cjs → lightweight-i18n-BnIqQ5Cf.cjs} +1 -1
- package/dist/chunks/{lightweight-i18n-DJpMke7a.mjs → lightweight-i18n-Cj88c8gl.mjs} +15 -0
- package/dist/chunks/{messages-Bb80Csl3.mjs → messages-0EAci1N0.mjs} +15 -0
- package/dist/chunks/{messages-BkgBVEiE.cjs → messages-1jzEPeW5.cjs} +1 -1
- package/dist/chunks/{messages-CATH_uz1.mjs → messages-4CqVAaCY.mjs} +16 -1
- package/dist/chunks/{messages-DFHNw6PQ.cjs → messages-4Qka94cm.cjs} +1 -1
- package/dist/chunks/{messages-CBbgEXuO.cjs → messages-7wDDggws.cjs} +1 -1
- package/dist/chunks/{messages-B7st3eF_.cjs → messages-9VzB9Djh.cjs} +1 -1
- package/dist/chunks/{messages-B_cHWyWC.cjs → messages-9bK437Rh.cjs} +1 -1
- package/dist/chunks/{messages-CZddldga.cjs → messages-AtTKtjsQ.cjs} +1 -1
- package/dist/chunks/{messages-HJnG9r2l.mjs → messages-B1inw201.mjs} +16 -1
- package/dist/chunks/{messages-B-u5B7tY.cjs → messages-B4ZVgIh5.cjs} +1 -1
- package/dist/chunks/{messages-CJFSq8Fl.cjs → messages-B5bSFRCa.cjs} +1 -1
- package/dist/{messages-HVgCKIww2.mjs → chunks/messages-B705gxqz.mjs} +16 -1
- package/dist/{messages-Dq_RdEny2.mjs → chunks/messages-B7F3tX4u.mjs} +16 -1
- package/dist/chunks/{messages-NJAjMFXd.cjs → messages-BDer7I67.cjs} +1 -1
- package/dist/chunks/{messages-BV22YIZ_.cjs → messages-BEEGgtPo.cjs} +1 -1
- package/dist/{messages-bN4Zmk2m.mjs → chunks/messages-BGlEWmsB.mjs} +16 -1
- package/dist/{messages-Ao2vCNAH2.mjs → chunks/messages-BJJsuZhy.mjs} +16 -1
- package/dist/chunks/{messages-DjrfovG0.cjs → messages-BJcc9h6p.cjs} +1 -1
- package/dist/chunks/{messages-Hfo6uCIC.cjs → messages-BL-E2Kw3.cjs} +1 -1
- package/dist/{messages-8__-vX3U.mjs → chunks/messages-BLJGzntb.mjs} +16 -1
- package/dist/chunks/{messages-Au1UpgkX.cjs → messages-BLNieIb-.cjs} +1 -1
- package/dist/chunks/{messages-DQJcAz4s.cjs → messages-BRbGk1-Z.cjs} +1 -1
- package/dist/chunks/{messages-BwOUSDJR.mjs → messages-BUlZRNoO.mjs} +15 -0
- package/dist/{messages-BKGg4XYm2.mjs → chunks/messages-BWQF9y2r.mjs} +16 -1
- package/dist/chunks/{messages-UXS3zL9A2.cjs → messages-BWh9I5Jv2.cjs} +1 -1
- package/dist/chunks/{messages-B6dbtt1c.cjs → messages-BX86v7Ev.cjs} +1 -1
- package/dist/chunks/{messages-DIFwztFo.cjs → messages-BYpvkG9S.cjs} +1 -1
- package/dist/{messages-CDueJTe8.mjs → chunks/messages-Bdw_vAdq2.mjs} +16 -1
- package/dist/chunks/{messages-DIetNXTx.cjs → messages-BeQVEC9q.cjs} +1 -1
- package/dist/{messages-CusWGsl42.mjs → chunks/messages-BeTZTXZm.mjs} +16 -1
- package/dist/chunks/{messages-CmsAbu_A.cjs → messages-BhTlR4PP.cjs} +1 -1
- package/dist/chunks/{messages-C3rGCBi_.mjs → messages-BhnZTZbQ.mjs} +16 -1
- package/dist/{messages-BpwhHkFb2.mjs → chunks/messages-BqJdoBHm.mjs} +16 -1
- package/dist/chunks/{messages-zPUQsksJ.mjs → messages-BqPiH7QC.mjs} +16 -1
- package/dist/chunks/{messages-CjfEt9bg.cjs → messages-Br_qu_wR.cjs} +1 -1
- package/dist/chunks/{messages-DtaPHADA.cjs → messages-BscUmD6D.cjs} +1 -1
- package/dist/{messages-Cg6a88Bb2.mjs → chunks/messages-Btapuvxo.mjs} +16 -1
- package/dist/chunks/{messages-CEySIig9.cjs → messages-BwCw0TJ1.cjs} +1 -1
- package/dist/chunks/{messages-BO-L5oN9.mjs → messages-C3RDWmXV.mjs} +15 -0
- package/dist/chunks/{messages-CZJZ-au3.cjs → messages-C3SZsVB9.cjs} +1 -1
- package/dist/chunks/{messages-DQr7bXMD.mjs → messages-C65mLRVK.mjs} +16 -1
- package/dist/chunks/{messages-CT8djlV6.cjs → messages-C73dKqPT.cjs} +1 -1
- package/dist/chunks/{messages-DOqJ6ogK.cjs → messages-C8f4kUOB.cjs} +1 -1
- package/dist/{messages-yuNINyS1.mjs → chunks/messages-C9vfrzKQ.mjs} +16 -1
- package/dist/{messages-Grhv5SE_.mjs → chunks/messages-CEbe34zh.mjs} +15 -0
- package/dist/chunks/{messages-CcsM8Q7g.cjs → messages-CHEpECGv.cjs} +1 -1
- package/dist/chunks/{messages-DEg5FAg1.cjs → messages-CHP0nYRn.cjs} +1 -1
- package/dist/chunks/{messages-CjCn5IsF.mjs → messages-CIbh-0oJ.mjs} +16 -1
- package/dist/chunks/{messages-BHCepNsI.cjs → messages-CJC94Hci.cjs} +1 -1
- package/dist/chunks/{messages-ByNR9m9W.cjs → messages-CMP5dbqQ.cjs} +1 -1
- package/dist/chunks/{messages-DpNmnBjK.cjs → messages-CMm8FeOL.cjs} +1 -1
- package/dist/chunks/{messages-Da8yFc7l.cjs → messages-CNMPtWiq.cjs} +1 -1
- package/dist/{messages-CqZgsrVP2.mjs → chunks/messages-CQUr4lBa.mjs} +16 -1
- package/dist/{messages-Cvq2EFPT2.mjs → chunks/messages-CUuOX1k3.mjs} +16 -1
- package/dist/chunks/{messages-BsTzdfZF.cjs → messages-CVerbLou.cjs} +1 -1
- package/dist/{messages-CGiVoFTi.mjs → chunks/messages-CWW9OwKO.mjs} +16 -1
- package/dist/{messages-Da1woCgt2.mjs → chunks/messages-CWqap54p.mjs} +16 -1
- package/dist/{messages-CEGqNXnP.mjs → chunks/messages-CXNQBFHp.mjs} +16 -1
- package/dist/chunks/{messages-DgF8OkNr.mjs → messages-CYDhuSF4.mjs} +16 -1
- package/dist/chunks/{messages-Bp0LdGKj.mjs → messages-CYy3Zd3F.mjs} +15 -0
- package/dist/{messages-CUcUO5bC2.mjs → chunks/messages-C_BM_XHo.mjs} +16 -1
- package/dist/chunks/{messages-IqNWRaNS.cjs → messages-CbKOqXT6.cjs} +1 -1
- package/dist/chunks/{messages-Bmq--WTx.mjs → messages-CbxXU_Um.mjs} +15 -0
- package/dist/chunks/{messages-CVrOxu3J.cjs → messages-CcO-3_4k.cjs} +1 -1
- package/dist/chunks/{messages-D41fO84y.cjs → messages-CcTkC677.cjs} +1 -1
- package/dist/chunks/{messages-Cy-2_lIA.cjs → messages-CdEZ98_p.cjs} +1 -1
- package/dist/{messages-KyJQtOkh2.mjs → chunks/messages-CeZ1lo-o.mjs} +16 -1
- package/dist/chunks/{messages-D7H9JUDr.cjs → messages-CgFAGUJX.cjs} +1 -1
- package/dist/chunks/{messages-Dwl878eS.mjs → messages-CprMp4Va.mjs} +16 -1
- package/dist/{messages-wmS6Z2m82.mjs → chunks/messages-CtXDxPOq.mjs} +16 -1
- package/dist/{messages-DcQNeoPB.mjs → chunks/messages-Cutnm4AZ.mjs} +15 -0
- package/dist/chunks/{messages-cSBZ5qVe.mjs → messages-CwIC_sse.mjs} +16 -1
- package/dist/chunks/{messages-7LWW3BVh.mjs → messages-CwbTr9lL.mjs} +16 -1
- package/dist/chunks/{messages-ETJ7tQ9O.cjs → messages-CxIknrw4.cjs} +1 -1
- package/dist/{messages-VIQ8vjvS.mjs → chunks/messages-D0Jj7Dtw.mjs} +15 -0
- package/dist/chunks/{messages-SozFksSf.cjs → messages-D0th-I9S.cjs} +1 -1
- package/dist/chunks/{messages-gh5G15J1.cjs → messages-D1XTyTJz.cjs} +1 -1
- package/dist/chunks/{messages-DNtAzacv.mjs → messages-D24EoKbO.mjs} +16 -1
- package/dist/chunks/{messages-DDJkNbJy.mjs → messages-D2chjyLM.mjs} +16 -1
- package/dist/chunks/{messages-CqRtDVFO.cjs → messages-D6y13edn.cjs} +1 -1
- package/dist/chunks/{messages-EsX3eikv.cjs → messages-D86p1eQY.cjs} +1 -1
- package/dist/{messages-BwCgJUdq.mjs → chunks/messages-D8L_8amv.mjs} +16 -1
- package/dist/chunks/{messages-C2ptZOWh.cjs → messages-DC-yFRyF.cjs} +1 -1
- package/dist/{messages-BgPDPIkO.mjs → chunks/messages-DEPxxCNU.mjs} +16 -1
- package/dist/chunks/{messages-VeyvtTce.cjs → messages-DG9l5agQ.cjs} +1 -1
- package/dist/chunks/{messages-B6794_2J.cjs → messages-DIeUb2w-.cjs} +1 -1
- package/dist/chunks/{messages-COx3hDcP.cjs → messages-DJ-RX5EJ.cjs} +1 -1
- package/dist/chunks/{messages-Cvg_uTfy.mjs → messages-DJ7lbpBX.mjs} +16 -1
- package/dist/{messages-Cn3uvFq3.mjs → chunks/messages-DJH_kd46.mjs} +16 -1
- package/dist/chunks/{messages-CWFSMzug2.cjs → messages-DLYl6KdK2.cjs} +1 -1
- package/dist/{messages-Ch-SytN8.mjs → chunks/messages-DMcCkOtN.mjs} +16 -1
- package/dist/chunks/{messages-BdL8bmTL.mjs → messages-DQks74hq.mjs} +16 -1
- package/dist/chunks/{messages-D-QWtvWg.mjs → messages-DTiqggtG.mjs} +16 -1
- package/dist/chunks/{messages-typO_i_k.mjs → messages-DTsSYToI.mjs} +15 -0
- package/dist/{messages-CwvHO8gp2.mjs → chunks/messages-DU6TOz6v.mjs} +16 -1
- package/dist/chunks/{messages-DmjccFHq.cjs → messages-DUbZA-w_.cjs} +1 -1
- package/dist/chunks/{messages-DCwrXfck.cjs → messages-DVAU-8rH.cjs} +1 -1
- package/dist/chunks/{messages-Z4LYgfWc.cjs → messages-Df6vXDrY.cjs} +1 -1
- package/dist/chunks/{messages-9B1F-k48.cjs → messages-Dh79ToYM.cjs} +1 -1
- package/dist/chunks/{messages-CuMtymEs.cjs → messages-Dh9psewW.cjs} +1 -1
- package/dist/chunks/{messages-BjYhwRk7.cjs → messages-Dln2T0zJ.cjs} +1 -1
- package/dist/chunks/{messages-DT-JnTnw.cjs → messages-DoA0Ecoi.cjs} +1 -1
- package/dist/chunks/{messages-D-p-omWy.mjs → messages-Dre_MB7o.mjs} +16 -1
- package/dist/chunks/{messages-BveiGA2x.cjs → messages-Dv4oyF7T.cjs} +1 -1
- package/dist/{messages-C6R0bzW5.mjs → chunks/messages-FMAQkmg4.mjs} +15 -0
- package/dist/{messages-CQzOV7VE2.mjs → chunks/messages-GownDU-J.mjs} +16 -1
- package/dist/chunks/{messages-CH9SBR-i.cjs → messages-JYea02IT.cjs} +1 -1
- package/dist/chunks/{messages-BXp_U3uS.cjs → messages-JyzorVPx.cjs} +1 -1
- package/dist/{messages-BIgvPd6I2.mjs → chunks/messages-L70TFQAY.mjs} +16 -1
- package/dist/{messages-CskLLTiG2.mjs → chunks/messages-L8CslBBZ.mjs} +16 -1
- package/dist/chunks/{messages-CM8uUnPx.cjs → messages-LX9PuLsT.cjs} +1 -1
- package/dist/{messages-BarhGl2S2.mjs → chunks/messages-NFUr7Sxk.mjs} +16 -1
- package/dist/chunks/{messages-CGdCpNZ62.cjs → messages-PpRvDwOa2.cjs} +1 -1
- package/dist/chunks/{messages-ZMG7qQ3D.mjs → messages-QQ5-ET8y.mjs} +16 -1
- package/dist/{messages-DbE-riR52.mjs → chunks/messages-Stf6rn2I.mjs} +16 -1
- package/dist/chunks/{messages-D7sU-NqL.cjs → messages-T8hCmys7.cjs} +1 -1
- package/dist/chunks/{messages-DDX6I6MX.mjs → messages-TZtv60nI.mjs} +16 -1
- package/dist/chunks/{messages-CeLv1whu2.mjs → messages-V9bvFOFc2.mjs} +16 -1
- package/dist/chunks/{messages-tR0BdeVG.cjs → messages-Vp8CpYh3.cjs} +1 -1
- package/dist/chunks/{messages-DUWCkLYR.cjs → messages-XqCqLy0k.cjs} +1 -1
- package/dist/chunks/{messages-B01D7aHY.mjs → messages-_1y6EX7X.mjs} +16 -1
- package/dist/chunks/{messages-CXk-iM-i.mjs → messages-aTZglEV9.mjs} +16 -1
- package/dist/chunks/{messages-BHdKd7pH.cjs → messages-cSHh8Zgv.cjs} +1 -1
- package/dist/{messages-P5B61L1J2.mjs → chunks/messages-e8gxPi7a.mjs} +16 -1
- package/dist/{messages-CUFac9Qe.mjs → chunks/messages-hiwlrrr-.mjs} +16 -1
- package/dist/{messages-CeqpzGl9.mjs → chunks/messages-icPZhZ81.mjs} +15 -0
- package/dist/chunks/{messages-Byn9-dhb2.mjs → messages-lbwOYFxS2.mjs} +16 -1
- package/dist/chunks/{messages-B6yMdb3B.cjs → messages-lvo-9Be0.cjs} +1 -1
- package/dist/chunks/{messages-B2iaIrIu.mjs → messages-rm6jXexA.mjs} +16 -1
- package/dist/chunks/{messages-Ciq5_PsP.cjs → messages-sTyGkmbu.cjs} +1 -1
- package/dist/{messages-CF5MeYEt2.mjs → chunks/messages-uBwK-Ze2.mjs} +16 -1
- package/dist/chunks/{messages-BaVZgTrw.cjs → messages-uOISsfmj.cjs} +1 -1
- package/dist/{messages-L4sYfXpM.mjs → chunks/messages-wq0vxJ3p.mjs} +16 -1
- package/dist/chunks/{messages-CQT8TtiF.cjs → messages-z2DzBRgO.cjs} +1 -1
- package/dist/chunks/tools-BKjP4TXy.cjs +208 -0
- package/dist/chunks/{tools-fEI-EVng.mjs → tools-Bw8_CkhE.mjs} +2537 -2469
- package/dist/full.cjs +1 -1
- package/dist/full.mjs +3 -3
- package/dist/locales.mjs +82 -67
- package/dist/{messages-BlvF3pot.mjs → messages--sHkn4PD.mjs} +16 -1
- package/dist/{chunks/messages-QCaF3d92.mjs → messages-4mxgsYKZ2.mjs} +16 -1
- package/dist/{messages-zOl6jpOa.mjs → messages-6fQMeTiI.mjs} +16 -1
- package/dist/{messages-BRLWpOnP.mjs → messages-B22G0fir.mjs} +16 -1
- package/dist/{chunks/messages-kqnzps-X.mjs → messages-B908tGhl.mjs} +16 -1
- package/dist/{chunks/messages-C8_r3iTR2.mjs → messages-BAUvKI0_.mjs} +16 -1
- package/dist/{chunks/messages-BpMWMH9_.mjs → messages-BAfMwQnT2.mjs} +16 -1
- package/dist/{messages-Bmu3rJwO.mjs → messages-BCk7o4Pz.mjs} +16 -1
- package/dist/{messages-D4dovFzY2.mjs → messages-BG2BE6Sg2.mjs} +16 -1
- package/dist/{messages-DR2JBU-H.mjs → messages-BGe6GS_j.mjs} +16 -1
- package/dist/{chunks/messages-CeIzWL_4.mjs → messages-BHLrVFmZ2.mjs} +16 -1
- package/dist/{messages-BOWDzfkH.mjs → messages-BJjffJIu.mjs} +16 -1
- package/dist/{chunks/messages-DHv7knYB.mjs → messages-BSGZSOH32.mjs} +16 -1
- package/dist/{chunks/messages-BRTWvTAP.mjs → messages-BT6TyDYQ2.mjs} +16 -1
- package/dist/{messages-jQsl7cga2.mjs → messages-BWZ0_zUi2.mjs} +16 -1
- package/dist/{messages-lnxRz5rM.mjs → messages-BWhJgyhl.mjs} +16 -1
- package/dist/{chunks/messages-Xh-c7d4C.mjs → messages-BXqLaS2g2.mjs} +16 -1
- package/dist/{messages-gTUy3xjb.mjs → messages-BZiuFv_O.mjs} +15 -0
- package/dist/{chunks/messages-B_d857cA.mjs → messages-Bb-XEtyl.mjs} +15 -0
- package/dist/{messages-C9Kq8259.mjs → messages-BkKAnFz_.mjs} +15 -0
- package/dist/{chunks/messages-BUvlOqll.mjs → messages-BoLu5_Wp2.mjs} +16 -1
- package/dist/{messages-RXQKVFCJ.mjs → messages-BpqRzil6.mjs} +16 -1
- package/dist/{chunks/messages-CkosWGGH.mjs → messages-C2E8rQjb.mjs} +15 -0
- package/dist/{chunks/messages-jEbEE90_.mjs → messages-C2JJxi5D2.mjs} +16 -1
- package/dist/{messages-jsrzDVZy2.mjs → messages-C4d1_z432.mjs} +16 -1
- package/dist/{chunks/messages-MPe9FjYe.mjs → messages-C5Igyij0.mjs} +16 -1
- package/dist/{messages-CAnOyxrm.mjs → messages-C8fscB2J.mjs} +16 -1
- package/dist/{chunks/messages-CX0tJpOb.mjs → messages-CD8CgT1F.mjs} +16 -1
- package/dist/{messages-BKG0QHVl.mjs → messages-CDxPk3xE.mjs} +15 -0
- package/dist/{messages-hGop1gbF.mjs → messages-CGXrIdHT.mjs} +15 -0
- package/dist/{chunks/messages-DuHFGMCJ.mjs → messages-CHfkCxGQ2.mjs} +16 -1
- package/dist/{messages-fAEKzdUq.mjs → messages-CMvNzksb.mjs} +16 -1
- package/dist/{chunks/messages-t26x8wxB.mjs → messages-CSIhcdDx2.mjs} +16 -1
- package/dist/{messages-UJx6PvCL.mjs → messages-CW5z5hSz.mjs} +16 -1
- package/dist/{chunks/messages-CUPup5e3.mjs → messages-CYvpDx4p2.mjs} +16 -1
- package/dist/{chunks/messages-Ca9zIrBo.mjs → messages-CcTlmrYs.mjs} +16 -1
- package/dist/{messages-CcF4SO9W.mjs → messages-Ce8ShL0-.mjs} +16 -1
- package/dist/{chunks/messages-CbyVDo8y.mjs → messages-CgT32m3D.mjs} +15 -0
- package/dist/{chunks/messages-ypIHI9Fr.mjs → messages-CmCBnNtA2.mjs} +16 -1
- package/dist/{messages-DGAWjKQZ.mjs → messages-CneVTJSN.mjs} +16 -1
- package/dist/{messages-C5IE9mXm.mjs → messages-CpNerer8.mjs} +15 -0
- package/dist/{chunks/messages-D-s4_vnU.mjs → messages-Cs0L1gyv.mjs} +16 -1
- package/dist/{chunks/messages-BYCHzM7F.mjs → messages-CsGnp1wE2.mjs} +16 -1
- package/dist/{chunks/messages-D4u2PSQw.mjs → messages-CvbIxcr7.mjs} +16 -1
- package/dist/{chunks/messages-DuD4ut1b.mjs → messages-Cx6oQfMR2.mjs} +16 -1
- package/dist/{messages-BvutXmgt.mjs → messages-DBESJOhQ.mjs} +16 -1
- package/dist/{chunks/messages-Cl_pM31G.mjs → messages-DGOBPnKC2.mjs} +16 -1
- package/dist/{messages-B13GEFFu.mjs → messages-DK2AwXAp.mjs} +16 -1
- package/dist/{chunks/messages-CI2W7P-j.mjs → messages-DMbnesQ9.mjs} +16 -1
- package/dist/{messages-4drvqPN8.mjs → messages-DOiSOenW.mjs} +16 -1
- package/dist/{chunks/messages-5339ApgH.mjs → messages-DQqp_ANL.mjs} +16 -1
- package/dist/{chunks/messages-Ck1-ddbo.mjs → messages-DThG2jW-2.mjs} +16 -1
- package/dist/{chunks/messages-DQfi2DoY.mjs → messages-DXlB7Vpm.mjs} +16 -1
- package/dist/{chunks/messages-DjZoCJ00.mjs → messages-De0NAxJt.mjs} +15 -0
- package/dist/{messages-BI5B9bxE.mjs → messages-Depz8d3Q.mjs} +15 -0
- package/dist/{messages-BQaiScU1.mjs → messages-Dfsw3a8v.mjs} +16 -1
- package/dist/{messages-D9qgYMU82.mjs → messages-Dhmmqob82.mjs} +16 -1
- package/dist/{chunks/messages-DiODNXET.mjs → messages-DmNUAv0e2.mjs} +16 -1
- package/dist/{chunks/messages-3jkCmB_A.mjs → messages-DrQaJrDO.mjs} +16 -1
- package/dist/{messages-B2YnZIU2.mjs → messages-DsksuHtZ.mjs} +16 -1
- package/dist/{chunks/messages-m-A6ArIM.mjs → messages-DzOVTq3E2.mjs} +16 -1
- package/dist/{chunks/messages-C0swL9CY.mjs → messages-Igpt5WIA2.mjs} +16 -1
- package/dist/{chunks/messages-CIBYzzFy.mjs → messages-JhpGtjjS.mjs} +16 -1
- package/dist/{chunks/messages-BbZz_Anm.mjs → messages-WCy1wiGS2.mjs} +16 -1
- package/dist/{chunks/messages-CRUb9o5k.mjs → messages-XbA-jXKc.mjs} +15 -0
- package/dist/{chunks/messages-BeS9EOEO.mjs → messages-imHGm9SI2.mjs} +16 -1
- package/dist/{messages-BQhHthxq2.mjs → messages-yTNyxXRV2.mjs} +16 -1
- package/dist/react.cjs +1 -1
- package/dist/react.mjs +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/components/i18n/locales/am/messages.json +16 -1
- package/src/components/i18n/locales/ar/messages.json +16 -1
- package/src/components/i18n/locales/az/messages.json +16 -1
- package/src/components/i18n/locales/bg/messages.json +16 -1
- package/src/components/i18n/locales/bn/messages.json +16 -1
- package/src/components/i18n/locales/bs/messages.json +16 -1
- package/src/components/i18n/locales/cs/messages.json +16 -1
- package/src/components/i18n/locales/da/messages.json +16 -1
- package/src/components/i18n/locales/de/messages.json +16 -1
- package/src/components/i18n/locales/dv/messages.json +16 -1
- package/src/components/i18n/locales/el/messages.json +16 -1
- package/src/components/i18n/locales/en/messages.json +15 -0
- package/src/components/i18n/locales/es/messages.json +15 -0
- package/src/components/i18n/locales/et/messages.json +15 -0
- package/src/components/i18n/locales/fa/messages.json +15 -0
- package/src/components/i18n/locales/fi/messages.json +15 -0
- package/src/components/i18n/locales/fil/messages.json +15 -0
- package/src/components/i18n/locales/fr/messages.json +15 -0
- package/src/components/i18n/locales/gu/messages.json +15 -0
- package/src/components/i18n/locales/he/messages.json +15 -0
- package/src/components/i18n/locales/hi/messages.json +15 -0
- package/src/components/i18n/locales/hr/messages.json +15 -0
- package/src/components/i18n/locales/hu/messages.json +15 -0
- package/src/components/i18n/locales/hy/messages.json +16 -1
- package/src/components/i18n/locales/id/messages.json +16 -1
- package/src/components/i18n/locales/it/messages.json +16 -1
- package/src/components/i18n/locales/ja/messages.json +16 -1
- package/src/components/i18n/locales/ka/messages.json +16 -1
- package/src/components/i18n/locales/km/messages.json +16 -1
- package/src/components/i18n/locales/kn/messages.json +16 -1
- package/src/components/i18n/locales/ko/messages.json +16 -1
- package/src/components/i18n/locales/ku/messages.json +16 -1
- package/src/components/i18n/locales/lo/messages.json +16 -1
- package/src/components/i18n/locales/lt/messages.json +16 -1
- package/src/components/i18n/locales/lv/messages.json +16 -1
- package/src/components/i18n/locales/mk/messages.json +16 -1
- package/src/components/i18n/locales/ml/messages.json +16 -1
- package/src/components/i18n/locales/mn/messages.json +16 -1
- package/src/components/i18n/locales/mr/messages.json +16 -1
- package/src/components/i18n/locales/ms/messages.json +16 -1
- package/src/components/i18n/locales/my/messages.json +16 -1
- package/src/components/i18n/locales/ne/messages.json +16 -1
- package/src/components/i18n/locales/nl/messages.json +16 -1
- package/src/components/i18n/locales/no/messages.json +16 -1
- package/src/components/i18n/locales/pa/messages.json +16 -1
- package/src/components/i18n/locales/pl/messages.json +16 -1
- package/src/components/i18n/locales/ps/messages.json +16 -1
- package/src/components/i18n/locales/pt/messages.json +16 -1
- package/src/components/i18n/locales/ro/messages.json +16 -1
- package/src/components/i18n/locales/ru/messages.json +16 -1
- package/src/components/i18n/locales/sd/messages.json +16 -1
- package/src/components/i18n/locales/si/messages.json +16 -1
- package/src/components/i18n/locales/sk/messages.json +16 -1
- package/src/components/i18n/locales/sl/messages.json +16 -1
- package/src/components/i18n/locales/sq/messages.json +16 -1
- package/src/components/i18n/locales/sr/messages.json +16 -1
- package/src/components/i18n/locales/sv/messages.json +16 -1
- package/src/components/i18n/locales/sw/messages.json +16 -1
- package/src/components/i18n/locales/ta/messages.json +16 -1
- package/src/components/i18n/locales/te/messages.json +16 -1
- package/src/components/i18n/locales/th/messages.json +16 -1
- package/src/components/i18n/locales/tr/messages.json +16 -1
- package/src/components/i18n/locales/ug/messages.json +16 -1
- package/src/components/i18n/locales/uk/messages.json +16 -1
- package/src/components/i18n/locales/ur/messages.json +16 -1
- package/src/components/i18n/locales/vi/messages.json +16 -1
- package/src/components/i18n/locales/yi/messages.json +16 -1
- package/src/components/i18n/locales/zh/messages.json +16 -1
- package/src/components/modules/paste/handlers/blok-data-handler.ts +39 -23
- package/src/components/modules/paste/index.ts +38 -4
- package/src/components/modules/paste/notion-blocks-v3.ts +932 -0
- package/src/components/modules/paste/notion-preprocessor.ts +67 -0
- package/src/components/utils/media-empty-state.ts +48 -5
- package/src/styles/audio.css +348 -36
- package/src/styles/tables.css +10 -0
- package/src/tools/audio/constants.ts +6 -0
- package/src/tools/audio/controls.ts +53 -7
- package/src/tools/audio/cover-picker.ts +126 -0
- package/src/tools/audio/disc.ts +137 -0
- package/src/tools/audio/index.ts +114 -6
- package/src/tools/audio/liveliness.ts +26 -8
- package/src/tools/audio/ui.ts +39 -4
- package/dist/chunks/constants-R70ypRAK.cjs +0 -606
- package/dist/chunks/tools-DcbePIHF.cjs +0 -208
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for Notion's proprietary lossless clipboard flavor
|
|
3
|
+
* `text/_notion-blocks-v3-production`.
|
|
4
|
+
*
|
|
5
|
+
* When you copy blocks in Notion's web app, the clipboard carries — alongside
|
|
6
|
+
* `text/plain` and `text/html` — a JSON "record-map" under this MIME type that
|
|
7
|
+
* preserves EVERY block's state (checked, language, collapsed, icon, colour,
|
|
8
|
+
* nesting…). The HTML flavour cannot carry that state, so this JSON is the
|
|
9
|
+
* high-fidelity migration source for Notion-web → Blok-web paste.
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: this is Notion's INTERNAL record-map, NOT the public REST API.
|
|
12
|
+
* Type names are internal (`text`, `header`, `sub_header`, `sub_sub_header`,
|
|
13
|
+
* `to_do`, `bulleted_list`…), rich-text is nested flag-arrays (NOT
|
|
14
|
+
* `annotations:{bold:true}`), and `checked` is `[["Yes"|"No"]]` (NOT boolean).
|
|
15
|
+
* See docs/plans/2026-06-22-notion-paste-migration-design.md §0.1.
|
|
16
|
+
*
|
|
17
|
+
* The output is a flat array of {@link NotionParsedBlock} (the same
|
|
18
|
+
* `{ id, tool, data, parentId }` shape the Blok clipboard handler consumes),
|
|
19
|
+
* which the two-pass `BlokDataHandler.insertBlokBlocks` builder turns into a
|
|
20
|
+
* correctly-nested block tree.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { colorVarName } from '../../shared/color-presets';
|
|
24
|
+
import { DEFAULT_EMOJI } from '../../../tools/callout/constants';
|
|
25
|
+
import { DEFAULT_LANGUAGE, LANGUAGES } from '../../../tools/code/constants';
|
|
26
|
+
import { matchEmbedService } from '../../../tools/link/registry';
|
|
27
|
+
|
|
28
|
+
/** MIME flavour that carries Notion's lossless block record-map. */
|
|
29
|
+
export const NOTION_BLOCKS_V3_MIME = 'text/_notion-blocks-v3-production';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A single Blok-ready block produced from the Notion record-map.
|
|
33
|
+
* Shape-compatible with the clipboard handler's `BlokClipboardBlock`.
|
|
34
|
+
*/
|
|
35
|
+
export interface NotionParsedBlock {
|
|
36
|
+
id: string;
|
|
37
|
+
tool: string;
|
|
38
|
+
data: Record<string, unknown>;
|
|
39
|
+
parentId?: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A Notion record-map block `value` (loosely typed — fields are optional). */
|
|
43
|
+
interface NotionValue {
|
|
44
|
+
id: string;
|
|
45
|
+
type: string;
|
|
46
|
+
content?: string[];
|
|
47
|
+
properties?: Record<string, unknown>;
|
|
48
|
+
format?: Record<string, unknown>;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Result of mapping one Notion value: tool + data, or `null` to skip-promote. */
|
|
53
|
+
interface Mapped {
|
|
54
|
+
tool: string;
|
|
55
|
+
data: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse the `text/_notion-blocks-v3-production` clipboard payload into a flat
|
|
60
|
+
* array of Blok-ready blocks. Returns `null` when the payload is not valid
|
|
61
|
+
* JSON or does not look like the Notion record-map (so callers can fall back
|
|
62
|
+
* to the HTML path).
|
|
63
|
+
*/
|
|
64
|
+
export function parseNotionBlocksV3(json: string): NotionParsedBlock[] | null {
|
|
65
|
+
const parsed = safeJsonParse(json);
|
|
66
|
+
|
|
67
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const blocks = (parsed as { blocks?: unknown }).blocks;
|
|
72
|
+
|
|
73
|
+
if (!Array.isArray(blocks)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Merge every subtree's block map into one id → value lookup, and remember
|
|
78
|
+
// the top-level (selected) order from the `blocks` array.
|
|
79
|
+
const byId = new Map<string, NotionValue>();
|
|
80
|
+
|
|
81
|
+
blocks.forEach((entry) => {
|
|
82
|
+
const map = (entry as { blockSubtree?: { block?: Record<string, { value?: NotionValue }> } })
|
|
83
|
+
?.blockSubtree?.block;
|
|
84
|
+
|
|
85
|
+
if (map === undefined || map === null) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Object.keys(map).forEach((id) => {
|
|
90
|
+
const value = map[id]?.value;
|
|
91
|
+
|
|
92
|
+
if (value !== undefined && value !== null && typeof value === 'object') {
|
|
93
|
+
byId.set(id, value);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Require record-map structure: at least one resolvable block value.
|
|
99
|
+
if (byId.size === 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const topLevelOrder = blocks
|
|
104
|
+
.map((entry) => (entry as { blockId?: string })?.blockId)
|
|
105
|
+
.filter((id): id is string => typeof id === 'string');
|
|
106
|
+
|
|
107
|
+
const result: NotionParsedBlock[] = [];
|
|
108
|
+
const visited = new Set<string>();
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Depth-first emit in document order. `parentId` is the id of the nearest
|
|
112
|
+
* EMITTED ancestor (null at the top), so skip-promoted wrappers (page/tab)
|
|
113
|
+
* lift their children to the grandparent without leaving a dangling parent.
|
|
114
|
+
*/
|
|
115
|
+
const walk = (id: string, parentId: string | null): void => {
|
|
116
|
+
if (visited.has(id)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const value = byId.get(id);
|
|
121
|
+
|
|
122
|
+
if (value === undefined) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
visited.add(id);
|
|
127
|
+
|
|
128
|
+
// Tables expand into a grid block plus one paragraph block per cell, and
|
|
129
|
+
// consume their `table_row` children — handle them before the leaf mapping.
|
|
130
|
+
if (value.type === 'table') {
|
|
131
|
+
expandTable(value, parentId, byId, visited, result);
|
|
132
|
+
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const mapped = mapValue(value, byId);
|
|
137
|
+
const children = Array.isArray(value.content) ? value.content : [];
|
|
138
|
+
|
|
139
|
+
if (mapped === null) {
|
|
140
|
+
// Structural wrapper (page / tab): drop it, promote its children.
|
|
141
|
+
children.forEach((childId) => walk(childId, parentId));
|
|
142
|
+
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const block: NotionParsedBlock = { id, tool: mapped.tool, data: mapped.data };
|
|
147
|
+
|
|
148
|
+
if (parentId !== null) {
|
|
149
|
+
block.parentId = parentId;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
result.push(block);
|
|
153
|
+
|
|
154
|
+
children.forEach((childId) => walk(childId, id));
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
topLevelOrder.forEach((id) => {
|
|
158
|
+
// A top-level block with no resolvable value has an EMPTY subtree — Notion
|
|
159
|
+
// ships linked databases, collection views and link-to-page references this
|
|
160
|
+
// way (their data lives on another page, omitted from the clipboard). Emit
|
|
161
|
+
// a bookmark to the Notion page so the block is not silently dropped.
|
|
162
|
+
if (!byId.has(id)) {
|
|
163
|
+
result.push({ id, tool: 'bookmark', data: { url: notionPageUrl(id) } });
|
|
164
|
+
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
walk(id, null);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Expand a Notion `table` into a Blok table block (a grid of cell references)
|
|
176
|
+
* plus one paragraph block per cell — Blok stores cell content as child blocks
|
|
177
|
+
* referenced by id, not inline. The owning `table_row` children are marked
|
|
178
|
+
* visited so the walker never emits them separately.
|
|
179
|
+
*/
|
|
180
|
+
function expandTable(
|
|
181
|
+
table: NotionValue,
|
|
182
|
+
parentId: string | null,
|
|
183
|
+
byId: Map<string, NotionValue>,
|
|
184
|
+
visited: Set<string>,
|
|
185
|
+
result: NotionParsedBlock[]
|
|
186
|
+
): void {
|
|
187
|
+
const tableId = table.id;
|
|
188
|
+
const format = table.format ?? {};
|
|
189
|
+
const rowIds = (Array.isArray(table.content) ? table.content : []).filter(
|
|
190
|
+
(rowId): rowId is string => typeof rowId === 'string'
|
|
191
|
+
);
|
|
192
|
+
const columnOrder = resolveColumnOrder(format.table_block_column_order, rowIds, byId);
|
|
193
|
+
const cells: NotionParsedBlock[] = [];
|
|
194
|
+
const content: { blocks: string[] }[][] = rowIds.map((rowId) => {
|
|
195
|
+
visited.add(rowId);
|
|
196
|
+
|
|
197
|
+
const rowProperties = byId.get(rowId)?.properties ?? {};
|
|
198
|
+
|
|
199
|
+
return columnOrder.map((columnId) => {
|
|
200
|
+
const cellId = `${rowId}:${columnId}`;
|
|
201
|
+
|
|
202
|
+
cells.push({ id: cellId, tool: 'paragraph', data: { text: richText(rowProperties[columnId], byId) }, parentId: tableId });
|
|
203
|
+
|
|
204
|
+
return { blocks: [cellId] };
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const tableBlock: NotionParsedBlock = {
|
|
209
|
+
id: tableId,
|
|
210
|
+
tool: 'table',
|
|
211
|
+
data: {
|
|
212
|
+
withHeadings: format.table_block_row_header === true,
|
|
213
|
+
withHeadingColumn: format.table_block_column_header === true,
|
|
214
|
+
content,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (parentId !== null) {
|
|
219
|
+
tableBlock.parentId = parentId;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
result.push(tableBlock);
|
|
223
|
+
cells.forEach((cell) => result.push(cell));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The column id order for a table: the explicit `table_block_column_order`, or
|
|
228
|
+
* (when absent) the property keys of the first populated row.
|
|
229
|
+
*/
|
|
230
|
+
function resolveColumnOrder(
|
|
231
|
+
order: unknown,
|
|
232
|
+
rowIds: string[],
|
|
233
|
+
byId: Map<string, NotionValue>
|
|
234
|
+
): string[] {
|
|
235
|
+
if (Array.isArray(order) && order.length > 0) {
|
|
236
|
+
return order.filter((columnId): columnId is string => typeof columnId === 'string');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const rowId of rowIds) {
|
|
240
|
+
const properties = byId.get(rowId)?.properties;
|
|
241
|
+
const keys = properties === undefined || properties === null ? [] : Object.keys(properties);
|
|
242
|
+
|
|
243
|
+
if (keys.length > 0) {
|
|
244
|
+
return keys;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Map one Notion block value to a Blok tool + data, or `null` for structural
|
|
253
|
+
* wrappers that should be dropped while their children are promoted.
|
|
254
|
+
*/
|
|
255
|
+
function mapValue(value: NotionValue, byId: Map<string, NotionValue>): Mapped | null {
|
|
256
|
+
const props = value.properties ?? {};
|
|
257
|
+
const text = richText(props.title, byId);
|
|
258
|
+
|
|
259
|
+
switch (value.type) {
|
|
260
|
+
case 'text':
|
|
261
|
+
return { tool: 'paragraph', data: { text } };
|
|
262
|
+
case 'header':
|
|
263
|
+
return mapHeader(text, 1, value.format);
|
|
264
|
+
case 'sub_header':
|
|
265
|
+
return mapHeader(text, 2, value.format);
|
|
266
|
+
case 'sub_sub_header':
|
|
267
|
+
return mapHeader(text, 3, value.format);
|
|
268
|
+
case 'quote':
|
|
269
|
+
return { tool: 'quote', data: value.format?.quote_size === 'large' ? { text, size: 'large' } : { text } };
|
|
270
|
+
case 'divider':
|
|
271
|
+
return { tool: 'divider', data: {} };
|
|
272
|
+
case 'code':
|
|
273
|
+
return {
|
|
274
|
+
tool: 'code',
|
|
275
|
+
data: { code: plainText(props.title), language: mapLanguage(props.language), lineNumbers: false },
|
|
276
|
+
};
|
|
277
|
+
case 'to_do':
|
|
278
|
+
return { tool: 'list', data: { text, style: 'checklist', checked: isChecked(props.checked) } };
|
|
279
|
+
case 'bulleted_list':
|
|
280
|
+
return { tool: 'list', data: { text, style: 'unordered' } };
|
|
281
|
+
case 'numbered_list':
|
|
282
|
+
return { tool: 'list', data: { text, style: 'ordered' } };
|
|
283
|
+
case 'toggle':
|
|
284
|
+
return { tool: 'toggle', data: { text, isOpen: true } };
|
|
285
|
+
case 'column_list':
|
|
286
|
+
return { tool: 'column_list', data: {} };
|
|
287
|
+
case 'column': {
|
|
288
|
+
const ratio = value.format?.column_ratio;
|
|
289
|
+
|
|
290
|
+
return { tool: 'column', data: typeof ratio === 'number' && ratio !== 1 ? { widthRatio: ratio } : {} };
|
|
291
|
+
}
|
|
292
|
+
case 'table_row':
|
|
293
|
+
// Consumed by `expandTable`; a stray row should never surface as a block.
|
|
294
|
+
return null;
|
|
295
|
+
case 'equation':
|
|
296
|
+
return { tool: 'code', data: { code: plainText(props.title), language: 'latex', lineNumbers: false } };
|
|
297
|
+
case 'bookmark':
|
|
298
|
+
return mapBookmark(props, value.format ?? {}, text);
|
|
299
|
+
case 'image':
|
|
300
|
+
return mapMediaFile('image', props, value.format ?? {}, text, value.id);
|
|
301
|
+
case 'file':
|
|
302
|
+
return mapMediaFile('file', props, value.format ?? {}, text, value.id);
|
|
303
|
+
case 'video':
|
|
304
|
+
return mapVideo(props, value.format ?? {}, text, value.id);
|
|
305
|
+
case 'audio':
|
|
306
|
+
return mapAudio(props, value.format ?? {}, text, value.id);
|
|
307
|
+
case 'pdf':
|
|
308
|
+
return mapMediaFile('file', props, value.format ?? {}, text, value.id);
|
|
309
|
+
// Standalone service embeds carry their URL in `source` / `display_source`;
|
|
310
|
+
// resolve via the embed registry, falling back to a bookmark.
|
|
311
|
+
case 'drive':
|
|
312
|
+
case 'embed':
|
|
313
|
+
case 'tweet':
|
|
314
|
+
case 'gist':
|
|
315
|
+
case 'codepen':
|
|
316
|
+
case 'figma':
|
|
317
|
+
case 'maps':
|
|
318
|
+
case 'miro':
|
|
319
|
+
case 'loom':
|
|
320
|
+
case 'typeform':
|
|
321
|
+
case 'invision':
|
|
322
|
+
case 'framer':
|
|
323
|
+
case 'whimsical':
|
|
324
|
+
case 'abstract':
|
|
325
|
+
return mapEmbedOrBookmark(props, value.format ?? {}, text);
|
|
326
|
+
case 'callout': {
|
|
327
|
+
const format = value.format ?? {};
|
|
328
|
+
const emoji = typeof format.page_icon === 'string' && format.page_icon.length > 0 ? format.page_icon : DEFAULT_EMOJI;
|
|
329
|
+
const { textColor, backgroundColor } = parseBlockColor(format.block_color);
|
|
330
|
+
|
|
331
|
+
return { tool: 'callout', data: { emoji, text, textColor, backgroundColor } };
|
|
332
|
+
}
|
|
333
|
+
case 'page':
|
|
334
|
+
// A `page` block inside content is a SUB-PAGE reference: its body lives on
|
|
335
|
+
// a separate Notion page that is not in the clipboard. Emit a bookmark
|
|
336
|
+
// linking to that page so the reference is not silently dropped.
|
|
337
|
+
return { tool: 'bookmark', data: { url: notionPageUrl(value.id), ...(text.length > 0 ? { title: text } : {}) } };
|
|
338
|
+
case 'tab':
|
|
339
|
+
// Tab wrapper — skip, promote children to the same level.
|
|
340
|
+
return null;
|
|
341
|
+
case 'transclusion_container':
|
|
342
|
+
// Synced-block original: Blok has no synced primitive, so flatten it —
|
|
343
|
+
// drop the wrapper and promote its children to the same level.
|
|
344
|
+
return null;
|
|
345
|
+
case 'transclusion_reference':
|
|
346
|
+
// Synced-block duplicate: a pointer only; the referenced content is not
|
|
347
|
+
// in the clipboard. Drop it rather than leave a stray empty paragraph.
|
|
348
|
+
return null;
|
|
349
|
+
case 'table_of_contents':
|
|
350
|
+
case 'breadcrumb':
|
|
351
|
+
case 'copy_indicator':
|
|
352
|
+
case 'link_to_page':
|
|
353
|
+
case 'alias':
|
|
354
|
+
// Structure-only / derived blocks carry no migratable content — drop them
|
|
355
|
+
// (a paragraph fallback would leave an empty stray block).
|
|
356
|
+
return null;
|
|
357
|
+
default:
|
|
358
|
+
// Unmapped types fall back to a paragraph carrying their title text.
|
|
359
|
+
return { tool: 'paragraph', data: { text } };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Parse JSON, returning `null` instead of throwing on malformed input. */
|
|
364
|
+
function safeJsonParse(json: string): unknown {
|
|
365
|
+
try {
|
|
366
|
+
return JSON.parse(json);
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Map a Notion bookmark to the Blok bookmark tool (or a text fallback). */
|
|
373
|
+
function mapBookmark(
|
|
374
|
+
props: Record<string, unknown>,
|
|
375
|
+
format: Record<string, unknown>,
|
|
376
|
+
text: string
|
|
377
|
+
): Mapped {
|
|
378
|
+
const url = firstHttpUrl(props.link);
|
|
379
|
+
|
|
380
|
+
if (url === null) {
|
|
381
|
+
return { tool: 'paragraph', data: { text } };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const data: Record<string, unknown> = { url };
|
|
385
|
+
const bookmarkTitle = plainText(props.title);
|
|
386
|
+
const description = plainText(props.description);
|
|
387
|
+
|
|
388
|
+
if (bookmarkTitle.length > 0) {
|
|
389
|
+
data.title = bookmarkTitle;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (description.length > 0) {
|
|
393
|
+
data.description = description;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (typeof format.bookmark_cover === 'string') {
|
|
397
|
+
data.image = format.bookmark_cover;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (typeof format.bookmark_icon === 'string') {
|
|
401
|
+
data.favicon = format.bookmark_icon;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return { tool: 'bookmark', data };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Map a Notion image/file to the Blok image/file tool when it has a usable
|
|
409
|
+
* http(s) URL. Notion-uploaded media uses `attachment:` refs that require auth
|
|
410
|
+
* and carry no loadable URL, so those fall back to a filename paragraph.
|
|
411
|
+
*/
|
|
412
|
+
function mapMediaFile(
|
|
413
|
+
tool: 'image' | 'file',
|
|
414
|
+
props: Record<string, unknown>,
|
|
415
|
+
format: Record<string, unknown>,
|
|
416
|
+
text: string,
|
|
417
|
+
id: string
|
|
418
|
+
): Mapped {
|
|
419
|
+
const url = firstHttpUrl(props.source, format.display_source);
|
|
420
|
+
|
|
421
|
+
if (url === null) {
|
|
422
|
+
return attachmentFallback(id, props, text);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (tool === 'image') {
|
|
426
|
+
const data: Record<string, unknown> = { url };
|
|
427
|
+
const alignment = mediaAlignment(format.block_alignment);
|
|
428
|
+
|
|
429
|
+
if (alignment !== null) {
|
|
430
|
+
data.alignment = alignment;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
Object.assign(data, captionFields(props.caption));
|
|
434
|
+
|
|
435
|
+
const alt = plainText(props.alt_text);
|
|
436
|
+
|
|
437
|
+
if (alt.length > 0) {
|
|
438
|
+
data.alt = alt;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const crop = mapImageCrop(format.image_edit_metadata);
|
|
442
|
+
|
|
443
|
+
if (crop !== null) {
|
|
444
|
+
data.crop = crop;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return { tool: 'image', data };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const data: Record<string, unknown> = { url };
|
|
451
|
+
const fileName = plainText(props.title);
|
|
452
|
+
|
|
453
|
+
if (fileName.length > 0) {
|
|
454
|
+
data.fileName = fileName;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
Object.assign(data, captionFields(props.caption));
|
|
458
|
+
|
|
459
|
+
return { tool: 'file', data };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** A left/right block alignment (the default `center` is omitted), else `null`. */
|
|
463
|
+
function mediaAlignment(value: unknown): 'left' | 'right' | null {
|
|
464
|
+
return value === 'left' || value === 'right' ? value : null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** A visible plain-text caption as block-data fields, or `{}` when absent. */
|
|
468
|
+
function captionFields(caption: unknown): Record<string, unknown> {
|
|
469
|
+
const text = plainText(caption);
|
|
470
|
+
|
|
471
|
+
return text.length > 0 ? { caption: text, captionVisible: true } : {};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Convert a Notion percent-space crop region (`{x,y,width,height,unit:'%'}`) to
|
|
476
|
+
* Blok's `{x,y,w,h[,shape]}` (same percent space — a key rename). A full-frame
|
|
477
|
+
* crop is a no-op and is skipped; a non-`None` mask becomes a crop shape.
|
|
478
|
+
*/
|
|
479
|
+
function mapImageCrop(meta: unknown): Record<string, unknown> | null {
|
|
480
|
+
if (meta === null || typeof meta !== 'object') {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const crop = (meta as { crop?: unknown }).crop;
|
|
485
|
+
|
|
486
|
+
if (crop === null || typeof crop !== 'object') {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const { x, y, width, height, unit } = crop as Record<string, unknown>;
|
|
491
|
+
|
|
492
|
+
if (unit !== '%' || typeof x !== 'number' || typeof y !== 'number' || typeof width !== 'number' || typeof height !== 'number') {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// A full-frame crop changes nothing — don't emit it.
|
|
497
|
+
if (x === 0 && y === 0 && width === 100 && height === 100) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const result: Record<string, unknown> = { x, y, w: width, h: height };
|
|
502
|
+
const mask = (meta as { mask?: unknown }).mask;
|
|
503
|
+
|
|
504
|
+
if (mask === 'circle' || mask === 'ellipse') {
|
|
505
|
+
result.shape = mask;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Map a Notion video to an embed (provider match) or a direct video block. */
|
|
512
|
+
function mapVideo(props: Record<string, unknown>, format: Record<string, unknown>, text: string, id: string): Mapped {
|
|
513
|
+
const url = firstHttpUrl(props.source, format.display_source);
|
|
514
|
+
|
|
515
|
+
if (url === null) {
|
|
516
|
+
return attachmentFallback(id, props, text);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const embed = resolveEmbed(url);
|
|
520
|
+
|
|
521
|
+
if (embed !== null) {
|
|
522
|
+
return embed;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const data: Record<string, unknown> = { url };
|
|
526
|
+
const alignment = mediaAlignment(format.block_alignment);
|
|
527
|
+
|
|
528
|
+
if (alignment !== null) {
|
|
529
|
+
data.alignment = alignment;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
Object.assign(data, captionFields(props.caption));
|
|
533
|
+
|
|
534
|
+
return { tool: 'video', data };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Map a Notion service-embed block (drive, tweet, gist, figma, codepen…) to a
|
|
539
|
+
* resolved embed (provider match) or a bookmark carrying the live URL.
|
|
540
|
+
*/
|
|
541
|
+
function mapEmbedOrBookmark(props: Record<string, unknown>, format: Record<string, unknown>, text: string): Mapped {
|
|
542
|
+
const url = firstHttpUrl(props.source, format.display_source);
|
|
543
|
+
|
|
544
|
+
if (url === null) {
|
|
545
|
+
return { tool: 'paragraph', data: { text } };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return resolveEmbed(url) ?? { tool: 'bookmark', data: { url } };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Map a Notion heading to a Blok header, preserving its toggleable state. */
|
|
552
|
+
function mapHeader(text: string, level: number, format: Record<string, unknown> | undefined): Mapped {
|
|
553
|
+
// Notion's clipboard record-map carries only whether a heading is toggleable,
|
|
554
|
+
// not its per-instance collapsed state, so toggle headings default to open.
|
|
555
|
+
return format?.toggleable === true
|
|
556
|
+
? { tool: 'header', data: { text, level, isToggleable: true, isOpen: true } }
|
|
557
|
+
: { tool: 'header', data: { text, level } };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Map a Notion audio block to the Blok audio tool when it has a usable http(s)
|
|
562
|
+
* URL. Notion-uploaded audio uses `attachment:` refs with no loadable URL, so
|
|
563
|
+
* those fall back to a Notion-link bookmark (like image/file/video).
|
|
564
|
+
*/
|
|
565
|
+
function mapAudio(props: Record<string, unknown>, format: Record<string, unknown>, text: string, id: string): Mapped {
|
|
566
|
+
const url = firstHttpUrl(props.source, format.display_source);
|
|
567
|
+
|
|
568
|
+
if (url === null) {
|
|
569
|
+
return attachmentFallback(id, props, text);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const data: Record<string, unknown> = { url };
|
|
573
|
+
const fileName = plainText(props.title);
|
|
574
|
+
|
|
575
|
+
if (fileName.length > 0) {
|
|
576
|
+
// `title` is the player's visible label; `fileName` drives the download name.
|
|
577
|
+
data.title = fileName;
|
|
578
|
+
data.fileName = fileName;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return { tool: 'audio', data };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Resolve a URL against the embed registry into Blok embed data, or `null`. */
|
|
585
|
+
function resolveEmbed(url: string): Mapped | null {
|
|
586
|
+
const match = matchEmbedService(url);
|
|
587
|
+
|
|
588
|
+
if (match === null) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return { tool: 'embed', data: { service: match.service, source: url, embed: match.embedUrl, kind: match.kind } };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** First candidate that resolves to an http(s) URL (string or rich-text array). */
|
|
596
|
+
function firstHttpUrl(...candidates: unknown[]): string | null {
|
|
597
|
+
for (const candidate of candidates) {
|
|
598
|
+
const raw = typeof candidate === 'string' ? candidate : plainText(candidate);
|
|
599
|
+
const trimmed = raw.trim();
|
|
600
|
+
|
|
601
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
602
|
+
return trimmed;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Whether a Notion `checked` property (`[["Yes"|"No"]]`) is checked. */
|
|
610
|
+
function isChecked(checked: unknown): boolean {
|
|
611
|
+
return Array.isArray(checked) && Array.isArray(checked[0]) && checked[0][0] === 'Yes';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Pre-built lower-cased display-name → language-id lookup for code blocks. */
|
|
615
|
+
const LANGUAGE_BY_NAME = new Map(LANGUAGES.map((l) => [l.name.toLowerCase(), l.id]));
|
|
616
|
+
|
|
617
|
+
/** Map a Notion `language` property (`[["JavaScript"]]`) to a Blok language id. */
|
|
618
|
+
function mapLanguage(language: unknown): string {
|
|
619
|
+
if (!Array.isArray(language) || !Array.isArray(language[0]) || typeof language[0][0] !== 'string') {
|
|
620
|
+
return DEFAULT_LANGUAGE;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return LANGUAGE_BY_NAME.get(language[0][0].toLowerCase()) ?? DEFAULT_LANGUAGE;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Concatenate a rich-text array into raw plain text (no escaping, no markup). */
|
|
627
|
+
function plainText(title: unknown): string {
|
|
628
|
+
if (!Array.isArray(title)) {
|
|
629
|
+
return '';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return title.map((seg) => (Array.isArray(seg) && typeof seg[0] === 'string' ? seg[0] : '')).join('');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Convert a Notion rich-text array into Blok inline HTML. `byId` resolves
|
|
637
|
+
* page-mention (`p`) flags to their referenced page title when that page is
|
|
638
|
+
* present in the pasted payload.
|
|
639
|
+
*/
|
|
640
|
+
function richText(title: unknown, byId?: Map<string, NotionValue>): string {
|
|
641
|
+
if (!Array.isArray(title)) {
|
|
642
|
+
return '';
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return title.map((seg) => segmentHtml(seg, byId)).join('');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Convert one rich-text segment `[text, [[flag, …args], …]]` to HTML. */
|
|
649
|
+
function segmentHtml(segment: unknown, byId?: Map<string, NotionValue>): string {
|
|
650
|
+
if (!Array.isArray(segment)) {
|
|
651
|
+
return '';
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const raw = typeof segment[0] === 'string' ? segment[0] : '';
|
|
655
|
+
const annotations = Array.isArray(segment[1]) ? segment[1] : [];
|
|
656
|
+
|
|
657
|
+
// Colour flags ('h') collapse into ONE <mark>; content flags replace the
|
|
658
|
+
// placeholder glyph: 'e' equation, 'p' page mention, 'd' date, 'u' user
|
|
659
|
+
// mention. Every other flag wraps the resulting content from the inside out.
|
|
660
|
+
const contentFlags = ['e', 'p', 'd', 'u'];
|
|
661
|
+
const isColour = (a: unknown): boolean => Array.isArray(a) && a[0] === 'h';
|
|
662
|
+
const isContent = (a: unknown): boolean => Array.isArray(a) && contentFlags.includes(a[0] as string);
|
|
663
|
+
const inner = annotations
|
|
664
|
+
.filter((a) => !isColour(a) && !isContent(a))
|
|
665
|
+
.reduce<string>((html, annotation) => wrapMark(annotation, html), segmentContent(raw, annotations, byId));
|
|
666
|
+
const style = buildColourStyle(annotations.filter(isColour));
|
|
667
|
+
|
|
668
|
+
return style.length > 0 ? `<mark style="${style}">${inner}</mark>` : inner;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* The base inner content of a segment: an inline `<code>` LaTeX equation (`e`),
|
|
673
|
+
* a resolved page-mention title (`p`), or the plain escaped text. Equation and
|
|
674
|
+
* mention flags carry their content in the flag args / record-map, so they
|
|
675
|
+
* replace the segment's placeholder glyph (`⁍` / `‣`) entirely.
|
|
676
|
+
*/
|
|
677
|
+
function segmentContent(raw: string, annotations: unknown[], byId?: Map<string, NotionValue>): string {
|
|
678
|
+
const latex = flagArg(annotations, 'e');
|
|
679
|
+
|
|
680
|
+
if (latex !== null) {
|
|
681
|
+
return `<code>${escapeHtml(latex)}</code>`;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const pageId = flagArg(annotations, 'p');
|
|
685
|
+
|
|
686
|
+
if (pageId !== null) {
|
|
687
|
+
return pageMentionContent(pageId, raw, byId);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const date = dateArg(annotations);
|
|
691
|
+
|
|
692
|
+
if (date !== null) {
|
|
693
|
+
return escapeHtml(formatNotionDate(date));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// A user mention ('u'), or a malformed date mention ('d' with a missing /
|
|
697
|
+
// non-object arg), carries no renderable content — drop it rather than leak
|
|
698
|
+
// the raw placeholder glyph.
|
|
699
|
+
if (hasFlag(annotations, 'u') || hasFlag(annotations, 'd')) {
|
|
700
|
+
return '';
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return escapeHtml(raw);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** The string argument of the first `[flag, arg, …]` annotation, or `null`. */
|
|
707
|
+
function flagArg(annotations: unknown[], flag: string): string | null {
|
|
708
|
+
for (const annotation of annotations) {
|
|
709
|
+
if (Array.isArray(annotation) && annotation[0] === flag && typeof annotation[1] === 'string') {
|
|
710
|
+
return annotation[1];
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Whether any annotation carries the given flag. */
|
|
718
|
+
function hasFlag(annotations: unknown[], flag: string): boolean {
|
|
719
|
+
return annotations.some((annotation) => Array.isArray(annotation) && annotation[0] === flag);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** The object argument of an inline date (`'d'`) annotation, or `null`. */
|
|
723
|
+
function dateArg(annotations: unknown[]): Record<string, unknown> | null {
|
|
724
|
+
for (const annotation of annotations) {
|
|
725
|
+
if (Array.isArray(annotation) && annotation[0] === 'd' && isPlainObject(annotation[1])) {
|
|
726
|
+
return annotation[1];
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Whether a value is a non-null, non-array object. */
|
|
734
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
735
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Format a Notion inline date object to plain text. Blok has no inline date
|
|
740
|
+
* pill, so a single date, a date+time, or a range render as text.
|
|
741
|
+
*/
|
|
742
|
+
function formatNotionDate(date: Record<string, unknown>): string {
|
|
743
|
+
const part = (dateKey: string, timeKey: string): string => {
|
|
744
|
+
const day = typeof date[dateKey] === 'string' ? date[dateKey] : '';
|
|
745
|
+
const time = typeof date[timeKey] === 'string' ? date[timeKey] : '';
|
|
746
|
+
|
|
747
|
+
if (day.length === 0) {
|
|
748
|
+
return '';
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return time.length > 0 ? `${day} ${time}` : day;
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
return [part('start_date', 'start_time'), part('end_date', 'end_time')].filter((p) => p.length > 0).join(' → ');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* The canonical Notion URL for a block/page id. Notion's share URLs use the id
|
|
759
|
+
* with dashes stripped; `www.notion.so/<id>` resolves regardless of workspace.
|
|
760
|
+
*/
|
|
761
|
+
function notionPageUrl(id: string): string {
|
|
762
|
+
return `https://www.notion.so/${id.replace(/-/g, '')}`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Fallback for uploaded media (image / video / audio / file / pdf) whose binary
|
|
767
|
+
* is NOT on the clipboard: Notion exports private attachments as
|
|
768
|
+
* `attachment:<uuid>:<filename>` references with no fetchable URL, in BOTH the
|
|
769
|
+
* JSON and HTML flavours. Rather than drop the block to a bare filename
|
|
770
|
+
* paragraph, emit a bookmark to the block's Notion location so it is not
|
|
771
|
+
* silently lost and the user can open the source to re-download the file. The
|
|
772
|
+
* filename becomes the bookmark title.
|
|
773
|
+
*/
|
|
774
|
+
function attachmentFallback(id: string, props: Record<string, unknown>, text: string): Mapped {
|
|
775
|
+
const fileName = text.length > 0 ? text : attachmentName(props.source);
|
|
776
|
+
const data: Record<string, unknown> = { url: notionPageUrl(id) };
|
|
777
|
+
|
|
778
|
+
if (fileName.length > 0) {
|
|
779
|
+
data.title = fileName;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return { tool: 'bookmark', data };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Extract the human filename from an `attachment:<uuid>:<filename>` source. */
|
|
786
|
+
function attachmentName(source: unknown): string {
|
|
787
|
+
const firstSegment: unknown = Array.isArray(source) ? source[0] : undefined;
|
|
788
|
+
const first: unknown = Array.isArray(firstSegment) ? firstSegment[0] : undefined;
|
|
789
|
+
|
|
790
|
+
if (typeof first !== 'string') {
|
|
791
|
+
return '';
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const match = /^attachment:[^:]+:(.+)$/.exec(first);
|
|
795
|
+
|
|
796
|
+
if (match === null) {
|
|
797
|
+
return '';
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
return decodeURIComponent(match[1]);
|
|
802
|
+
} catch {
|
|
803
|
+
return match[1];
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Render an inline page-mention as a link to the referenced Notion page. Uses
|
|
809
|
+
* the page title when it is present in the pasted payload, otherwise `Untitled`
|
|
810
|
+
* (mentions usually point at pages outside the copied selection). Linking — vs
|
|
811
|
+
* the old behaviour of leaking the raw `‣` glyph — keeps the reference
|
|
812
|
+
* navigable so the user can still reach the source page after migration.
|
|
813
|
+
*/
|
|
814
|
+
function pageMentionContent(pageId: string, raw: string, byId?: Map<string, NotionValue>): string {
|
|
815
|
+
const titleText = plainText(byId?.get(pageId)?.properties?.title);
|
|
816
|
+
const label = titleText.length > 0 ? titleText : 'Untitled';
|
|
817
|
+
|
|
818
|
+
return `<a href="${escapeAttr(notionPageUrl(pageId))}">${escapeHtml(label)}</a>`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** A parsed Notion colour token: a preset name applied as text or background. */
|
|
822
|
+
interface NotionColour {
|
|
823
|
+
mode: 'text' | 'bg';
|
|
824
|
+
name: string;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Parse a Notion colour token (`orange`, `gray_background`, `default`) into a
|
|
829
|
+
* preset name + mode, or `null` for the no-colour `default` token.
|
|
830
|
+
*/
|
|
831
|
+
function parseNotionColour(token: unknown): NotionColour | null {
|
|
832
|
+
if (typeof token !== 'string' || token.length === 0 || token === 'default') {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const suffix = '_background';
|
|
837
|
+
const colour: NotionColour = token.endsWith(suffix)
|
|
838
|
+
? { mode: 'bg', name: token.slice(0, -suffix.length) }
|
|
839
|
+
: { mode: 'text', name: token };
|
|
840
|
+
|
|
841
|
+
// `default` / `default_background` mean "no tint" — surface as no colour.
|
|
842
|
+
return colour.name === 'default' ? null : colour;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** Build a `<mark>` style string from one or more Notion colour flags. */
|
|
846
|
+
function buildColourStyle(colourFlags: unknown[]): string {
|
|
847
|
+
const colours = colourFlags
|
|
848
|
+
.map((flag) => parseNotionColour(Array.isArray(flag) ? flag[1] : undefined))
|
|
849
|
+
.filter((colour): colour is NotionColour => colour !== null);
|
|
850
|
+
const text = colours.find((colour) => colour.mode === 'text');
|
|
851
|
+
const background = colours.find((colour) => colour.mode === 'bg');
|
|
852
|
+
const parts: string[] = [];
|
|
853
|
+
|
|
854
|
+
if (text) {
|
|
855
|
+
parts.push(`color: ${colorVarName(text.name, 'text')}`);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (background) {
|
|
859
|
+
parts.push(`background-color: ${colorVarName(background.name, 'bg')}`);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return parts.join('; ');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Map a Notion `block_color` to callout `{ textColor, backgroundColor }` names. */
|
|
866
|
+
function parseBlockColor(token: unknown): { textColor: string | null; backgroundColor: string | null } {
|
|
867
|
+
const colour = parseNotionColour(token);
|
|
868
|
+
|
|
869
|
+
if (colour === null) {
|
|
870
|
+
return { textColor: null, backgroundColor: null };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return colour.mode === 'text'
|
|
874
|
+
? { textColor: colour.name, backgroundColor: null }
|
|
875
|
+
: { textColor: null, backgroundColor: colour.name };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** Wrap inner HTML with the Blok inline tag for a single Notion mark flag. */
|
|
879
|
+
function wrapMark(annotation: unknown, inner: string): string {
|
|
880
|
+
if (!Array.isArray(annotation)) {
|
|
881
|
+
return inner;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
switch (annotation[0]) {
|
|
885
|
+
case 'b':
|
|
886
|
+
return `<b>${inner}</b>`;
|
|
887
|
+
case 'i':
|
|
888
|
+
return `<i>${inner}</i>`;
|
|
889
|
+
case 's':
|
|
890
|
+
return `<s>${inner}</s>`;
|
|
891
|
+
case 'c':
|
|
892
|
+
return `<code>${inner}</code>`;
|
|
893
|
+
case '_':
|
|
894
|
+
return `<u>${inner}</u>`;
|
|
895
|
+
case 'a': {
|
|
896
|
+
const href = safeHref(annotation[1]);
|
|
897
|
+
|
|
898
|
+
return href === null ? inner : `<a href="${escapeAttr(href)}">${inner}</a>`;
|
|
899
|
+
}
|
|
900
|
+
default:
|
|
901
|
+
// 'h' (colour), 'e' (equation) and 'p' (page mention) are handled before
|
|
902
|
+
// wrapping (in segmentHtml / segmentContent), not here.
|
|
903
|
+
return inner;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Validate a link href against a scheme allow-list, returning the trimmed URL
|
|
909
|
+
* or `null` for dangerous schemes (`javascript:`, `data:`, `vbscript:`…).
|
|
910
|
+
* A malicious Notion clipboard payload could otherwise smuggle a script URI
|
|
911
|
+
* into `data.text`, which the link tool's sanitizer preserves verbatim.
|
|
912
|
+
*/
|
|
913
|
+
function safeHref(raw: unknown): string | null {
|
|
914
|
+
if (typeof raw !== 'string') {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const trimmed = raw.trim();
|
|
919
|
+
|
|
920
|
+
// http(s), mailto, tel, and relative/anchor links only.
|
|
921
|
+
return /^(?:https?:|mailto:|tel:|#|\/|\.\/|\.\.\/)/i.test(trimmed) ? trimmed : null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** Escape `&`, `<`, `>` for HTML text content. */
|
|
925
|
+
function escapeHtml(text: string): string {
|
|
926
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** Escape a string for use inside a double-quoted HTML attribute. */
|
|
930
|
+
function escapeAttr(text: string): string {
|
|
931
|
+
return escapeHtml(text).replace(/"/g, '"');
|
|
932
|
+
}
|