From 5764fd228f8719b2e3f4f663e9e7563e8a4b70fc Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:38:50 +1000 Subject: [PATCH 01/42] docs(windows): update text and image for windows 11 --- .../help/basic/make-taskbar-icon-visible.md | 13 +++++++++++++ windows/src/desktop/help/basic/uninstall.md | 15 +++++++++++++++ .../help/desktop_images/win11-taskbar1.png | Bin 0 -> 6946 bytes .../help/desktop_images/win11-taskbar2.png | Bin 0 -> 80979 bytes .../help/desktop_images/win11-uninstall.png | Bin 0 -> 9075 bytes .../win11_km_install_anywhere_1.png | Bin 0 -> 22033 bytes .../help/start/download-and-install-keyman.md | 2 +- .../src/desktop/help/troubleshooting/index.md | 2 +- .../install-app-from-anywhere.md | 18 +++++++++++++++++- 9 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 windows/src/desktop/help/desktop_images/win11-taskbar1.png create mode 100644 windows/src/desktop/help/desktop_images/win11-taskbar2.png create mode 100644 windows/src/desktop/help/desktop_images/win11-uninstall.png create mode 100644 windows/src/desktop/help/desktop_images/win11_km_install_anywhere_1.png diff --git a/windows/src/desktop/help/basic/make-taskbar-icon-visible.md b/windows/src/desktop/help/basic/make-taskbar-icon-visible.md index 62b90be5d46..eebd699e0a6 100644 --- a/windows/src/desktop/help/basic/make-taskbar-icon-visible.md +++ b/windows/src/desktop/help/basic/make-taskbar-icon-visible.md @@ -37,3 +37,16 @@ Here's how to make the change with Windows Settings: 5. The Keyman icon will now always appear in the Windows Taskbar near the clock, if Keyman is on. 6. Continue on to [Step 5](../start/tutorial#step-5-) of this guide. + +- On Windows 11 + + 1. Right-click on the Windows Taskbar. + + ![](../desktop_images/win11-taskbar1.png) + 2. Select 'Taskbar settings'. + 3. Under 'Other system tray icons', expand the list, + click the toggle button beside 'Keyman Engine x86'to turn it on. + ![](../desktop_images/win11-taskbar2.png) + 4. The Keyman icon will now always appear in the Windows + Taskbar near the clock, if Keyman is on. + 5. Continue on to [Step 5](../start/tutorial#step-5-) of this guide. \ No newline at end of file diff --git a/windows/src/desktop/help/basic/uninstall.md b/windows/src/desktop/help/basic/uninstall.md index 7e7cb2274c7..6d986456138 100644 --- a/windows/src/desktop/help/basic/uninstall.md +++ b/windows/src/desktop/help/basic/uninstall.md @@ -15,3 +15,18 @@ title: Software Task - Uninstall Keyman 5. Click Uninstall. 6. Follow the prompts to complete the uninstall. + +## Uninstall Keyman from Windows 11 + +1. Exit Keyman. + +2. Open Windows Start Menu and type "Add or Remove Programs", and open that app. + +3. In the Apps > Installed apps search box, type "Keyman". + +4. Click the three horizontal dots to the right of Keyman in the list. +![](../desktop_images/win11-uninstall.png) + +5. Click Uninstall. + +6. Follow the prompts to complete the uninstall. diff --git a/windows/src/desktop/help/desktop_images/win11-taskbar1.png b/windows/src/desktop/help/desktop_images/win11-taskbar1.png new file mode 100644 index 0000000000000000000000000000000000000000..ec72256a713c9cd7da10c4ca11cee40d0cd73345 GIT binary patch literal 6946 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�$gqKe#%bIsHiqMd!#;lGsiDsUi;&(Zl}{oP$T}*H zeSg%y2J6y^LTKq4bdeE>IlME`9ex($l~PXoX?mc z+nZYd;sM%dZJBN?(J^@|R;y@w8J8O2{+*MAU7eMr@1n*fwGXA;)t%~d>!R+BTqd)k zF9?^+*cZsKwhtvC+U+KE)ftl96JfCCwB|zRz>5S=D@v{q1c9_o!lOOepT`$=Xh z2; zDr2n`zjH8_)Bn$_?L#u7%;_NaCQ@3Mqx}Jo?7v6$&3Endd0Xd~zr6Tzqh+l`h>gXe zjW_za(+5G;u3^0&e`}#NgpLA5*hK)o7ZjK>-FA`wcEt5jAjIHQ*q32ENzR6|X%W_1 zQDAaWP`eTf9=LO)q<3juu>K{A2lmH{Ro!wT<&q`C?CS|c3C(9Mthr=`^70X1gCSYs zJ2<$Nbsj&e5)W7$`S93p-V={=u$G+{o zmEPceciXyYUBKPyQNI%JkX|q+Tgpx6BsWmi$DGmPMw>)-? zS$)za4=yxNkdmj}ZJ}ZGXAe@X$5S%tzMN$Bj|9%QYleXQvh2yK8TbZgv=E#mE{pu; zIM+*hHUEBY^JX^6Lb2w}=TWFuwRisMZs&iHHJByqRL%}Hjdt%b2RL-xcE~ZSQs~ss zKcR-Wd3LN_q$}K8?_m8rH2keiT5$T~e>680ytekjzBz9g$V zR2?hp!Sk!fl%DkQRL1a|82(V>$6ha1yToCtgH6(*yhA7dpCaj{hr%l6+?Qlf1O6#uGfO? zt(LZ0`@aXaYLg-Ux(nIyh{g7~^{MM4?X~64jcpajjhkcl*%Cdrn(74G+gXH4@eHTv z*~|>mRfm>UG@5{g^}-%pF~q_hwLpbJr1@}@)NnBVX2*Xy^CbjNVYZ$w;MGt9xtoj;?XZV%oB-e+1)#f5zKXIB3-%D$sM$3hYB4YlXg@*;NRg2z2^Vt8^+{5e0Q|uTkNqDBbckWk~e&>HHJ!TMS zSI*Cs7Y|8A0qcRQA9a`T%IAzU;{=rQ_W0l8RG5rQGJrOJm&qUt4Ud&OkZko-f(^eIrBdd@i>uWq> zad~Artpl_E{qwc@STB($+3A$Ys>vw7)LE}+(84-1^C{fbb4U#&+ZoM)oG?_3leJ)+lM5rikKvwa4iWTfO5gZ+?#_b^rws`5!0-^T^p?S96PU zVyQAxU`*|pdHYeCPX%E-J#r9*a|xGA5=Z+%P55(J}goi9fIJ=X}-Ad-lgGX)#W` z-G#?0X%lyG+@&n`NR@HC@P|_aobRm3!zzvcHL@k4kM=yI^wpq#VTSvW*@Qsl_r!o@ngmQJ*eQfCM7yzXd zHy2}}0Tg<4b_2AfsS$Iut0UBl(l75n(b`KFwY29B#rd|vPJV08g+5Ko6LC0!cLG*x#Ez7HP*GmQPeW80Mw*yA7@pN*QxF*H3`9RybrRMx_-NP^8c1JQ@ z2Wqf6Z&b+K(1qDdXiI>~#Xq$0hz{?|Yhd%* zXGnZ>tQ$#^*^K8i%S~C15;cgZH6MTrGsqQ)wi{JCHfXJ~)r{$l-vvS5)3sktRb~Cx zmigqmY*nrf4=EKU`058>3ZVjsg{t-(XO)nd@#z^?}Ro19#gk5X(92>;K)G?9^TQmW1hvTUywSL8HNMk3+eOO}(G z)Q)d(c0#t-e>|J!v+_fcf!dtDAKtDdYiG?>LKXI^yY`wy|G!VQTAS{OdFs~5>Vh)9 z^EJZjxA1G@pxDvpy|b*6DVQumrT9M)!xO^)QO+Xmhi{3Ayn$O*#PxUZ|FsBoL57-@ IPhN)qKfC16ZvX%Q literal 0 HcmV?d00001 diff --git a/windows/src/desktop/help/desktop_images/win11-taskbar2.png b/windows/src/desktop/help/desktop_images/win11-taskbar2.png new file mode 100644 index 0000000000000000000000000000000000000000..a349d65aa9feea72041ccede01d52e585e570c2f GIT binary patch literal 80979 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

-`&`^V5dO1{5qf7G24bz|fB5 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 v@Xj!H zYvXIQQ)f(73A4o{cbhP@2~UC6rK%FXD;bY3^y4cVy=B*G<|B3DjPQLe1q!alZ|qoG z$~SSpPHq&)y2g`>igAg>%=d7f{lV(qmlo0q5Zn&&lONx>GhMNB?{JKwyqBulJ=WB6 z#P8S5!X?v|_Mc6+a@j-*Dvo&k){wGo&DZ&OeV6W(FRg9RAl1NfZE}ZeNtzY5q*SmI zZbjy+@BHGsmTOE`_YPGU*H9h8Q?8kGdo3pU+34tT%zjNoNtN989IN5w0wNGS^dxO2 z;0~z9a@kT{xW|7OQ#wn)_I<;-*w8-z>$;|F6eK&&rbm|+5ehYmH*|@hNe##6EoU|t z%j@H0ZFqDUjWX0Y+{8X*Rc5sD#Kb#%`V4c@RTSEE=-no2PvO!~BNnmVqDDkG4P%j( zctF@#S_DGat7#pI;mO|#Z`2Oj`ALs2ETd4&r7F;@-fiPXwL^hzyRZAbncrGfAb2sA z8TK@Hx84-nj&%0$aQ~AlQz_r8XW3HPtH6H8I_nr8Pe2&586#PV1k>kKCk%nt4PpQK z6wj1n&2Zlmx<(%{@*25?SI!fbdRTdtt6u(b96ABJmiFUoGp_=huk-3pGOB@-eQ;oV z7OO4QM$xu2DOlK)Tnk;r;Eht|UEJbXBiDo7%xR+&ciVn*rwM9Bz}fXcb$iy8muO}w zZx*IcA&`sxRVH#NffBodO{hcZrW#qdtk`}MHkl#hbbkzp@tFF#k*#|YVR7S)V0l@$ z7*DHt-(I$z>tWII&xewP>7@tYEDxNtB4+#ij%4!=HfbY9^ea>UVRw)In8l=S*OB+?sI6d9l}Y`kit8Ih{}uEDC@gi_|0dIeK2T2HMsDAO*Eb+uyYjn3U-heTH;5G&*O{{~0u|H$ z75YIRug2>n$5Oi{)Cs6RFRsc{qzm&ck-rmeec_0o&Ej*K9r7`My5Z``~&g&QE?-Vj>Uo z%4o~Bof$w53;ylrQw|A`tP}7VhhqE!*b?#7PKgLZCu*)k51A_So!*B?E+hbl7@=XN zk0MCLx``ie(~uwbc)aSxL9q_E0wl5_fNyeksHqCtos13bAyd3f%8#Rj)g6O|od-D4 z??=vK+Wala3-qsZO*3&Pzyhg%VN9N(^a$bo4+ushgY~ye&sTRfO~)3FZ7(a`o(!5?*5K9Fspg1lPO1wdt;K>;p(?d3+TPg1YTJ~ zJ7q2h5|z+oCtoS?N^Y8@GV*9k>S*F~3}qqNAD5*Ss0pC$=xgP(I|kmGtb1Yb{2&;D ztRiESjQh9|D&}4ob{9mmnj@;T4b0_pIR0t?G1_#n{Wal9o~SZahZV=6mq<;!Ydu80 z&AwPe3ZYRYSTk4iPWkcH__ECzxB)AYfvEeDl$7IN4xpD820W3JZ0wC^NpzC+ZAjI2 z?L}yNE7i4TpK~42YIMd=5CQH}#C()PEA%9VL?&Z$LiIS+B9m7*UB}Z)R|{64tR?~T z-iBx~5g)2Q-y}}*LtRZ%-coI2rpv8@{gY`A9bO-th)28Bdu5Tv(TDYQ2KPgZ#g*0o1ki^^x%0s|a zI4lpe4Y-1OT@tRx2cKI0W}u ziE^kETHRbseXO)`MpEx1YdXN8Z5ifY{3V$=`jF@oH=w-RkOa>Bz#$Jr6ap4VyFq5VcTMgl zq8vjX2*i0QabEp!<$BL^d|)4>OOt?;Dx5?DM40alhbnPg-*xChHM4OKwi`l+qnOdG znH(Ja5kXuQ&z8~}Cqp4~)EgGOF;gEQ8sSP9(3^F7+`Pv?z^Py;i>A*Wc%FQ+WfW8x z&qnK!oc)fK22t#i3z85J#fwcPnKJ7}B6;DUi#rmw_s%o+Y^VKonJ+?<^u~E>gi^gy ztB*#$lblx04Y^93Nu2InzNQub;im%U)rRKzM&O)!BU2aU$aH%tcWzZmmmApmP-#rc zJ<1^GWOL-OTQ@xG!w`c}XbfF@%~|i2GeUNIgG>zaYdLUfL5fun8bt40fOg&%o<$YU zG%Al9HKQUn<6awNS}2I>6umYQlNAv%Ig@Og_*1Azwi89!2q7{I1)TMoxlZSfk3<6# z(G!ntJlpJJ?gZ+^rq4a2c8o(??nI%L2c$~`C0Pa7>`xv(D_-_yaeB}Y08*ocy}RQe zi-+?C=7TFDlu`Mbq$DIRIJ0ss?5#ivhgBz>@MrS`@sWfgC1;9~1(>R%#zL(gt=}s< zV*&B%*Tc44h5>DBnWW&TwaD13#HDVTVH$2bs>|ae+tYHHg`Znm9NswWPxgKP!LL69 zk$mfI5)aUFeV1OM$$M*fpHi59N%=5|0|CdtXELI+=|u{JNEr;z zbZEsVeivX?YIj4_jH^G(0y;5{BsH}35Es`MtXkkA7Qr0H4?wmKDy$9#JK=0X*!G5& z_&i%3g907azUEJw41}~BInZV5U_1yik^;;A9kuKC?Fu!V?1!{LI!GIoHtHp8M@c&! zMJ4L?9NW+I2+Rd~NW92j!7QZ5-iE}NGVR6S!;+1iAxEU!JHN{W8|nrJ3Du*mC|(_8 z#?o%+64oCanu-2GH|vco^G8}hJ+N`ThXSCl-FV*6ctJZ6l1Ae3@$2(n%to4Tw*(Lw zX)LX0DQ?-}!R|W;PSMIO*+!ESW!tNqWl3s-*u%A|7*23aRN-lg8?v@5miHP`!aAL% zq}{%L_G>Hip1ElN)kd!ENQfh~p`~rt=?;6<5foc^{Kdc8I5UF8z0!SS<4wF-ora_T z)Z#9BkL861A6$C6lGp^1_d!>it};D+m=qZ0GSv7LB1n?WY~B4 zDDIuia^dzXMWds3AW};?Q@4d0Ql_4@%pD|-IM6(_| z>$Xs`Wwd&QRoC3ouEWgF@7GSlOgB%_)2`YOH3RSU$}1SH@y}KrX1TDdCDGAFs5^IU zFCUnC$92sZH^IBM@Pr$#lF^S3*P)!fygc>kv7WNds(pZibX}Uq^6{vPxBY?hd$Lk8 zg^K#~mD{aBez5Jm`z|utR(G3i6tz8xEOol#)!M@VP?@TK`pqOLRe1kF7|%{$PwQg9 zYv%xCt^_$Uksiq7^*g~Hu61RK$9JJ;4wN`_pdos?YLICv21?DEk834;RY=7w)}8KKn}})=b1+ zFdyp-(XQjt@k#U?+q;0pyJYA%9d2HIX(l9TviwJq@#{&wKW^capZK?~Sd%g;s8$NSL^p>Mpqw#llE*x8uq{czAZj*M4$wbxl(t{LTKGLxm3I4E$_ zZ@$wVWtg;bkN(F;Rt-|a)oMt*ro-di>bR{kMO_fm&yA4zJQc8y zHM@e@Ao86opctFN*+^N=tU5QC(2u_%8Q=mH=f%XvMzh&`_v<^{fpU3&cSREz}#*tCxpMFM18OOH+%zoqDr<31(o>BfYoNNr&^N8so7YS zmDF(Rch@LiVbOzgt=9EsL=T=1G~;}G1<}XljY62Ukd^6hVk~#52mDC)5GNMI_ldP4 z45T?esQ@V~7@2yeGQ-#ieD~}TRnA3x5Ic+RQu!Z{CJIeSn`)#T_DFT@&WRtgIG>U| z|7tB?C_4auRi-#IDKH!F4xYDIMhc1p|Ssu`-ZrX7+8MWNBEi zg(!M?0i|N&lyd43e!MY?e>N%yN!r8CZ)?P@$ARLDgT{ zR@cPMjlS~@Hq8P1FEdHMA?DJFzA$Uy3E#aDQ{qqlWmUC^NOrf;)^l0BJW_{WLXhQG z@neu&hm%T6d-J`EGYcmGDki2$oQNiN6K+^=P5v^z*$YtJPVX|0iH5yBz4^TDMSRb! z6ZY444$9nPsD4(Kz##8F<_m@+wsi2482fF7p-nC^4oh;=%kd6|+QP}M0wd>; zpXJ_uhC+9nqO)v-weXl^;3uT8R75RWo2Wfg}(y>@&H)Q!-)aQImfGjDd^$_T9S&5G`@qo2ZZ z6HE)`GA?B9KWk`aDjaQIk6nH%z0%W5KI?3pancoH^TIT-cc`hatQfpbpCC-Uy=37| z6VIlM{=f49Qmf*&Z7=Z^bElJs z>c%BB=Ei(q?phw0Q?jULF0z9aK&gj9tgP|~z7nZL&7SH-h+0$a-pnKOh` zQt%KkjO<6J9wl_1EksI`r#T>20OwG#rCqC8c4^dml5ggI5ffz8VKy8r7%JxtN+jou z>&Ays2T18CLGH_Sw+@`2AiNnv#@*En+JMh|&dO4gzZoTH62!*sAaxvwoIrZdK!KbK z6L>mx-J!}6pf;>&t zo`e{fRZ>h+KQMn!UyPJk{~>kXf1@%+0Cy$yTh8jH#;dIbYB-t^;-Ug%*BJ0T&8M!v ztnapgIoUW)uV;^KTbArznuyM2R%ec3B)uvYj(0AJ9P6gaI#46u*y9X!O@W~U*po=N zLnJE!;IKzHGGsEEGle6E0_9cbCDzQxI}chwuXq2>`#Ep;$_o2q=Uf|ap3c7wlv4MY zS_s&Aqomx^qSBGlC`d?9sc8CHu59PsRKeU1C*?brRe(M*5r}Ve!oLrx6G#SB2yEnHQR^A2Fv?B7P1K-v5Fz zT1M(84J4qp$z98U12Ye6y#RIo3Ask#gC~}XSw(;kjWyJH>oKTBTY0wheF;|biIw}- z48*cD@RT*@vavFcmWA<#J^w)#o^(`s6RniRfwbd93;nl!dK->9kyV6d!}JGnrrmbu zwcsO|3c)x;!bg|D^Q?ETVJ&f+YfuSVfv*Mv7c;JFhw-sX&%CtN84TdTOn16>=R2w; z=**NXJATAzs!+{svXZU5Si6*tcP+kk_51Kykmf1SVFQjwQA_Zn;mweP4akj50paR` zsRbb^n>ahdn>V(Segg)@vxKoelCJRG6;{JyCxu3N;;)@Nmzo2OdX612jrnv1nY=2} zysQ?cVGiIBrxstrDh|?rF|Zh-ZM{-{rk?y2qozMU$iMx0(!et?2d*m!;A%kb_!B=~ zvMOY%Vb5Qb4{-G}D0Fqk)DhDK#49v8!y|tzhSZ`P`lK{N6>63XhP!LLVVNPbH(z0L zLN;fq$?)qte*=YCLC|TmBksKgl0?m?0AV?NZKjCB6)3-xnh9Z-oVmKwBjyI9&LVp~ z6Vpkp#I5>;6EAc1pN37}Eh`FsKG+tC77P#IzECNmfbqSWZe>vV-fqhllBNYWbYYy!Z`%R;rin#|1mk*Cx_Fmi))f! zlIlEVBla2gC=sJ?Jk9aSY4h_VVBU$eoz>ge9GK1sL06C9y=KIFQg%_c-XJZon0U`GZ#6Rq}V@K9+j)PaugB6=)ouyzpnQ~lEOiXM&S>yZnzjo^z>Ms+6h#ScP z-@!kv?Y;g@m+z%1(!kzj2k4E@pRMX=ZEmHs>@D5^!}lPV&njR(^}E2C_(8qozpGm_ zH}XcV|7(j*w0+|SqVbKlsksIE)Afhuzq2&||C^S1?B6y=Wi!)B5Lx|86SW%5w9XzK zDH+*BR?hAuTK4h;6;LWR%5^Nn8@~#p<>iYX za0)EkIE`89X8eS>>NX4=!NmA0#kWX0yhG?YL=a29Jwpc zE+Z9zH)6k4P}Q|M9Bd|#qpjUk4qnN$;>B1q;{B&#FQt3 zU?nLL4^gMP*G@J3Z?G!6200xflQQj73+df(Zs7Q4hUT)3L*Ga!#CdBreF?q~8dZ|UIFeaSL8W|M@zZ0f^ zJ=U8lL%qRN^ahr0A8##q?rNXyAR3qfGP)r4EDL76{Ia9x6nQ3axvAQPbGK%AoUPFL?nqH%JUYPD51Cv95ROgT}bYR}VLN{zL zg-E7~0TpipUtn(~?E9vLQ{RIYlaw6NW*#Hj=x2%Z`iQqk*nm{MZTI1OFcjcj6pc{= zMA9WaLCM`Vylcu0*E}Y_430kMm0?Tz6}3z~>Fw>+qZdizSwcqWfEjEs}N3>bUHUlE@#`?xu9 zi_k8a=I`Hx4>(Qp7DcadbDpj!_%d7@)kju)1SADACmT~6ac*3vi75lT_n(WFTYB$v zhVvRdO^g@ZFI?Z9MnZ-c078`oY=Kb3o3$j1`A%fedOBOE&u3RqhZfBG#Oc{sq_oY1>od^0#{HEmB?>jz z#liu;!Wkg^&6GdR-}NeW2k{VsHKd3;52zr+z&lA@^Fz|Q3(|Tn^BjrPl<_2A-0L1j zTq#>+5K7Rd^SUeB65;EBK+$o*ju$_k7_erP8%4~1j|sd=yl#5g!gYWz?PfwWGzt>l zb9RV-u8``|3i|gkWE5wvjqgPSj2n{%NCa#N7g6`}w*`9xS$+ST6A`m1{yQtAux-+L z1pjOb+*)d-x6Nyp4rlo9U8(^6NyB7U2*fVG@tIoa?ttQXzSIbI?3dDry6o@@Kr+J3 zLK?^3C^aLBqTJ8k{XP5HWH60KZlk2-ECw|N20BZUuinu zFkM{*u34c(4z{f;-%^H+O|>h%d1nD6%=?a2qb?%xJjNW8f;(8w01IG*Mr%Ld+(DXh!pyeM&Q0Ex4SdXOzu?)Rgf;xfPU8~j)e{NjdtHBy zZxNh*(!b?wak%fi@u))%5_lbT!TL2@>9ySUWE6z6GmxSiYlCAQg@WF(_XFw*5SRBi zg>~Jq&M(>29Q8>L@odv{Jx;9yn{Xap8MFEfz z^q2anevoz(@ZdGv`!6VYFRW3KRvFCA_IT7^E^9h@7UO&C7MWYZ3PT!~%pPkj(jEP5 zlPxSIML#6o-UWNo(kT<(t4Ldc!XhC?L-%j+v3}6b4P;!kQLtrrt%w* z5BL+}`W@Vbgt?*1jbdj)M-r;rwe|buiT+0g>vaZ8$t#(v9@Q>ivVuoyj9w)kO#yAp zS@p(|B)q=DiaGyJO@*+M;OX?LhGswiiLG5)u3@^l_Qov_??o&r&dgxqL(=!U%BE~q zVFW8UM7x}Y&D$=)>M3j&XC$$bDf1qa1^3jyQUOB?z?CQH9AExJj|P@tcXw{zF~gaH z>37R}UK}$3Ls;f!;%Kmbcv#{w7@u=zP(vlt$Ej1@Uezrv0=$#~xqi4-C0x$8v32nw5V~LW;uz zM+s;;PB-p6*I^2}Ezn}mdwH=BFiz~!%1bOE$|B(9tzW7!Z#sDYxwIPp2Geak51aK! zXJ$GNE$0uTE_A>L`EXxZ^c^=7{XO-WbA4lue*XTwcBeqjciJ7k{oX`Rdt*DV4cZb5++QIav|! zlHRwFEg$D_sfJ-4o$SE99O(Y)p942O{4yM3Eks zFL~Y?q=&8v^X8E>@A9b`KlI-G#R=momAuZZEVdw<*9rO`2PEt(l-+9HyBn6CeL|nx z6=^ViT`Di>5$pd1bXL6n%N^?)(QBmV#!33j;prsJ99r3vIrmNB3ws2O62vEibvr#g zQ@$Z_Zx{#)FN;>4Ap{Hy_CGxla-TPHM(#JjIAlhi<>CZDZQ60Ah^0$nR&}x@OND>{CiIz2>Da?=`#G zWA(Ugu8YpewUxhGb4_RR5|_?PJ~)}2bD7A8k-~c7vlQ!f|J4kmIQ>_2qn4Q;odQ3n zru{Png;6zbOA@UmK@GBxyrgxr6~xbJyV9Vhec$Zl3SZZ!$8wCAd#7hFQf#!-fuq*L zS9lEvRB1_6ybfZ`g0{lks`BxTr<~(6#>c`R5i)$l zWe3WDw1etk>F>v5em>tKR~ND{YfXaWm2zE-)GpalEBe~`E;3_w7wSTs?Acaj>W7lF zIRH^^?YSXaBz&wX`pbe@T2pJ0PxaF#pFOy8f^5YB2KuovH$??cV<=|Ni&S)cX_r z+5htW|5s<`Z$#ILJopicd*j)^-S~g?<^R(&w28^;%l}9y)J0jCe*Pb2g|>|$a0LsE zmuO@Gulo(v--_GPv~RpbTSo3580)HQOe?O&%X+>`T$?vX*|WH_hi_mvX12O~{=_1= zCx%OHZ{d;8j6!krx-lYri|mkLNo3=j@vR^~BB zRFo*)zmIUPmkuwtY0FA<9q(v^Y1X~=;>J{D=oO%ToPWn9*+~Ol`Z{u7vccG3cZQ8K z7Z{dkX}L4Jeq>CXM}x3(IE)UagRKyAyDG#rH*&XD#w?<0fIY%!`U~VfDU_cTS7?T8 z{zW8WXMlmZ(Cr}R@>@Mu1*lP@dj_P~+1hWec-$Q5r5 z)JTqY3W_@W40It4V`{N}tDZ0orM)Ekqs}mN;tHV$8gO$Dp)u4VZ8-Ua>9)>OM{0P5h?zfkfZz zzP#R$=XM`0V=lQm#S}A61?d_&2C`L?jCd%7H;auPeXkUW;JLH!X6--3X6fZi$8>f@NJw24lPaJ*Hu#%6}4MSE5LSPjy}0(H7Pww}rP&9IYA6x+{A3P0?sM zTHnSbk5aCnGx5GfNchAF_%6T6Duvc>M5RiS=OC5*?~XWNs-JlE97 zJBHBV$SWcLu++WfcWu4B9HC-7=&1s|qI%xrG}RCPkTIasNnit8&2Y5_ZJV*jLjPZQv6pUILO!?8K3kS1v}52?QtBxXnc<(V zpIWV6JKHC&N#Uxjbv}9po89{Qy)~~acIl_1_jFB*za%ah@||aNDDEZp#+5;4`b~iwo?B|LW|G|IYs}JCS2#}|5T4JR<)*c@%lfgee|MVo>sX`L zuIw3`XKdz51g|9tT27}g*=dC}s`RAz^I*{ZK=fvX>sQ|&axD3DV7z{GDiJOP3*xfD zu%dMF)3l?+%pW$QGyN8gWbT|@G`zTWk(xCYHF|Gs{$lmESdVO;AA`TvM?)UXNs;W~ zXW6|g>GfcLs0pdG+dYZrzT;h~-;LNYf0jBjN`_vvH0;0tP~$}ZQGs%FnY;aIP=N+iv+CBBjGvcC{uXd zTpCZLLDX)#e&Jj&W>ET2e>W|6X_mmX)kq)zqfwn=FyTxTw=Yr4c-Pnv^$y9$kpMOq zpp_6rPbYEz=M_-j&ijjh9|Wzcv%_O}i!izl`*Rnn!wigpP7`Z0GU`pRg=hbZ1^V3p z0hx&JLNs>r%iS_t<=KO)bM3e%{o5!z<|R{tdCuQM zjbOE~d*~BX+f!MbSodvLoG0gs&yXRu`E%X6#H6FN72v^vU+=iiJqBNwXnI8 z9Um%dK5BHr+zlt6wQ2V z>QSi+YPkijjY23t9%lV~eMz0%)ewlPB!RLC;1bToZE0s<6VKXr>tfl`{DcE?vcq(b z{6!c-oP~&;77uY4A=%}^TAoWVngRo^5__X#2&f=n3d2)_e0UfBRoyvHOv0gK1*Z3O zu)VFLYFv-aN05x;GbgnGyRFUMrSBOYqjj-ps+ToKVt}Kg;HaQrl@n^fAmmc5KtOG6 zEpTi2_C{`=wOug|=OK0$VhJo0ETL?1twENYE9bn7=!p|24u~0M-FfC+f7-@3MMN<4 z^3)BK@qWP;%u>$hJFT~qr2{g5xjr>;?=Rr|>uF-u&#_Onyo0k&(6_hm`6J*{=yEfu zAJU-TdFM!f*k{3UL0Ekqw&*IeaZWjD6 zo7rJ%nRbu5;6J_#m|L*MpG+7|y%hH9TvYbmUb&%Ktrk({(z6QBH{~^SjJeKME{C-cud%B zZZV10bhfS?bDEdd=Kai;d?~SHy?QZxN2BwYQx46MMvI2Y=h7oB4ZT)93i=Zh4RdCO!%d_+8=Iz| z3{#Kyj?n7K6Q#vvQ&ZjRA_ZLE!ipMM%X?KAsJ_r5@3`W{e%mtb_52@(-ODp2uGjMf z(iPvt;DBf5fFeMU{_?%;NVXGePzeI2*h{zra^KRO5W=PFego;?@WmHMBET(a1@ z*rh*{pLDD^6~pv!hb_=2*A>s6lCBul%JYWgC1*p@jK1Ma)d9mCuOLkILYsuWk?s5U zH5e*>2nx`(9Lel8OJ!fbH|y!({A2y~vANGMZh8-q7XAU*}-Dp%Y6rfk8Uq&dC zI{iAj#y;r@!L>G<(3$K4P{7t^7y+gjBfcJ&lGoii6RDYLl)~UWeJ}S5N}OwUyfm)g z!+4>S?fPQP(u&(wybxz?s%rV0Uw@~Q-r+CAWzn56K|El;=9Cnj)`mW2F@|p{3U-mS zf0Jy9FAT24pJ#qv@|XPmTD=C^kh@Z4>()5v>d9qhNp38jkry-O>-ki~EhHi4JH$A~ zWue?c+})UoZ4A)slY|BlLF%PTcnAGvsnW48_rLH(mHu8cuwa#7NwjD09DYx6rb7{T zPeK2SDgD9m7Xx1MLuP3VolNZLXN{yA;+a*`E5&%`SsmNZ+3Xd$W}y=LOZ({j*HBUn z{Nsu76G-rk!gY_;v8dA%3vkNmBbGhBpOZcyrMdX8nXxw?0%M9UWmgw5+{=vhJ-Iy- z)b4mXMa|Ck@o@JJXGRyjrdOQbbj-_MXILAq4<3}bYdO|WBuAB+D{SPtdTdlya@G#1 zkm0t9EQuvPLqfXgJq7|XDqKhE6|*2|xCdxO2_!I1xMC*XY|GLDFdL*IHF`J~RM7F( zve7*y9^IXTmG~ux(B8bf7n6PcPl`zPeYO()pZ1j5?}s$Vu!44YCn=88c9J~RWLSbA z<-o`UqyThQ^zYByPYn!d?-td}8 z-2&vbCs4@%-BCdGP@eRhmGzYp>ukn69TWJVVp=TvnsgXO!xBiEsueE%x*Z`L@m=YfuFc|< z(o{cEs?=z(*nkJfvI~%u9t4P>Xg>hTbc-EZ{fY335W=TMb)SgAZ>+7t*qLjleD$f$ zLGYs$Rzc|W+-t;FTJ6o`1c#IJgUODRfVoGeIq_3~ySq)0X5+jTl@X53Q0E{5Jb*7l zy5pi&{i51w4y^!kJ!qEhfE?bfu@Y%Igg^hanCHUXr_%M`>g2c1np_$j_jKu~x)8fr z*~!1iXU<7e_h?mlxJX-2Jc^s;vW29@D7e|8DsIwjUlYl#zZQ3CtutF!lYQee#9+-L z$%c^%M|Cmp0G=^Oy1~lR6X)0Rek`ybPI0i|Mq-1BKS*pZ~w}_P}Q@8az zCZ41EI4FzD*al<*Y>=@H%#vmJA|vX2$*|9V^r@`qi=U?kiZa>AUbM&^yhf|kfwya` zS9^=BhtMDZCtEdA`uo*=oD{~>o)2i?7rE_Yf)c~{ZgD6MLF|?P-%i_ZgNgHW^UTHc zYhQLsw`(6s(P#?1g81n&L$qFt!?FNTIy@48o_gh`8l&s3I`l@q@0!<hZkk18tX-aX+cz?V{L6?&ZTc^byW#6BHGo8{;1yHl8uRSK(Lm0(R&F!-G%WE@%Qn*MrkQ7bQNOx;bsltW5=cbE!Aud-sDGaG8QlPjL7SbtUiXuEMPi~_@8*WRk(bjAmt$n6u#}IcchEMD zlMv%~$$qy>%htu|sd8M*@)6Q0?R#P3P7G(a=|6<9CN^RB-$lN8V*|>~d=bKTJ=zZO z^Pc#yne5HZqrQ%TSi_1evvQOcuR3IZkTcV@#3Tc*Oz{r|UTkOoo}Op}tj%&^`!X@AoyT*~>4S&51X3geQ9 zx1k|j^$C87N!RfmKZKR~&>x;&lMTJrQKiavZPN4MZ7{jWs~8y-9X)MN8$E=iwS(mb zV%=dgl(xJK(Tgvw0=h|j#X7kE$qd+Qm+$m(hrS~tEWx!Y?-7E40ok>UFt=7-50W#r4r)~@D2|Rz){tb_!$I$j?wBA*xdPu#H+H&#{9zV9J(6}9 zAH%Xdb|=r>iuvLSJfPPhR z)nMqn$;#-ywyu@n^VZ)~2toe9Z!zlq=FOYA;zXY!(LAMXe&-*fb4j{ndzY!MlOGV4 z!|(92{uf^>QkagA4FY5QOh34Vo@!k5c7=rqxG7mq><|~cpT1GXrr}+tEu3nT)%jKKmMQ-Y97NW!ZUMTp0b{?V>2^N zmt3QGRyM^VB=NVSx3w5K$+te!dlj^`zaT~WZv^NWRv59jw*UV>X?wK4-T9xlKm5Nk z%&ni|wMqf2#eaBu;CG)DJG*2lPB5aBgD8Y*85B#qqGRChu@IW5glMMXulc=}Ta&vdLq6-|dO^ap)P z`zMD!j1~noo2@afBYe8O8#R+ekq;ifYCV^(W!3YpmaygJ=U>Ra(M;^g>y*1R73?&- zkINVbaSpDHELSYuMqfzuL--^T;5(PFe;V$kp@0%Ll zS(Auc{gCm1+$B7BXo#mD%>-D)74oEaV4AQNFif65ELa8z3abJ~Qew&0l{Emy2-W6Y z-cO5s2Bi!u@4W<-+`FDEld(F%06mk-A3Q8Id8C<{IxC+#cI1?)2UbbmgGsnF(EuZo z4-OEgXgGm~D*{b#zejSK6BGi=N04+RHQg_>P{=h{2vLn^Oyx1e)Ky&Z1SMtF$6fZ^ zRFulnoszVAurN;djHXS}`g1-N@tD@8OcGN^e==5PrwO~%mkLdMZ^3OV>boIXKGY+- zB{Fq7!Lp9r=~0;Fi=X)$H@v>umj81FSs+~*({4_DX_mW|p)RDXPBlZ~GlFN2tU!vV z0gT4@!E#u34aZX6wgu{u>O5>Ty{W{PHX^NG8r;?9F0Aj9?8?q8M0#zIu=y+iJzYlv zxcY}Y=Vy--k#X28BOlaK^QE-uBhX0?&{^4jL-U}08Au^g48( zZEeg4LNW+Y+O5$tWcuZ6C^BCoLa${%FIUYo*U;& z2EO%FK~aq~bEe=C-_r9ESsP&2hBj>a0r~_b=g%kHlyVL)fy-K9X>zr7@_UWEetrZ2 z{4v8~JAF|;IaZ8E4ABBt#qeYx+N&jmilb+(Qz{**S3esl~Byem;6Aca7pJ1IaVUu^`0?b z3N8z2dvc)|bKQETuC|Y6Tjk)Io+>vYmva#j8n=JD^mTk zL4fh~_6Ojx&G!<>v$ zI$DTAml>WU{fuM0u=BQ4BpU#`j{5>kal}K2>QVYDC+)SbUbSLUhd0Ivl&Ea^ zwUK?b;jBn5kcr16AEjvw`occzs>O$?nqDf3Jsonn4!`rtB8K(VwWFHx`Ea?a2vh2; zi_~REU-#YUzPk`C*78^CCpySAORXG7l+3Y4mTJxHNUuZ*FG&t_oshO)O>GyAPW9XN zhaC*%wQm#o7@q32dP3NWjPUHs+5stFHJUlFCpqU#k=yx|F`a~~J^SRF99vOYkm!L2 zK=_M$R}ln^>czaZroH47&T2q0KJ^z5n!Je$*L^njC}*q@wJ#wk5mr)NJA=`&%Nj+m z(%&R7j5WWe1QIj4z;)HUPZEal~YaA4Vcfs$wCcF!;l;g^z4~@J2h@IV@ZM(TB zk*S}ViXY7XSZvov0`_NHPJIJoO} zBT2@BUAD_vD(BSILOf|xW8(s^G)0ro2KlVQY&pz&!;;U{Gmrg~K&390K+v?*EoBn>ya5##iueU zaJ(!t#Lu-=Ay0D#L?sTN_l+`^?O7Rgkf$V6JZ8Q>$ZPQ|H9#;8?1v9WL{=ESaZc}3 zQp~0RId_WGFt0vpi{yB*y{$Xz-KXi&ttK(}N zON4hQByF`iPG}IXs>Ugc6|02Wz}4cN@oPWk6x?EGFn2YB(K)>r0tGh2ztKE(?f9aUZ-E$vUiizZ6@a;ORo@4H9yn8K(C^*V~fI z)ZFTRRnrsQO>KdO2lwU;d5>ZxDh0J|T+qbL};^k40y8ok>qjKc&iZ$(D-bCQ&6=JogTs zS6AEPBGDyVWD||f>v~LvARg5A|+kgdK8PuF{{~KCkcvs=iKi) zTlTls^0Qn)sw`pqsQUnr=>OsR$oT06co;(iEF|=v~JyO{G2%!0`l+rBzhO{rqsK zR?q9B?D)4Vwy{wd9Ew53a`~`eOr6|0c>22>XdUo#Ow|~Nq$k8^-XdF*(Dw-Tap}AY|b%pgCG3$`baRD~VGT~Rj_M(ZRnP&jhxCb+ex~Pu3Gx#75 zqj?Q{k)*Z1VZXE=->ivMQq)ap-g9FcixwxiCsoD(B`iXMzahrw(5*pbwHXjXS;8#q z; zbu(F)3>|b$q+gPe9bzN{Lcog7=sL3i0e1PYba=PNHfEMu;x@esAM^}_Xt5ERQfQNN z60t3h2&&7HFBjbPpn(!;mSdIsv#$_a9vAoU)xfQo|Aijs0 zdL#-b4thShd!ih@^C;!)*FG3V)$OV3aln}NuDjoKZhCgcK-YZYCi~eB)R3MOQAM(* z*`db3r`18X8*f8{-*|KMM9)Ops_19T*9Xw8R6&{as$YW7rQEV`Z-75)MTXT8_3h$3 ztTmjjhNL;=jCNK6yw#U&I&W(}KMRq8^V{XsDAuRE>EM4aQwl#H(v&z^|eTuY-W zZN6Dxe<<()a-Q+4@6NSPD8`w_+}W%a_#Q+*@{}$&DSH{>a0YYl`I@+oMV2<=dgTQ_ zpr=~@xhB-y`2hVDb2G);3+CT}Toc~Z(XOAXce>p7uj{vauy34YH2qlmz8U=Nnh;W5 zaRp)0R^osN@y+0)%%%lHzsS_ zA3MHNl}w+VeikD6QgIqRbL<(({~KN@p#gD?aeey$I=5Fpycl%B(DqY zWmS;!iP?$Qt=BmAOzs?G4adu|axV`1k)5n>QAr1hy(to!-?~R|Kjs(7TX0G?GO;Cc zGk7x3eaWh%)u3eP5YO`#w+H-{9a5DK?8n?U5i>u?O zp5v~!N13z!iL*};X(3r1G(CUF=R=1r_G7Zhnt>0j)P&76UUO_%m>m0Am)(JGba}S@ zoVb8(SNcoDg3dY=r?@To+DUP4Ol}ehqId88jqBp)DqI7A)Ag#d{>|kf(m}Ot$)rl{ zzd4FQkZE?0zJR^8ae(Y@T}fAtdk^PsMQgghAvTiF;8N_jHOYlSJDc=f3A(-3H%6#% zPS26r*%RTiAOCRcqfUn^8zB-)JC{Q{mmRM#$Bk_tAg6~w4=`H)`AejSjZ^48R-oEL zWg26a>ZjPLjZX?Xj(w8ef1R12!dGK_ew2QWP@H-89_lFj(R7@mIf2ZM7OuMF%Bw2a zUXGGh3TV%pe#q{V?jE=Or6}vek)^RXj&z%7(gl$Q_ow5k zGaJ9?6}faBW~L~%F`blMIa#1}?5AAKPyaN<`3Sja+_g-nuY%PmPxW>?5_)al@S2M0a*?p+ zAu;Uz^lvIlUGfCRh^81n_F?*|>z&=fQ_w;A;A$JUL!~)_Mn>nT#6WmesmsTX{1Lw^ z*wcW8-tSaB<;r^}x;RoX(S^e`1eLU3v2i*f_cL!MmuK~IR-1xbwD#~_BH}+boBW0e zOI*rW$I9QrRdc4cVdh_$)}S+FSWTtuYviW_8ztzf@UiaWr_oaOb~Hw}B!nn2WEA4*VJ~O@SlFLZvY4nh5I`9L_LYe&@#&Z%vM*9xjn!!5Oinbxq5+$NAQm z0Ef_sd_aNlwj`lBgTL6L(79(;S{^2%P6f#RBo1S-q|4eTUe9a`lX|QhK%CzMR{g{q zBa>(^ZlfQrY&VE<^1!kN%b(2wsm)sbyJp1@%u$qn&W15gB-dXI>m|s3zWey09 z>(T}4igTm=S|@d`==XEeJzUkpx+hBJ$~?#?3ErQw%hoMkl=k4(FSo3CY#P}9Y~j=K z{m4$e@Jn5|vj4)AN#lS+*mc|Pf^420r4;1NffEkbcmAplZQt=ioZtlO9jo>y=g;>u z^{76z5d`mVrb5hRsvUKbuD?>71{&i0f<#Vz#-r{xjBsu;DyMu3P3I4RuOZw3WyfRh zK-a?qR~V3X-1)tOmgkOUm$oFpUeX#BGdJSo^%_psl*~8`<=yN@NyrL{Q7=r3yX|Au z1-7-9@b;Hu{{y}i@OO16 zfEL`vM9;n7CK0KWg1lh~w5R|CyiKkPNg_8R;CBAIY6rq1SB6 zzK-U0M*Nxx2A7FR3jvvR-EGM+cEfV#4BMcG0QDFE;g5&Bk`<%@w&7gW_|JM?H*)(j zqQgd7zDwlr^T6enz+_p0&aGLWb;$8Yw>uACybnvb9GelS>a`WKYP#S2XOTP`m$376 z&&E^YO%hu5Tb1jNwuBsa3lL!s%B;fQz(cy9ecU`e!;Dn6uJewL6cS_nNMA~5sc%jq z2mL*5p+UZ(AK_$|kS6I^%lif1g6d(B8-ZMofeMHhIkhzoYk#!|akwG?-nNjsWCTjw zvAfq@S1~{iML@!Pxi|&(Yvm?QfWxY1%hI?dcpa-*9p^4-0V=72lD;?zG+zM5>_I!Qr*s%U22ah9EsXT4H0ssZ}2s^V#Hx z+m55CN%k-Vig~d`{jg{!D_+$}u`z(uGIjudGGzq3aG=*W;RyFX`(k>pZ{h^f92lVY zEi>Y_^z22jg=!6zZs(U>KANrYPnZePRjuZPHBCfii?v*SkHt?G`BXhit%s04LlaiQ z@lBY&jm9WBZKSP-W)Go5$kUkf`;ietSP{>6*~6JmIdjrJ>b2r+sOb zF@&DyxW|%SuPKbyFbC%c){u1sk|`{qt+lCz%D0#l&ztUw`P181o0P6)cJaYCMarx8 zol5Z1kO#u{#PQ>FZ`DQi(n&oe=BGH@y9^c;kG9gySy&AW$|+h6VZzS^D7e&)efy3Am2qEWP{XAZ5ArdCYOD3?!$DG*Ol{)mTJIzxr2tYv0!)rUratvSH-SK{e27 zYxS45%hc({-%eE!3YL$!-73*VnbxHl(^!R!PDk|B^_|wsc_~l={e&8iPGGRmcH)xiuu#3+j^HLJB?`slce#-5`)eW+?3+#U*Fht zX~`pqL_tk=EOKAgLAB65NnL9{VVZZ|MkL-KiLHSBmyayA8vZBp-DuL{;G$ZtL{Z7Mn* za4+`SWuW*R1@qWVK)?{XB@<)$wlMrgOG^u#%AwkF*46WRPw@eYfkB12eQD1!CA+;# zX>j3_iFN;zIqUL~9r7EdL(Z>w#ow$!H06nD89Bxldw=a+U2};tyK`?stWMIyDBHb}5j)jyqq*kYX8geNeA$3~RSg0zw{lQa{>0v=x z3&EMfy|Zz{X}x!X>By>#<#Q+y;SI`gvBv}T;vT7&hlEwy4po*xZ}d{mmjp;`=W&0J zfcI3urRGz;p*;!FWK9H|M_ostC1`BcH2LuW0Gx8`_Ht{IfIgGDT)_Oz2yZCcU~&oE6X1L7P2Glj|7N90AeA?mzsNWjHjP?|5_yb zt~bioC(+P$i_9#Vt;_(W3o-YWsA1E99V(lBO6t{a);XA0pSj+-@}3HAzUpWRpS6d& zdnN=#wZ*ZT&NHaKHP1vRa$uccOf+I}z^fL{0=zmVJq~E#xTk#gu})q5N6)Q$(|26~ ze<9i-r{&KPvq96F_M6NFj4S1j%?IygnagYhAP&ggd1YJ^7!cBo)B)?9<9m{}*8S54x3Nm3&sqhSv5)x@ z-`$*EVWtMc_de_lNr#Os&?jZr1B*~ysAAHNP!R9NU=-Es6HvmeE|A$tnfeu!xRj|< zu1BiI+~cqXmEUSeSuS0yorzpw%+dyr-*xHrAOj7Nu7TxXLHQe~#0PGAo`8&)^{ zJ9!dzT1-kEdt2$)gU7a7Y3?I(@fHG=L4zfA=noAFZAi#%x4 zhRsL2@$(_GS7Ywt#9Sx4sMqoUb|m^pPr3Z*z2;y5}%Jz z`U<8_w}c>auBJvp&t+nR4?Hj;0LBRPu$Tu2&lC&KW55 ziuX;$e<2JfHJJ+B;6v{F*3ViGwf@O^Z{*=mFm-e?eA#Vj5I3L`ezl6Ctad?Hh92UxFjy5$mKzt#)N4N9I9-zDj zmW>n8kHAv>UH0N}L!JXYkf%d=tJoU(5d=B0C7Yz%8ofGw0w17WItdtp$d?-l7}kin z#gcQ(dijl)vd$L zm@N3j1N6)-4_LV4j&D`2rJf_@cMR4X47gf89ZBeRvb+z=a|2{qRz00CE`-ERh?XpB z@%3!md2E01n9ob?zjnquY7|@%cQCB>au0BAGI_bYuY@=c5}yXJY9G3Vj65qsBm2S9 z3?#_ieFdi^H)jb!LZ*=%dkgCRAzaJK&B~goL}$+)IWnh7V7HjugRQSE=hnTTaju=g zf*Yo3$V*-y#MNBr;5{C2p9Q^hFjB)?`o(cssn_`vngT^3n%Gk3EF?o*R@3h3*z7KI z9pKDtb;$WmO*~*08#imX4Pm_|;yMP58}v$hnhM=0w#!Jp`x*oeZ$*7M5YzoXt1n$t z!)hf~=uEo&Bnpwfsj6*XUjq!Aiex1|dEaeT9zh@|YoA;JFuR^MoP@XGK}^>W05xAb z@WF!KOg3$m0%w`md9Kt^>=Jh~rNDlbtWcMdjuZuaxd1)^E*0{^8c>v(zjx_eX-YI|#rQ zi7htngDo2!sv^yXA_sbwEtF3vpCGA(OCG-=~gUgXxv2~<}>^B9DL7fXJOb&)zv)k?L8i>kDI9|&a z11oKeXxa&^IkbJBAg{*Hd*T3u6YgS@?^B@ zxuVE_>P`a;zCo^28G}+WNXm#*q8cJlLNs1*A6vqwm5owoIZ>*`F!HvSV^Yg0BXEzt4YQDcZKU8kAB=K7N=aj_Fq7kLgLMfN&GU z{LzykF9J;%=>2W!s4S(><2p4y-U#zP&D$oDq;a+TZ&Io@Mmn1U-)nD}*VB47(61;j z&VfMFzGj1wiC9d;-8s)^-dCLNkf}Pm;`Qs!PiFepfk2)S$|lHm?DcBnADgDFcV2+| zd@LKOkSwKho;QaRyXi+f>$7<%#sPZ{X{66tEea8-bnx&63F=)pd*=a$wCV(Ad4~S? zt!`}}s?Zn@@^uDJ|Iw z)Xsup@_M&>(0bMnqk3uGjEs!A`aSCl7wrd1OY3Ll<$ZXIOv~Ss(H`&Dn2Ge86DHIy zxuBMMfN;azh5!!zR-8aLo{#)Nv!&#?m`8``FF}j+UI~$vH3p(OieJlcG5CL}mKBID zecl}Uu|!n$rZ2g-QnJ9nSHrfmPYzX8 zc&?11{;*(DFt6kHH|hpdt&T?vpV098DlpJidH?AcUw^&mEV(Daval6gq~7>OUR4hd zj{z6{V2!V7mfEvZp}J(+wHG%>sm_g z#i0HYSD#O_C!Hi|cbBz0&&Jgy*WL3qEW(YoL>x!qmyfjnEl_%3(*##eBr->P@}50e zFSwS~OOmhJ^x0IL4+v5H=hb#JcDHRABt?B199EoAi93&1Q2TO-r-X=eg|L}D&#z@A z5*?=COvD*WonstOsK28|2`h<$B+$#2v>aeV|%)yj|3HSq5DF3qf*Q0;;$tB9Ih z^-oo^QqHDT7H`xxEGpN3t?!m_P)~zWu2b2^35utlt^6%iS>y4TxuUvqFYGDcG+qMNwxdZh##Np z=6jAyQdT_A_Zco{p;N-qGhWYoCIzF|7r_1Ye$R9Ni?ano)WlzRG+4t;(sEV0UKvP? zK-A-t@8a1y_gvwtUJ+ETsMg}Lm%T7!N_m3*_|Waonf3>LjW&+01F_=LO7hSW$2{Op$9NQjr}0aeGG|7Gz>3@wbSV(va4 zf<-?{h_Q$gQAZ7Fe@W38^Jf$OE0uE&Y1_uCpdPUQ`3xQE^($FJZ8UMecJrSc9qA+y zuNiYxf8B&`sGVSI6evALI)@u&(_UqlTIFA(X1LCb(tm>K)k^H;F z-L(J8Uhh7Atmh-ur}D~;ZLdO<>c3`2DAuRludk8wdbB&>p%mxv_gfQECY{&RM^t65 z$IL4hwt8fv15s?Db?jHr!Mk<61#WgpYV%qf#O>oO%4!MRexBCMk(0PIp|DhR+;{Iq zV=@){tR<4&bpSV>5gq?yD-i=>zkxmyc*IbtZoJ{VgwmX4Wf`Keq)F zy(SYjJhdNazw%w5HqF`oNunO}?qH6vW@*y9PZCvH9Km$~54$gURH5&5{dwy>&M~DS z*EqWl)ww}(a+^i8K6di!%P`n{UFo*dh)J#28orJA?qm7_C&fuJm`=%NjOh5}Lz)yV zEqTn@=Mbv~3akAkbKM9$Myy0Q!=uD1?7J#cy; z-{Z%zUv$HN;#vY3#+{NH50lRZ7~GD;<5H^yL4=^y`@q)MS(x^e?jD>HvQ)G;TQ%;0 zIB&4qttp9SUf)vzG04qwM$(mO%(gB50{VFc=k|{&?=b(U{hAB-q7rF|I>x~dFi`wd z`UM1jjwfLb$+`^0&2$y2uX`V`Kz`+gfU_*U#$Cr+0_Dmh%r0OsO5*#x!BM;w?|Y>$ z3FLbpuKp_~tPZj<_{II-_Sj>{bCE5K{eSn9#^iO!jnfG658D+j(D(iR>0tbSsC{Ms z|Co&ax75C}dC5-wb^{vTur#u9Xda znsY0fJ6I=+hq?Qr^xV-#66Nx13qlOXo+pkrGU3H_I5qcvQtMH!TdaPIk&%%cyAdFb zCFnFCAK&4A7bL2z6?B7{o`i(v^1130w#*UV{F#j;|2(4+NWs5+v}ndGw?(&A?}f7Z zSP07%Nv*I*BJpVO)?YzI#KxLHFuuKwLFt{Z?zH(h76M=$X;nxUs`s`Q@HN&hMU; zsU7_NY>gD{hhdTJn*)_Bsw)|X@mJ=?;L9ms7b*R>zk!7#12Xopcdio)QJ*HsWad~7T=@)Ka; zNSB9^C?OM+k;P`U+bfV{4uLW#%lV@7?F0GJ`x?A;4oAMh*B52u-%#CpEk`-~KfOB3nN8`!8ce>#Ry>=f z~U{`6tL)`{bB2hl0nBidhAF&UL1Rdagh|qiOWt zcb;&|fqZ|aL6GeHo9nRfcXFj!mRbksl?!gHoQ)ZHXj2J{Xb(haAhtw*0V@Oy*cMes%0*li z=itV>QCrr;Q;(2#K7tPpWst#){{h z4FaUawRy=RT9Cx}0lvE%Lg&Q~ECWUjMOYjzMtRK9)`BO2&^=pswRx%efVeM`V?*r8r2Xd9zw6ezoo{Lh zDQ9Vv%};c1{>z(#V0iX$h&I{0C*HkU$u;>#wWV8tKPT4wng1(1wYU$~y_TRwXks&A zFTW>qfb{}+MqRN+z~5%E5rqsD`{YYzd<19AN|Yu|b#V0?1( zn)+c9HkFf^YPvn zI03Z^1nv;)k3k&n-c}hn)#+ce4!+Z$IE1_*Sw;BRAz9n#Ap|giEQAKns=Bm@+frY| z-L(&20;Yeu=X?pTC~Y{!OqqRwzZdih35G}y>A-?bTFm(PFz9LL+;#aCnIQC=dxEV( z6bJfbld6G-n)NATt)LO13y~ZJvjog%+}jw=$HgJ-9JUi#a6F$E%jWd2>J(+&vNfr< zQ?eq&EDiU$Hi!*Zj6a_GqmllIZ+2Q~*8e!o*w>&>`U1^@ezULeg4;h@T3?gSK0v)- z@8G|DkQSKYr)vK4TFNF+*7hDUq)RG_BS>}6qdX&0=^~U+H~?(6>yxY2(*h7am$BTW zQhfOJFnkfD&&UyJ^`1es9rs;(pv#xCn%B&7ha;_T1PI1Q2}UEM{8Och-G;au>Er%# z`&AVuu1**>qt+E7t%+MUW|pvXTD5Qhrq~Pnam_poozDIi4q=$L2TTs%(K)UuZ%kTa%Xqpy z;Q|IX44~>S9qw0VCZYxLy38~cLA9kwb(A-|U4Y<6_69&h-ic3O#NOQ>HZVuM^%^5g zL@%tO1fR_=si+CPPcSyGPuZUd&bXW}BZ!6~Ya<8A*&=d0;l;UbN$Fw#4Qipci!rHC zfRA3R6I!j`S)(^-&~0o+qr4{?%oi8of5{ciG9FF$cF7l(oImo$wEi~}ZcQ&;jk+d8Qx*q+L}D(ZRj?*FR(zvf zscfPmeY9f(T+$~+4r59=1qZq`>f0;a<}2^C{cP()934>DmqzT!3Oe|u_wJsv17lB; z-bl|ZbNLkNS~(QFKTfowtG#xp$tZb0Bx5d8VSVq;^kC(a!Sz}r?hW#hOAdFc-pZ;y zh}6UB^xprpWY1GE6~JY3zgub8chg&xFNzd%(YSe4Mb%pl+=EKvIAICL@1M7OD*I>>iAoB%apcH}U01$b7| zdJG2(cH@VtETTC*_SE=v!SHf+KIgG;I*HnQYLB6T>)^P1XytyvA75o%hNS;f&V$vw zPJhXfdIjxLH^^yRBlG&8#(fT-yVhkY*{N~EQ`<#>?{@;Co(e;%e_lh%g?Vmxb-(;P zs=ttC;B2;ubn8*?jT#->ktjmG>s8TnKia$}*=<+eZx*;ZY}QT*_t7fY6Yt@)ZWdgN zpK^y7d}Cjiyo+i}GJ_wTKZl*H)w^o2(j?Zp`xB~z@eQ9Q#9oB-IkJQs-Ids>AGJka{i|Xt60D5L_bxC3HilEo=La&F^5ek=U({;RIXPxsc4+PS6 zHJ<*2pAG4pFPXlv(Y=@1oa@Mzm;BzuvjRV`K#iUuXu26Lb#c}nV-J8S`YVJ0b+Ph| z1g7#OzOw=_sTfIh1=z<5nBTm;JW_u;H4B%0KPdel+wKxJ1JEh5Sx(u)4}zh69%8C( zrZ`%^eQ={%L%FdA{G|39KswI!=pIAmfs~S^0`$6rwnZ_!4xRmucLF1cJp<$hHo!I% zLGFs!L(7~;d!;@jtYk?OK(uz?aEJK5a@W}@;w8lWf|N{DK!)F-YoSlo1_1G{jT{rN zRm<^kuruWmj2J6=%+k}@VTMHQtDw$o<9(iAD>r-puDY=U6V_{AX?v0--chqHY2nOT zTcINR1<2JkM(ThiayBwR$1wS14Dd9!kq8H?^a;c$XD$gU20Y?DT{p1VK#a@I>Wxf_ zZ5iB7xcc8zN|?Q4@dR`2(h43igxmGyZ>M%$%qD}WWECAZ^lr(DSCb7sc6*-(A!W%_I9=bt82An|^H!m?gxGDn1A{i1J?diDl&dhW=)9H6&$hrxhS@Iw}-|bCC9_T-K!3uVl#q<0OZ)4zJXm%zM#X!mTgs13+pYxP$9yYK9kP}nUo2J`WiFWS$w z3mTP-FgcNWf)sLY!JN#;9M5kk<`3$FVkvxNwqDJ^CpB+M&cp8((wjpCcJEVT%XVQzQ0TTnh&Tu&h0HhK$~+cZU@076zRuykt{nO`Xhh@3bI)04Z{@>i76s z&93t$-V%Sx9t4aNJq|~qW!Htzy2QC*^7S2*hVwM6Dzkj^@J_yZbDamxk6-tBc}k|( z3>)A8iy3&Gy7f}$ZA?N>R4T4Ij`%TIPWBPKvfiC(_|_#67P57>!^ckI(vj=wzu_(v zesOcxE32q{7gmp4lWdTWktn;WPP-O6i@3ZXeWux}Y>A|2*6sYMiYeW6qs%i4M9*IM zLaxB!{hJy6E<+X2uO)4MxNAfKbHty+2Vz^~Zr_HDTa{7jCf>4T*UR*y%9gr`gXl#&)b zZq&ULL8g{^CJiDthdX{RoixXf}po|4!ob(`*tHw=jD&@INp8#$42F8|1|P(Yu@H! z_am{ZNLUYaTEY$Z8bWg;YC}Y>zDorkf|xo%rk<{s{#A8!5JFyi3vJ@Xf#$Xc^rZ#r z6WKgM+qo&X!Pl(V4kdd;KU;F%8!DbQm&p~IoovqICmfiP+j=@B72t+pdRP3bov3Zg ztXEb)MKVV0{%oTo_YpZh5M+hCdPVO)E#e_gPx#0(Fm)9fVHI8TDHl5%j|d82HHCf7 zS=|`A4_BkokVOPh;vo@ZmY{;qcbxk~Lz|`80zA+ZpvF81KBrdkmGQ_2>g6*uz~MHU zA!uu42uIY-H7<9I)8WqXjgw8kzdwtM9xjjLsi_|)A?la5mWv&AiQ^QOPz?4jZKzgc zv4+L;WkKD|RTV~@hk~zMOeJz(Ht6D&_?RNeWyW3QP^fJLWsl7HP{ zr%<3kGVfTtvtU&+EhOA5tZ)eR`kj!m^QY%$ry&2suJg=ypJ^Huy`U`uO4sRi)6{qi zrXjwf2U5$X!1w8Z88?WyZR)b^Vbx{4!F8LoiAVuSFdF@1ooTdyXy|GyA~-ZJUxs)< zt+uM_C%Z5pCH$kY%uZK)IwxEhS-@8D;Z=>Bm(p!Jz>&KST4q!D{kx93sPkAvEwB*l z)|sDI5oa4`O91tu$z@#*TsR)|=N?4VM7)tXlAhe_<7js*OMc*!n-SO(8B37n7?%@6 zZv0aV^hwyKgDYQLK8>vpcet?ff4;pK?Hh<5Yi(A^5igS%Fh1A1o`K!VCf3DEX$~Mw zMChB9A)RdH%??u{cAMXd+F~+5qR|Q96!VXmTxql4)&guCW5(S66pM=g;*u&kwi(H| zG{!lQRnsQ%wK;4Vz1~vhvQoNI&knP!$(+%_g&TFTk`CS*PCG#Ry0>dqky0^uBH!At ziIjA^1XfwCT%*UlI^u)a%UhkL{9dUaGERmcW^4U+9Jw@C(Li0z&}NkL;5EBpXMz3) zaqA(*?ID~Ju_uK9)C)XkKf4MxgOzul*5 z<8$EeDO!3D_s+@a>O~Hn6P1WPVi|lI_M7-bY|RXY2g`>yT-2i8OXDlX>JyIUeAi|( zL0ev)V6N#l$9Co6{mjpI!^dKVy}|Husbzj`o`0?>EfUn|d`H@|d*~$5`p*Yg%4EXT zH5fa=UsLBgo40mT`B~xtsY=f(+QxRN>hQy}m*LI2L}=39xD-_pC&52wG5|#IBx$=^ z_JOorB#q2BmyDLv1I8y?UGQ$LLK`kA|Ge9MB`YSjYQQ?9I>x+=Jx-L@R6BfmG{U-g zB|8&)F0-SE?9^s2sH3x18VMP4GtEgP_WFuYXOoihn8HuroO~TD=Eg5388Vn~enEXr zpIf4kOPW*1k6>-mCPZ$QZ4IrIrVPU<{G@#Jx_AHj0PAw{mP2{emkODX`r-Lxe!Jc- zmFoaohX~$Oj|AAoRwvJZl{fT(+fYZS@)n0i`)E^Cg!;7;YjKoNayHzp8fXm|_Ky=T zu03Rz!#L-mP5TP7Xt_Jm<$HCYc~3jp#6>HnHWcEV0h-vfS2O%`4kWF6teX|+W-(%T zW~G`J#J}fNuMJ1&m=3xgh^KjZNI~p8k=az8Q|B)~2VHD=t{W=NWW9&Zo1ey*;mwvA;*E|X61-h`hyOtSiz+)x`S;WCe(RPnH1U{Jr!rTIw->sxgZMU}co ztqX>to-pAtdLc%fb8J$i}4>hshMcf83x8(ae^4b z&+K#f8v3&2=Mvn{lg>IEf7^~)7n~`t7cyZb`n(>|VJq^Lhz5JPM+2Eh{vL6aCp zSNw4mj3WLpw*_lY(sHA9j#xMem(*P0SO=23i!;q>DW)b$A19k(q~3)PW>oV^ImZS0 zRSq3Ty#^Clqg$*i1c6NfZ~CAeH4?{PSe~kXa;*(7@o3BM+w4 z_vA3hA}%^!?+shh|;j=6WV9BBtsJ>U8RxH{-`rD$ZL< zF^VQKy2sdAxm{F{SBDO4GZq?G`sZk$KT_lH_dh*fd81@JOY`!Q{E0-*7j23;Qc;); znO;RhoJoWkTk^DNbof3l0YBWhc)*JD^Od7qQyXd?8sSMkO(hc5h+bYaCOm2M2~+^w ziKVm%p}Z5PQ;&`+NN#*i3dO-Dgp3k5E0MUx6^OhZLksw&R8te1Wv~~P!Y)jFH;`qI zF5`QERfAobp1SHgG;NTXjlfTzM#{IizPtm-jt#7+zxXO#^mU&9;2YUNmQlNO<0Vv2pB@9% zamY=q+V)0gsZHVz6U zwvW?$ex8x4bLW3}V!wYUk?DgIavdSZ+cv)&ziOXuAHQxqyZvRVTZjLvzXkc~tv`M} z0s4Pnf!#iC(I5d0$nhxQ+K(;yBUuG`+EW|OAPT|>2|LhG{r%zN$L#F{88egIRPQ`Z za(B3e1?wHoDD1wF-uW}OmgtR3Z2weQsnNYg7~Un9R=|bCi%@Y@>{w?t z+gK5Ow`|$k-E+y5=?<^9ABiW^J8i5tRMJBq)3|;iEWD2^;bhv960~_;MUkER-76Kl z&GW2c^pH7kN}ranmAONtdj(o}o@n1?{S>GDjDvN#r`K}$>EOWq0{`OP@Z|izM&Am& zJQr2A^D2()sFJ3aG9@Ty&A!b~;>fo*?&hpFIlWu9-iXkXt(hPZe8e9Ncua-qp7P?n zbq;9?Kn{H7q5WbtO;;9OL27O!koc*Spj218x8z;mq?3gJ_1ul`{49f9o(a6-b36)d$(eJHNH|Am&reAQo^u~HfwD4`L9C2Bp z6nGYiZfs_rCndPI>!q93L+1{(2_i7ZS2aULDs3)VH^jMD&s>$5lxN=k_he))v9omK zK-$Kvex-8CEu>%ev9Cqm7ZU&ZDfRG&eD2M!S!ita1Fcu!O%7@O_b=&J?E=fN_P{XgXz50$!Vx-2oU0A^ z!bx|=cAGw!a|!AF{mAv&FcSN?``Cj=XvZOMt2e+=wtY&-rTX1G;^kYZ&lMTeV3Ev7hZr(X~U z3Tnnm^>b=nO)8@2viF(h_N=j!KbT!=-bl2Vb@t1Usb=r{LU4rq5es0>_%2@?X^oZI zy}e2PQfM=5(G+=wgh$P=@}rzOkvSubFBhuPvT3GaHWw`~`GPaOC2ug>u)gmS+T5Q_ zya$H)C<5c!2s$8f)T@TE#9~Yh?Rnx!s-G?_f>$LnB8XYlyIsw@(?+`kb1w;JSnbTw zmuV=F)XixMCukX*-5@;9v(m0sHX{kt_Zb}yD3(cfrtP0kOvJOHGv5hSs^CICHoD~q z@ty_Wa*27>;Uqs+!N)-E9mOS6@|e}f>8(yOm0xbl#Hck7xu>WO5T>85#OE@pxmV=Q zx@)fqw)u(cTFO*MFV_f9w->doy#g3`#I2ua{cno3YKhs_DnK_jdiX&(K`OowO#GY)qh zop0<;#@hp#*Lr{HBg^r*e{v0KwNKOMjjwTp2rg9X;WY!!G^SVL_vGuNi=}gHt<^Q+ ztt``8GqVyeS(&{I&$FCd)3tIoP0`c>I=YW&ou#T)j-{xH&c8o}ZWrWDelQ9noJ_=0 zP4P1Y)?di9Edk8=hm}@HGuV*ll78Sxfr}&TQQh*4Gn7j_l$(E{)!0T3uh27mT#b zNS69E(KPYL&Ebp2O%1e>4CN->KrSM|nc7WT_~hxY zt7}UNy`Y9=Z$i|MSH|M-PdhKa!Ms z9m$8Xr?lEcLKr3p8|q#4aeSv4_nNtvO60=w{prK|_eIM*??6B78Cykkk~P>E^>ld0 zEmvWR`5N?6fA>3{68D8-xW14*0Ku zI~~$J#UI*}v`TBS(1+o1t5uBF?c#j31~)p7@s;T7SKF>x6t5K8WG-*Cd}TRSuNer+!%tyVBxYw0`}~8bb!Y zi3@h%-y9M3h?{;F|3X3kZBx-2_aThaDE~3Wzd4^RjG^sLOoWoX6R%gqaeY7_PQLxL z*2+%ZxROJYJ{LCrnqahVRAIIaZQk?rW8Jfze(c#beCzceiz*LPAqi>cj)yMW#{Cs^aS1FQlpUH*Ph$(^^)>F=%g0oDIpl((kU-U+b=+BeX$#?uHUo z98JO(>VOJu-LFF)ZVR}ZG!-466;U@5W65dR<`4lyXuH+hg|6aQq_+~$WTyGcpczxh zpDy6-RIImm1r-t=J{fJW>MJP7?Vrr!B?+davDP3=aZ?GTrg?ITPG~7ghI^OzpEEx+ z=E}Sm7onVQYs}+GSz?Se3$zLDN4WF4^_R znO8mtJuos@-Z`uh%ZV5EQ4gC8q!RY+*wg?lNMsjvpaH?i0ZqSw0uMwFu)~HsC2)%VsnYB zG=DQ#ap2cLO83RCKbG?BOI~y>)+-!6v4y{!9pAnDcVUU8%jmM6Y`1x9VWPOhbeetL z>Pq)Px0@O{Pa87~ht&w2vDXP_39e1dorB%Q`n`b}SJs1F_W66N1~5*L%^mGJpE?z( zP*L55e3PnCRhMunkITG}iHEUs<2=G8iKkL_ykOGuR_|l9G@~M*tVJu!8Q9lmCW|cn z(>~pBt%9!Bp19>Y|DsHII75%8=x2DfnnaV8?J*=Q@Pj5(?ohyo@`GUni3T$$j$kX=$EQjddVTROYH!X}3K zj=cNi+3Srft`{X(W7eo(lYVv8af1>%?W30w$d$}R zt=VQhW20v7Ql^(~VUdcnYF~Df+(r(WXVab?r}X6`H;VUjOYXB>bgbo>fL?a1$XcVl z429B$lfP5;AJu|E+j&(7mGNCCX)i6UdOwOhs__#}Dy~8a;BTz<@^^$em-x>w|HmwJ zZ?l6UF54N~R9ESr-9XxnkmIWIGy0!sZa&SZ`zLTq`Nr^*tpJ!O_x#*@-=9fAS2-AM zesCdFiXTRVc>O;IZvQRF7(wNo`w5FT|+z^lRWgMUDRC$`}5yBxL z_JAK^2WeNIVnY8x_}>SDHtFvoDe2q4=Y3m2f%xdS`H`P7lP~uEf}0<}vxJ!82 zFIcfZ;~l8DiY$J;;E&TyK+UoJC#hat75sJHr(d8m!ll=ijvYqhmgNNYY@>Hrotgbn z`vQdWGKDB2+OGpLyf_nc2$c`D2ksLx(mXj zBC(iD_d}(4OtZ6aSoEc?wI)%K?*%|5R^~=R|8Ed%+(}BhP#N&F$D{EK_p#cSx|pra z82xb>ovD7)$@Z_e$;G$xX8na3Xo*kEy%kH=o}7%s>zIn1-L?IOX>IC%s=PuwK30xj zzYGc8e}6z&hgmC>t;8>opG{s80mE6Z(%uiZt#3nLH;b6I&7m_ek6VHAY%X%I= zsp=qq9^{G+;_>EH^LAg#ch;9F@OYyz8^K>kWn!Ac^~e%7!0o0P`$&*Vh9CbVF`pb; zU#VQ4k(A)S5LRq6yL(}+p=}sn>p*QQI||7apr{%UJyMSya4H(hAWHmYCNe04P=p?)U} z0KKlKYg-l7gz%xwzXaSRc}j3X$w30?$%FK-RyYRB3#*?Q;X^xmTNPJ;GR5r%WyJ0*^ttadZ^(; zB4($cxbDR(85jHd6c$_`o$C3n3NA-;K3DgCg((>1uV5vn4Ey%84-&(kLSpVDq6+R* z44;jlH7k40$HsV$>ybZsuQIXIInRBc`O&D|QNjU~*oCoX`O=X*$t*^#9^;C#FE*5J zcF5kl#kRJhO?D~hx7QXhJnOB61jcVG3D7&owT`=59&$meDnmi-e%NUT`FZ^H)K^H< zsJnJ4e3#Vwpr(NxROh2t-6~6x-U~N z!`ZodpK#$n+{YXz^0bH~qJCLuj{>TDqHi*G!J*qQltZWdTn`cK;bPX374XSLT77OE zh6^1lsd}bZH~fO3@A^;!_mndoiW-?N-Miy%W+cE6^G6!X=32@$*(tU&HKI32El5|g z$TV?Z4nW8ueC7?-!u7vpD__wSG91|WBJ~NA2P&&KWaE(Qhcl$~1CF_W9a;t?K1O}o znIt4G_*%Qv(Pn^xJ)fv3%XGK+va+7--#c~}I!#uXS9(%gY3vB1%g=4fGbb18dkfK9 znW<~hw1NgjO*nm05XFw29q12W!s=*kg6ru$r9c*C^&Bhg)k^%03r3i-s z-b}G?Np3S(3!7sHav*n$;(6lpA!O$e+`^gc_JT>z+q9>9Yy%^pc7f&!DDtPTpYpy$ z6L_PM%hrp)MJ{nIXD#}RzcHQvDPrcTR)3!`nOZe2W;fHADXuwS$36#o0eN2$0*saF1XtQiVd!3Q_2La`b@y`56Hz`gY)+>K&!>E9z^FduY*CR( z6H#{`%$4vc1?NU($mqn<f&t=9WE&iRH}Oj>W&gqnj+U{DJ=>cUaOXJ zHuy$>IV17x`>?D@la5=~CAHY7T<#4Fqm2EfUbZ6+1(wZyYqwDq#oR}vt||E~Qm*h6b4`cp{XumV zdY^a%1qF}v=CH0+n^v~mHM{GG8ZMbCjM$$OT`_GjyE$yTj;)o|zF;7&Ydo9Psjgkn zk=?uL^fE)cB**M#`S_Z$z>`;P{I8WGliXlObuLPUPNKi**y6gL#mZXqS-NhQi@xQ4 zj)uX%fWhSqtTloRsWZ@pmHAd(Mns*t$Ni#6a(rWhwU@eOJM(i6IGU0E0Ffl9F~6`K|A9M z(7P*Wx}NCOB+j~x?--M@!}<2B&&(~hNELgMRQE1DPaJ*nbKj4;4-C-NV<^t|APW&l z6LTFLx~Pq*Sai66SgW{t9&PN;+gxY8x1K+4VHuuFn-r3P$PKGH7of2A2a5(5sh%)i z_FBjyV4lt%UC_9)XqzSW=DH2iZKK1f4_v3;n@O?5Huo52b|rA5Wr$Xrtmh<WcaUt{~zKD|2N;g z8vN6<`QbmsF~^2$o<8#3JV(XFz4jBG|3>18OaVWstCa3n0@_z4%#^hJSi9 zLQ5k88!}s|cJDSYq?806;>j4mZ~Grp zzJ&`}njsYppkvgG5VA~%fWwR(NaQbM(e{EY4uKo_5HzEA_hH%#-*h4Lp8p*}1}%ID zEBLHQ&n7k(aX?3RGITl1zto zz*PubG)?yYjiFrVmzkC6$g%Xe+!M#kqLu4hjPkp$9l(54&7j^aZF!)8v`olTljNQ& z0&SAqwmd-b3hRTvcL;$)4skikzqwF@q%B_=k*f^Khq3~71P?Vv2=j-%cID!Fj~`_| z@(0xc18o12&{77iW$)6MwwmA55Y`Ww4O1zGH*+i~%g+o+Y3BAfG1`(P$-H8Q(qFjX zT{vEJ?wtriu|S9m5pfeU9=`Y7{puD-{HLG@9Q#-ear|F-CbLIPHX_7jF!zcO8`+cI#_~n7+J~Zzz+Kp>;a^Vx-}c88QD> zo*8NCNlJ$9g*s&q6Z&-h$j2M}mb>*vpqnqZ`j_Cc}IDVfscz6TVfo z?{MnjEQ|VVlP?(iOYIkOF7k3~Lm*I-q;hZHgC#T5!&$tf`O2sHgvxBaeIw@36lf1> zz1;@zbgq8jw7W7IW-tW`_X1Ff6`De*V^+oH5)lb3=)DG|kbuM(44^xgN++|31aM_hLhvaI#k3t)xyTtAGfM@@)A!K|V^P6XB!pe6WU4PsGR4 z3-R<5wLW`~-mCVZbEw8W=M(8rN?t~->oBkeh3u2GTv-~ux_?`5sr!l^MZ*A7P;Ly>H>v_3AJ`Al3>H-GVXoL>4A z=XY`0CKB;uegm^Sk5UODNp>}83`>oiN`$zjeHGk(0czAt}( zr?~%QcZ6qNaHMQ%XQd$z>O^WqHt=y^9O0iHgA?&Qp95n8)SGe=TuidVwJg@Th;$hS z-e&b7Blarit@q_s zn|apHkQCDw3TsdA)CYFF-$%^Owq>t(&nwl~BG-Sfum;!WNf6ue*)&Isbi0Bm6-bjT`6*TVO&D5?;o{Ri>$HD+7$mt_7(uX6 zN_8%@6W8%&4d> z+?J1;8(FCnuH4%$v1ZVjoyD;Rr(NPlz;^8Jv@Ga9e5d>NwN40oMqO^|cIK~<nMtQyrxOho1(AE}{D&qXKd!&qY}GpA(n7UPk5weHvh0=%GxRr$$@%B7pOIDW z^id_h`-Hp(OBPE?_p~R6>=?*~A2M;%*nCE${fVRAt^7{T0c&@@FkKs>J-U>^xc_Cx zwdATu$UiT#;Z3td1_4$&8$9O@v`Ls8#pU~rdcczEL?MY$8)*#^=9vtfzq%obvfsT} z;wCQJ)sKfg`qq||>N0$qu4#PL5ZnSiJGWW|485orrOCS9dsbd+kyKX`@I3r|4VmXS zH1iX36`&Y$PwjU)QXyc80wLI*0^HeG#K*Sq-IHaaUEhj}t||G*F=>4?pye?fxaXIy zVH`2roIU>bF6rSa0V|uDDINRj4<5nCD&uhv(gfBx6gG@k4{)n#X=w=rrvXrN8VXLv zth-{=$}domWL}&mjsJ~>&N-v7>%196vGG67{pNFjbymedrq?H!YOc*fny4inL#j}z=|YjZAbsonooN~U+VgDt=L;_mL+$dP(a z&osmm)nW*u7bl#GcPq8x?XM3$woAX-5>(WGfseFPztPWlR;Q7%R|Xft-Hh1-o`*ww zN0cQG=x5bUb9Yz4Fsjd$5#tQ6w(6g2HcIle*nTFtf5Yo!^F}st=UoOyG45-r5o!!r z@i}l|!geVQ${J^!ks#syNyiUwFMcO-PKIO~=Fn!xwWb)7&GfG9EV`KE&=t4SrOU6v z(#(3lZ;wxZ+jQrU;8bEfrKjUs)WgtjX&M4jJ1#QgKmA#p4JFNar!9xiaq-zce3hzO z;u%MYMWrKxdVf=qr<#J+oranu5$i8GG-T8-WVM=oQ}KR#U7Om~)Evit`)&sz;xr<~ z-sfliX?(q-&1P$a-@~Rl8b(K}!>vwp>u^~q9!*hDMEgyI*Qy&$gpYtR9^0uQy@V^~ z21Pf;^VG((NpaZ2%+xj9fcckx5R8ij)RsHW;@7T5NqV>oP_IhEPLGP3b>;m+(KWTE zQ)#*e^;RvX9d6nu(dNW)a^_=+M)~$Jfgzd&1j8{Ms=d))%x5kbUfmHQGP|fRuhenS z88R29V4<)Jf*kE#wlu{+lv}`Xj4<&wycZ3!1p-ZjH6_d-@Iyp0?H+ap@x>OY8G?tF z?n3SziyAea40U<>JlCoIBh{BAza7}PzKZlhZnR#)auEXutL;k6usXz5$Tr9hV3(8# zdoA}kitov{Ig?VG;V8E#STUxoibq=o1GrkTtjXqw11KVL!p)ot-QC?Y#kNpAJ~Q(? z+p1p#@n=j}eTg^cNBO7jNeHLs`dmC5C~7fWi!P~DItd#m2XahSzuxf*XhuV#Utccn zxC&($A&E&ze;T;tZ_~vet)(;amYbxFwbc5H)p)}#^FlUFI)w=Nw_yKN!NxVwxWRjx z!ytln`NK)`mq#m{2AR+pnx~2bs!mHVCU18Ib?e(*i`8$7agLxtLBGfoJ@>8+ogxWw zgb5kYpZs&zAK9}k9MYCFG9YZ()Ya_-190AnI46(?PKcxjvO9M)U%KpH}^SIv%0aT{6*ugdJthrpg^v93UWL5jGks+x{{U@ zK9Xu~b`gL%&}XVIJ1txFNscP@zzS!WX1;T`z__#8sbS6br;3W=+4+_ZB}uSsE*DgA zlzJgVIDNYy?m9=m-~-TW&4kN32&ukDg)Dj%ab?2lVvJB9Zdhca?Ig@DtH!AG5-Z!g z@e_w|zEciEv6at$d6Pk`IQc`hifEQfjzVu-!%{zrgt`gs;eOdMVTfh6ZvOb`klrP@Do!>5;gmG_@uoh1UDOL)uRN0bQpo#vT48 zasJKQ{###sobwk*-17iw7($NUQsw`|bE~=^`~^JepWXaLbN>dR^lyKm;@?sJ*ApJr1XpgiA}EIWe%+6K*z1P8#%voLk^ep8=kQN*8ZwVg{i2gk|B^E& zT>U9*{>U%;^WlH`_U|8*<%hBWcvH5IS5&_q{eHgx=hz+2kO*fe)IbMahOclqLu6c_ z(b;06LTjEw$Pw)B5(!flp)m6ljL47n6tC5P2hGcNKTUPGphG?O2_Q?*BZ16;UNW;42_I>AQIXV^qok-p*BP<3Y;xMsY)Yv8IWvvrd#>_sXZ*80bbQS z5p32*ht@)Y;YhMH^tlW{uEBK&2IBnnG-HCB30kuPKD}1M$#=C@q{0RKi zQgCipN9=&t=t8=OAYU2jN2qUVD*BQ^MdO~^Pe7OSfP>3&pwy`{^0UgpYb5Jl``ln5 zUXcL+y;2DI-)hpap{&ekr_m3hn(=?MeR`5!qNfHh`8_2SRt3+w9lFSrY|F_chV?{y z@w9>DSm|(7LGgkxHSwH@%>uh8GR=8Auk;Ly1S5c-$_$pooOM1$rQ8f~t{2rElsPt3 z_4dA{**2nW6fnTvw!F`2sWl@H*WLNQvKyX?V@vzX^dibcMEmn+&V3DXx*5_P*ASjx ztZSJ@jtv(KXOV0`HqTHHpQv^OA^=JuT|sX3kAzCE-Mz2{wm|%Ntl;b<71=yjfIn|}(ayShK-Y%nyAy#{mPUmxk-!siK z*G?1SR2vHosO*UPcco z9j{*RuJgtyFU`8y1;PpC($$U0xl7X#e;RPCAYNrC8qEN%xH9+~aLDEeCm!WA`o_u? zQY?gAzdjJ=aozy0?h*et2sp@4AhwH@2@RY&IPK%%7FJ8!t55_$X$q=HKlDC*Dro7@ z=G5*4Cz9g7cxnW==@~+f-9|!SGuD#Hr7>~j8#W}QwXlLz0%X5eqOUbHN7^)l($lW_ zA8 z;4N(z$`ImwLzyv%&k<>0iV$I!y)zG~zzPf-^GHMK9NR!)&zZ;mAjb3)!|8M|_s!FS`V5VDXVQA?w1S zKZTLR>Jpm_Nw(=^K36`#X0G-AEW%CV@V+Xx1PBj|yFcp4i`dFiByYzBk{=DxtOEUC zC`#iSV%dqu(f%zfN5m99&=)WwOz7F*?svaZz9=)z@@ZRgbuYn~v9$9G`piMVFD|%8l#P78H>mZs5GNacQIex8#hcv=Zh|E;=&N zp(gR+5@bX4ASojVWA5U1xucc%m?qh-G66$FOPgwUP{?ERn;wsZw`z}qr5wi>5MyRi zDBQ;eI0YaHh}`EoSlTK&?0-?GCe$N?-RA_98FA0g3 z)HzamTwm^6^>ENw8Ks?~E0eIRzeGUyi-(4+#kAG<(xH&jJ*MM~G7uv{``USN{NNqV zNcSBe&4^uJy33?Mle(COu^7}Ywv{jRAXI@^a<|kRXM%E!ro@4E>d?Sh#C9?q_fG@n zS*la~Oz^OWeusOc^JFUF)t&mUe&wIi$?X&-e%(lN(36l(>92OJDrg6!>XHZZ2VE|&KtAvuH38smku2~LE#4uO) zs(NdjoRqrG>H#AJAHr(v?6dOAlqEa;1niR_ZNt?eKQNU(^36mP*Qb&ylAa^Zjuit# zC#4E}F=+vMc)%8A@_jT=GtHhq`5cik&Zlkm{K2Alb|sX^DOcRpYx__%+d|RSFRFdP zfTev!e0`q0b82|CtbdWtW5R&g$aY_SuZht`>*(>MX&P#E$V;$mT!jo2mHRO&`evRf zzGwPd&$Lo2g~@iinHGDD^ZCS)Df^$$_^ak6=-py1KYIbvv6${N=Jj{IkY_TH-k_i0 zOjlNs67*&mJXjWdzpS{%ZpvoXZ0svsZXAi`Ea)I_qNx~K1yQS`4RB0o^QUR6NcgC5 z#<@ZF^$esD5Jrrk6ACX8T8HJ;i*q}x;@sPbS6EX1U8@T9`K7id`?_~Z`B)j=m1j3? zx${dOuM6dtk*#QI?E|3bgBSnEclvYLWxm3H{#dDxTC3pVQJrsG>9}2txyz_gJ}DcE zBMq;QDa*{R)d(onjcYYo3-@)WkS3(gc()xk8>QP_38ebliM()Eu&mc;j0juBz}eu2U<+=@mHN2ffKw?@HYZ@omx@@u`1 z1eHgL?h&qCs>%`xN)<}pt==j?$>^wv3udXrC?kUFYcIIMs~k9C}ko(9~p4j`G?~XYZEr5r(u=P(_hk zaw5)tw0*5m_uLan^oB%t$WJkz`QPQAbYvWSIRNLo*&@48pB$)-#r z1^aQjWNImDO9?_$hUH+#8B4sW=}}Sc+#7E9&;u1kSgnGrmIIXE8_)O>Ny<}62x>~A z&9>j|t%0-!Kqr`(XIDvy*S?&?%*w2laF;g?Q;;Nx*KC~3 z7__=*y?IjOB(jM>=wF%~C?o$Iz8~NFC(0bPi(0aW!1WoV3)a9((E^r*AL|OhE5G=U z2o`P@REE+Ho@y7RzJpbQ`JVpn2R}C64k2pPY9;p5eT|+S>SVyino}%PC^g1I_wyhY z_K_p}RnSc6t2uowuQtvv-}L=W!A;H>rIf35I)sWm;gBAASA>4gZb(n1BE9FIVnwsH^G>MDZ`$ zn&IT`rDM$KsQlwcsHtutgP4Sb9~Gxns_z_9;(PPYATRe~=!5@>A^2as^Dp@{khVV+ z;{PIT_uOE-FYO85N91_>P}x)ZM>kgZ?)0JD4TIV)4uMJ19D&_HbgV+%P(ID+@FM;%GIOKlo7cA zD$$pRNsN~b^fVc>B6l-$oztN42Z3MaJ-Mk%dtLdEah)q$tKjsH>m5Etdgi66C;_#6wPe2hm>&84weS}oeN2NbDR9I- zn&7LqgsBhYkzHUCn(oRvr4eUgA_`Nt0vJytS)NFtI%I*qyD)fH8u=LYr-19gXY+v; zSoyZ~BMAGhdKYnW0-yLE8D^>^-N~Aon$o+ws{AE>8vaga?XM4bj4w?#Uh_1wKk`|I zU*v_bH?c2Q*JnMp%%N1k(A!%hj=e>4ed?kMpfB{>PDrTN+yCkL%?9P3+`jcB-d>?y zuVG>pa1;H(8r7nYEzmnxVH?A2ie>4{=eVf$r%J=5V1|Jpg_If0%o+0LCF335zE7i) zyS{_zxPKwcH9WJJV`h}={0IA{D`QQsEg`XRqOjF}s(Q_p;k*XumPHf^z(QVu6}<73F0Sh-;la3xcgpCSNkBmUtO#y(cv)DHfUwKuG295M5u7eg6F;iM; zy*?v91rEaPSTXFPZi&5RnsxyJY41d8CVByRZdWuS#pxsUuc~;w4e|R`!f+N`C*Su7 z=au7TVYRt!KCg0dx`=kbMqh1EX?GX#qS`Lohn?vv)yr2BGCJ?}Fa13W#=|^#g|)!5 z>&3Ymz$!{A-`aE$1=)4~R|vhg08O@4;RU6?lLAHZh&^MUoFR*H8X%_)v3(c@^v$BA z?6=re?7B~rlg+ZynC2Kn#Jw#YXO<;P7PEC=eF@2SZVWD#DI?zmUjzIkm|h~V56YVT zPr>J%Ci>8@w{x~DCbN4V3*KbUnAO}jOf)<^0&geIO(P*F|g>N~7rzy6}vID+(j9Rw1F^BJ6hsC@@#d=hP8Yy=l zwI!nQUIGL7owN%00Loq`p91rCJ^;}Ubo_h7+YIf^Ga_6%*9PLGX&q#)wPqKSl&RFT zoHwexX}!+d%J23|Lw5J>M>@-YfUl_irYw7>bH7jwSF6%-wj?c|o!XjSqf6=6a-2y{ zw`5rCYbr_E^}3bZvQ3yL@6=ADzHWq2IQ}E`iT_9&evbaw#JFqqp&c&LJRCCv0~v>Q zH@$e}r3Vwvg$&|4*#yHXCWRg-)Ky%_)y2#cOj$+jGLh6|eM|M~@FW|{iy`+)Ya_;Kp5MqNj` z7ZuhM+=~uu^YZMddo{1i@8xU=%rG$-aZZJ=wON?A#OVqMt2ZC=<*VWI|AV`qv6j+X zK+J<2FcjS{t{2_7=z2K;Gm{T>ok`qN%3X0uuG^~ZWdLL)?r&WmC1G|l-Zza7S%sCO z->y*->dyP--3JC-W-Vc2l@mcUU0d4jKmQ78OxWu3)L0UWT*ZYZ@B<#4y)!|n~snjslKLulIB2lffGqc*KvrtAV9!d*K8ow_Mg;O zcdkW_mrlBx^!Ds7q7X{@E+Xp{GEoHz?A?Zv0?=pXSJ2g~=7)Z;!6;RSJzciQR)!w0 zd$C5#&dQ9H&df5Ecd~n}#&mgDW&u9H1a;bod?s{z3B&!c4RrQ-`6TwMH!FTWTj_T^wXSH2zO zz4LZv>Prcy+rD*c=MSrymbp_m)%fP(*&*+@_|__lYR)Zc7kUX|&u(g~nh67BFkv}F zdEr>Kkq{z?l1SyMrxm`)BYQi9mvg!uLd!hQKQYus)M+mUd%Rh6OcJT&aO!z19l0j< z9pZi8s97LVc_~?NF3XxX&rsaFBk3_%v`Y+S<#s%~&OxnV7Ls)Q#*mu+qFnS^v+Sar z?lvhbHxxvdEVj`zww^tY(+rYWOG#@`zI8gB7${uL@YTdEF_E^Q%2NPWPYJ!Lz^Fbq ze#}JD^-UQYR}O!=2cJTxshRG$_xe{~Bv-gFP-bahTx_Cr{iq|7*nknbo{)4dr$q;oc|9km-}bK zuO&Di@z#?!O@it9rseYM$s2L}di--RBj+i1`TTTz?z8D<28-v?i}Fp#5xa(fUoQ7U zn75wXvE2XPdF~6!`#Yw<=3=&2U=_I&3T!@axtP}%kbi~#eaU-O{@EoL-n`o>O$a^5 z9$&1SMWQ)>|LOCX?>XO!3z4%;_mT9QjM;wf@g{zD^1r0;=K>Q$awcg%!c*N+etMke zqU<~mIWN$lR$kb%8wk;R5_kDvId|gX2~W=%&U>Byr}MW{{i=8hq%jB&u zXG?B!FfTNHr`*>DyB2!ZUgj>hbN|Ax%aJqPEt5AtcUiZTo0fl`AJ?Ddoy)oZ-?^Ng zL$9fp^Y4e-6CFrsbsBfu4&H~n_gTy2d@$U9Isg7xwx;JiXGJY@+jiT!d5dk`vduEL z@33fgHLDh_p0i}mzf(C&<^DaFbL2Vy9%?<>Nw*CR<~}()hjac7ber`Pb}p3nIybE6 zod4V;|C}p#xzL0h{Je>`@sKX-Zi9rOBp$D=`ipiay@C+78q9PiGTgqO*`ygo?<_94&C5kHjw)%nLH z-|puqxOo((lS<^TWy07*qoM6N<$f<7wtX8-^I literal 0 HcmV?d00001 diff --git a/windows/src/desktop/help/desktop_images/win11-uninstall.png b/windows/src/desktop/help/desktop_images/win11-uninstall.png new file mode 100644 index 0000000000000000000000000000000000000000..a0611a8b3943049023b611b64fe90e2d69f273eb GIT binary patch literal 9075 zcmbVyXH-*Lv~Cn53R2FIfTDsPk5UCxiWCVVA}#dL0)!$8i4c_D0-^yy0Yw1?=`9H* zfYi_oh$ux`=pB^aAp{c2+i~3Y-gsl&H|~4)2kfk|_Fj9gwdb7co8Px04E67GaR_pN zKp-w{Ee&H3h|L!G9e?6D@c-v4%W~jy#KZW$I;imH`FWsm?9mFraVMOJ6cyFT`)+hc&Gl8M zv1q}>vIU7%v+2Eyi!!+y7b7m-u>QWaKFuU=&E>>di`)bs&uw z6MwFMUG_Z!G}XSIU`-Fidp%Tub=7$NpLZ7v?uR_}^YiA zP*|d@*oT|S)30wX6;zCs6gxH_*ImdBSgq~9T~Ty|?RcfrYW-ndK=kD71^zL&AF)@w z_i6p?Xx419A}8WBs=b#VH@rBx%~&K2-=>e8mMlQ>oq@zN=&Qn#gfz9?r18CPX89KO zxyjy%HolWM=CPZr<-=}yBW?$KyYhiM?sQ+)ScJ>ISFZbQJQyyre;##S@rH%*;nF~H z`#|+51w}dRD6q)*ny$m z{PB>D39NB;cDDC`eK^>INuvFAe+ZXg?beFRfOE3WhD9vz z49T6kZ)RpDQ*pn*tY*k_{voEo#`SQ=9k*}O!@fE=K1TXri^WtW9gz2In2Xh`15*QA z-6c-tbFMJV@GDGHPPXo7>00|?85mRN!a25sL=R17*{#k$X%Wk{nEjc&ypTuYj$c~; zdfH%MWwd%ZV4Rr}b>@15Ywnw^wMHJqaJefT6`IStJ%lEKJ!V1&-(0#o;)p1J>=L8O zz#~Xk1BqH<&VIQm{zNnf-it#UH73wkfKW(M_9;Zv`>sz3hF=7e%?hllg7Q}5L(>^g zn@lWvHlq@flP`?0g85CP6_;())5M`e8>R`uXIYC`pGT_bC?t>d9n9sHdk+{qZjXZ& z_nKQj$_qdEs=7~FThKa~62vFFO=rSa#5Wd>5@Z>qZ;=;i6n`vc9hQAKfVg~f5R z)~=SYr=hjE;fdX1A3T?b%mT)ztsu8gHwR_ zQWH3ViKxk<FJu(rz6mYHHT4;MB6aJ?j*Z#Aa5HhBWvBeW?tj)OpL!&$6LVC<-kqi=djB?#Ad#XeB9FaidY-4 znHevqy;R6D!55cjZDnS~mmdS0FZA^jG*oXt(o52vY-N|gT@wCg`jNvtg8E3u-nAzO zRwy=gFxQoN|98mQ)mnNAELlm;;+-mLVJA~~|NCto7=6?y_1xNBhzuCY#s3dS4C)2} z`#1wZa~d8uJPDX}o`|X?*tsbh*&OJ?hp}n>+p1OqPH7{Ht2b@9jK|lR>U`yB>o`cN|(2g+}0a(>1pcrOt2BM+ZnqD7q&n3L%1h z!v)XVP9VB++)?`G(Q$j0s6U@`X~Vq0>i0Hj`lYbmui^0n@87Ffv6qC^gyRRDo4p~I z5@RrefMFq9g7S|)oWn7Gh@_Uf^yF0et^RsvP>$SRFn3GNVXoGXN2_dnw_6^Qs@rj> zXjh#5bXsySDIQ&mbrU_C93DM6Uk5FR9w^kUN7-GACG<}Is%NwxcyBc&3>z3cMi*Eu zoi$dyTp72#3*HX*F&oTChP9PEb#S@X*kURs0atDs2reGp7%=7Md}4<3UO3nDc)yx- zJVv!r_6v^T<1)ZX*>=IA7W2C6iXJlq=P)&s4R4R%W40m>9Hi@ZGNR7j{N{DRk1?M+ zx-HIM>fF_UU4MnF+%1YYEkZ8lphoJ(MCAq{J-hDpWoe&guEzwQmmIxIfgu~vHAm}% z!PkQRI-GdUL;f;*Hwkma3!ito@rMs|-naTQy70niNj4{f7*cgafiI(__i>Trc8<#f zsA6?;@@Q6P3kOF5RYCbRY!ZuHV4o4UUD$)ORZC-qCzGvesG z&=~AfE!ls{iH1oE81Hw-y_6YqnWJPG$gwGQUtNuK-C{^t-q)C8H7kZ3J;>!?Up_O@`*BrLmGKZbFXwCS{2DRdNbM!QXE zs^p*1yL`fy@+&MU#vL%J^PE*C4@!+2u;*ejUx?|~H5lhKZNBGGs!=WtK@!BnAOurU zJRSvE;mo8+#beMObwLPl73#Q0_+euxSt!D^8)!0vE3&rF%EzDYHZlq zBq3p_R=3r4awDqmfzOg>q};DSN#~}cy^76cMN^>}kypcro??%|&rzn5CfQ39?9wbF zI2IhUuZEc9=naZ!5Qy2&1|uUQt%D^DV5jc2|An4H9x@WpR-(iQ7koyZXg7|PIJGWC zN)#5Q?k_zmcWULY{vD5q*+j zlS#SjHQ8Ys3lyoKHL~_&-D?~n*5X4XnArU{x<{73GtyzRFx9-1ByqxCDiA`zShxcg zg3^w3eaDS6`YNpD#OpG$#0N0JOoYI<(&XI8USMOf+}%`@yYBeylt4Kjc?(gtFbub8 z(p&7E)oX`jx((%Cgb3#t+n5PZt=7b&udK&*8%sokiOJd3u8x*BtvBqAvkyF6jw|@wD0qNXN3^s33oowJ5Mz^sA=p zQnVl!qMn{9^!(vd&$JN;#nP+C;oNj7=njQv8>RW-rTY7ZzO*~fN7RHqKWyjdyLbU` z>!9K%j*_wvL`0578Y<~pJ_lu;F*1eHc9TjzENTLt09xxv0Ng; z2KKtmeWKxl1w~3ITl^b5nr^xsESgs~L8I3nj_SZE;!(KVfvga(r>_F57b>H+pHFWV zbgZ`*^xB78em=kNZ7%tsseG^mm7Tmz(OWEWAbZ(dZzl)seDGq7f&oT?-Ip zC8V!?m@#tihR-Vle0YqS4qLJa@f{bZ-s$KimyXAJz@+g7~xFC8)=U>4rvCja$iuYq<@hh)s%Fw z=Z(iCs?X&@sPfj$uMaEIHoFf0ZD4jezPqj2p$#3t^K7m$f&7&wr91~sv*zVQ80e+0 zghh|@bD9-0;MUfP^9?!z2)|-lc#~9M{lKS3?N+6w5hf|6;X*+TZe*GQB_$(WCmdUb zsE1F?XBgHC9;^l*4%>hXKFHXoM*Cxv{5EH*T&a4-0j= z^8V>kWLJQ7y8i6n^{EeHENlJC+EbzR8w0iew5P4W?y`^xq`nOCZ^(xog=eY^pXpW< z|DZ7=VGN^6u0?1Cp-1R5^fu#V`$xG(Y(W17@9|Nep?5vG#{we4hCZX_+V}SM?6jM} z*-B>^m_c|%1nI%N&uQxMKJ&As!a)JT+YOx3m7EiD=)PmR+nr5CLthz4NIyF3&Aa@X zypqB&#@1{`y2`Zli=I1|j07lxLZ+T7uzB~xeR}mmxUD(d%X3`XT+Pr+ul3#~lU6y> za*(efsP`%CU^Ua#^BL52NLFFj6(Y%}U#c3Y;+ogrlh9ITaBcIBMNCimP(t6Lg_3Yr z`DqdT{1MVm3W+n-V@D+;cCI#0{OuUVKgQV@Cw35&u}S4EhS!F}_cx)5c>0V;YM=v9 zv3YE!jK@TuE)1LB&nUXWQE>=Vb|dmbF$Xp-Q21?4`o5yrYbO04AX@~P=23J#kL-L2 zq7Sl5m<_}a+%wP2mu!7`ZM-PnRKCgA&Bw8afxom3fE$>K8Nu&u z4M|&9b}l!X2lIWR{xYHT9&+RAztc_qj7}Ij_c3UT{nZb1Q&OH|vuzf)tXQS5TUA5E zL5@`+Z^6|NBb_a5SHc;8%)&lVyRkZKT*Uw2oPwJt2?k%a$E~id`r&`r0|+H1+8 znz>T#+LdvyP}{*K{nb6W@osRv)IeivMJai8ow!<}E(bf=nR?H6 z?_PDekyBd_E-@O8i!|dIAH#lq-5`N7;F*<(zwhx$lTzb~76X)Z* z5l5Jsa_~W1sO=+Wi7w4g**+7~SrEvkID4wV$GQ3Co>LdIJq+PG5M?7LZJ0K^_dRi} zk-=oHO-sBI#NP#s^e>>{LK7vzl6Xlzx4Dh}Y%?>LUX)Kh=XYCAxfK*!^e{Kz`a%H_C*BHNL<&2#k-wJkJMr z<>h#ZBzqeFRunVbQN1;vwI42F{U|kPTeUYDze9|bVt7%6Ry^P~GO#Sm&kt8JY)A!( za^HBK#ywzK%X@&Jnjo zN#(S>R}T^Xen+5${L=i2-hN-$sk2k!<~IN|D#G>GHsYbudKoB z1oQrGXLpsyYzyyIAm79&Er`+&`Zq{eT3!G=Yl@ypuTBhQ^tT#h?9XG;yf`vn2}pZ4 z7c5*C`Q#po9>e?H)|@>f{1ce(!YeE01!*c5`6V33<%Q%6mcTjCT8(dhWqVt=5A@5* zvhVxUfX)XJ&wS$vD~5KL2?I6ST`-07)(hvf_nxx-y8T$-K5mGt+#j+=Hl1(@XoJrG zdyIu(K`+lxSkwUMAI&uuF&OgluNqeKqQn2}Zv4Rp45)5t`7tgbp?Xme2I7VZF4&l8 zYKA#I%&F&&dwk1}#br8o@|6SVcLZ$ad(_=|16lL_{rk#}@T;)IO5S*>y=dT;TfgRx z<`orn3M#lQNfippp)}@>azCk?ic3yjRz1CS*61AEUDXeWujg5ql2@-@T@w)znXKpl zrA=gaoCq?TyLnlMWmV@zf}cG(iT8-J@7CG$4vwIQ8WAiORq#AlR{B+(DH)Wdo(f^K zbv|l-h9{!pjn|AhR65@Qv%+5^?_Bacf!*hnoyfVJhG_=sS!=DjE|dPTeMUqt}vIh=Y48a zkp7v2z^dX~soHe_T}L?&%9?p4UFXi`N`2oDpBhUrVl6J7hp)z}!tFxa5j9jfz_B4I z5)fG|i|777S3N|@VQ*($-tJ3?9k<$Db=IR=6T7pi71_E8Rsg9PdL`hTvUh%gMR7wI z_XR;og5qtn=5>J9!~`+6PR@u&r+aih@!y*JOA>4-{O5DVii(OBy}f+@xJSTZKPeTF zs{bh2D;_9?uYS16JfPQOI#ikYUh~~e02JZ>-%kycy1b*Uj4FQ^z8gneT6#QO`NXm4 zUqMAh#qx*kiGXoIf1F0kVU+B(!lGGyD#ie?{+JR9ri=&;t@N5By=@@bg`q5^P9QGe*c}aOwJiN@)z6%fgAYZC;iO64OE;LTl2I)!ZxOQv9Q`Yq?-Zo-c>oc zh_+9PJ+7DOV*#Y(;Dg=rz}=^dgP(H|S5;rQAV6NiESFE?=*cyx0CJ2Ox7(8*n8M&9 zseWZ893Zv&;hzI}jZ^cRg#z02q5GZ+P~SB>M76i8ldx1lssbjZIyDlyH_6jSFv)G< zNK_w)y`no3^uLA$R@mo`?lax=`;+(WB~uuIwTA~xiZvJ~L+gp1X-_hjx7h+-%RiEr z5}L?(&n2u{v!Mcn312?gu>a5JBBrPHmZ8o2#}}q!p!!M;c7dtaX!RU1fdh3x%n4gu z^abVPbmPsA;Der@x28`C)OiYV;6MxPtT0rQ<0~BtSiRl<$sJ&^Dc!Z#04T7}ogloV z4m%yZ(S1UH)zs5(<#1F5q4rBj1X2Ny*UaN;aX04Ec5Ee zJCW+~5i-E7j=p@s;!}eFI@!CCOQDj+@enAV7Uk7C`Jk^Z~)mkieQ9*Es-v9ADNSKir4)#6#I2=%m}y6t7jtAZnJ zYNLN#3ePTUx}Tk3i>esMDnCboXuL44BS2p0d6Oh~^d&c|0enHB|36LTzpX|9@A|(8 zHX0AUgdCB_?(Nude@Xu%<-GZ_*#X7e4d>>X+3ws37slTDkY|;u8k8S=ctEjUwk&qI z=Ptdw2{NoEa@htmhsPiH65(+@05kBo<-HhXhzR(@$nC4W%h^H9ZA$Ha=JfohbFpke zFc9bq<@&?6Gs=tkpF^wm6}%Rx?;C|e$YR-%{b5|^mip@t%94O+n5(Pm+V?~h**&|x z`|BO${IB$Ip^_`Q@y7s>Y{JlUtLr{H9YAJcU{Yx4(InL%a#A6ALA+uKjbtWP!*V}; z`ZOU_!+*`{7#(mXo6M_$sFvJFYInA-&1vW;eb3*3-aCt4pK5X&Hb(~a(%9a4FAoil zRC$bUtYJOt9h;;3DlkYNB=hyS_e|SIx3eMSyl+7@d(eezi1JE=QQSf!4>bSrjL*UD zEIWET$GDnkRX3PXPW$cCK07l*)}{w-YAi3(`jI}Zp0C|M-;<*R%zS(#PB*tH(_?PH zM9QS=Yqtqb_K9>Df6zN^S7B^m<-%+N+H$Atgu>~X^>61SQeRz74)CoFem$Px7F6vH z1?s;Aeq07X|25e3j0A4BZ_`M@wKw#xWGuT?{pMfn&VJ=ORQjW^j)AHO1azF0R<~== z*-swghxMdgprWbt8#VtfDC_(t2q3iFlwlW?JXzV4ynKWT69Y@bGx${b&Qt4r=_{l1 zPo^{~^zF0ENw(WtJqE*omyeWJjQQsw1PmzmW8k+wUUv*Da~4 zirs3>W^_`&qwJq0@oF?gN?BIAr*mr0rUVow8EEvMShy+8_`BRBSAPYI#nx5J8#^QF zv9+8QKlD;m1}~TlyL8jkH)qr#Gbt$Rbh$`+Kucp1v$i;aDZig~hOQZtksH zdyP~+lf^PjauScRfVKNGWuN5CJ~VVFwJ%)a#gHcK0BYiH4AtP0CF@QV*c2gjUsdz&haY5}Eq?%P}h5qdI%?K$uR5N=lXj z2m$O7FsD1GFdlgGgRXQ?F|!?m?Gf_(^;h*$No!~}(>PDq<{cm#wo27*8Iv!AXlhXm zUxH4icywqyQC7NkD;7~0nGkB}YmB=(_DRvhdWXfxF26U?e{v^VT=gry0Wwxm*Sp@_ z7alFJA|3a#4zd7jlU!$Se7zm>N{TFM@wwswQ*nQ*=Xn66{i+in#;ur9RW2?KIL3Iv zB<{gPX?HHnFfp!UHG`u^K_TS3_L_&Q^f(dtN9vxzvwY&i5T-8dRM2>l22xT1-6vW3 z$eU_(e{iS_J?b|{)+z`AQC;PPtQ!v*44A^`uh6#*?-;5Co>-zG@gG9%(|UUUA!PB({MeS~%#*v_UX{y5>NujDxX0mcN3eGV$5{>`RK*^^L07BlaGW8=imAES zPTH?cP~4Q^pQ06K&h%3a;gUva-FD|VL>GLCSYr-F|JRtSy#a@eM z=xMo0U{!L9yz|jRrTlOCeCVT7S$B$;l#(x8ln6~8< zrK+6q2kvr3OQlddI6=nQk>>ANClS&pMkaURH5qkpj&{oM@$-{AP6c^u;8<8iFiSH^ z&=Sa1adB~AU637;@p_;Poxp5+`8NyZ$!alXtYF-m8yon*BU)OiT#VP9X@W*()i1`^ z0btE#<>yz0R?otG1}Kgtx6Ffw=hQ~Ci?YQN7|$3TVfecVS$s8H&wh&lQ)TG%-ylyo z;Qb~#vvn%3;tD*$Qcyt$I5SV)cfL_uOh#Eb2@}p*m5b5hkFX)!!VOdK!DiiY4zb5_ zn6XctfVIGRm7TV%I)@)tC(V>AHTCuN-GNjLAg7b;ebnRMY0_mAGKJb~?L&L=D+O=Dq zSAW-g+`rgfyY{?L^O4dsKa1@-!g}-P4VU}kydh#7#2U)`%8O4P`qTQkK;J!mlz4HC zUMZi5Lgzlk93Roj)Av3?PX$%qkr3QW;t3@ovhc?v8ZM^UP^EmzDSe48gD<3b8y?TY z9gYXG`&KX^IdF%`n#~g5(i+UO2U`3yn=|=!$%kV6rVriY@{QJXEWaLOCem}kKMyuP zYMB@o82TGLo3c+`MU{SNJzGVTiZm6JL+^jc%`eiMK2OPP^vAp{;Iv^!Wo4a}4i2R? zyOwz?jJq#hW}#b=U<4J1^@K-FTTV&-%pfqdE@si?hxKw zbuemhcIFz#gmO6YuW;@d>Tkty-Z<+~8n;35YFM^Rx(_-FET2^5-r0;vvvfZdoqBrk zl)dbJK%*(9=-cdv0mTs*Tc3|1$t=uyZnpz1)U1fZezSaIf=0G{To-r2RLwVZmG@P5 zX882NM?`n$h28!9^m4NJW36k;azkQz(<$r?=NvESv-m~S-~*>;|l9~zRxRW1i-MyR$Eu#b?k48EP$as1HB zo+ioDo%CGO3of&uKbQ%aDO{CUc6UpE(qE^^HwX-NVx;Ev!!!%3L9 z(CF=-w=|UE&>8164vWqilHx%d>OZ<83@{s63S@YlC!Z(XUmtK%#h05k!~e`q96T;7 z9}?mqtztHGa+vJiLC8Q!?^h_U&G8Yu9Gy82nl~(!sncSuFt!0_%R#Cx%`$|om1An3 zKKgsjfCr|yabYcZ+oJ#6uvN;l();^1pY;9|HPzL%p-gi8&$o?j-Q1jAH`%XiC~0&@7Z^{?la>4qJTxUS`R3qTfVoM1MM%S`Badi{J;``Fjb_5C(1& z9e%XW{`-+M38->ZplI;JVa&Wm4^;nu*Yhu;)*a!k1UYx^jEJ?gw7BieHk?CM0pZHD?oH$@D=I1< zDtr3%r0tW4O?P8oO3_L1Rm(b$JTUt`>`k9kq8e4{OnzJzTx}xYxYIHi{(z145#t5v@Sq6Ga2A+7}`vY@1@{ERuf>~k!C9W3FvVmqZDUH6G zTCouENfvqk>NesSVB)Kpl-MB&(q3yP%^KRkVrWq7f0J+;`*I<1c(okc59h<=h#*Pn zuek5r;eL{Ak66FjL)FO7F@k^4cRaUqfo++TTwnPrg%;=`BR`D{AF$`ZqPgRvF~-0! zZH4z!$o6AQuUdZpBklup3GjHY)hBzQTgQj8z)-pOWhJ!)8*C5mFZi<`HL56Vd3Juo z{7y?zKdTo&WM4`?A;s1E)5FF`PfD|U8XUyyFA`pqXC)nkl^vxUY}M&@&!f&iXar2Wr}%XNy!+qgf04;GW;u7oPHB-hH_Ts#e9xobFU7IWA* zO0RV}k*~DA*ATam3Gw$Z4uivYsJ!}vvM9a|&99#>jVe~qKDJi*mXW^HvO1zzok{Ed znh_XHbS8ts7$F1O=Uw_Wx1jAg}lMa~mY7TiACqIqbShy{GSh(>2AU!&hJw_Ji z9zp_PGV+-#SSuegzhlVnDopC9U--A5Oad#F9XXuio8E!qRM4+$>}AXwvxl*ESLZFb z%0H}y?_2x)*q_1EFT;J`Z-Jt*Z-T*!WM+D@*)2w=%KIGA9T;Dy07~#X0yk4;AA8&Dj!qU!X1$GR za8kI#NVVTSe`5Dn9<^H9=Rb^F?P8rOFmt!ak00fW*nvXBz!jrqc)9pS8;2CXZ{4Hb zN)=dCl8=z%3*MR?0`VzKH3h!+y0luOAvw+$*lF5TFp{qeyu`F!2KkOZH^K&k%Yra- zlR>)y3+Qv9=Dkp)3w$uJtvc;WjU+|r$u5sQSA4aD9L)|WWm*}Ig*#SE9>Of@4}S8^ z2?g)RTA)X+Sh|(Ob%4 z#~kYBe6&vUQ(S;ox2>tlz9I#67SBf`{~TUOX}QvTwiV*yQ)bo?v}X$@aQg<|c}Ne&V6<+#7I_-H!BSjWn|Uc~(@lms-7Loi zJDvIwXg~S`-)Y?B3F_J#X#k5`oCmIdifX)XPqGI02O;qn}|+jER`haJA{wVHW7 zsxud`+k(1$2h`T#T3=lMiM;uCA-LUwq2dgSfcju&z4Q*i7I84L!#MZAPRSyf;RA2?SdM=W))Uj9n0w_R@x?+~uFQ+b7;lR1T z-^>(*b}$HSDM+)JDXstgt`)|Uon{%_5U|%yI7i4HxES?hS$)tlbva(P>~cGY-ooz_ zyIS0211fj3tZ7F^F}p2r$m#aaz*@3Y!U@J(D4kCZxe$9d9j4`Up1|UD?lkk?mhKB% zO!e#+dr_?od^l91$}On+ z-S00MkNr@6zOJr#peR5km~#HT7NkJlS>AFygFQB~$Rr29t4l)+`sv1c_iTK&!Bq|2 zs0C%;@?^qh)hT9G#+p_QJvoNVi%ysCHzaunZp#so>Su-=>g33bFJMlXi_)A^aWoN5 zVs*`z7r40zGxzNtolLR5g)kLg4>1i;ge~kJbYw*cZ zYj@1Gk<7@0Gz1xfl!ZKmGb70^?S%y62Q-7a z;p9&~Yv$gvnl?T3W_LX=LH9Hpu$NHD3MkM-p<(xjfj#$NG;`f*s#&95stFI{3}%46 zvzMW$;c!GNrM30rLDRyy+X49F4GKBo4KD?vnt?l;cGF|gw<}Byo>lA|O_(8G0GmlX z?4$RDNsnI7;S#f^2~WGbVWaU*dGHY*=BOMR)JjiL$v)-N-P6w8Zv*voSw6P|50NMk z$!Hifw*(e)gn3K6TU?D~c1ss}VTGGQl(f->E=a|l)tsB;?|o$KCtA(P+d`MvRq0Iu zt6|-f)&VnZhk#sKv1{i_do~I3Yl=M@J(WFdo4Y#ht=K}@#Wy$3Jl4#-$Dao&mZT;w zG~V@?^;?d^MJ2Gw`#+3{48wL(wOKPCSxu?u*;>3ZO%ndx~%SVtq{?z=a zMs6i=CcIDIcA{#ja+20|)+K+so_8SxBUdt?*Rt#ltWa>GyRT}`D3@i=UqLde0zC;E z(?$dixQoA+i_wU)1J%ebB5hN2fNI(N7DMt8gOl$;<&l3%;6qH5escRis4tA+?q3A% z+GHQoO&D1~y7ZhCF0lySpc|fNgS*g6Tt1?K)o1FH;<8&$#{~0)wrQjoX*<)XNn@uN zXQ^-q86VR1`ZXk@TV3hpIc5RFXw?}Q#p+{xd2#1EPddl1iX+i@?EscXlzx=287A6K z*Go7GhP@)ZT`SrNT4Ao7FKV|cDBm2R@J*L{5->Q>Ec@9ioYQfjSkt*%^rbqh_p?g{ zlzF8qJdLVSsI!BR(j&4R)bqg=V}KIF(L9xNA+?g95TLrOU--Qb_txr@*Imuz^F@Z#a5Ex{Q&kymIAz6 zwD)Wdg39Tm2^tOG;6(QeQ!a|gqeT?^tt}Mlluf@+Zt>(45~smMIf11eZI`&VCSZ*HH&UrdZ)k`9WB`-h=<*tp=H2iLW*xirYk!1f z!eR4#X0mmH-GeG|6`#Pf5!)|Hj-*Gh6H?%|;D|TdJaFx`tP6bxzgO~rYLy6XGwAO) zyGdd-o$5(|<0O;oWq{wBP*-n%l)mOql~{!^7VL?AJzcfoV5ZFw*rOqd3$qKyr7Vh* zO?+YfITCC4V~gcwLpEQn>!Ftp^$-0MtEq*X*^%gMn?BFqewA&VZxDD^PDCan4=ad^AZr;8$*m#c zZy-amyK?N;B6{yd4aW2u+kWB^9FStprQ#$K6PwhA|G2GnHwZ24o?VEqbCS1Vy&VTR zS&B-zdh%06;YK&J`WRX=xcLK9CqTApRnDqoeSP z*!X~QsdBbkDx-aFClkmC)p70ca8!?7Kfs|CowG}(A%u<)5Ao!PzC1~|)Ho#|X!A5M z423a(?b*fORaedWv2&4C5#KuSXkNCffJ!TBn_96ub&HNrYX7JiyXmpNPl1pvi>u)H zarVck?eVFTq_<$$T3io{SFHOdD^?hKXoCT%A*^=&I_|sECeb|6y3~%?f~{1kNO~&H z%df4>?qH?w#)w9m?>5?g*w5ej_m%fq`b1`m6x41_t=s7ZiX53U6*mMFTP8HifeEiw z-_St!@i%z5B8XA}(W6C27IIg}krSko(rP$X72&9S8peM1eV5%y=xf)&453}PVkX-} zMFw#ADQs*Ow|uPSqM7vR8eAj`(YM&n72j5#eGe+m07nnVD%}}m`TF%EodRx zNj)4yIE}s(Yu8pmKk4-83Q8&Z5`)AaTI68Hx)sA^_ z$%5vB8$Xp@jZ7C~{1olRYb9(LD=Sjd0w__$$aGrs@V1m5W~b3+;Aqx_dD(QwYMwHI z>VD52zMpiOcK;JmO8MO(*O%`!E5MwBrgivGN=& zJ{1b@Vr5AszvV= zjNJablHs7^M|E8&5Ulzujc&b5$rVJ&5+RiRkr-yTRmx?5sw8N*h11b&S%+CEV?0B$oI;rNp%vpJ*L zJMy|l3sIodj(L&o#WZZIzv5a^Y{|7UHo(F7c&9|k{t_PN!iOmBQsP3SFy>Nd%j#Ok zRpk_UZ+g4gx*zwafbBD%8+YyPHT*V}svY}THOT3t#mLsG@%DQ{M|=s+Ce|vsjs~s= zPKNXg)SYB1k*?m97aCBHn7SojwIXRcC3Ay}a zlqgsx4;R6c$Gc71`h}{B-aYY?z-YdR9z*pwSGihAsxY8e93=vV<5|5zosZdZ$*7~P zjB0#ZaXmSsK8Xm#c}S4fA{{}XwTFDVUx?-eGLo?sJtT_@#yi?meQ;3=p;<1|F>+K`aguBFiN1}HrBb$nzHFa=dT#thJ%IsW)gREFG|XRT zx^+zSj0s!$+G*fmK0kvuw>;@7G?Oi3Z&hRsFt#y506s``Gx+^OWQ?`n@v7OvY3i;! zfs;IvYCWmaeFai%9ji>~JSeLq>^KitEBWQ+8SXQcZ<}bH*l z*!t%4)hd)d-cLq^jMKO7MdZ5&i_s2zlP5(jbMu=BY4`OfMztwEFD7PTQJ&R)@~5VRPU z8lD7lgg8!xgSHt3o#%-}*T9IuagvA3v=gxv@h2wT{7Zpy%c`yhr9+wBk%Lx~=3aw9 zUp%+{pDaOGau;~YR{NCoVct67C|wKqf<1p8-4K1Kai&n#ULbMU!=brq8*1O2cWIi# zKMvNi+;hu)bby7U6C@`Luuf^MuJh>b$P8^}Qo`%4<_P7{UP6M<79Gl?!_|fQfPb;} z#(`|N?eZeOoMQ03)3^*^BS&dkLgaWg9!|eYs|@duB(!h2cb8_OF+x1lHoa3@#62w* zv~g1z;=Tg9>6J<+I=O@nS%Rm4M?%8@Q@z#>vHhf|O&!JTCew#3!yoKpYzeUBf zpcb)xIk{pbVzGGH9jUsdNaoXEl|fcDCP?%3hbe>cx91M1Y;;V}EqKrZuc z=3YZx$>E+dDQWI+EV?#JEXh7pC-YIHVhW`jmxiSKr0s2rPXE3A}K@6#J$#*NhW}q zN6Ct1ohoqUe2rDem{a5K^3Su^)*pLLq!zJ1comWkk6IEt*YYHWfrIS_{x)O!UIFhH z+V^3mbrts=j6uD@q)?A6GBHM5(VeqZP!De)!2>O0v|YQZ;UHJW*`DNNZv|XeCX{WB z6z1hDQJb|*GU|-f(MiOoq>&02e|@v>CP5_NtIk?lLD5{lIh`~*m%sZ&A_t`Sgul|a z?a~X%%0&{5W=Glmia^p5f}0~mgfsvfTe&&?M|eM30(CzbbeZxddI?LvvnworvwOdJ z@PwEQkgFtn=X+CdGESoGcNbHBQ}0u*32sj}Ps1H>rcLVXL1U9UjG=OoB4rb3*Ze5d zBGJ&+>VrkevoB9p@cBJ6d3CebbrrC-m&&Qa_w0gelbBkn*`0P@Tb_=jCAYRd{N@-b z?e>$AKv)jj3MnvXowP=pWi>JST5K%V?)7bwuN@1Y z21Kj<>t0~;EPM|L<#$>E`O+Jd0>I+@H*pR|v#kJD<>tto>G>{N!=?)Q^aXH6Fc2z7% zVwp`Yr_S~YcBT<7VXC(~tF{olsxC~a@+x>hvtnj{d^Az`pZLk0xn5@ZHj!6)nrtWF!D%rRW7^Y~Xg=7GR3J z*&fQGVwSQ9@sZba-jm$0{4b`H*1Wk{;NR$)68m}BRFpX>q;AB|T4iHH);n?ZRiBnf2Li=Aqq_02gNY0G`9%aBqx!6!>g8dU7+m8S) zIqzCMtk%bix+S%E4Kt!8)KD{F+Mnau&Di$~LR8y3MxH7@VcdCzUiI;sO#MJPx0nW< zqvp`}PnZ<~{SLlT{Ov;LE!lx0F92TrvtU$njC{+RyKVtlrG+khY`Q{20W{1((XC>a zk(B?EIl$}NocY!Kp-L zbWHaE9NfM+c$^6~u76%h(Fg+iL4CJnznl4M&2JiW0L>D|jy0&oF|FDUWp4l= z<+!cJMX*g%y*fLfP5UTO&NtbiXPyg+Qce8c*D4x|Bz4~ zfCIN)XypjWT^vY1qGrq;|5*K=M`+A^KZ5x2L!q{_R}1dOw{*hTAS+4N3yQUwCE(Vr z9u|+2OEuXx$-;Ce=*tubvW-tWc+>}+2VZNbXLrkSsmC9Vf`nd3xtoF>O!WDPp4BhM zqdQ%g-&tYvlOP%(umdEJpuhTpVbeBvqD#!jrD`Oh`iQn&X6t0yzcYx%fgz)NLZxwQ zntPFml_YF2HVw%m*K z9kJLt*QN3oUP}_DjUC&zQeZ2V^H_(urK%H`DT_i>xd3RE;ItFcx$J<78H{P(Nb+Uv zK8QErcW*7Di}9DQhRZ_iSr~HW!*_@#xk&3E}yAR z1Oe9Z?qU0~ zo+zPn0I+OA5&{954nj){FMKO5`-5wz7pn;~dg1+nf<+QAJTI1P*$j=X@h=&vd9>Fp zg(hD3zNiEIG2EJz_CG*b;MZKA8L$G^_tUQ-vp|gmfz!#YT=7yJt^fdkfz}H?puUV4 zs=6h!&H?NGY+1Wm0*}Wb7cSN&YY+;!6UeA2fym=`SBeIgF*%YPMRp4pB9?5J9dtac z0O}F_E?TNczEN--4VIGFD!xL<5d-}q6+s_rT21+TNkRT4n8GZ#gH28YIDkj<*qlzl zPyZC-j)Ze<)D~5`GY9Afdt6M&0swp~C`H|)jOHtqO@mPD$!Eo=^@Pex#vZX)3atae zWpf+Z-4={Rn|UETGkmHR>9!e+?YBJLxYB+w?A_(`$boyfj6>n`jUw1iU52;kPI9mr zd(T|$Xa!@*v-+Njtc11gPt0+4FKzd*k#sY?tf{~Ex-5u zM({c35PcTCYN608Ryt|>Po|rLNHsBksBnp*9L6z{p}*y6Uq<+UFj}p`=~sCLbghi z`^^4up?6TN1seHPP6}w7SQU#SF0@pwnUR;|tj5jR_+5D_T^C>oKmU}9#TYT8UH?NO+ zDp1xhVo|W&T~r3AsH!1l#i*_<*5=paFwe5VO!0n)H`13GL-8iDWQ8w`v^!71Y}t`P zJ!rL5w*I?tcjB8{C&IZKQi7badM@%T^kxJZ7PSIDYLTMYe$P8n__*)2eM3={qchF- zM1ztxD|$sp$4%YmB|bYgR#hN6y~^hUNPBum9FQ)PPxCO;Bi6`XY@gg!800dWdvg|q z$_;S4M4d2*WG{aYig1oDoXU1K$5o))CIaJLVsAS5&U!|fK~_J^0sMmGhUU*;qFvIX z+?2UDu)~_N*Aj9@5MJcUoTsiNX&1As-G!6>R845xEBa(XcLiZyij>;02)pZ0i8c$C0OD8L| z`}MPcn1bGU&te}h?2sGF(aUA6xMcwrGaWDHYRN{p&Uc3>n+RWbGy2kX|Ldg|5*>>R#9WqdP zYl{A$`b$@bB_yN9@GY&5ir@TLfUZv#fbS?-=;2Rm*{x^v1K_6+<|hfdq|)bQ zSjn}BHx)X_CJCP#3R-MRZFp%A!yEW;ff@&u>kfB9>A*} zTPJ6g8MCxiiCk$0KYa9vq;kMMyK)Vb(NQws7<=1Xv`DU#QW2#@5ISPo9VYZz6#e?< zTUBA!8m&||ClYjS8{7&MNEZenR!cG}#>0Ej@3@zSuqF6@Oj$B>R>HNtomD$;+Do~Y z{k|Y>b*51#8_S>9d;0Qy@aWAHjvs5q5vI*~2?~u+{|P0Y5vQPb>HR_XV0ar{LXftH zO1E?g?Y&}+!AS6N1KmsZsXbvoB9Yv~zcJu``#IYOa1XiVbFwmP(V^pv{jJR^#T)H4 zE}Orl#qo}r-J6FgmUG+pxpfRdDFwZUkV)$=xi*HjEhb3EcZJ0!e;O{B8(PV)v*C-< z?9nt_R1LL=v2g8Rk6pNWqS0BQWJAvtkoy7WN0aB4W0^vF6Tvnxj}}t^eHTGVYg5mI zm^4*dFiE=NC6f(mcgmiM2SA?GMMsZC4M+|?7!5}}qgAqx;(_javDqdwu-S|Dt)P|T zKM!W~`u#eL6SB?h+zZ4P4+{*jU$VRYzI)z#@hJ!}c793%4>I&>6IPQTB%u3SMvohb^|L-roL1@I z(fO$L5LUM^colGV{^KIQrJf0{gpw(-p$HhcgyUnyYb*vks-)-h+eIxPnUJzB$3HH( zu#Xc%Unw_>AfH+&Ls4%#(0twdWEjwO)(oT6K}dhFgVgeLxt%ke<4fNQEDGa(Lnx7R zkcPfcjl1`J~n?@imh7NmNdQnOQv&#)slH%J}*g3SZ~;?D&=`{Tf_-{^|Q|A z`FF+I>B677H}OXHV@U?n08q#WWIvEze*a14(s>E<-yh(c zR?|Q#_Erb87>Ly#UFX(MP;RWloAZNT zs{V3QMwZijQ;H60$3A?Urlv~jt`;2FbS6*{us+Mu&WT=l|63;(@XSU6|0>UL<{gds zuvulX=lKmJIvI4~x1pJnG_DE{fFegvK)L`c=bgkdi%AP|M=bBRPx{iVU0!xmQ^Y?G zWx&`d0KkgEo&g1~t|YfBd+qA_f0OTKT-<81EQZm48AT+`Wbwk~pZ`3RrFTO{FfIr-wUZg_*3z%iUxJ}cpsXX09< zaWxXtt0y(G4xiaHKj|w@yrNv0>RjDo{;GTR{fz3rHUEE5&-ky|8#N_t`kh(+r%*56 z$6XZqAg|?d!Eb;6$V2ZwVZf720o;zp`7xmBJEa}Sw&H)B5o&LZ`9hXk<9)`>vkEuQr3m^{U7+O1l{4zLSz4#7K;EjWm7r0? zDFnT8J=;c12;`@h*C4=mjf^1s`mkFvy`{j3?S|W~^98&@chLy3QFkcL>>u}ZX7`|9 z$f$zv>Gw}jY<~*1Gk#y*BAA#X?v?wcDGiv$W?us?iFvaZ0XK*WYEja5aoYE5Vj6H{ z=h9ja3swvQexOYK$jQ|F=^kam>PUJ6 zR&wqoUmRS>|Gq;HtM6;cg+REOb7{>i;0ouGx#q-4%=EHC&-VL@y2a2_l3en0(Rj1= zblr*Vx57UB*Ap%}zV~WA(U(;rupa4DT$)K|K3>dBB$YKMv#IWGI5Oj`dv4^3L_9#Nc_L?t05(F^2 zRD_sa`E~b;vXjaB)ZU+ezd>=rlhbx_R8II_EPUA!?`H^6kC#xGQeJkD_`= zYt79Rp|0mdqihJik^53Y_gT;P6QEm{f8<WN1SYE`RR-;M(WmH<60qVX z%bw(&46o*BqOy{!wQBjJ1?iTR#?>8y<%9~#ud@aA}F;QZ2ew`Z(J)U+5QZydg zu3OldRKGs~z%A3T3@!#v`I3aA-#}>l7}_3}ASNQj`?7R{9H_|e`1@%F^l01&g9gQ( zYk1KfJ2T;_n6ZxnBrc@F8_+-<-~t@6Q`F+}Q*;lOPX_QG5fh*La7Lv2{u%okY_@KE zycETr8@r9X&mPd(Hx-+G<7*?L>3M?```1e#?8duRdQ%w{drNwEkAT+DSi( za?rUxzBGfl;Jx7&B`@l-P0IdHcawRf%?TsHotuf+4oE}p|Bc8Kg;CRD@7y3nS1Ay8FSjk@n1~ zKG`F}mFWXF;n7q~JnOZhUWtIKqF z@wr0Bylx362qd%x-WsJJV%{M{mX+ARe96#bDQrKMJFqrO!Jll}M9EYqXy0K}{oIgM zQi8So3dqLq!iA)mCw@Z_4DR^#5-{r)@W$Yb;o2%l%56d~_(wh~N~|QyI}&9o=bEli zn-ui2(|HcW&@BPq)>fHu&55;1?=z*?@)KrX7RY%mp%)maZ6VXQU@Y@{nSKX4k>o45 z1nkp&=<;TGGZW+sL%(=Q%(64GqY`ez>Qh z!U8@Tp>mCxb`ROI{Of_Wkg3{|%@Rg}7DnETqdQ;DxomXR3Fb3zJdGRBts*1UQ7W(h zO~P&!LX`8zOtyXi(Z2Ms%I57cF`Klfb;ylnhN0?#&Q~V80IO)dC#tOR&jaY5bOqgF z3jn9I{-M9gFm@*c@Y>20TT%seSrXks#V30F8}0!x$aBS8o*(>6H=K!4gd3mxZsNqr zb$)hiXhMyvsM#q-9uq$vPDwvUh)&bV97}UMMs64NmB+OKXVu(;;*#XpZ7)F0V2TjO zv^^r5#xd2*iY>xisGWZAN1uqK(nibFNj}8O9~&0;eplaQC+m9c?pcE7a)n*d6qNRSCvXK;0z|NgpQJIx|mH78XBvo zi0%mS#~9wo=caK7RmB_7O|;8h(GwI6d2{_fTk_)KuveN+@kNpi<+9xAh92}!ks<&u zo*nwQ^LTaaqo$^O=`sF0tJ)ODc7Ag0)v>&+>TD3({-NuwHVXiU{yMIEL{}{ab*4$! zU(oP;qO#nZEas;cMTRsott1MFfW*+gswu-3AI!st)p1a8l@(eQR|u%J#lt>tOJnL$bG{%T5UnQ@OZ#!BG)L#CpAGb!5k*o zAIlDz)I0oo91ZSE`9(JEQD(*|x!mAWlGxNMA7=GE1o! z>cMuR;FnXpE<(6y>-;$9dnHqvvd1-RHpYJObkeP2D%|Ya7Vm9t@4);Zsv~aom0f>9o>f?t+=TLN7@}b(+sT zO|3$5E3L!;(S3cpRRbGZ*Fn^(k7aS#>HRfL&z8Z&cUXZf`7!K7oRNyVok78T(LpM7 z5ddTMncuj0{zk?!_+g3%TgRq7KcvLm>^4d-&62 z*48=r4`x9wP;`Ng7Fl9&6Dohsxzkm`q~|_qhD`e{zy@QEE6lZjMaEr?Qn zf=sIjN{Mhz1y2dR_T+S!Z+vNccbm;>G;*QMGh_qcP^Ow%IRR+0RU2&==5_Y`-o^X6 zQpg|aAwBt{XO|5_r&2?;*Brg$#zrc`(N_n50FVw-Xatj&_smCf(P471OBn5@so!$M7O4+dSw7Hai#%t zDe7@fNFZI7CzEK4X>>XPmJ+f}=r_dJQl6IUh-u6!AugY>XpV-77P{eeC+d1HU}eG% z2RqZ6YL-^l;9YcPCyv=XsXi(kuFs;P=8W1-pDPM>6Dtu5UuO#;p=aP`&*q<9|D!~# z)I2YDq{|+oSEeH|{ln5204)U2)@6VliXE}xeJ#L3nhUmN6hm@*QNL?Tzt2jXAl^Zm zd~v78G@tdJHS?2Y9n&jRN)d7YM^!wcX{$<0R22qTATZ5^vlG0!0gi$nvk6v~XXf;Q zp09$EGC0EAl&pJO(_h*S^)^REfU*TC^|%j_dfuH!1J^tR6m|V98$~wxMmqw zy5v`gFEplK{f8xb7tL9W-?Wmf1w>+nI-hMa(0xc_s|DS9bWn)$&)-WD8!m}Czx^{y zvbz=?)L+$`=Aj$@jwU_XgnSkM8~CS-bFP29m*3ovk$42`;;^U#^)mHD;km6m826xg z2wlsiH6dR#?3@^I8}lesE}k93iuYa4C!zQB)?0+i2<5`0bb_W(ReVq>_9w^o58i4w zYx&ej<6*JzNm{hN$Za;#iiOOAr=N<~WWLRPoZLF!a!^F`G7l0-z5Sl%GF-^|OvpgM zEyHJR&b{sYREt%Llen>DCe_=6^5pjKJ1ZbbX!G%m^Mz14fpufLVDZfr-!@#1dzrM` ztW=wM|3gaSu?N?QpD?LDFDs3rmJ#Ck>gylNDk}kSy1MdKy4D;l9&=+wwN>l^G@4>- zfp@mLlll}$t~$)nZlW|P+qP5@PK!9l0HTFEM?fm){WrO7P7|Nh*z{F>Em(Ff;}qr3 zPlykWR{^XgFug^HIq9HNJr*T7z4{sld#ce4 zGrk}DWO@4>_F6QkABv^|dtKh~RiAW!)fBtu8+M0>(?*`(trvDbD)I{p6c5zW$q%Xi z=!q&Ughb57IZgV%b2?;Nk&WoNe(&aQ*}H<28Y#f)QX4(Wh_g zlqQK8rz}*!cmLQkA6g|a3nDD^aXvtn3vNBVHz!uzud}b?EKr0!aIgIw{pybKP2Cg) z3(5}S4!?=(0dAq%H?H4UK51#&gUDEHQ5DAAel^or#F@@v&~+B6k&}`mMyFaBZ=dTz zxVhqWhR^!V*WayV;o`Usw{YvRS3E#b-91Oc*&DvVb)}lVPw|RH*v?shWgn#a^4tmr zZw3y{1qTcAcIk`<-36?c7F!)zFa|`7bH_PxYrUTm>9QseIb&9~HI`1zPZFXH=;q!9 zj(WVN*54ym3h1sUVqTr}B8&SjdurxLtTeot%d+&jq34^dyevtvH{#G$5KO!(83-G*WkS=-cTU+9^rj+(4pExU81 zjs4&Zxlzy>9~QjD*ikV^p#Y3*c(*fY~B$nOkz+?XBXlY$CBB&7w!0i;l}* zN`Ri_xwARp!XaB0^CV$xTOlbS$@b0%>IND zUyTCrOo@0%=l2k7$LnE41%EzgHtaU#8fj9kRFJj}{ihEWiFA=E5~Oq)JZ@^4l0i%p zuA_`2E@(J81Pxa}^469w-`0wp-d}XDZ{bHy*!6JykeF$9;RDBD=sR7`mo6l-DQa(x zn66x{ttj4mjzViG%W73BvI4u-zFCmm*_}IR)iwF;Ozlp3#Rvggn)_poKuFw)?$5%9V4uM~>$G>YQ0$?13oNT3*Gwg#b8?<_ejl#NNK;465J34*0`GF(p}I z;W!1slI7Np;Fr=aLOOJW23$RFn6yb2({u6kEf%f04{2zUrEM}|c~dftJWj4sRtV{Q z^CJR~~Fp~SYyml)V&5@4e@*n>avn@81R!CSQaKm)t{wpe6tia7^H}}w-Ey;O_p4pvER3#_Y}*%J zvO5lCOS`@0PSsH8>j?dxyvCBfmA%sJ8tt%952<6?EUXpUY|zwqRu|?S(H--kzdvX3 zvdpU|*w~XVXls@@FJLZ4C7DX#;fBBwpT3NjNYdjJgZqH4syp~+(pqiENj-Tj+ zt!m9g3_nl0ghW*}KgS>Yp(WQTeG>HO3IYB3t&R~Do{%H^O1nJ3tjqNZJ$xg++-z@K z-c+sz8Y)P8F{}fLOk*+po4EK~Y>L^8{J0fU{*Kw`JmB~GA988p$0Rwr=f746}G>5I+Go**k9e^~iDyFWp zpf6tRM!T(IHR$sg28b%JB&Kt<^(w zPwv+{1X^nr%ucJ4*uTGL{DvfPE1{PWx^Xk%9XUFrhaoZL1 zGw#dJt^i{e)i8Pfr{~F*RhhCmDd^QHgTchudnU7{UxySw+kWhx<;tSkN=nVIi)}qm z4s@gegFASV(gd^UY90KjzcD=+Cqwo&a3^?sACKT_-<>&wQbFH{`#FlAAD`LnQ0Yji zIV#W378Nl*OCJl&|73!%>DVpdJu2EPL3?+VmLtQoQl`kq;-Q|X6YbqDd<~m##LM%3 z21levr%5%TEwm|Cf2mlHi+QK0lOiY}RU`PkIs#(gdz+KxNRkwpuN=fVPt)^5^ZCOJ z62;zUeA~EG6>k#x+6bzWxI&sWQvPDsNrtBRGvvht(PJ{fvy%6ovuugOyG;P_7q~LF z@|-Z%aSz>d-+8sjm(?+`Joq<#UGzgP+h5iaAg6PAPtRdS-@h;x0nqwY=JbYlix;Y~ z!3*_o`uE0zv90I-lfnGsQtIY)Sg^m+qx|ee-bd7^XeKHbxRIQc z*4J0`Wn3K;SGO|y5P@#)!ugKx4sPMH^>UUt)x@6B_X^)XKL#>AngFPogE1xUjW;al*b{a-R&<5}0PazM zEbkqBDHN@Nxk^io96?;1{-$qRKL)T6XmHRHsij7@6b5LkNi&6~0Z3NlXS}oMKLvP` zr{yvJ)BC5L>=%o`X?xo;`T)Q&cx+T3y~FKwQlzv(=Kmhg4IgF=^z>>btKX}Tv(F`R zs*HU#D02I>of$H~rNqh_e3-{helqJ~yCyl~mJArKNB*4J7@@JoweAM98`_ z+5iw>jtTvND2JAt+8L1F=L6({m|nJW|H_i_{>JV*_C&qGxvZfZ<134|3QWm#4v_r^ z(7G&wv8xy72ftJ(8UQR+uckrZ;3~QG&2Bigu0|w~hX|Voe&zu9jROq;z^My-LybE~ zYumawoowTQ$xH$;^ci~DHhU0A>ISl;sOn>{(&}7!#03oDqQPe;*w9%wfTI?GT@)-f zeEU7u6j2_y%GXB;mcZNQPBrc*ZQ5xBBqzy!y*Re`;_BBYY?VY`{vcW?i8rOseBK>0 z6G3mW9tFx}df?<|+fn z`;1VQkSv#ML$^V)k2P76Jv3dz*q7`RLc!gr4Ldp)q-Hzt=T8L0WB4L;3PrQ#xcCEjtx7XZ?!4sN51cZz z>(MR=%4^j`#x!gYW}0%sr5Q9t0&?a0+qRrc-I(-ktfWO8s>a-l--04x+i)JgPd2D@ zf6G8&H;Kttm@ax8`-s<5B3{O>y(%c{4psGYMete+h1nzQhFT? z_Fy2*QijI`G{pm;A~rIrp8}RGdQcfCy@q8AW22U8-{Ui|yGPY&Glz-gpfmOjR3H8j zC-_bSXfs)k!q;skpqr4hOR8&L@NjL*G|@EmXmt~fP>cXd?fa+lM#{@&>Rs@@V^nMU zSK*wJw7$v-pEjP8SnQe9y$)YH22=ye`W~f>1jj36u1;;nVym6Vm&$ozzYEsn-Lyi=sk@$}B2$pAQp>b5JCM_cPER zFt5AhuW;T|$o()Z^4{U-p}wTUQhxeXs`5L`Y}8}Ps!mtjYB}WV`W!{JU`YDd@PuTd z>Oig_N#qI}qx|4U`TikC;G6V0037BIVy}<x8{0scWgoK*w3S0$8}*fmKjmR@K2Pd+=t`B-}yAhz}|f6c(IL>?L)(BU&N zpW2|w$&z2q1zQ1gDr19Yg-3wSW7fKEBRU?1;ZkTrv!-c|X{f5mNDTb`FA>5P+#h^3 zJSJv7OU_YeKnX72I9P4vk`sTe&3za6HJ2~1Ihhva^t|EVym^uamKmwsMO4OZK5mLe zs_@~@$%Yrnm)8bsZ_==i6*Q9TFSgA7=Kv>*6>GT3 zDtNnvFQKP=d2d#AX4zj&V?NV}7>f8)V&tfM9y^BJ;e!xuJ_l*Nq?xhCMQEi3pjR@m z)m}~8q$AJRKFp;*Y%V-?a7o!wWHr)fRDq>-NnJ!}J#0^>GN#(RC{yiSMr@28DlbSZ zW4ogGN&LmbrxE+z6Xr6OT8vdQELfngkh7u>#n`Bsj_X`_su)>iQw%2SNR55aG%5iX zo>@p!ek}WhBY)46P4OAA0AZ+p0@am_-UYUL50Jznp$H10!?Z{zP0;jcrynKcm zVsC(NW;0u$=8}Es9l;28Y)I8YRoSKgu;2990psdZI6)7ei_heJNCp6MR0V^iKJVqa|^Nif);Y!4$@E98xuL zUkJaqk3=<6B7^i#5=zdlGd^Aux2Z&LOL!@mOCc(32om3#>GxX1*JyIyG!02E;WiZ9 zx^_*HX&o@iC1GO34i>IE0bh23S~Pk}($Z>RMyv@C4tpj^F z$(%@G$G{t%BbAQy9saN&`{`k6MsRslo=Kg7@yg~@q7Z;~y0j}~ z-%5>`^!0krZsOBELoA}lfhS!#7rq&5Li2@M9=!e{x&qGa{Wf6l@)Ear8uNE3QDa5^ zy+*V#f?rGP2UdJ`cVPsId?~x?WA?r0ZIkTeRkDEFsR#omqs;bWfiCt9#bpg2Tb|(W zhNp9Dg)@aa{UsX6pY%Ce9PPMO@=l1*6w)>Eb zOG9A5sakouhhKnEciWx)tZ9`}2s8!mT!dPtIZm+%(x`4VAlYL#O@~_cXlfqL5|T^? z+Gjf_X0R3r=^rP(#;Sm-2D)0qp0Y(IPJVu=nR1rG9b8cd5)R%0t=XdnnVIMS&sqUh zSXub976>drw+u$3tx2F0|4oIzC?UBT$_%PYx8%tML#aN3P?`=qKp#v!+TfY_W4;A6 zd5o`-&+UAaDX&)?zC#SKyyN@iXBtDL{0jh7OHaq@$xXOMgTxSIw2`hKCkqA)X~2j% zuv+TJOeF6Fc!ZWcwgPeI`tzC-Qf!)lalDHy$nEv$xF10cs{0Q)#T?h}w-|C&Z zRRve~z`yAS32fk2s1E_Ix=9Eu>k@#S_Un_o$DnOClLs|<)#i_?tX_hDT0{d7WkM>; zbM})e#R#b8;kUiAEfbK)!&n0bbDMx`0!%Un`x#0e0Y(Igx{tpX(+Hrul8Q`6^fvIS zZoEG{S2{{g{mp!034RcS%s0sUi2tNc_pJMLZH4AG9XQ8>jMNVXnHkdcfL0(Sh+OW3 zTp8ck;I!~}0JdhsZG$M72W(MyTildSFXY-1>#lnPcjbL3 zG=l!ovU#@fEW4p5MLR;G;MGr11+WR1%p zu*!^A_8>cdH)Kb+xA2FhKk$yOJbIKcCRe;Isr)j^14R66=N9=oU`Fcm(`-J5v4+PF z8H8tEh{*0b9n!~U4A!u($XoFXc-w+kApozKfX)ia5$f(su)FcR9>sw@+$)F1i30or2jB7tf^d|GqMB*avc)A&4m5k zUhAKfb9juT0`a^ES!$e&a=|y%$;AnaI5~GYH!LTUiu*=M?D#A`=vg4T*E`LAYG1Vl1OAYpIW@A~T z8TnE&<)pq#7ZeRz=#%TAYo`e@9f~1xwtwCSlSeI2+(xmO&UGeG!q4><6u1KkX4tu< zhSKvIYL6AzL#3g5zJK) z&C`Qp_n15*7EauSB1CXKW2)3$MBViD%0*f9yFVZvdHi{vty`6Ddp6Jli0HBulKZCtJmyl`H zWvKIEdf#!faWX}y(yE~?J-a=S%u(}L!PzO;z1ww4?~sZz(;cg+(+kI(%c4EYNHM610V40yZ-CqzS>`q|zQ{B?Q@S7w zOYc^1fP-e~1dvlHq3#Lzm3pu~?Ilhd`mQOLK`+1>iNE!9csu0WI3nNhQ_>LY0XL`) z5{|%!0g3Sadin~I{^oK%2I#|z5U{KQ{*9NDQ?UAdtmAgOfXTUB!HZuWPQxa0Ele$r twr&APFgx@Z5F5ANSS Date: Thu, 5 Oct 2023 14:51:01 +1000 Subject: [PATCH 02/42] docs(windows): List some apps that use TSF --- windows/src/desktop/help/advanced/text_services_framework.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/src/desktop/help/advanced/text_services_framework.md b/windows/src/desktop/help/advanced/text_services_framework.md index e7a977a062f..650196c0e92 100644 --- a/windows/src/desktop/help/advanced/text_services_framework.md +++ b/windows/src/desktop/help/advanced/text_services_framework.md @@ -25,4 +25,5 @@ a much more intuitive manner when using TSF. ## What applications support TSF? -TSF is supported by a wide range of software and Windows components. +TSF is supported by a range of software and Windows components. +This includes: MS Office Applications, Firefox, Adobe Create Cloud Apps, Notepad in Windows 11 From 21c79374c35b78e558d086dc48dd50b3908fe5e6 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 9 Oct 2023 10:15:02 +0700 Subject: [PATCH 03/42] fix(web): enhances integrated test stability --- web/src/app/browser/src/contextManager.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index 492c627146d..cbe3ab30c48 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -184,7 +184,17 @@ export default class ContextManager extends ContextManagerBase, sendEvents: boolean) { From 35bebda7280326ee74da74ebf51670186b51b9f6 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 9 Oct 2023 10:23:43 +0700 Subject: [PATCH 04/42] docs(web): comment referencing the PR --- web/src/app/browser/src/contextManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index cbe3ab30c48..42b79109ef7 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -188,7 +188,7 @@ export default class ContextManager extends ContextManagerBase Date: Mon, 9 Oct 2023 18:45:17 -0500 Subject: [PATCH 05/42] =?UTF-8?q?chore(core):=20Some=20failing=20normaliza?= =?UTF-8?q?tion=20transform=20tests=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For: #9468 --- .../keyboards/k_008_transform_norm-test.xml | 136 ++++++++++++++++++ .../ldml/keyboards/k_008_transform_norm.xml | 74 ++++++++++ core/tests/unit/ldml/keyboards/meson.build | 1 + 3 files changed, 211 insertions(+) create mode 100644 core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml create mode 100644 core/tests/unit/ldml/keyboards/k_008_transform_norm.xml diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml new file mode 100644 index 00000000000..21f815c457a --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml new file mode 100644 index 00000000000..e54c3f6e6e8 --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index b83c74d1414..536e6e06e6e 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -34,6 +34,7 @@ tests_without_testdata = [ tests_with_testdata = [ 'k_001_tiny', 'k_007_transform_rgx', + 'k_008_transform_norm', 'k_020_fr', # TODO-LDML: move to cldr above (fix vkey) 'k_200_reorder_nod_Lana', 'k_210_marker', From 15e6fb60dbf3bec894e7ae178cd1595b5986b35e Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Tue, 10 Oct 2023 17:51:12 -0500 Subject: [PATCH 06/42] =?UTF-8?q?chore(core):=20dx:=20improve=20test=20out?= =?UTF-8?q?put=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - colorize ldml test For: #9468 --- core/tests/unit/ldml/ldml.cpp | 37 ++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp index 31a4cded731..de0c9e5556c 100644 --- a/core/tests/unit/ldml/ldml.cpp +++ b/core/tests/unit/ldml/ldml.cpp @@ -295,8 +295,8 @@ run_test(const km::kbp::path &source, const km::kbp::path &compiled, km::tests:: * Run all tests for this keyboard */ int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { - std::cout << "source file = " << source << std::endl - << "compiled file = " << compiled << std::endl; + std::wcout << console_color::fg(console_color::BLUE) << "source file = " << source << std::endl + << "compiled file = " << compiled << console_color::reset() << std::endl; km::tests::LdmlEmbeddedTestSource embedded_test_source; @@ -306,7 +306,8 @@ int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { if (embedded_result == 0) { // embedded loaded OK, try it - std::cout << "TEST: " << source.name() << " (embedded)" << std::endl; + std::wcout << console_color::fg(console_color::BLUE) << console_color::bold() << "TEST: " << source.name() << " (embedded)" + << console_color::reset() << std::endl; embedded_result = run_test(source, compiled, embedded_test_source); if (embedded_result != 0) { failures.push_back("in-XML (@@ comment) embedded test failed"); @@ -327,26 +328,40 @@ int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { assert(json_tests.size() > 0); // Loop over all tests for (const auto& n : json_tests) { - std::cout << "TEST: " << json_path.stem() << "/" << n.first << std::endl; + std::wcout << console_color::fg(console_color::BLUE) << console_color::bold() << "TEST: " << json_path.stem().c_str() << "/" << n.first.c_str() << console_color::reset() << std::endl; int sub_test = run_test(source, compiled, *n.second); if (sub_test != 0) { - std::cout << " FAIL: " << json_path.stem() << "/" << n.first << std::endl; + std::wcout << console_color::fg(console_color::BRIGHT_RED) << "FAIL: " << json_path.stem() << "/" << n.first.c_str() + << console_color::reset() << std::endl; failures.push_back(json_path.stem() + "/" + n.first); json_result = sub_test; // set to last failure } else { - std::cout << " PASS: " << json_path.stem() << "/" << n.first << std::endl; + std::wcout << console_color::fg(console_color::GREEN) << " PASS: " << console_color::reset() << json_path.stem() + << "/" << n.first.c_str() << std::endl; } } - std::cout << " " << json_tests.size() << " JSON test(s) in " << json_path.stem() << std::endl; + int all_count = json_tests.size(); + int fail_count = failures.size(); + int pass_count = all_count - fail_count; + if (pass_count > 0) { + std::wcout << console_color::fg(console_color::GREEN) << " +" << pass_count; + } + if (fail_count > 0) { + std::wcout << console_color::fg(console_color::BRIGHT_RED) << + " -" << fail_count; + } + std::wcout << console_color::reset() << " of " << all_count << " JSON tests in " + << json_path.stem() << std::endl; } // OK. + std::wcout << console_color::fg(console_color::YELLOW) << "---- Summary of " << source.name() << " ----" << console_color::reset() << std::endl; if (embedded_result == -1) { - std::cout << "Note: No embedded test." << std::endl; + std::wcout << console_color::fg(console_color::YELLOW) << "Note: No embedded test." << console_color::reset() << std::endl; } if (json_result == -1) { - std::cout << "Note: No json test." << std::endl; + std::wcout << console_color::fg(console_color::YELLOW) << "Note: No json test." << console_color::reset() << std::endl; } // if both are missing, that's an error in itself. @@ -358,7 +373,7 @@ int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { // recap the failures if (failures.size() > 0) { for (const auto& f : failures) { - std::cerr << "failure summary: " << f << std::endl; + std::wcerr << console_color::fg(console_color::RED) << "failed: " << f.c_str() << console_color::reset() << std::endl; } return -1; } else { @@ -402,7 +417,7 @@ int main(int argc, char *argv[]) { int rc = run_all_tests(argv[first_arg], argv[first_arg + 1]); if (rc != EXIT_SUCCESS) { - std::cerr << "FAILED" << std::endl; + std::wcerr << console_color::fg(console_color::BRIGHT_RED) << "FAILED" << console_color::reset() << std::endl; rc = EXIT_FAILURE; } return rc; From daa0523dde797d179c983809481c3bac17f44417 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Tue, 10 Oct 2023 19:01:14 -0500 Subject: [PATCH 07/42] =?UTF-8?q?feat(core):=20ldml=20normalization=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 0 steps forward, 4294967294 steps back For: #9468 --- core/src/ldml/ldml_processor.cpp | 13 +++- core/src/ldml/ldml_transforms.cpp | 67 +++++++++++++++++-- core/src/ldml/ldml_transforms.hpp | 8 +++ .../keyboards/k_008_transform_norm-test.xml | 6 +- .../ldml/keyboards/k_008_transform_norm.xml | 22 ------ 5 files changed, 85 insertions(+), 31 deletions(-) diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 40b263a3532..ec8d1fcb089 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -235,9 +235,12 @@ ldml_processor::process_event( default: // all other VKs { + UErrorCode status = U_ZERO_ERROR; // Look up the key - const std::u16string str = keys.lookup(vk, modifier_state); + std::u16string str = keys.lookup(vk, modifier_state); + ldml::normalize_nfd(str, status); + assert(U_SUCCESS(status)); if (str.empty()) { // no key was found, so pass the keystroke on to the Engine state->actions().push_invalidate_context(); @@ -248,7 +251,7 @@ ldml_processor::process_event( // found a string - push it into the context and actions // we convert it here instead of using the emit_text() overload // so that we don't have to reconvert it inside the transform code. - const std::u32string str32 = kmx::u16string_to_u32string(str); + std::u32string str32 = kmx::u16string_to_u32string(str); if (!transforms) { // No transforms: just emit the string. @@ -263,6 +266,9 @@ ldml_processor::process_event( (void)context_to_string(state, ctxtstr); // add the newly added key output to ctxtstr ctxtstr.append(str32); + // and normalize + ldml::normalize_nfd(ctxtstr, status); + assert(U_SUCCESS(status)); /** the output buffer for transforms */ std::u32string outputString; @@ -270,6 +276,9 @@ ldml_processor::process_event( // apply the transform, get how much matched (at the end) const size_t matchedContext = transforms->apply(ctxtstr, outputString); + ldml::normalize_nfd(outputString, status); + assert(U_SUCCESS(status)); + if (matchedContext == 0) { // No match, just emit the original string emit_text(state, str32); diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 5507c6ed86d..842b6e6a638 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -462,9 +462,13 @@ transform_entry::init() { // TODO-LDML: if we have mapFrom, may need to do other processing. const std::u16string patstr = km::kbp::kmx::u32string_to_u16string(fFrom); UErrorCode status = U_ZERO_ERROR; - /* const */ icu::UnicodeString patustr = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); + /* const */ icu::UnicodeString patustr_raw = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); // add '$' to match to end - patustr.append(u'$'); + patustr_raw.append(u'$'); + icu::UnicodeString patustr; + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + // NFD normalize on pattern creation + nfd->normalize(patustr_raw, patustr, status); fFromPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); assert(U_SUCCESS(status)); // TODO-LDML: may be best to propagate status up ^^ } @@ -545,12 +549,21 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons rustr = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); // and we return to the regular code flow. } + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + icu::UnicodeString rustr2; + nfd->normalize(rustr, rustr2, status); + assert(U_SUCCESS(status)); // here we replace the match output. - icu::UnicodeString entireOutput = matcher->replaceFirst(rustr, status); + icu::UnicodeString entireOutput = matcher->replaceFirst(rustr2, status); assert(U_SUCCESS(status)); // TODO-LDML: could fail here due to bad input (syntax err) // entireOutput includes all of 'input', but modified. Need to substring it. - icu::UnicodeString outu = entireOutput.tempSubString(matchStart); + icu::UnicodeString outu_raw = entireOutput.tempSubString(matchStart); + + // normalize the replaced string + icu::UnicodeString outu; + nfd->normalize(outu_raw, outu, status); + assert(U_SUCCESS(status)); // Special case if there's no output, save some allocs if (outu.length() == 0) { @@ -831,6 +844,52 @@ transforms::load( return transforms; } +// string + +std::u32string &normalize_nfd(std::u32string &str, UErrorCode &status) { + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + if (U_FAILURE(status)) { + return str; + } + icu::UnicodeString dest; + const std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); + icu::UnicodeString src = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); + nfd->normalize(src, dest, status); + if (U_FAILURE(status)) { + return str; + } + + UErrorCode preflightStatus = U_ZERO_ERROR; + // calculate how big the buffer is + auto out32len = dest.toUTF32(nullptr, 0, preflightStatus); // preflightStatus will be an err, because we know the buffer overruns zero bytes + // allocate + char32_t *s = new char32_t[out32len + 1]; + assert(s != nullptr); + // convert + dest.toUTF32((UChar32 *)s, out32len + 1, status); + assert(U_SUCCESS(status)); + str.assign(s, out32len); + delete [] s; + return str; +} + +std::u16string &normalize_nfd(std::u16string &str, UErrorCode &status) { + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + if (U_FAILURE(status)) { + return str; + } + icu::UnicodeString dest; + icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); + nfd->normalize(src, dest, status); + if (U_FAILURE(status)) { + return str; + } + str.assign(dest.getBuffer(), dest.length()); + return str; +} + + + } // namespace ldml } // namespace kbp } // namespace km diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index 9d231bfe323..27449f2db87 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -25,6 +25,7 @@ #include "unicode/unistr.h" #include "unicode/regex.h" #include "unicode/utext.h" +#include "unicode/normalizer2.h" namespace km { namespace kbp { @@ -270,6 +271,13 @@ class transforms { const kbp::kmx::COMP_KMXPLUS_TRAN_Helper &tranHelper); }; +// string routines + +/** Normalize a u32string inplace. Returns a reference to the same string. */ +std::u32string &normalize_nfd(std::u32string &str, UErrorCode &status); +/** Normalize a u16string inplace. Returns a reference to the same string. */ +std::u16string &normalize_nfd(std::u16string &str, UErrorCode &status); + } // namespace ldml } // namespace kbp } // namespace km diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml index 21f815c457a..8fd842ca4a8 100644 --- a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml @@ -9,20 +9,20 @@ - + - + - + diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml index e54c3f6e6e8..cb4cdb533ad 100644 --- a/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml @@ -40,17 +40,6 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke - - - - - - - - - - - @@ -59,16 +48,5 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke - - - - - - - - - - - From 522d2ef5835e7ce7d9a496f468a24939e5be841e Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 12 Oct 2023 10:10:37 -0500 Subject: [PATCH 08/42] =?UTF-8?q?feat(core):=20ldml=20normalization=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change tests to NFD for now For: #9468 --- core/tests/unit/ldml/keyboards/k_003_transform.xml | 2 +- core/tests/unit/ldml/keyboards/k_210_marker-test.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/tests/unit/ldml/keyboards/k_003_transform.xml b/core/tests/unit/ldml/keyboards/k_003_transform.xml index 814b6829d8e..5575bd01dcc 100644 --- a/core/tests/unit/ldml/keyboards/k_003_transform.xml +++ b/core/tests/unit/ldml/keyboards/k_003_transform.xml @@ -4,7 +4,7 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-keyboards.md#element-transform @@keys: [K_Q][K_U][K_BKQUOTE][K_E] -@@expected: qu\u00ea +@@expected: que\u0302 --> diff --git a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml index 24f104f4d71..dd7d7be0092 100644 --- a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml +++ b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml @@ -29,12 +29,12 @@ - + - + From e64f59613ae0cf3277ef9ba700fca7777ed813ef Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 12 Oct 2023 10:10:51 -0500 Subject: [PATCH 09/42] =?UTF-8?q?feat(core):=20ldml=20normalization=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - split out 'process_key_string' For: #9468 --- core/src/ldml/ldml_processor.cpp | 166 +++++++++++++++---------------- core/src/ldml/ldml_processor.hpp | 3 + 2 files changed, 85 insertions(+), 84 deletions(-) diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index ec8d1fcb089..6074722ef8b 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -235,98 +235,17 @@ ldml_processor::process_event( default: // all other VKs { - UErrorCode status = U_ZERO_ERROR; // Look up the key - std::u16string str = keys.lookup(vk, modifier_state); + const std::u16string key_str = keys.lookup(vk, modifier_state); - ldml::normalize_nfd(str, status); - assert(U_SUCCESS(status)); - if (str.empty()) { + if (key_str.empty()) { // no key was found, so pass the keystroke on to the Engine state->actions().push_invalidate_context(); state->actions().push_emit_keystroke(); break; // ----- commit and exit } - // found a string - push it into the context and actions - // we convert it here instead of using the emit_text() overload - // so that we don't have to reconvert it inside the transform code. - std::u32string str32 = kmx::u16string_to_u32string(str); - - if (!transforms) { - // No transforms: just emit the string. - emit_text(state, str32); - } else { - // Process transforms here - /** - * a copy of the current/changed context, for transform use. - * - */ - std::u32string ctxtstr; - (void)context_to_string(state, ctxtstr); - // add the newly added key output to ctxtstr - ctxtstr.append(str32); - // and normalize - ldml::normalize_nfd(ctxtstr, status); - assert(U_SUCCESS(status)); - - /** the output buffer for transforms */ - std::u32string outputString; - - // apply the transform, get how much matched (at the end) - const size_t matchedContext = transforms->apply(ctxtstr, outputString); - - ldml::normalize_nfd(outputString, status); - assert(U_SUCCESS(status)); - - if (matchedContext == 0) { - // No match, just emit the original string - emit_text(state, str32); - } else { - // We have a match. - - ctxtstr.resize(ctxtstr.length() - str32.length()); - /** how many chars of the context we need to clear */ - auto charsToDelete = matchedContext - str32.length(); /* we don't need to clear the output of the current key */ - - /** how many context items need to be removed */ - size_t contextRemoved = 0; - for (auto c = state->context().rbegin(); charsToDelete > 0 && c != state->context().rend(); c++, contextRemoved++) { - /** last char of context */ - km_core_usv lastCtx = ctxtstr.back(); - uint8_t type = c->type; - assert(type == KM_CORE_BT_CHAR || type == KM_CORE_BT_MARKER); - if (type == KM_CORE_BT_CHAR) { - // single char, drop it - charsToDelete--; - assert(c->character == lastCtx); - ctxtstr.pop_back(); - state->actions().push_backspace(KM_CORE_BT_CHAR, lastCtx); // Cause prior char to be removed - } else if (type == KM_CORE_BT_MARKER) { - // it's a marker, 'worth' 3 uchars - assert(charsToDelete >= 3); - assert(lastCtx == c->marker); // end of list - charsToDelete -= 3; - // pop off the three-part sentinel string - ctxtstr.pop_back(); - ctxtstr.pop_back(); - ctxtstr.pop_back(); - // push a special backspace to delete the marker - state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker); - } - } - // now, pop the right number of context items - for (size_t i = 0; i < contextRemoved; i++) { - // we don't pop during the above loop because the iterator gets confused - state->context().pop_back(); - } - // Now, add in the updated text. This will convert UC_SENTINEL, etc back to marker actions. - emit_text(state, outputString); - // If we needed it further. we could update ctxtstr here: - // ctxtstr.append(outputString); - // ... but it is no longer needed at this point. - } // end of transform match - } // end of processing transforms + process_key_string(state, key_str); } // end of processing a 'normal' vk } // end of switch // end of normal processing: commit and exit @@ -339,6 +258,85 @@ ldml_processor::process_event( return KM_CORE_STATUS_OK; } +void +ldml_processor::process_key_string(km_core_state *state, const std::u16string &key_str) const { + // found a string - push it into the context and actions + // we convert it here instead of using the emit_text() overload + // so that we don't have to reconvert it inside the transform code. + std::u32string str32 = kmx::u16string_to_u32string(key_str); + + if (!transforms) { + // No transforms: just emit the string. + emit_text(state, str32); + } else { + // Process transforms here + /** + * a copy of the current/changed context, for transform use. + * + */ + std::u32string ctxtstr; + (void)context_to_string(state, ctxtstr); + // add the newly added key output to ctxtstr + ctxtstr.append(str32); + // and normalize + + /** the output buffer for transforms */ + std::u32string outputString; + + // apply the transform, get how much matched (at the end) + const size_t matchedContext = transforms->apply(ctxtstr, outputString); + + + if (matchedContext == 0) { + // No match, just emit the original string + emit_text(state, str32); + } else { + // We have a match. + + ctxtstr.resize(ctxtstr.length() - str32.length()); + /** how many chars of the context we need to clear */ + auto charsToDelete = matchedContext - str32.length(); /* we don't need to clear the output of the current key */ + + /** how many context items need to be removed */ + size_t contextRemoved = 0; + for (auto c = state->context().rbegin(); charsToDelete > 0 && c != state->context().rend(); c++, contextRemoved++) { + /** last char of context */ + km_core_usv lastCtx = ctxtstr.back(); + uint8_t type = c->type; + assert(type == KM_CORE_BT_CHAR || type == KM_CORE_BT_MARKER); + if (type == KM_CORE_BT_CHAR) { + // single char, drop it + charsToDelete--; + assert(c->character == lastCtx); + ctxtstr.pop_back(); + state->actions().push_backspace(KM_CORE_BT_CHAR, c->character); // Cause prior char to be removed + } else if (type == KM_CORE_BT_MARKER) { + // it's a marker, 'worth' 3 uchars + assert(charsToDelete >= 3); + assert(lastCtx == c->marker); // end of list + charsToDelete -= 3; + // pop off the three-part sentinel string + ctxtstr.pop_back(); + ctxtstr.pop_back(); + ctxtstr.pop_back(); + // push a special backspace to delete the marker + state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker); + } + } + // now, pop the right number of context items + for (size_t i = 0; i < contextRemoved; i++) { + // we don't pop during the above loop because the iterator gets confused + state->context().pop_back(); + } + // Now, add in the updated text. This will convert UC_SENTINEL, etc back to marker actions. + emit_text(state, outputString); + // If we needed it further. we could update ctxtstr here: + // ctxtstr.append(outputString); + // ... but it is no longer needed at this point. + } // end of transform match + } // end of processing transforms +} + km_core_attr const & ldml_processor::attributes() const { return engine_attrs; } diff --git a/core/src/ldml/ldml_processor.hpp b/core/src/ldml/ldml_processor.hpp index e0129115b21..f061ca6e43f 100644 --- a/core/src/ldml/ldml_processor.hpp +++ b/core/src/ldml/ldml_processor.hpp @@ -94,6 +94,9 @@ namespace kbp { /** emit a marker */ static void emit_marker(km_core_state *state, KMX_DWORD marker); + /** process a typed key */ + void process_key_string(km_core_state *state, const std::u16string &key_str) const; + /** * add the string+marker portion of the context to the beginning of str. * Stop when a non-string and non-marker is hit. From 736af4b957e462e05cd3f7f012cc4c9573d15f91 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 12 Oct 2023 15:12:25 -0500 Subject: [PATCH 10/42] =?UTF-8?q?feat(core):=20ldml=20normalization=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change more tests to NFD for now For: #9468 --- core/tests/unit/ldml/keyboards/k_210_marker-test.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml index dd7d7be0092..91fd7d4520f 100644 --- a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml +++ b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml @@ -7,13 +7,13 @@ - + - + From a577bd2f5394aa64f72d224cbbb03983c2b094f8 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 12 Oct 2023 16:57:37 -0500 Subject: [PATCH 11/42] =?UTF-8?q?feat(core):=20ldml=20normalization=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - more progress in the normalization pipeline. Trying to keep it from leaking. - normalize output to NFC. - normalize json test data to NFC (both context and expected). - we do NOT try to normalize the 'embedded' strings currently. - yet more test data fixes (turns out u+00e0 ≠ u+00e8) For: #9468 --- core/src/ldml/ldml_processor.cpp | 175 ++++++++++-------- core/src/ldml/ldml_processor.hpp | 8 +- core/src/ldml/ldml_transforms.cpp | 47 +++++ core/src/ldml/ldml_transforms.hpp | 4 + .../unit/ldml/keyboards/k_003_transform.xml | 2 +- .../keyboards/k_008_transform_norm-test.xml | 2 +- .../ldml/keyboards/k_008_transform_norm.xml | 12 +- core/tests/unit/ldml/ldml_test_source.cpp | 9 +- 8 files changed, 172 insertions(+), 87 deletions(-) diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 6074722ef8b..64237e5b2f8 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -6,6 +6,7 @@ */ #include +#include #include "ldml/ldml_processor.hpp" #include "state.hpp" #include "kmx_file.h" @@ -260,81 +261,100 @@ ldml_processor::process_event( void ldml_processor::process_key_string(km_core_state *state, const std::u16string &key_str) const { - // found a string - push it into the context and actions - // we convert it here instead of using the emit_text() overload - // so that we don't have to reconvert it inside the transform code. - std::u32string str32 = kmx::u16string_to_u32string(key_str); + UErrorCode status = U_ZERO_ERROR; + // We know that key_str is not empty per the caller. - if (!transforms) { - // No transforms: just emit the string. - emit_text(state, str32); + // we convert the keys str to UTF-32 here instead of using the emit_text() overload + // so that we don't have to reconvert it inside the transform code. + std::u32string key_str32 = kmx::u16string_to_u32string(key_str); + // normalize the keystroke to NFD + ldml::normalize_nfd(key_str32, status); + + // extract context string, in NFC + std::u32string old_ctxtstr_nfc; + (void)context_to_string(state, old_ctxtstr_nfc, false); + ldml::normalize_nfc(old_ctxtstr_nfc, status); + assert(U_SUCCESS(status)); + + // context string in NFD + std::u32string ctxtstr; + (void)context_to_string(state, ctxtstr, true); // with markers + // add the newly added key output to ctxtstr + ctxtstr.append(key_str32); + ldml::normalize_nfd(ctxtstr, status); + assert(U_SUCCESS(status)); + + /** transform output string */ + std::u32string outputString; + /** how many chars of the ctxtstr to replace */ + size_t matchedContext = 0; // zero if no transforms + + // begin modifications to the string + + if(transforms) { + matchedContext = transforms->apply(ctxtstr, outputString); } else { - // Process transforms here - /** - * a copy of the current/changed context, for transform use. - * - */ - std::u32string ctxtstr; - (void)context_to_string(state, ctxtstr); - // add the newly added key output to ctxtstr - ctxtstr.append(str32); - // and normalize - - /** the output buffer for transforms */ - std::u32string outputString; - - // apply the transform, get how much matched (at the end) - const size_t matchedContext = transforms->apply(ctxtstr, outputString); - - - if (matchedContext == 0) { - // No match, just emit the original string - emit_text(state, str32); - } else { - // We have a match. - - ctxtstr.resize(ctxtstr.length() - str32.length()); - /** how many chars of the context we need to clear */ - auto charsToDelete = matchedContext - str32.length(); /* we don't need to clear the output of the current key */ - - /** how many context items need to be removed */ - size_t contextRemoved = 0; - for (auto c = state->context().rbegin(); charsToDelete > 0 && c != state->context().rend(); c++, contextRemoved++) { - /** last char of context */ - km_core_usv lastCtx = ctxtstr.back(); - uint8_t type = c->type; - assert(type == KM_CORE_BT_CHAR || type == KM_CORE_BT_MARKER); - if (type == KM_CORE_BT_CHAR) { - // single char, drop it - charsToDelete--; - assert(c->character == lastCtx); - ctxtstr.pop_back(); - state->actions().push_backspace(KM_CORE_BT_CHAR, c->character); // Cause prior char to be removed - } else if (type == KM_CORE_BT_MARKER) { - // it's a marker, 'worth' 3 uchars - assert(charsToDelete >= 3); - assert(lastCtx == c->marker); // end of list - charsToDelete -= 3; - // pop off the three-part sentinel string - ctxtstr.pop_back(); - ctxtstr.pop_back(); - ctxtstr.pop_back(); - // push a special backspace to delete the marker - state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker); - } - } - // now, pop the right number of context items - for (size_t i = 0; i < contextRemoved; i++) { - // we don't pop during the above loop because the iterator gets confused - state->context().pop_back(); - } - // Now, add in the updated text. This will convert UC_SENTINEL, etc back to marker actions. - emit_text(state, outputString); - // If we needed it further. we could update ctxtstr here: - // ctxtstr.append(outputString); - // ... but it is no longer needed at this point. - } // end of transform match - } // end of processing transforms + // no transforms, no output + } + + // drop last 'matchedContext': + ctxtstr.resize(ctxtstr.length() - matchedContext); + ctxtstr.append(outputString); // TODO-LDML: should be able to do a normalization-safe append here. + ldml::normalize_nfd(ctxtstr, status); + assert(U_SUCCESS(status)); + + // Ok. We've done all the happy manipulations. + + /** NFC and no markers */ + std::u32string ctxtstr_cleanedup = ctxtstr; + // TODO-LDML: remove markers! + ldml::normalize_nfc(ctxtstr_cleanedup, status); + + // find common prefix + auto ctxt_prefix = mismatch(old_ctxtstr_nfc.begin(), old_ctxtstr_nfc.end(), ctxtstr_cleanedup.begin(), ctxtstr_cleanedup.end()); + /** the part of the old str that changed */ + std::u32string old_ctxtstr_changed(ctxt_prefix.first,old_ctxtstr_nfc.end()); + std::u32string new_ctxtstr_changed(ctxt_prefix.second,ctxtstr_cleanedup.end()); + + // drop the old suffix. Note: this mutates old_ctxtstr_changed. + remove_text(state, old_ctxtstr_changed, old_ctxtstr_changed.length()); + assert(old_ctxtstr_changed.length() == 0); + emit_text(state, new_ctxtstr_changed); +} + +void +ldml_processor::remove_text(km_core_state *state, std::u32string &str, size_t length) { + /** how many context items need to be removed */ + size_t contextRemoved = 0; + for (auto c = state->context().rbegin(); length > 0 && c != state->context().rend(); c++, contextRemoved++) { + /** last char of context */ + km_core_usv lastCtx = str.back(); + uint8_t type = c->type; + assert(type == KM_CORE_BT_CHAR || type == KM_CORE_BT_MARKER); + if (type == KM_CORE_BT_CHAR) { + // single char, drop it + length--; + assert(c->character == lastCtx); + str.pop_back(); + state->actions().push_backspace(KM_CORE_BT_CHAR, c->character); // Cause prior char to be removed + } else if (type == KM_CORE_BT_MARKER) { + // it's a marker, 'worth' 3 uchars + assert(length >= 3); + assert(lastCtx == c->marker); // end of list + length -= 3; + // pop off the three-part sentinel string + str.pop_back(); + str.pop_back(); + str.pop_back(); + // push a special backspace to delete the marker + state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker); + } + } + // now, pop the right number of context items + for (size_t i = 0; i < contextRemoved; i++) { + // we don't pop during the above loop because the iterator gets confused + state->context().pop_back(); + } } km_core_attr const & ldml_processor::attributes() const { @@ -402,10 +422,10 @@ ldml_processor::emit_marker(km_core_state *state, KMX_DWORD marker_no) { } size_t -ldml_processor::context_to_string(km_core_state *state, std::u32string &str) { +ldml_processor::context_to_string(km_core_state *state, std::u32string &str, bool include_markers) { str.clear(); auto &cp = state->context(); - size_t ctxlen = 0; // TODO-LDML: is this needed? + size_t ctxlen = 0; // TODO-LDML: not used by callers? uint8_t last_type = KM_CORE_BT_UNKNOWN; for (auto c = cp.rbegin(); c != cp.rend(); c++, ctxlen++) { last_type = c->type; @@ -413,7 +433,9 @@ ldml_processor::context_to_string(km_core_state *state, std::u32string &str) { str.insert(0, 1, c->character); } else if (last_type == KM_CORE_BT_MARKER) { assert(km::kbp::kmx::is_valid_marker(c->marker)); - prepend_marker(str, c->marker); + if (include_markers) { + prepend_marker(str, c->marker); + } } else { break; } @@ -421,6 +443,5 @@ ldml_processor::context_to_string(km_core_state *state, std::u32string &str) { return ctxlen; // consumed the entire context buffer. } - } // namespace kbp } // namespace km diff --git a/core/src/ldml/ldml_processor.hpp b/core/src/ldml/ldml_processor.hpp index f061ca6e43f..0845aa70ac9 100644 --- a/core/src/ldml/ldml_processor.hpp +++ b/core/src/ldml/ldml_processor.hpp @@ -93,6 +93,12 @@ namespace kbp { static void emit_text(km_core_state *state, km_core_usv ch); /** emit a marker */ static void emit_marker(km_core_state *state, KMX_DWORD marker); + /** + * Delete text from the state. + * @param str string with text to remove, from the end + * @param length number of chars from the end of str to drop + */ + static void remove_text(km_core_state *state, std::u32string &str, size_t length); /** process a typed key */ void process_key_string(km_core_state *state, const std::u16string &key_str) const; @@ -103,7 +109,7 @@ namespace kbp { * Convert markers into the UC_SENTINEL format. * @return the number of context items consumed */ - static size_t context_to_string(km_core_state *state, std::u32string &str); + static size_t context_to_string(km_core_state *state, std::u32string &str, bool include_markers = true); /** prepend the marker string in UC_SENTINEL format to the str */ inline static void prepend_marker(std::u32string &str, KMX_DWORD marker); diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 842b6e6a638..651919312bf 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -846,6 +846,7 @@ transforms::load( // string +// TODO-LDML: copypasta -> refactor std::u32string &normalize_nfd(std::u32string &str, UErrorCode &status) { const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); if (U_FAILURE(status)) { @@ -873,6 +874,7 @@ std::u32string &normalize_nfd(std::u32string &str, UErrorCode &status) { return str; } +// TODO-LDML: copypasta -> refactor std::u16string &normalize_nfd(std::u16string &str, UErrorCode &status) { const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); if (U_FAILURE(status)) { @@ -888,6 +890,51 @@ std::u16string &normalize_nfd(std::u16string &str, UErrorCode &status) { return str; } +// TODO-LDML: copypasta -> refactor +std::u32string &normalize_nfc(std::u32string &str, UErrorCode &status) { + const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(status); + if (U_FAILURE(status)) { + return str; + } + icu::UnicodeString dest; + const std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); + icu::UnicodeString src = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); + nfc->normalize(src, dest, status); + if (U_FAILURE(status)) { + return str; + } + + UErrorCode preflightStatus = U_ZERO_ERROR; + // calculate how big the buffer is + auto out32len = dest.toUTF32(nullptr, 0, preflightStatus); // preflightStatus will be an err, because we know the buffer overruns zero bytes + // allocate + char32_t *s = new char32_t[out32len + 1]; + assert(s != nullptr); + // convert + dest.toUTF32((UChar32 *)s, out32len + 1, status); + assert(U_SUCCESS(status)); + str.assign(s, out32len); + delete [] s; + return str; +} + +// TODO-LDML: copypasta -> refactor +std::u16string &normalize_nfc(std::u16string &str, UErrorCode &status) { + const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(status); + if (U_FAILURE(status)) { + return str; + } + icu::UnicodeString dest; + icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); + nfc->normalize(src, dest, status); + if (U_FAILURE(status)) { + return str; + } + str.assign(dest.getBuffer(), dest.length()); + return str; +} + + } // namespace ldml diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index 27449f2db87..3a8e3cca428 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -277,6 +277,10 @@ class transforms { std::u32string &normalize_nfd(std::u32string &str, UErrorCode &status); /** Normalize a u16string inplace. Returns a reference to the same string. */ std::u16string &normalize_nfd(std::u16string &str, UErrorCode &status); +/** Normalize a u32string inplace. Returns a reference to the same string. */ +std::u32string &normalize_nfc(std::u32string &str, UErrorCode &status); +/** Normalize a u16string inplace. Returns a reference to the same string. */ +std::u16string &normalize_nfc(std::u16string &str, UErrorCode &status); } // namespace ldml } // namespace kbp diff --git a/core/tests/unit/ldml/keyboards/k_003_transform.xml b/core/tests/unit/ldml/keyboards/k_003_transform.xml index 5575bd01dcc..97984340122 100644 --- a/core/tests/unit/ldml/keyboards/k_003_transform.xml +++ b/core/tests/unit/ldml/keyboards/k_003_transform.xml @@ -4,7 +4,7 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-keyboards.md#element-transform @@keys: [K_Q][K_U][K_BKQUOTE][K_E] -@@expected: que\u0302 +@@expected: qu\u00EA --> diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml index 8fd842ca4a8..025fe36a483 100644 --- a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml @@ -20,7 +20,7 @@ - + diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml index cb4cdb533ad..b66ff1a4b6a 100644 --- a/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml @@ -13,11 +13,11 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke - - - + + + - + @@ -30,7 +30,7 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke - + @@ -46,7 +46,7 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke - + diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 1a5905d1db3..4ad5d5a3a72 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -24,6 +24,7 @@ #include #include "ldml/keyboardprocessor_ldml.h" #include "ldml/ldml_processor.hpp" +#include "ldml/ldml_transforms.hpp" #include "path.hpp" #include "state.hpp" @@ -467,6 +468,9 @@ LdmlJsonTestSource::next_action(ldml_action &fillin) { if (as_check.is_string()) { fillin.type = LDML_ACTION_CHECK_EXPECTED; fillin.string = LdmlTestSource::parse_u8_source_string(as_check.get()); + UErrorCode status = U_ZERO_ERROR; + km::kbp::ldml::normalize_nfc(fillin.string, status); + assert(U_SUCCESS(status)); return; } @@ -485,6 +489,8 @@ LdmlJsonTestSource::next_action(ldml_action &fillin) { if (as_emit.is_string()) { fillin.type = LDML_ACTION_EMIT_STRING; fillin.string = LdmlTestSource::parse_u8_source_string(as_emit.get()); + UErrorCode status = U_ZERO_ERROR; + km::kbp::ldml::normalize_nfc(fillin.string, status); return; } @@ -502,7 +508,8 @@ int LdmlJsonTestSource::load(const nlohmann::json &data) { this->data = data; // TODO-LDML auto startContext = data["/startContext/to"_json_pointer]; context = LdmlTestSource::parse_u8_source_string(startContext); - + UErrorCode status = U_ZERO_ERROR; + km::kbp::ldml::normalize_nfc(context, status); return 0; } From b3de1ffc4abef1bb3dd8a5f69477a0604979adec Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 12 Oct 2023 17:38:38 -0500 Subject: [PATCH 12/42] =?UTF-8?q?feat(core):=20ldml=20normalization=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix windows build For: #9468 --- core/subprojects/packagefiles/icu/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/subprojects/packagefiles/icu/meson.build b/core/subprojects/packagefiles/icu/meson.build index 128fc9fbf97..7b329e27df7 100644 --- a/core/subprojects/packagefiles/icu/meson.build +++ b/core/subprojects/packagefiles/icu/meson.build @@ -35,7 +35,7 @@ uconfig.set('U_ENABLE_DYLOAD', 0) # no DLL uconfig.set('U_CHECK_DYLOAD', 0) # no DLL uconfig.set('UCONFIG_NO_FILE_IO', 1) uconfig.set('UCONFIG_NO_LEGACY_CONVERSION', 1) # turn off file based codepage conversion -uconfig.set('UCONFIG_NO_NORMALIZATION', 1) # TODO-LDML: may want this +uconfig.set('UCONFIG_NO_NORMALIZATION', 0) uconfig.set('UCONFIG_NO_BREAK_ITERATION', 1) # TODO-LDML: may want this uconfig.set('UCONFIG_NO_IDNA', 1) uconfig.set('UCONFIG_NO_COLLATION', 1) From 95ad1c47e426f03c746f5111bd96b3738ed95ead Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 12 Oct 2023 17:46:41 -0500 Subject: [PATCH 13/42] =?UTF-8?q?feat(core):=20ldml=20normalization=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix unnecessary test churn. For: #9468 --- core/tests/unit/ldml/keyboards/k_003_transform.xml | 2 +- core/tests/unit/ldml/keyboards/k_210_marker-test.xml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/tests/unit/ldml/keyboards/k_003_transform.xml b/core/tests/unit/ldml/keyboards/k_003_transform.xml index 97984340122..814b6829d8e 100644 --- a/core/tests/unit/ldml/keyboards/k_003_transform.xml +++ b/core/tests/unit/ldml/keyboards/k_003_transform.xml @@ -4,7 +4,7 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-keyboards.md#element-transform @@keys: [K_Q][K_U][K_BKQUOTE][K_E] -@@expected: qu\u00EA +@@expected: qu\u00ea --> diff --git a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml index 91fd7d4520f..24f104f4d71 100644 --- a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml +++ b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml @@ -7,13 +7,13 @@ - + - + @@ -29,12 +29,12 @@ - + - + From bf15feb1160a0d9994076eda87874f3d6e32e319 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 12 Oct 2023 19:13:52 -0500 Subject: [PATCH 14/42] =?UTF-8?q?feat(core):=20ldml=20normalization=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix int -> auto For: #9468 --- core/tests/unit/ldml/ldml.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp index de0c9e5556c..85ba3ae19c3 100644 --- a/core/tests/unit/ldml/ldml.cpp +++ b/core/tests/unit/ldml/ldml.cpp @@ -340,9 +340,9 @@ int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { << "/" << n.first.c_str() << std::endl; } } - int all_count = json_tests.size(); - int fail_count = failures.size(); - int pass_count = all_count - fail_count; + auto all_count = json_tests.size(); + auto fail_count = failures.size(); + auto pass_count = all_count - fail_count; if (pass_count > 0) { std::wcout << console_color::fg(console_color::GREEN) << " +" << pass_count; } From 6a62d01481421f7184ae536957702d7a564cb6c1 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 13 Oct 2023 15:11:58 -0500 Subject: [PATCH 15/42] =?UTF-8?q?chore(core):=20dx:=20ldml=20test=20subsel?= =?UTF-8?q?ection=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add a filter option to run just one subtest - support backspace event, revamp test JSON format For: #9468 --- .../ldml-keyboard/ldml-keyboard-xml-reader.ts | 13 ++-- core/tests/unit/ldml/ldml.cpp | 64 ++++++++++++++----- core/tests/unit/ldml/ldml_test_source.cpp | 40 ++++++------ .../src/kmc-ldml/test/fixtures/test-fr.json | 40 +++++------- 4 files changed, 90 insertions(+), 67 deletions(-) diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts index 5e7c1ab2d30..b325cefad22 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -393,15 +393,12 @@ export class LDMLKeyboardXMLSourceFileReader { r.stuffBoxes(test, $$, 'startContext'); // singleton // now the actions test.actions = $$.map(v => { - const subtag = v['#name']; - const subv = LDMLKeyboardXMLSourceFileReader.defaultMapper(v, r); - switch(subtag) { - case 'keystroke': return { keystroke: subv }; - case 'check': return { check: subv }; - case 'emit': return { emit: subv }; - case 'startContext': return null; // handled above - default: this.callbacks.reportMessage(CommonTypesMessages.Error_TestDataUnexpectedAction({ subtag })); return null; + const type = v['#name']; // element name + if (type === 'startContext') { + return null; // handled above } + const subv = LDMLKeyboardXMLSourceFileReader.defaultMapper(v, r); + return Object.assign({ type }, subv); }).filter(v => v !== null); return test; }); diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp index 85ba3ae19c3..18841ac0078 100644 --- a/core/tests/unit/ldml/ldml.cpp +++ b/core/tests/unit/ldml/ldml.cpp @@ -213,6 +213,7 @@ run_test(const km::kbp::path &source, const km::kbp::path &compiled, km::tests:: // Run through actions, applying output for each event for (test_source.next_action(action); action.type != km::tests::LDML_ACTION_DONE; test_source.next_action(action)) { + // handle backspace here if (action.type == km::tests::LDML_ACTION_KEY_EVENT) { auto &p = action.k; std::cout << "- key action: 0x" << std::hex << p.vk << "/modifier 0x" << p.modifier_state << std::dec << std::endl; @@ -294,9 +295,12 @@ run_test(const km::kbp::path &source, const km::kbp::path &compiled, km::tests:: /** * Run all tests for this keyboard */ -int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { +int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled, const std::string &filter) { std::wcout << console_color::fg(console_color::BLUE) << "source file = " << source << std::endl << "compiled file = " << compiled << console_color::reset() << std::endl; + if(!filter.empty()) { + std::wcout << "Running only tests matching (substring search): " << filter.c_str() << std::endl; + } km::tests::LdmlEmbeddedTestSource embedded_test_source; @@ -304,7 +308,12 @@ int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { int embedded_result = embedded_test_source.load_source(source); - if (embedded_result == 0) { + if (!filter.empty()) { + // Always skip the embedded test if there's a filter. + std::wcout << console_color::fg(console_color::YELLOW) << "SKIP: " << source.name() << " (embedded)" << console_color::reset() + << std::endl; + embedded_result = 0; // no error + } else if (embedded_result == 0) { // embedded loaded OK, try it std::wcout << console_color::fg(console_color::BLUE) << console_color::bold() << "TEST: " << source.name() << " (embedded)" << console_color::reset() << std::endl; @@ -325,24 +334,32 @@ int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { if (json_result != -1) { const km::tests::JsonTestMap& json_tests = json_factory.get_tests(); + size_t skip_count = 0; assert(json_tests.size() > 0); // Loop over all tests for (const auto& n : json_tests) { - std::wcout << console_color::fg(console_color::BLUE) << console_color::bold() << "TEST: " << json_path.stem().c_str() << "/" << n.first.c_str() << console_color::reset() << std::endl; + const auto test_name = n.first; + auto qq = test_name.find(filter); + if (filter == "--list" || (qq == std::string::npos)) { + skip_count ++; + std::wcout << console_color::fg(console_color::YELLOW) << "SKIP: " << json_path.stem().c_str() << "/" << console_color::bold() << n.first.c_str() << console_color::reset() << std::endl; + continue; + } + std::wcout << console_color::fg(console_color::BLUE) << "TEST: " << json_path.stem().c_str() << "/" << console_color::bold() << n.first.c_str() << console_color::reset() << std::endl; int sub_test = run_test(source, compiled, *n.second); if (sub_test != 0) { - std::wcout << console_color::fg(console_color::BRIGHT_RED) << "FAIL: " << json_path.stem() << "/" << n.first.c_str() + std::wcout << console_color::fg(console_color::BRIGHT_RED) << "FAIL: " << json_path.stem() << "/" << console_color::bold() << n.first.c_str() << console_color::reset() << std::endl; failures.push_back(json_path.stem() + "/" + n.first); json_result = sub_test; // set to last failure } else { - std::wcout << console_color::fg(console_color::GREEN) << " PASS: " << console_color::reset() << json_path.stem() - << "/" << n.first.c_str() << std::endl; + std::wcout << console_color::fg(console_color::GREEN) << "PASS: " << console_color::reset() << json_path.stem() + << "/" << console_color::bold() << n.first.c_str() << std::endl; } } - auto all_count = json_tests.size(); + auto all_count = json_tests.size(); auto fail_count = failures.size(); - auto pass_count = all_count - fail_count; + auto pass_count = all_count - fail_count - skip_count; if (pass_count > 0) { std::wcout << console_color::fg(console_color::GREEN) << " +" << pass_count; } @@ -350,6 +367,10 @@ int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { std::wcout << console_color::fg(console_color::BRIGHT_RED) << " -" << fail_count; } + if (skip_count > 0) { + std::wcout << console_color::fg(console_color::YELLOW) << + " (skipped " << skip_count << ")"; + } std::wcout << console_color::reset() << " of " << all_count << " JSON tests in " << json_path.stem() << std::endl; } @@ -386,10 +407,11 @@ int run_all_tests(const km::kbp::path &source, const km::kbp::path &compiled) { constexpr const auto help_str = "\ -ldml [--color] \n\ +ldml [--color] [ | --list ]\n\ help:\n\ -\tKMN_FILE:\tThe ldml test file for the keyboard under test.\n\ -\tKMX_FILE:\tThe corresponding compiled kmx file.\n"; +\tLDML_FILE:\tThe .xml file for the keyboard under test.\n\ +\tKMX_FILE:\tThe corresponding compiled kmx file.\n\ +\tTEST_FILTER:\tIf present, only run json tests containing the filter substring. --list will list all tests\n"; } // namespace @@ -402,20 +424,28 @@ int error_args() { int main(int argc, char *argv[]) { int first_arg = 1; - if (argc < 3) { + if ((argc - first_arg) < 2) { // if < 2 remaining args return error_args(); } - auto arg_color = std::string(argv[1]) == "--color"; + auto arg_color = std::string(argv[first_arg]) == "--color"; if(arg_color) { first_arg++; - if(argc < 4) { - return error_args(); - } } console_color::enabled = console_color::isaterminal() || arg_color; - int rc = run_all_tests(argv[first_arg], argv[first_arg + 1]); + if ((argc - first_arg) < 2) { + return error_args(); + } + const km::kbp::path ldml_file = argv[first_arg++]; + const km::kbp::path kmx_file = argv[first_arg++]; + + std::string filter; // default to 'all tests' + if ((argc - first_arg) >= 1) { + filter = argv[first_arg++]; + } + + int rc = run_all_tests(ldml_file, kmx_file, filter); if (rc != EXIT_SUCCESS) { std::wcerr << console_color::fg(console_color::BRIGHT_RED) << "FAILED" << console_color::reset() << std::endl; rc = EXIT_FAILURE; diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 4ad5d5a3a72..e4f961fba8f 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -462,40 +462,42 @@ LdmlJsonTestSource::next_action(ldml_action &fillin) { action_index++; auto action = data["/actions"_json_pointer].at(action_index); + // load up several common attributes + auto type = action["/type"_json_pointer]; + auto result = action["/result"_json_pointer]; + auto key = action["/key"_json_pointer]; + auto to = action["/to"_json_pointer]; // is it a check event? - auto as_check = action["/check/result"_json_pointer]; - if (as_check.is_string()) { + if (type == "check") { fillin.type = LDML_ACTION_CHECK_EXPECTED; - fillin.string = LdmlTestSource::parse_u8_source_string(as_check.get()); + fillin.string = LdmlTestSource::parse_u8_source_string(result.get()); UErrorCode status = U_ZERO_ERROR; km::kbp::ldml::normalize_nfc(fillin.string, status); assert(U_SUCCESS(status)); return; - } - - // is it a keystroke by id? - auto as_key = action["/keystroke/key"_json_pointer]; - if (as_key.is_string()) { + } else if (type == "keystroke") { fillin.type = LDML_ACTION_KEY_EVENT; - auto keyId = LdmlTestSource::parse_u8_source_string(as_key.get()); + auto keyId = LdmlTestSource::parse_u8_source_string(key.get()); // now, look up the key set_key_from_id(fillin.k, keyId); return; - } - // TODO-LDML: handle gesture, etc - - auto as_emit = action["/emit/to"_json_pointer]; - if (as_emit.is_string()) { + } else if (type == "emit") { fillin.type = LDML_ACTION_EMIT_STRING; - fillin.string = LdmlTestSource::parse_u8_source_string(as_emit.get()); + fillin.string = LdmlTestSource::parse_u8_source_string(to.get()); UErrorCode status = U_ZERO_ERROR; km::kbp::ldml::normalize_nfc(fillin.string, status); return; + } else if (type == "backspace") { + // backspace is handled as a key event + fillin.type = LDML_ACTION_KEY_EVENT; + fillin.k.modifier_state = 0; + fillin.k.vk = KM_CORE_VKEY_BKSP; + return; } // TODO-LDML: error passthrough - std::cerr << "TODO-LDML: Error, unknown/unhandled action: " << action << std::endl; + std::cerr << "TODO-LDML: Error, unknown/unhandled action: " << type << std::endl; fillin.type = LDML_ACTION_DONE; } @@ -711,7 +713,9 @@ int LdmlJsonTestSourceFactory::load(const km::kbp::path &compiled, const km::kbp auto info_author = data["/keyboardTest3/info/author"_json_pointer].get(); auto info_name = data["/keyboardTest3/info/name"_json_pointer].get(); // TODO-LDML: store these elsewhere? - std::cout << "JSON: reading " << info_name << " test of " << info_keyboard << " by " << info_author << std::endl; + std::wcout << console_color::fg(console_color::BLUE) << "test file = " << path.name().c_str() << console_color::reset() << std::endl; + std::wcout << console_color::fg(console_color::YELLOW) << info_name.c_str() << "/ " << console_color::reset() + << " test: " << info_keyboard.c_str() << " author: " << info_author.c_str() << std::endl; auto all_tests = data["/keyboardTest3/tests"_json_pointer]; assert_or_return((!all_tests.empty()) && (all_tests.size() > 0)); // TODO-LDML: can be empty if repertoire only? @@ -722,7 +726,7 @@ int LdmlJsonTestSourceFactory::load(const km::kbp::path &compiled, const km::kbp auto test_name = test["/name"_json_pointer].get(); std::string test_path; test_path.append(info_name).append("/tests/").append(tests_name).append("/").append(test_name); - std::cout << "JSON: reading " << info_name << "/" << test_path << std::endl; + // std::cout << "JSON: reading " << info_name << "/" << test_path << std::endl; std::unique_ptr subtest(new LdmlJsonTestSource(test_path, kmxplus.get())); assert_or_return(subtest->load(test) == 0); diff --git a/developer/src/kmc-ldml/test/fixtures/test-fr.json b/developer/src/kmc-ldml/test/fixtures/test-fr.json index 895360dd85f..28694d9534e 100644 --- a/developer/src/kmc-ldml/test/fixtures/test-fr.json +++ b/developer/src/kmc-ldml/test/fixtures/test-fr.json @@ -29,44 +29,36 @@ }, "actions": [ { - "keystroke": { - "key": "s" - } + "type": "keystroke", + "key": "s" }, { - "check": { - "result": "abc\\u0022...s" - } + "type": "check", + "result": "abc\\u0022...s" }, { - "keystroke": { - "key": "t" - } + "type": "keystroke", + "key": "t" }, { - "check": { - "result": "abc\\u0022...st" - } + "type": "check", + "result": "abc\\u0022...st" }, { - "keystroke": { - "key": "u" - } + "type": "keystroke", + "key": "u" }, { - "check": { - "result": "abc\\u0022...stu" - } + "type": "check", + "result": "abc\\u0022...stu" }, { - "emit": { - "to": "v" - } + "type": "emit", + "to": "v" }, { - "check": { - "result": "abc\\u0022...stuv" - } + "type": "check", + "result": "abc\\u0022...stuv" } ] } From 55bf0642a5077aed65e4afc1a02df3d6def5a7f0 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 13 Oct 2023 16:56:00 -0500 Subject: [PATCH 16/42] =?UTF-8?q?feat(core):=20add=20backspace=20test=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test needs to be "t" not "ta" For: #9760 --- .../unit/ldml/keyboards/k_006_backspace-test.xml | 15 +++++++++++++++ .../tests/unit/ldml/keyboards/k_006_backspace.xml | 7 ------- core/tests/unit/ldml/keyboards/meson.build | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 core/tests/unit/ldml/keyboards/k_006_backspace-test.xml diff --git a/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml new file mode 100644 index 00000000000..7ef89381aaa --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_006_backspace.xml b/core/tests/unit/ldml/keyboards/k_006_backspace.xml index b3f126eeabe..940da405e15 100644 --- a/core/tests/unit/ldml/keyboards/k_006_backspace.xml +++ b/core/tests/unit/ldml/keyboards/k_006_backspace.xml @@ -1,12 +1,5 @@ - diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index 9dc2efc263a..079d1e4d1d6 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -21,7 +21,6 @@ tests_without_testdata = [ 'k_003_transform', 'k_004_tinyshift', 'k_005_modbittest', - # 'k_006_backspace', ## not quite there yet. TODO-LDML 'k_010_mt', 'k_011_mt_iso', 'k_100_keytest', @@ -33,6 +32,7 @@ tests_without_testdata = [ # These tests have a k_001_tiny-test.xml file as well. tests_with_testdata = [ 'k_001_tiny', + 'k_006_backspace', 'k_007_transform_rgx', 'k_008_transform_norm', 'k_020_fr', # TODO-LDML: move to cldr above (fix vkey) From f4501fea813c924ee487c1c3a6d1b943e223543b Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 13 Oct 2023 17:38:08 -0500 Subject: [PATCH 17/42] =?UTF-8?q?chore(developer):=20remove=20an=20unused?= =?UTF-8?q?=20error=20code=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/web/types/src/util/common-events.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/common/web/types/src/util/common-events.ts b/common/web/types/src/util/common-events.ts index b8b33005504..65743d686d8 100644 --- a/common/web/types/src/util/common-events.ts +++ b/common/web/types/src/util/common-events.ts @@ -43,8 +43,4 @@ export class CommonTypesMessages { m(this.ERROR_TestDataUnexpectedArray, `Problem reading test data: expected single ${o.subtag} element, found multiple`); static ERROR_TestDataUnexpectedArray = SevError | 0x0007; - static Error_TestDataUnexpectedAction = (o: {subtag: string}) => - m(this.ERROR_TestDataUnexpectedAction, - `Problem reading test data: unexpected action element ${o.subtag}`); - static ERROR_TestDataUnexpectedAction = SevError | 0x0008; }; From f89be2d21b16b9e3ef2ae24e4af75de70c9b9385 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Mon, 16 Oct 2023 10:24:49 -0500 Subject: [PATCH 18/42] =?UTF-8?q?fix(common):=20ldml=20test=20fix=20?= =?UTF-8?q?=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the wrong test case was updated previously For: #9468 --- .../ldml-keyboard-testdata-xml.ts | 33 +++++++++++-------- .../test-ldml-keyboard-testdata-reader.ts | 22 +++++++------ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts index 86a05e38934..acbcf0ead44 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts @@ -41,34 +41,41 @@ export interface LKTTests { export interface LKTTest { name?: string; startContext?: LKTStartContext; - actions?: LKTAction[]; // differs from XML, to represent order of actions + actions?: LKTAnyAction[]; // differs from XML, to represent order of actions }; export interface LKTStartContext { to?: string; }; -export interface LKTCheck { +/** + * Test Actions. + * The expectation is that each LKTAction object will have exactly one non-falsy field. + */ +export interface LKTAction { + type?: "check" | "emit" | "keystroke" | "backspace"; +}; + +export interface LKTCheck extends LKTAction { + type: "check"; result?: string; }; -export interface LKTEmit { +export interface LKTEmit extends LKTAction { + type: "emit"; to?: string; }; -export interface LKTKeystroke { +export interface LKTKeystroke extends LKTAction { + type: "keystroke"; key?: string; flick?: string; longPress?: string; tapCount?: string; }; -/** - * Test Actions. - * The expectation is that each LKTAction object will have exactly one non-falsy field. - */ -export interface LKTAction { - check?: LKTCheck; - emit?: LKTEmit; - keystroke?: LKTKeystroke; -}; +export interface LKTBackspace extends LKTAction { + type: "backspace"; +} + +export type LKTAnyAction = LKTCheck | LKTEmit | LKTKeystroke | LKTBackspace; diff --git a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts b/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts index 80a78a1187b..f42debfe933 100644 --- a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts +++ b/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts @@ -2,6 +2,7 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; import { assert } from 'chai'; import 'mocha'; import { testTestdataReaderCases } from '../helpers/reader-callback-test.js'; +import { LKTAnyAction } from './ldml-keyboard-testdata-xml.js'; describe('ldml keyboard xml reader tests', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while @@ -35,16 +36,17 @@ describe('ldml keyboard xml reader tests', function () { const test0 = source.keyboardTest3.tests[0].test[0]; assert.equal('key-test', test0.name); assert.equal('abc\\u0022...', test0.startContext?.to); - assert.sameDeepOrderedMembers([ - { keystroke: { key: 's' } }, - { check: { result: 'abc\\u0022...s' } }, - { keystroke: { key: 't' } }, - { check: { result: 'abc\\u0022...st' } }, - { keystroke: { key: 'u' } }, - { check: { result: 'abc\\u0022...stu' } }, - { emit: { to: 'v' } }, - { check: { result: 'abc\\u0022...stuv' } }, - ], test0.actions); + const expectedActions : LKTAnyAction[] = [ + { type: "keystroke", key: 's' }, + { type: "check", result: 'abc\\u0022...s' }, + { type: "keystroke", key: 't' }, + { type: "check", result: 'abc\\u0022...st' }, + { type: "keystroke", key: 'u' }, + { type: "check", result: 'abc\\u0022...stu' }, + { type: "emit", to: 'v' }, + { type: "check", result: 'abc\\u0022...stuv' }, + ]; + assert.sameDeepOrderedMembers(expectedActions, test0.actions); }, } ]); From d46e08267ef010f62ffbdfcb46c66c546c1f4c7e Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Mon, 16 Oct 2023 10:52:39 -0500 Subject: [PATCH 19/42] =?UTF-8?q?fix(common,developer):=20ldml=20test=20fi?= =?UTF-8?q?x=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - There are two cases. Added comments to cross-link them. For: #9468 --- common/web/types/test/fixtures/test-fr.xml | 8 ++++++++ .../ldml-keyboard/test-ldml-keyboard-testdata-reader.ts | 5 ++++- developer/src/kmc-ldml/test/fixtures/test-fr.json | 7 +++++++ developer/src/kmc-ldml/test/fixtures/test-fr.xml | 3 +++ developer/src/kmc-ldml/test/test-testdata-e2e.ts | 3 ++- 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/common/web/types/test/fixtures/test-fr.xml b/common/web/types/test/fixtures/test-fr.xml index cfd63c4f758..c705605521b 100644 --- a/common/web/types/test/fixtures/test-fr.xml +++ b/common/web/types/test/fixtures/test-fr.xml @@ -1,6 +1,12 @@ + @@ -17,6 +23,8 @@ + + diff --git a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts b/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts index f42debfe933..a0e1202f851 100644 --- a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts +++ b/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts @@ -9,6 +9,7 @@ describe('ldml keyboard xml reader tests', function () { testTestdataReaderCases([ { + // Note! There's another test case against similar data, in developer/src/kmc-ldml/test/test-testdata-e2e.ts using test-fr.json subpath: 'test-fr.xml', callback: (data, source) => { assert.ok(source); @@ -45,8 +46,10 @@ describe('ldml keyboard xml reader tests', function () { { type: "check", result: 'abc\\u0022...stu' }, { type: "emit", to: 'v' }, { type: "check", result: 'abc\\u0022...stuv' }, + { type: "backspace" }, + { type: "check", result: 'abc\\u0022...stu' }, ]; - assert.sameDeepOrderedMembers(expectedActions, test0.actions); + assert.sameDeepOrderedMembers(expectedActions, test0.actions, 'Static data in .ts file should match parsed test-fr.xml'); }, } ]); diff --git a/developer/src/kmc-ldml/test/fixtures/test-fr.json b/developer/src/kmc-ldml/test/fixtures/test-fr.json index 28694d9534e..9710e3758a4 100644 --- a/developer/src/kmc-ldml/test/fixtures/test-fr.json +++ b/developer/src/kmc-ldml/test/fixtures/test-fr.json @@ -59,6 +59,13 @@ { "type": "check", "result": "abc\\u0022...stuv" + }, + { + "type": "backspace" + }, + { + "type": "check", + "result": "abc\\u0022...stu" } ] } diff --git a/developer/src/kmc-ldml/test/fixtures/test-fr.xml b/developer/src/kmc-ldml/test/fixtures/test-fr.xml index cfd63c4f758..519140174fb 100644 --- a/developer/src/kmc-ldml/test/fixtures/test-fr.xml +++ b/developer/src/kmc-ldml/test/fixtures/test-fr.xml @@ -1,6 +1,7 @@ + @@ -17,6 +18,8 @@ + + diff --git a/developer/src/kmc-ldml/test/test-testdata-e2e.ts b/developer/src/kmc-ldml/test/test-testdata-e2e.ts index 3b706fd4cc3..eea3631759d 100644 --- a/developer/src/kmc-ldml/test/test-testdata-e2e.ts +++ b/developer/src/kmc-ldml/test/test-testdata-e2e.ts @@ -9,6 +9,7 @@ describe('testdata-tests', function() { it('should-build-testdata-fixtures', async function() { // Let's build test-fr.json // It should match test-fr.json (built from test-fr.xml) + // Note! There's another test case against similar data, in common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts const inputFilename = makePathToFixture('test-fr.xml'); const jsonFilename = makePathToFixture('test-fr.json'); @@ -19,6 +20,6 @@ describe('testdata-tests', function() { const jsonData = JSON.parse(readFileSync(jsonFilename, 'utf-8')); - assert.deepEqual(testData, jsonData); + assert.deepEqual(testData, jsonData, 'parsed +test-fr.xml should match -test-fr.json'); }); }); From 9ee183c0eec9f4c12b65c74239250827778b7da4 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Mon, 16 Oct 2023 17:21:39 -0500 Subject: [PATCH 20/42] =?UTF-8?q?fix(core):=20ldml=20update=20to=20normali?= =?UTF-8?q?zation=20per=20review=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - simplify normalize_nfc and normalize_nfd functions For: #9468 --- core/src/ldml/ldml_transforms.cpp | 56 +++++++------------------------ 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 651919312bf..f9027755044 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -848,29 +848,13 @@ transforms::load( // TODO-LDML: copypasta -> refactor std::u32string &normalize_nfd(std::u32string &str, UErrorCode &status) { - const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); - if (U_FAILURE(status)) { - return str; - } - icu::UnicodeString dest; - const std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); - icu::UnicodeString src = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); - nfd->normalize(src, dest, status); - if (U_FAILURE(status)) { - return str; - } + std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); - UErrorCode preflightStatus = U_ZERO_ERROR; - // calculate how big the buffer is - auto out32len = dest.toUTF32(nullptr, 0, preflightStatus); // preflightStatus will be an err, because we know the buffer overruns zero bytes - // allocate - char32_t *s = new char32_t[out32len + 1]; - assert(s != nullptr); - // convert - dest.toUTF32((UChar32 *)s, out32len + 1, status); - assert(U_SUCCESS(status)); - str.assign(s, out32len); - delete [] s; + normalize_nfd(rstr, status); + if (U_SUCCESS(status)) { + // if failure, leave it alone + str = km::kbp::kmx::u16string_to_u32string(rstr); + } return str; } @@ -892,29 +876,13 @@ std::u16string &normalize_nfd(std::u16string &str, UErrorCode &status) { // TODO-LDML: copypasta -> refactor std::u32string &normalize_nfc(std::u32string &str, UErrorCode &status) { - const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(status); - if (U_FAILURE(status)) { - return str; - } - icu::UnicodeString dest; - const std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); - icu::UnicodeString src = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); - nfc->normalize(src, dest, status); - if (U_FAILURE(status)) { - return str; - } + std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); - UErrorCode preflightStatus = U_ZERO_ERROR; - // calculate how big the buffer is - auto out32len = dest.toUTF32(nullptr, 0, preflightStatus); // preflightStatus will be an err, because we know the buffer overruns zero bytes - // allocate - char32_t *s = new char32_t[out32len + 1]; - assert(s != nullptr); - // convert - dest.toUTF32((UChar32 *)s, out32len + 1, status); - assert(U_SUCCESS(status)); - str.assign(s, out32len); - delete [] s; + normalize_nfc(rstr, status); + if (U_SUCCESS(status)) { + // if failure, leave it alone + str = km::kbp::kmx::u16string_to_u32string(rstr); + } return str; } From 93e1c18af2b080f1e698af3697caf6ea0f0d28fb Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Mon, 16 Oct 2023 20:22:52 -0500 Subject: [PATCH 21/42] =?UTF-8?q?fix(core):=20ldml=20update=20to=20normali?= =?UTF-8?q?zation=20per=20review=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For: #9468 Co-authored-by: Marc Durdin --- core/src/ldml/ldml_processor.cpp | 4 +++- core/src/ldml/ldml_transforms.hpp | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 64237e5b2f8..cbb9d9f57be 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -263,11 +263,13 @@ void ldml_processor::process_key_string(km_core_state *state, const std::u16string &key_str) const { UErrorCode status = U_ZERO_ERROR; // We know that key_str is not empty per the caller. + assert(!key_str.empty()); // we convert the keys str to UTF-32 here instead of using the emit_text() overload // so that we don't have to reconvert it inside the transform code. std::u32string key_str32 = kmx::u16string_to_u32string(key_str); - // normalize the keystroke to NFD + ldml::normalize_nfd(key_str32, status); + assert(U_SUCCESS(status)); ldml::normalize_nfd(key_str32, status); // extract context string, in NFC diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index 3a8e3cca428..f482e56a992 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -273,13 +273,13 @@ class transforms { // string routines -/** Normalize a u32string inplace. Returns a reference to the same string. */ +/** Normalize a u32string inplace to NFD. Returns a reference to the same string. */ std::u32string &normalize_nfd(std::u32string &str, UErrorCode &status); -/** Normalize a u16string inplace. Returns a reference to the same string. */ +/** Normalize a u16string inplace to NFD. Returns a reference to the same string. */ std::u16string &normalize_nfd(std::u16string &str, UErrorCode &status); -/** Normalize a u32string inplace. Returns a reference to the same string. */ +/** Normalize a u32string inplace to NFC. Returns a reference to the same string. */ std::u32string &normalize_nfc(std::u32string &str, UErrorCode &status); -/** Normalize a u16string inplace. Returns a reference to the same string. */ +/** Normalize a u16string inplace to NFC. Returns a reference to the same string. */ std::u16string &normalize_nfc(std::u16string &str, UErrorCode &status); } // namespace ldml From cbe9ef77418092c9caf5628cb77016c2a2843f61 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Tue, 17 Oct 2023 09:11:21 +0700 Subject: [PATCH 22/42] added logging to learn about menu execution --- mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m | 3 +++ mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 1607e915e07..0e17d84c008 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -213,7 +213,10 @@ - (void)menuAction:(id)sender { } else if (itag >= 1000) { NSMenuItem *keyboards = [self.AppDelegate.menu itemWithTag:1]; + NSString *title = keyboards.title; + NSLog(@"Input Menu, selected Keyboards menu, itag: %lu, title: %@", itag, title); for (NSMenuItem *item in keyboards.submenu.itemArray) { + NSLog(@"menu item, itag: %lu, title: %@", item.tag, item.title); if (item.tag == itag) [item setState:NSOnState]; else diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 44df2ef6fa2..a7b3d69038d 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -781,6 +781,8 @@ - (void)setKeyboardsSubMenu { KVKFile *kvk = nil; BOOL didSetKeyboard = NO; NSInteger itag = 1000; + NSString *keyboardMenuName = @""; + [keyboards.submenu removeAllItems]; for (NSString *path in self.activeKeyboards) { NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:path]; @@ -788,6 +790,8 @@ - (void)setKeyboardsSubMenu { continue; //NSString *str = [NSString stringWithFormat:@"%@ (%@)", [infoDict objectForKey:kKMKeyboardNameKey], [infoDict objectForKey:kKMKeyboardVersionKey]]; NSString *str = [infoDict objectForKey:kKMKeyboardNameKey]; + // for debugging + keyboardMenuName = str; NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:str action:@selector(menuAction:) keyEquivalent:@""]; [item setTag:itag++]; if ([path isEqualToString:self.selectedKeyboard]) { @@ -811,6 +815,8 @@ - (void)setKeyboardsSubMenu { else [item setState:NSOffState]; + NSLog(@"*** adding item to keyboards menu, itag=%lu, keyboard=%@", itag, keyboardMenuName); + [keyboards.submenu addItem:item]; } From d94b84154febdf404e915d2ecc8f9ac341fabb22 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:53:19 +1000 Subject: [PATCH 23/42] docs(windows): remove text_services_framework page Removed the text services framework page and all links --- windows/src/desktop/help/advanced/index.md | 1 - .../help/advanced/text_services_framework.md | 29 ------------------- windows/src/desktop/help/index.md | 1 - 3 files changed, 31 deletions(-) delete mode 100644 windows/src/desktop/help/advanced/text_services_framework.md diff --git a/windows/src/desktop/help/advanced/index.md b/windows/src/desktop/help/advanced/index.md index 46d35ac6642..371f85521d7 100644 --- a/windows/src/desktop/help/advanced/index.md +++ b/windows/src/desktop/help/advanced/index.md @@ -4,4 +4,3 @@ title: Advanced Help * [proxy_config](proxy_config) * [locale_edit](locale_edit) -* [text_services_framework](text_services_framework) diff --git a/windows/src/desktop/help/advanced/text_services_framework.md b/windows/src/desktop/help/advanced/text_services_framework.md deleted file mode 100644 index 650196c0e92..00000000000 --- a/windows/src/desktop/help/advanced/text_services_framework.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Text Services Framework ---- - -## What is the Text Services Framework? - -The Text Services Framework, or TSF, is a set of components for modern -applications that support advanced input and text processing. It -supports features such as keyboard drivers, handwriting recognition, -speech recognition, as well as spell checking and other text processing -functions. - -With Keyman Desktop 9 and later versions, all keyboards are registered -through the Windows interfaces, and the key advantage is that Keyman now -automatically detects applications that have support for TSF and -upgrades the experience for those applications. - -For those applications that support TSF, the most important advantage is -that Keyman can read the current 'context' from the application. The -'context' is the characters on the screen around the insertion point. -Earlier versions of Keyman remembered the context while inputting text, -but as soon as an arrow key was pressed, or the mouse clicked, Keyman -would forget the context. This means that existing text can be edited in -a much more intuitive manner when using TSF. - -## What applications support TSF? - -TSF is supported by a range of software and Windows components. -This includes: MS Office Applications, Firefox, Adobe Create Cloud Apps, Notepad in Windows 11 diff --git a/windows/src/desktop/help/index.md b/windows/src/desktop/help/index.md index 6dd83bca6fb..f5d5ddf3780 100644 --- a/windows/src/desktop/help/index.md +++ b/windows/src/desktop/help/index.md @@ -33,7 +33,6 @@ frequently asked questions, tutorials, and videos. ### [Advanced Topics](advanced/) * [Character Map](basic/toolbox/character-map) * [Hotkeys](start/hotkey_set) -* [Text Services Framework Addin](advanced/text_services_framework) * [Use Word macros to switch keyboard and font](https://help.keyman.com/kb/93) * [Application Programming Interface (API)](https://help.keyman.com/developer/engine/desktop/current-version/api/) * [Keyman for Windows version history](https://help.keyman.com/products/windows/version-history) From 4a3556ca89b4f3e279264d2785714191a8d81080 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 18 Oct 2023 16:06:28 +0200 Subject: [PATCH 24/42] chore(linux): Allow to collect coverage on TC --- docs/linux/README.md | 3 +++ linux/keyman-config/run-tests.sh | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/linux/README.md b/docs/linux/README.md index 8ee91fbe81e..fe29b0d623a 100644 --- a/docs/linux/README.md +++ b/docs/linux/README.md @@ -102,6 +102,9 @@ sudo apt install -y lcov libdatetime-perl gcovr pip3 install coverage ``` +**Note:** You want lcov > 1.16, so you might have to download and install +a newer version e.g. from . + #### Creating and displaying code coverage reports All three projects (ibus-keyman, keyman-config, and keyman-system-service) diff --git a/linux/keyman-config/run-tests.sh b/linux/keyman-config/run-tests.sh index de81ec7f738..de99c55e8f2 100755 --- a/linux/keyman-config/run-tests.sh +++ b/linux/keyman-config/run-tests.sh @@ -16,10 +16,13 @@ if [ -n "$TEAMCITY_VERSION" ]; then if ! pip3 list --format=columns | grep -q teamcity-messages; then pip3 install teamcity-messages fi - python3 -m teamcity.unittestpy discover -s tests -p test_*.py + test_module=teamcity.unittestpy else - # shellcheck disable=SC2086 - python3 ${coverage:-} -m unittest discover -v -s tests -p test_*.py + test_module=unittest + extra_opts=-v fi +# shellcheck disable=SC2086 +python3 ${coverage:-} -m ${test_module:-} discover ${extra_opts:-} -s tests -p test_*.py + rm -rf "$XDG_CONFIG_HOME" From 09907b66e0a80fabcceb4a7c4f67b5866ac31ad8 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 18 Oct 2023 18:07:54 +0200 Subject: [PATCH 25/42] chore(linux): Additionally specify regular schemas path --- linux/keyman-config/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/keyman-config/build.sh b/linux/keyman-config/build.sh index 35608c69eb7..9c0588f796b 100755 --- a/linux/keyman-config/build.sh +++ b/linux/keyman-config/build.sh @@ -46,7 +46,7 @@ execute_with_temp_schema() { TEMP_DATA_DIR=$(mktemp -d) SCHEMA_DIR="${TEMP_DATA_DIR}/glib-2.0/schemas" export XDG_DATA_DIRS="${TEMP_DATA_DIR}":${XDG_DATA_DIRS-} - export GSETTINGS_SCHEMA_DIR="${SCHEMA_DIR}" + export GSETTINGS_SCHEMA_DIR="${SCHEMA_DIR}:/usr/share/glib-2.0/schemas/:${GSETTINGS_SCHEMA_DIR-}" mkdir -p "${SCHEMA_DIR}" cp resources/com.keyman.gschema.xml "${SCHEMA_DIR}"/ glib-compile-schemas "${SCHEMA_DIR}" From d9654e0e2e5d218213aa3234336e7973ca046e27 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Wed, 18 Oct 2023 14:07:53 -0400 Subject: [PATCH 26/42] auto: increment master version to 17.0.195 --- HISTORY.md | 9 +++++++++ VERSION.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index e1b0f257c92..bde203e3a78 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,14 @@ # Keyman Version History +## 17.0.194 alpha 2023-10-18 + +* chore(linux): Re-enable building for Ubuntu 23.10 Mantic (#9780) +* chore(linux): Add missing tests (#9783) +* fix(web): fixes touch form-factor default kbd on cookieless keymanweb.com page load (#9786) +* fix(developer): three kmc .keyboard_info generation bugs (#9784) +* fix(developer): handle invalid project folders cleanly (#9785) +* chore(linux): Fix build scripts (#9781) + ## 17.0.193 alpha 2023-10-17 * fix(developer): kmc crash on start (#9771) diff --git a/VERSION.md b/VERSION.md index 2c5f2b0026a..d684cef10f4 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.194 \ No newline at end of file +17.0.195 \ No newline at end of file From 9dbf70c94ea71c3313b80d31f903778c20182bbd Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Wed, 18 Oct 2023 17:47:56 -0500 Subject: [PATCH 27/42] =?UTF-8?q?fix(core):=20ldml=20more=20updates=20to?= =?UTF-8?q?=20normalization=20per=20review=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - verify marker string as it's popped off - add a missing pragma to debuglog.h - move UErrorCode out of ldml_processor, change normalization functions to return true on success For: #9468 --- core/src/debuglog.h | 2 + core/src/ldml/ldml_processor.cpp | 21 +++---- core/src/ldml/ldml_transforms.cpp | 77 ++++++++++------------- core/src/ldml/ldml_transforms.hpp | 24 ++++--- core/tests/unit/ldml/ldml_test_source.cpp | 10 +-- 5 files changed, 63 insertions(+), 71 deletions(-) diff --git a/core/src/debuglog.h b/core/src/debuglog.h index 1ecbaca6124..5b547132e7f 100644 --- a/core/src/debuglog.h +++ b/core/src/debuglog.h @@ -1,5 +1,7 @@ /* Debugging */ +#pragma once + #include namespace km { diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index cbb9d9f57be..5d995be4184 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -261,30 +261,25 @@ ldml_processor::process_event( void ldml_processor::process_key_string(km_core_state *state, const std::u16string &key_str) const { - UErrorCode status = U_ZERO_ERROR; // We know that key_str is not empty per the caller. assert(!key_str.empty()); // we convert the keys str to UTF-32 here instead of using the emit_text() overload // so that we don't have to reconvert it inside the transform code. std::u32string key_str32 = kmx::u16string_to_u32string(key_str); - ldml::normalize_nfd(key_str32, status); - assert(U_SUCCESS(status)); - ldml::normalize_nfd(key_str32, status); + assert(ldml::normalize_nfd(key_str32)); // TODO-LDML: else fail? // extract context string, in NFC std::u32string old_ctxtstr_nfc; (void)context_to_string(state, old_ctxtstr_nfc, false); - ldml::normalize_nfc(old_ctxtstr_nfc, status); - assert(U_SUCCESS(status)); + assert(ldml::normalize_nfc(old_ctxtstr_nfc)); // TODO-LDML: else fail? // context string in NFD std::u32string ctxtstr; (void)context_to_string(state, ctxtstr, true); // with markers // add the newly added key output to ctxtstr ctxtstr.append(key_str32); - ldml::normalize_nfd(ctxtstr, status); - assert(U_SUCCESS(status)); + assert(ldml::normalize_nfd(ctxtstr)); // TODO-LDML: else fail? /** transform output string */ std::u32string outputString; @@ -302,15 +297,14 @@ ldml_processor::process_key_string(km_core_state *state, const std::u16string &k // drop last 'matchedContext': ctxtstr.resize(ctxtstr.length() - matchedContext); ctxtstr.append(outputString); // TODO-LDML: should be able to do a normalization-safe append here. - ldml::normalize_nfd(ctxtstr, status); - assert(U_SUCCESS(status)); + assert(ldml::normalize_nfd(ctxtstr)); // TODO-LDML: else fail? // Ok. We've done all the happy manipulations. /** NFC and no markers */ std::u32string ctxtstr_cleanedup = ctxtstr; // TODO-LDML: remove markers! - ldml::normalize_nfc(ctxtstr_cleanedup, status); + assert(ldml::normalize_nfc(ctxtstr_cleanedup)); // TODO-LDML: else fail? // find common prefix auto ctxt_prefix = mismatch(old_ctxtstr_nfc.begin(), old_ctxtstr_nfc.end(), ctxtstr_cleanedup.begin(), ctxtstr_cleanedup.end()); @@ -344,9 +338,12 @@ ldml_processor::remove_text(km_core_state *state, std::u32string &str, size_t le assert(length >= 3); assert(lastCtx == c->marker); // end of list length -= 3; - // pop off the three-part sentinel string + // pop off the three-part sentinel string (in reverse order of course) + assert(str.back() == c->marker); // marker # str.pop_back(); + assert(str.back() == LDML_MARKER_CODE); str.pop_back(); + assert(str.back() == LDML_UC_SENTINEL); str.pop_back(); // push a special backspace to delete the marker state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker); diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index f9027755044..0ee01204658 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -470,7 +470,7 @@ transform_entry::init() { // NFD normalize on pattern creation nfd->normalize(patustr_raw, patustr, status); fFromPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); - assert(U_SUCCESS(status)); // TODO-LDML: may be best to propagate status up ^^ + UASSERT_SUCCESS(status); } } @@ -484,7 +484,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons icu::UnicodeString matchustr = icu::UnicodeString(matchstr.data(), (int32_t)matchstr.length()); // TODO-LDML: create a new Matcher every time. These could be cached and reset. std::unique_ptr matcher(fFromPattern->matcher(matchustr, status)); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); if (!matcher->find(status)) { // i.e. matches somewhere, in this case at end of str return 0; // no match @@ -494,7 +494,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons // TODO-LDML: if we had an underlying UText this would be simpler. int32_t matchStart = matcher->start(status); int32_t matchEnd = matcher->end(status); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); // extract.. const icu::UnicodeString substr = matchustr.tempSubStringBetween(matchStart, matchEnd); // preflight to UTF-32 to get length @@ -521,7 +521,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons // we actually need the group(1) string here. // this is only the content in parenthesis () icu::UnicodeString group1 = matcher->group(1, status); - assert(U_SUCCESS(status)); // TODO-LDML: could be a malformed from pattern + UASSERT_SUCCESS(status); // TODO-LDML: could be a malformed from pattern // now, how long is group1 in UTF-32, hmm? UErrorCode preflightStatus = U_ZERO_ERROR; // throwaway status auto group1Len = group1.toUTF32(nullptr, 0, preflightStatus); @@ -529,7 +529,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons assert(s != nullptr); // TODO-LDML: OOM // convert substr.toUTF32((UChar32 *)s, group1Len + 1, status); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); std::u32string match32(s, group1Len); // taken from just group1 // clean up buffer delete [] s; @@ -552,10 +552,10 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); icu::UnicodeString rustr2; nfd->normalize(rustr, rustr2, status); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); // here we replace the match output. icu::UnicodeString entireOutput = matcher->replaceFirst(rustr2, status); - assert(U_SUCCESS(status)); // TODO-LDML: could fail here due to bad input (syntax err) + UASSERT_SUCCESS(status); // TODO-LDML: could fail here due to bad input (syntax err) // entireOutput includes all of 'input', but modified. Need to substring it. icu::UnicodeString outu_raw = entireOutput.tempSubString(matchStart); @@ -563,7 +563,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons // normalize the replaced string icu::UnicodeString outu; nfd->normalize(outu_raw, outu, status); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); // Special case if there's no output, save some allocs if (outu.length() == 0) { @@ -578,7 +578,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons assert(s != nullptr); // convert outu.toUTF32((UChar32 *)s, out32len + 1, status); - assert(U_SUCCESS(status)); + UASSERT_SUCCESS(status); output.assign(s, out32len); // now, build a u32string std::u32string out32(s, out32len); @@ -844,67 +844,56 @@ transforms::load( return transforms; } -// string +// string manipulation -// TODO-LDML: copypasta -> refactor -std::u32string &normalize_nfd(std::u32string &str, UErrorCode &status) { +bool normalize_nfd(std::u32string &str) { std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); - normalize_nfd(rstr, status); - if (U_SUCCESS(status)) { - // if failure, leave it alone + if(!normalize_nfd(rstr)) { + return false; + } else { str = km::kbp::kmx::u16string_to_u32string(rstr); + return true; } - return str; } -// TODO-LDML: copypasta -> refactor -std::u16string &normalize_nfd(std::u16string &str, UErrorCode &status) { +bool normalize_nfd(std::u16string &str) { + UErrorCode status = U_ZERO_ERROR; const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); - if (U_FAILURE(status)) { - return str; - } + UASSERT_SUCCESS(status); + if (U_FAILURE(status)) return false; // exit early since normalizer pointer could be nullptr icu::UnicodeString dest; icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); nfd->normalize(src, dest, status); - if (U_FAILURE(status)) { - return str; - } + UASSERT_SUCCESS(status); str.assign(dest.getBuffer(), dest.length()); - return str; + return U_SUCCESS(status); } -// TODO-LDML: copypasta -> refactor -std::u32string &normalize_nfc(std::u32string &str, UErrorCode &status) { +bool normalize_nfc(std::u32string &str) { std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); - - normalize_nfc(rstr, status); - if (U_SUCCESS(status)) { - // if failure, leave it alone + if(!normalize_nfc(rstr)) { + return false; + } else { str = km::kbp::kmx::u16string_to_u32string(rstr); + return true; } - return str; } -// TODO-LDML: copypasta -> refactor -std::u16string &normalize_nfc(std::u16string &str, UErrorCode &status) { +bool normalize_nfc(std::u16string &str) { + UErrorCode status = U_ZERO_ERROR; const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(status); - if (U_FAILURE(status)) { - return str; - } + UASSERT_SUCCESS(status); + if (U_FAILURE(status)) return false; // exit early since normalizer pointer could be nullptr + icu::UnicodeString dest; icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); nfc->normalize(src, dest, status); - if (U_FAILURE(status)) { - return str; - } + UASSERT_SUCCESS(status); str.assign(dest.getBuffer(), dest.length()); - return str; + return U_SUCCESS(status); } - - - } // namespace ldml } // namespace kbp } // namespace km diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index f482e56a992..d7c5f2c6bf7 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -13,6 +13,7 @@ #include #include #include +#include "debuglog.h" #if !defined(HAVE_ICU4C) #error icu4c is required for this code @@ -31,6 +32,13 @@ namespace km { namespace kbp { namespace ldml { +// macro for working with ICU calls +#define UASSERT_SUCCESS(status) \ + if (U_FAILURE(status)) { \ + DebugLog("U_FAILURE(%s)", u_errorName(status)); \ + assert(U_SUCCESS(status)); \ + } + using km::kbp::kmx::SimpleUSet; /** @@ -273,14 +281,14 @@ class transforms { // string routines -/** Normalize a u32string inplace to NFD. Returns a reference to the same string. */ -std::u32string &normalize_nfd(std::u32string &str, UErrorCode &status); -/** Normalize a u16string inplace to NFD. Returns a reference to the same string. */ -std::u16string &normalize_nfd(std::u16string &str, UErrorCode &status); -/** Normalize a u32string inplace to NFC. Returns a reference to the same string. */ -std::u32string &normalize_nfc(std::u32string &str, UErrorCode &status); -/** Normalize a u16string inplace to NFC. Returns a reference to the same string. */ -std::u16string &normalize_nfc(std::u16string &str, UErrorCode &status); +/** Normalize a u32string inplace to NFD. @return false on failure */ +bool normalize_nfd(std::u32string &str); +/** Normalize a u16string inplace to NFD. @return false on failure */ +bool normalize_nfd(std::u16string &str); +/** Normalize a u32string inplace to NFC. @return false on failure */ +bool normalize_nfc(std::u32string &str); +/** Normalize a u16string inplace to NFC. @return false on failure */ +bool normalize_nfc(std::u16string &str); } // namespace ldml } // namespace kbp diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 4ad5d5a3a72..ab8423f7648 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -468,9 +468,7 @@ LdmlJsonTestSource::next_action(ldml_action &fillin) { if (as_check.is_string()) { fillin.type = LDML_ACTION_CHECK_EXPECTED; fillin.string = LdmlTestSource::parse_u8_source_string(as_check.get()); - UErrorCode status = U_ZERO_ERROR; - km::kbp::ldml::normalize_nfc(fillin.string, status); - assert(U_SUCCESS(status)); + assert(km::kbp::ldml::normalize_nfc(fillin.string)); return; } @@ -489,8 +487,7 @@ LdmlJsonTestSource::next_action(ldml_action &fillin) { if (as_emit.is_string()) { fillin.type = LDML_ACTION_EMIT_STRING; fillin.string = LdmlTestSource::parse_u8_source_string(as_emit.get()); - UErrorCode status = U_ZERO_ERROR; - km::kbp::ldml::normalize_nfc(fillin.string, status); + assert(km::kbp::ldml::normalize_nfc(fillin.string)); return; } @@ -508,8 +505,7 @@ int LdmlJsonTestSource::load(const nlohmann::json &data) { this->data = data; // TODO-LDML auto startContext = data["/startContext/to"_json_pointer]; context = LdmlTestSource::parse_u8_source_string(startContext); - UErrorCode status = U_ZERO_ERROR; - km::kbp::ldml::normalize_nfc(context, status); + assert(km::kbp::ldml::normalize_nfc(context)); return 0; } From f1b12a18482d27c8a2292884e4bd7c78367bd618 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Wed, 18 Oct 2023 17:55:08 -0500 Subject: [PATCH 28/42] =?UTF-8?q?fix(core):=20ldml=20more=20updates=20to?= =?UTF-8?q?=20normalization=20per=20review=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refactor internal normalization call For: #9468 --- core/src/ldml/ldml_transforms.cpp | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 0ee01204658..b3669661ce9 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -857,19 +857,25 @@ bool normalize_nfd(std::u32string &str) { } } -bool normalize_nfd(std::u16string &str) { - UErrorCode status = U_ZERO_ERROR; - const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); +/** internal function to normalize with a specified mode */ +static bool normalize(const icu::Normalizer2 *n, std::u16string &str, UErrorCode &status) { UASSERT_SUCCESS(status); - if (U_FAILURE(status)) return false; // exit early since normalizer pointer could be nullptr + assert(n != nullptr); icu::UnicodeString dest; icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); - nfd->normalize(src, dest, status); + n->normalize(src, dest, status); UASSERT_SUCCESS(status); str.assign(dest.getBuffer(), dest.length()); return U_SUCCESS(status); } +bool normalize_nfd(std::u16string &str) { + UErrorCode status = U_ZERO_ERROR; + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + UASSERT_SUCCESS(status); + return normalize(nfd, str, status); +} + bool normalize_nfc(std::u32string &str) { std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); if(!normalize_nfc(rstr)) { @@ -884,14 +890,7 @@ bool normalize_nfc(std::u16string &str) { UErrorCode status = U_ZERO_ERROR; const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(status); UASSERT_SUCCESS(status); - if (U_FAILURE(status)) return false; // exit early since normalizer pointer could be nullptr - - icu::UnicodeString dest; - icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); - nfc->normalize(src, dest, status); - UASSERT_SUCCESS(status); - str.assign(dest.getBuffer(), dest.length()); - return U_SUCCESS(status); + return normalize(nfc, str, status); } } // namespace ldml From 3723e6b3d756c452f8d799404dcd10756e610683 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Wed, 18 Oct 2023 17:58:05 -0500 Subject: [PATCH 29/42] =?UTF-8?q?fix(core):=20ldml=20more=20updates=20to?= =?UTF-8?q?=20normalization=20per=20review=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - consistency For: #9468 --- core/src/ldml/ldml_transforms.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index b3669661ce9..9becb93ba78 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -865,7 +865,9 @@ static bool normalize(const icu::Normalizer2 *n, std::u16string &str, UErrorCode icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); n->normalize(src, dest, status); UASSERT_SUCCESS(status); - str.assign(dest.getBuffer(), dest.length()); + if (U_SUCCESS(status)) { + str.assign(dest.getBuffer(), dest.length()); + } return U_SUCCESS(status); } From 14ea216b4ee5cf91871e60511b506a84ac4f7574 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 19 Oct 2023 09:22:07 +0700 Subject: [PATCH 30/42] fix(common): don't use URL in common/web/types Fixes #9788. --- .../web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts | 4 ++-- common/web/types/test/helpers/reader-callback-test.ts | 2 +- developer/src/kmc-ldml/test/helpers/index.ts | 2 +- .../src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts | 2 +- developer/src/kmc/src/commands/buildTestData/index.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts index 5e7c1ab2d30..80b6959149b 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -21,8 +21,8 @@ export class LDMLKeyboardXMLSourceFileReader { constructor(private options: LDMLKeyboardXMLSourceFileReaderOptions, private callbacks : CompilerCallbacks) { } - static get defaultImportsURL() { - return new URL(`../import/`, import.meta.url); + static get defaultImportsURL(): [string,string] { + return ['../import/', import.meta.url]; } readImportFile(version: string, subpath: string): Uint8Array { diff --git a/common/web/types/test/helpers/reader-callback-test.ts b/common/web/types/test/helpers/reader-callback-test.ts index 86758ad413f..907793cdf7e 100644 --- a/common/web/types/test/helpers/reader-callback-test.ts +++ b/common/web/types/test/helpers/reader-callback-test.ts @@ -9,7 +9,7 @@ import { TestCompilerCallbacks } from './TestCompilerCallbacks.js'; import { fileURLToPath } from 'url'; const readerOptions: LDMLKeyboardXMLSourceFileReaderOptions = { - importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) }; export interface CompilationCase { diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index db0704bf75f..c919e729ac0 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -37,7 +37,7 @@ export const compilerTestCallbacks = new TestCompilerCallbacks(); export const compilerTestOptions: LdmlCompilerOptions = { readerOptions: { - importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) } }; diff --git a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts index 19db56db99a..1b0c2b22e5c 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts @@ -48,7 +48,7 @@ async function buildLdmlKeyboardToMemory(inputFilename: string, callbacks: Compi ...defaultCompilerOptions, ...options, readerOptions: { - importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) } }; diff --git a/developer/src/kmc/src/commands/buildTestData/index.ts b/developer/src/kmc/src/commands/buildTestData/index.ts index 97650d272ae..834112bc130 100644 --- a/developer/src/kmc/src/commands/buildTestData/index.ts +++ b/developer/src/kmc/src/commands/buildTestData/index.ts @@ -14,7 +14,7 @@ export function buildTestData(infile: string, _options: any, commander: any) { saveDebug: false, shouldAddCompilerVersion: false, readerOptions: { - importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) } }; From cd9fe82079d99a6e5a8b2ebabd7025a76ad8a3fb Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:24:34 +1000 Subject: [PATCH 31/42] docs(windows): apply code review suggestions Co-authored-by: Marc Durdin --- .../desktop/help/troubleshooting/install-app-from-anywhere.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/src/desktop/help/troubleshooting/install-app-from-anywhere.md b/windows/src/desktop/help/troubleshooting/install-app-from-anywhere.md index 316f8dfad5d..23c4213b748 100644 --- a/windows/src/desktop/help/troubleshooting/install-app-from-anywhere.md +++ b/windows/src/desktop/help/troubleshooting/install-app-from-anywhere.md @@ -22,12 +22,12 @@ If "**Change my app recommendation settings**" is shown, select that and go to s ## Allow Windows 11 to Install Apps From Anywhere -1. You may be warned to use a "Microsoft-verfied" App from the Microsoft Store. This is because Keyman is not available in the Microsoft Store. +1. You may be warned to use a "Microsoft-verified" App from the Microsoft Store. This is because Keyman is not available in the Microsoft Store. If "**Change my app recommendation settings**" is shown, select that and go to step 4. ![](../desktop_images/km_non_app_store_1.png) -2. If that is not available Open the Windows 11 start menu (Windows logo) and select **Settings** +2. If that is not available, open the Windows 11 start menu (Windows logo) and select **Settings**. 3. Click on the **Apps** in the **Windows Settings** menu From fae6277b20dda9d7bbdb8519507a73d5ae30002a Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 19 Oct 2023 13:09:26 +0700 Subject: [PATCH 32/42] chore: update kmp.schema.json and docs for kps schema --- common/schemas/kmp/README.md | 24 ++ common/schemas/kmp/kmp.schema.json | 353 +++++++++++++++++++++++++++++ common/schemas/kps/README.md | 19 +- 3 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 common/schemas/kmp/README.md create mode 100644 common/schemas/kmp/kmp.schema.json diff --git a/common/schemas/kmp/README.md b/common/schemas/kmp/README.md new file mode 100644 index 00000000000..6c8372b7790 --- /dev/null +++ b/common/schemas/kmp/README.md @@ -0,0 +1,24 @@ +# kmp.schema.json + +* kmp.json file format, metadata included in Keyman .kmp package files + +Documentation at https://help.keyman.com/developer/current-version/reference/file-types/metadata + +# kmp.schema.json version history + +## 2023-10-19 2.0 +* Add relatedPackages, options.licenseFile, options.welcomeFile, + keyboard.examples, keyboard.webOskFonts, keyboard.webDisplayFonts, + info.description (all of these formerly were stored in .keyboard_info) + +## 2019-01-31 1.1.0 +* Add lexicalModels properties (note: `version` is optional and currently unused) + +## 2018-02-13 1.0.2 +* Add rtl property for keyboard layouts + +## 2018-01-22 1.0.1 +* Remove id field as it is derived from the filename anyway + +## 2017-11-30 1.0 beta +* Initial version diff --git a/common/schemas/kmp/kmp.schema.json b/common/schemas/kmp/kmp.schema.json new file mode 100644 index 00000000000..a28aa43195a --- /dev/null +++ b/common/schemas/kmp/kmp.schema.json @@ -0,0 +1,353 @@ +{ + "$schema": "http://json-schema.org/schema#", + "$ref": "#/definitions/package", + "definitions": { + "package": { + "type": "object", + "properties": { + "system": { + "$ref": "#/definitions/system" + }, + "options": { + "$ref": "#/definitions/options" + }, + "startMenu": { + "$ref": "#/definitions/startMenu" + }, + "strings": { + "$ref": "#/definitions/strings" + }, + "files": { + "$ref": "#/definitions/files" + }, + "keyboards": { + "$ref": "#/definitions/keyboards" + }, + "lexicalModels": { + "$ref": "#/definitions/lexicalModels" + }, + "info": { + "$ref": "#/definitions/info" + }, + "relatedPackages": { + "type": "array", + "items": { + "$ref": "#/definitions/relatedPackage" + } + } + }, + "additionalProperties": false, + "required": [ + "options", + "system" + ] + }, + "system": { + "type": "object", + "properties": { + "keymanDeveloperVersion": { + "type": "string" + }, + "fileVersion": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "fileVersion" + ] + }, + "options": { + "type": "object", + "properties": { + "readmeFile": { + "type": "string" + }, + "graphicFile": { + "type": "string" + }, + "licenseFile": { + "type": "string" + }, + "welcomeFile": { + "type": "string" + }, + "executeProgram": { + "type": "string" + }, + "msiFilename": { + "type": "string" + }, + "msiOptions": { + "type": "string" + } + }, + "additionalProperties": false + }, + "startMenu": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "addUninstallEntry": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/startMenuItem" + } + } + }, + "additionalProperties": false + }, + "startMenuItem": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "arguments": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "location": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "name", + "filename" + ] + }, + "strings": { + "type": "object", + "patternProperties": { + ".": { + "type": "string" + } + }, + "additionalProperties": false + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/file" + } + }, + "file": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "copyLocation": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "name", + "description" + ] + }, + "keyboards": { + "type": "array", + "items": { + "$ref": "#/definitions/keyboard" + } + }, + "keyboard": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "oskFont": { + "type": "string" + }, + "displayFont": { + "type": "string" + }, + "rtl": { + "type": "boolean" + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/definitions/keyboardLanguage" + } + }, + "examples": { + "type": "array", + "items": { + "$ref": "#/definitions/keyboardExample" + } + }, + "webOskFonts": { + "type": "array", + "items": { + "type": "string" + } + }, + "webDisplayFonts": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "name", + "id", + "version" + ] + }, + "keyboardLanguage": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "id", + "name" + ] + }, + "info": { + "type": "object", + "properties": { + "website": { + "$ref": "#/definitions/infoItem" + }, + "version": { + "$ref": "#/definitions/infoItem" + }, + "name": { + "$ref": "#/definitions/infoItem" + }, + "copyright": { + "$ref": "#/definitions/infoItem" + }, + "author": { + "$ref": "#/definitions/infoItem" + }, + "description": { + "$ref": "#/definitions/infoItem" + } + }, + "additionalProperties": false + }, + "infoItem": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "description" + ] + }, + "lexicalModels": { + "type": "array", + "items": { + "$ref": "#/definitions/lexicalModel" + } + }, + "lexicalModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "rtl": { + "type": "boolean" + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/definitions/keyboardLanguage" + } + } + }, + "additionalProperties": false, + "required": [ + "name", + "id", + "languages" + ] + }, + "keyboardExample": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "keys": { + "type": "string" + }, + "text": { + "type": "string" + }, + "note": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "id", + "keys" + ] + }, + "relatedPackage": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "relationship": { + "type": "string", + "enum": ["deprecates", "related"] + } + }, + "additionalProperties": false, + "required": [ + "id", + "relationship" + ] + } + } +} \ No newline at end of file diff --git a/common/schemas/kps/README.md b/common/schemas/kps/README.md index e4aacf97d8f..aea03f2a5b9 100644 --- a/common/schemas/kps/README.md +++ b/common/schemas/kps/README.md @@ -1,9 +1,22 @@ # kps.xsd -Master version: https://github.com/keymanapp/api.keyman.com/blob/master/schemas/kps/7.0/kps.xsd +Master version: https://github.com/keymanapp/api.keyman.com/blob/master/schemas/kps/17.0/kps.xsd -## 2021-07-19 7.0 -* Initial version 7.0 +## 2023-10-19 17.0 +* Version 17.0 adds: + - LicenseFile - a .md file, usually named LICENSE.md + - WelcomeFile - a .htm file, usually named welcome.htm (later versions will support .md) + - Info/Description - a short Markdown description of the content of the package, e.g. shown in search results on keyman.com + - RelatedPackages - a list of other packages which relate to this one, or are deprecated by it + - Keyboards/Keyboard/Examples - a list of typing examples for the keyboard + - Keyboarsd/Keyboard/WebOSKFonts - a list of font filenames (not necessarily in package) suitable for rendering the on screen keyboard + - Keyboarsd/Keyboard/WebDisplayFonts - a list of font filenames (not necessarily in package) suitable for use with the keyboard +* Version 17.0 removes: + - LexicalModels/LexicalModel/Version - version information is not stored in the models, but only in the package metadata (was unused) ## 2023-04-21 7.0.1 * Removes LexicalModel.Version, as it was never read or written + +## 2021-07-19 7.0 +* Initial version 7.0 + From 6871e74d3aa2e8a46441433bc06583d6086cf047 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Thu, 19 Oct 2023 14:55:57 +0700 Subject: [PATCH 33/42] changed keyboards menu to dynamically update in Keyman section of the Input Menu and eliminated Keyboards submenu --- .../Keyman4MacIM/Base.lproj/MainMenu.xib | 17 +- .../KMConfigurationWindowController.m | 2 + .../Keyman4MacIM/KMInputController.m | 76 ++---- .../Keyman4MacIM/KMInputMethodAppDelegate.h | 17 ++ .../Keyman4MacIM/KMInputMethodAppDelegate.m | 238 ++++++++++++------ .../Keyman4MacIM/en.lproj/Localizable.strings | 2 + 6 files changed, 210 insertions(+), 142 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Base.lproj/MainMenu.xib b/mac/Keyman4MacIM/Keyman4MacIM/Base.lproj/MainMenu.xib index 4062663c00f..0696c81eba2 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/Base.lproj/MainMenu.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -19,19 +19,16 @@

- + + + - - - - - - + - + diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m index 14ee3c9c351..70937eecbf3 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m @@ -364,6 +364,8 @@ - (void)checkBoxAction:(id)sender { [self saveActiveKeyboards]; } else if (checkBox.state == NSOffState) { + if ([self.AppDelegate debugMode]) + NSLog(@"Disabling active keyboard: %@", kmxFilePath); [self.activeKeyboards removeObject:kmxFilePath]; [self saveActiveKeyboards]; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 0e17d84c008..47d2bfceb58 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -183,68 +183,40 @@ - (KMXFile *)kmx { return self.AppDelegate.kmx; } - - (void)menuAction:(id)sender { NSMenuItem *mItem = [sender objectForKey:kIMKCommandMenuItemName]; NSInteger itag = mItem.tag; if ([self.AppDelegate debugMode]) NSLog(@"Keyman menu clicked - tag: %lu", itag); - if (itag == 2) { - // Using `showConfigurationWindow` instead of `showPreferences:` because `showPreferences:` is missing in - // High Sierra (10.13.1 - 10.13.3). See: https://bugreport.apple.com/web/?problemID=35422518 - // rrb: where Apple's API is broken (10.13.1-10.13.3) call our workaround, otherwise, call showPreferences - u_int16_t systemVersion = [KMOSVersion SystemVersion]; - if ([KMOSVersion Version_10_13_1] <= systemVersion && systemVersion <= [KMOSVersion Version_10_13_3]) // between 10.13.1 and 10.13.3 inclusive - { - NSLog(@"Input Menu: calling workaround instead of showPreferences (sys ver %x)", systemVersion); - [self.AppDelegate showConfigurationWindow]; // call our workaround - } - else - { - NSLog(@"Input Menu: calling Apple's showPreferences (sys ver %x)", systemVersion); - [self showPreferences:sender]; // call Apple API - } + if (itag == CONFIG_MENUITEM_TAG) { + [self showConfigurationWindow:sender]; } - else if (itag == 3) { + else if (itag == OSK_MENUITEM_TAG) { [self.AppDelegate showOSK]; } - else if (itag == 4) { + else if (itag == ABOUT_MENUITEM_TAG) { [self.AppDelegate showAboutWindow]; } - else if (itag >= 1000) { - NSMenuItem *keyboards = [self.AppDelegate.menu itemWithTag:1]; - NSString *title = keyboards.title; - NSLog(@"Input Menu, selected Keyboards menu, itag: %lu, title: %@", itag, title); - for (NSMenuItem *item in keyboards.submenu.itemArray) { - NSLog(@"menu item, itag: %lu, title: %@", item.tag, item.title); - if (item.tag == itag) - [item setState:NSOnState]; - else - [item setState:NSOffState]; - } - - NSString *path = [self.AppDelegate.activeKeyboards objectAtIndex:itag%1000]; - KMXFile *kmx = [[KMXFile alloc] initWithFilePath:path]; - [self.AppDelegate setKmx:kmx]; - KVKFile *kvk = nil; - NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:path]; - NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; - if (kvkFilename != nil) { - NSString *kvkFilePath = [self.AppDelegate kvkFilePathFromFilename:kvkFilename]; - if (kvkFilePath != nil) - kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; - } - [self.AppDelegate setKvk:kvk]; - NSString *keyboardName = [kmxInfo objectForKey:kKMKeyboardNameKey]; - if ([self.AppDelegate debugMode]) - NSLog(@"Selected keyboard from menu: %@", keyboardName); - [self.AppDelegate setKeyboardName:keyboardName]; - [self.AppDelegate setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; - [self.AppDelegate setContextBuffer:nil]; - [self.AppDelegate setSelectedKeyboard:path]; - [self.AppDelegate loadSavedStores]; - if (kvk != nil && self.AppDelegate.alwaysShowOSK) - [self.AppDelegate showOSK]; + else if (itag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { + [self.AppDelegate selectKeyboardFromMenu:itag]; } } + +- (void)showConfigurationWindow:(id)sender { + // Using `showConfigurationWindow` instead of `showPreferences:` because `showPreferences:` is missing in + // High Sierra (10.13.1 - 10.13.3). See: https://bugreport.apple.com/web/?problemID=35422518 + // rrb: where Apple's API is broken (10.13.1-10.13.3) call our workaround, otherwise, call showPreferences + u_int16_t systemVersion = [KMOSVersion SystemVersion]; + if ([KMOSVersion Version_10_13_1] <= systemVersion && systemVersion <= [KMOSVersion Version_10_13_3]) // between 10.13.1 and 10.13.3 inclusive + { + NSLog(@"Input Menu: calling workaround instead of showPreferences (sys ver %x)", systemVersion); + [self.AppDelegate showConfigurationWindow]; // call our workaround + } + else + { + NSLog(@"Input Menu: calling Apple's showPreferences (sys ver %x)", systemVersion); + [self showPreferences:sender]; // call Apple API + } +} + @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index 5214faf8212..e4e53e5972f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -38,6 +38,21 @@ typedef struct { NSString *apiKeymanCom; } KeymanVersionInfo; +// tags for default menu items, displayed whether keyboards are active or not +static const int DIVIDER_MENUITEM_TAG = -4; +static const int CONFIG_MENUITEM_TAG = -3; +static const int OSK_MENUITEM_TAG = -2; +static const int ABOUT_MENUITEM_TAG = -1; + +// the number of menu items that do not represent active keyboards +static const int DEFAULT_KEYMAN_MENU_ITEM_COUNT = 4; + +// the tag for the first keyboard dynamically added to the menu +static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG = 0; + +// the menu index for the first keyboard dynamically added to the menu +static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; + @interface KMInputMethodAppDelegate : NSObject #define USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE 1 #ifdef USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE @@ -55,6 +70,7 @@ typedef struct { @property (nonatomic, strong) NSMutableArray *kmxFileList; @property (nonatomic, strong) NSString *selectedKeyboard; @property (nonatomic, strong) NSMutableArray *activeKeyboards; +@property (assign) int numberOfKeyboardMenuItems; @property (nonatomic, strong) NSMutableString *contextBuffer; @property (nonatomic, assign) NSEventModifierFlags currentModifierFlags; @property (nonatomic, assign) CFMachPortRef lowLevelEventTap; @@ -88,6 +104,7 @@ typedef struct { - (void)showAboutWindow; - (void)showOSK; - (void)showConfigurationWindow; +- (void)selectKeyboardFromMenu:(NSInteger)tag; - (void)sleepFollowingDeactivationOfServer:(id)lastServer; - (void)wakeUpWith:(id)newServer; - (void)handleKeyEvent:(NSEvent *)event; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index a7b3d69038d..3425cf25b5c 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -700,14 +700,14 @@ - (void)saveActiveKeyboards { [userData setObject:_activeKeyboards forKey:kKMActiveKeyboardsKey]; [userData synchronize]; [self resetActiveKeyboards]; - [self setKeyboardsSubMenu]; + [self updateKeyboardMenuItems]; } - (void)clearActiveKeyboards { NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; [userData setObject:nil forKey:kKMActiveKeyboardsKey]; [userData synchronize]; - [self setKeyboardsSubMenu]; + [self updateKeyboardMenuItems]; } - (void)resetActiveKeyboards { @@ -760,96 +760,174 @@ - (void)setContextBuffer:(NSMutableString *)contextBuffer { } - (void)awakeFromNib { - [self setKeyboardsSubMenu]; - - NSMenuItem *config = [self.menu itemWithTag:2]; - if (config) - [config setAction:@selector(menuAction:)]; - - NSMenuItem *osk = [self.menu itemWithTag:3]; - if (osk) - [osk setAction:@selector(menuAction:)]; + [self setDefaultKeymanMenuItems]; + [self updateKeyboardMenuItems]; +} - NSMenuItem *about = [self.menu itemWithTag:4]; - if (about) - [about setAction:@selector(menuAction:)]; +- (void)setDefaultKeymanMenuItems { + NSMenuItem *config = [self.menu itemWithTag:CONFIG_MENUITEM_TAG]; + if (config) { + [config setAction:@selector(menuAction:)]; + } + + NSMenuItem *osk = [self.menu itemWithTag:OSK_MENUITEM_TAG]; + if (osk) { + [osk setAction:@selector(menuAction:)]; + } + + NSMenuItem *about = [self.menu itemWithTag:ABOUT_MENUITEM_TAG]; + if (about) { + [about setAction:@selector(menuAction:)]; + } } -- (void)setKeyboardsSubMenu { - NSMenuItem *keyboards = [self.menu itemWithTag:1]; - if (keyboards) { - KVKFile *kvk = nil; - BOOL didSetKeyboard = NO; - NSInteger itag = 1000; - NSString *keyboardMenuName = @""; - - [keyboards.submenu removeAllItems]; - for (NSString *path in self.activeKeyboards) { - NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:path]; - if (!infoDict) - continue; - //NSString *str = [NSString stringWithFormat:@"%@ (%@)", [infoDict objectForKey:kKMKeyboardNameKey], [infoDict objectForKey:kKMKeyboardVersionKey]]; - NSString *str = [infoDict objectForKey:kKMKeyboardNameKey]; - // for debugging - keyboardMenuName = str; - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:str action:@selector(menuAction:) keyEquivalent:@""]; - [item setTag:itag++]; - if ([path isEqualToString:self.selectedKeyboard]) { - [item setState:NSOnState]; - KMXFile *kmx = [[KMXFile alloc] initWithFilePath:path]; - [self setKmx:kmx]; - NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:path]; - NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; - if (kvkFilename != nil) { - NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; - if (kvkFilePath != nil) - kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; - } - [self setKvk:kvk]; - [self setKeyboardName:[kmxInfo objectForKey:kKMKeyboardNameKey]]; - [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; - [self loadSavedStores]; +- (void)updateKeyboardMenuItems { + self.numberOfKeyboardMenuItems = [self calculateNumberOfKeyboardMenuItems]; + [self removeDynamicKeyboardMenuItems]; + [self addDynamicKeyboardMenuItems]; +} - didSetKeyboard = YES; - } - else - [item setState:NSOffState]; +- (int)calculateNumberOfKeyboardMenuItems { + if (self.activeKeyboards.count == 0) { + // if there are no active keyboards, then we will insert one placeholder menu item 'No Active Keyboards' + return 1; + } else { + return (int) self.activeKeyboards.count; + } +} - NSLog(@"*** adding item to keyboards menu, itag=%lu, keyboard=%@", itag, keyboardMenuName); +- (void)removeDynamicKeyboardMenuItems { + int numberToRemove = (int) self.menu.numberOfItems - DEFAULT_KEYMAN_MENU_ITEM_COUNT; + + if (self.debugMode) { + NSLog(@"*** removeDynamicKeyboardMenuItems, self.menu.numberOfItems = %ld, number of items to remove = %d", (long)self.menu.numberOfItems, numberToRemove); + } + + if (numberToRemove > 0) { + for (int i = 1; i <= numberToRemove; i++) { + [self.menu removeItemAtIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX]; + } + } +} - [keyboards.submenu addItem:item]; +- (void)addDynamicKeyboardMenuItems { + BOOL didSetSelectedKeyboard = NO; + NSInteger itag = KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG; + NSString *keyboardMenuName = @""; + int menuItemIndex = KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX; + + if (self.debugMode) { + NSLog(@"*** populateKeyboardMenuItems, number of active keyboards=%lu", self.activeKeyboards.count); + } + + // loop through the active keyboards list and add them to the menu + for (NSString *path in self.activeKeyboards) { + NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:path]; + if (!infoDict) { + continue; + } + keyboardMenuName = [infoDict objectForKey:kKMKeyboardNameKey]; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:keyboardMenuName action:@selector(menuAction:) keyEquivalent:@""]; + [item setTag:itag++]; + + // if this is the selected keyboard, then configure it as selected + if ([path isEqualToString:self.selectedKeyboard]) { + [self setSelectedKeyboard:path inMenuItem:item]; + didSetSelectedKeyboard = YES; + } + else { + [item setState:NSOffState]; } + + [self.menu insertItem:item atIndex:menuItemIndex++]; + } + + if (self.activeKeyboards.count == 0) { + [self addKeyboardPlaceholderMenuItem]; + } else if (!didSetSelectedKeyboard) { + [self setDefaultSelectedKeyboard]; + } +} - if (keyboards.submenu.numberOfItems == 0) { - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"(None)" action:NULL keyEquivalent:@""]; - [keyboards.submenu addItem:item]; - [self setKmx:nil]; - [self setKvk:nil]; - [self setKeyboardName:nil]; - [self setKeyboardIcon:nil]; - [self setContextBuffer:nil]; - [self setSelectedKeyboard:nil]; +- (void) setSelectedKeyboard:(NSString*)keyboardName inMenuItem:(NSMenuItem*) menuItem { + KVKFile *kvk = nil; + + [menuItem setState:NSOnState]; + KMXFile *kmx = [[KMXFile alloc] initWithFilePath:keyboardName]; + [self setKmx:kmx]; + NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:keyboardName]; + NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; + if (kvkFilename != nil) { + NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; + if (kvkFilePath != nil) { + kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; } - else if (!didSetKeyboard) { - [keyboards.submenu itemAtIndex:0].state = NSOnState; - NSString *path = [self.activeKeyboards objectAtIndex:0]; - KMXFile *kmx = [[KMXFile alloc] initWithFilePath:path]; - [self setKmx:kmx]; - NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:path]; - NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; - if (kvkFilename != nil) { - NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; - if (kvkFilePath != nil) - kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; + } + [self setKvk:kvk]; + [self setKeyboardName:[kmxInfo objectForKey:kKMKeyboardNameKey]]; + [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; + [self loadSavedStores]; +} + +// defaults to the whatever keyboard happens to be first in the list +- (void) setDefaultSelectedKeyboard { + NSMenuItem* menuItem = [self.menu itemAtIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX]; + NSString *keyboardName = [self.activeKeyboards objectAtIndex:0]; + [self setSelectedKeyboard:keyboardName inMenuItem:menuItem]; + [self setSelectedKeyboard:keyboardName]; + [self setContextBuffer:nil]; +} + +- (void) addKeyboardPlaceholderMenuItem { + NSString* placeholder = NSLocalizedString(@"no-keyboard-configured-menu-placeholder", nil); + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:placeholder action:NULL keyEquivalent:@""]; + [self.menu insertItem:item atIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX]; + [self setKmx:nil]; + [self setKvk:nil]; + [self setKeyboardName:nil]; + [self setKeyboardIcon:nil]; + [self setContextBuffer:nil]; + [self setSelectedKeyboard:nil]; +} + +- (void)selectKeyboardFromMenu:(NSInteger)tag { + NSMenuItem *menuItem = [self.menu itemWithTag:tag]; + NSString *title = menuItem.title; + NSLog(@"Input Menu, selected Keyboards menu, itag: %lu, title: %@", tag, title); + for (NSMenuItem *item in self.menu.itemArray) { + // set the state of the keyboard items in the Keyman menu based on the new selection + if (item.tag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { + if (item.tag == tag) { + [item setState:NSOnState]; + } + else { + [item setState:NSOffState]; } - [self setKvk:kvk]; - [self setKeyboardName:[kmxInfo objectForKey:kKMKeyboardNameKey]]; - [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; - [self setContextBuffer:nil]; - [self loadSavedStores]; - [self setSelectedKeyboard:path]; } } + + NSString *path = [self.activeKeyboards objectAtIndex:tag-KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG]; + KMXFile *kmx = [[KMXFile alloc] initWithFilePath:path]; + [self setKmx:kmx]; + KVKFile *kvk = nil; + NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:path]; + NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; + if (kvkFilename != nil) { + NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; + if (kvkFilePath != nil) + kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath]; + } + [self setKvk:kvk]; + NSString *keyboardName = [kmxInfo objectForKey:kKMKeyboardNameKey]; + if ([self debugMode]) + NSLog(@"Selected keyboard from menu: %@", keyboardName); + [self setKeyboardName:keyboardName]; + [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; + [self setContextBuffer:nil]; + [self setSelectedKeyboard:path]; + [self loadSavedStores]; + if (kvk != nil && self.alwaysShowOSK) + [self showOSK]; } - (NSArray *)KMXFiles { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings index f14a66b0b4a..6b815a7b176 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings @@ -71,4 +71,6 @@ /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; +/* Text of menu item in Input Menu when no Keyboards are configured -- include parentheses */ +"no-keyboard-configured-menu-placeholder" = "(No Keyboard Configured)"; From 5d41ecf6c27f67826e837cd32581ae3065dffbfc Mon Sep 17 00:00:00 2001 From: Shawn Schantz <89134789+sgschantz@users.noreply.github.com> Date: Thu, 19 Oct 2023 15:22:17 +0700 Subject: [PATCH 34/42] use zero-based loop when removing all keyboards from Input Menu Co-authored-by: Marc Durdin --- mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 3425cf25b5c..b04236ed5b8 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -804,7 +804,7 @@ - (void)removeDynamicKeyboardMenuItems { } if (numberToRemove > 0) { - for (int i = 1; i <= numberToRemove; i++) { + for (int i = 0; i < numberToRemove; i++) { [self.menu removeItemAtIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX]; } } From 7b5b0d93aa11b391aa7191c3008260cd148f48fe Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Thu, 19 Oct 2023 14:02:37 -0400 Subject: [PATCH 35/42] auto: increment master version to 17.0.196 --- HISTORY.md | 7 +++++++ VERSION.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index bde203e3a78..05831dc9625 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ # Keyman Version History +## 17.0.195 alpha 2023-10-19 + +* chore(linux): Allow to collect coverage on TC (#9790) +* fix(common): don't use URL in common/web/types (#9798) +* chore: update kmp.schema.json and docs for kps schema (#9800) +* docs(windows): update text and images for windows 11 (#9689) + ## 17.0.194 alpha 2023-10-18 * chore(linux): Re-enable building for Ubuntu 23.10 Mantic (#9780) diff --git a/VERSION.md b/VERSION.md index d684cef10f4..9bd4bcef436 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.195 \ No newline at end of file +17.0.196 \ No newline at end of file From 4afe0387d63ba99f03a0faff6bb93bfcfe0de81b Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 19 Oct 2023 13:16:03 -0500 Subject: [PATCH 36/42] =?UTF-8?q?fix(core):=20ldml=20more=20updates=20to?= =?UTF-8?q?=20normalization=20per=20review=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - move meat of uassert_success() into an inline For: #9468 --- core/src/debuglog.h | 2 ++ core/src/ldml/ldml_transforms.hpp | 16 +++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/src/debuglog.h b/core/src/debuglog.h index 5b547132e7f..944695d669f 100644 --- a/core/src/debuglog.h +++ b/core/src/debuglog.h @@ -20,10 +20,12 @@ extern const char *s_key_names[]; #ifdef _MSC_VER #define DebugLog(msg,...) (km::kbp::kmx::ShouldDebug() ? km::kbp::kmx::DebugLog_1(__FILE__, __LINE__, __FUNCTION__, (msg),__VA_ARGS__) : 0) +#define DebugLog2(file,line,function,msg,...) (km::kbp::kmx::ShouldDebug() ? km::kbp::kmx::DebugLog_1(file, line, function, (msg),__VA_ARGS__) : 0) #define console_error(msg,...) write_console(TRUE, (msg), __VA_ARGS__) #define console_log(msg,...) write_console(FALSE, (msg), __VA_ARGS__) #else #define DebugLog(msg,...) (km::kbp::kmx::ShouldDebug() ? km::kbp::kmx::DebugLog_1(__FILE__, __LINE__, __FUNCTION__, (msg), ##__VA_ARGS__) : 0) +#define DebugLog2(file,line,function,msg,...) (km::kbp::kmx::ShouldDebug() ? km::kbp::kmx::DebugLog_1(file, line, function, (msg), ##__VA_ARGS__) : 0) #define console_error(msg,...) write_console(TRUE, (msg), ##__VA_ARGS__) #define console_log(msg,...) write_console(FALSE, (msg), ##__VA_ARGS__) #endif diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index d7c5f2c6bf7..f48ab8a47d8 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -32,12 +32,18 @@ namespace km { namespace kbp { namespace ldml { -// macro for working with ICU calls -#define UASSERT_SUCCESS(status) \ - if (U_FAILURE(status)) { \ - DebugLog("U_FAILURE(%s)", u_errorName(status)); \ - assert(U_SUCCESS(status)); \ +/** @returns true on success */ +inline bool uassert_success(const char *file, int line, const char *function, UErrorCode status) { + if (U_FAILURE(status)) { + DebugLog2(file, line, function, "U_FAILURE(%s)", u_errorName(status)); + return false; + } else { + return true; } +} + +#define UASSERT_SUCCESS(status) assert(U_SUCCESS(status)); \ + uassert_success(__FILE__, __LINE__, __FUNCTION__, status) using km::kbp::kmx::SimpleUSet; From 1d9820b02096402778b2d2c4aad0de92d1abc29d Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 19 Oct 2023 13:33:55 -0500 Subject: [PATCH 37/42] =?UTF-8?q?fix(core):=20ldml=20more=20updates=20to?= =?UTF-8?q?=20normalization=20per=20review=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - improve assert failure handling- fix a potential heap leak For: #9468 --- core/src/ldml/ldml_transforms.cpp | 67 ++++++++++++++++++------------- core/src/ldml/ldml_transforms.hpp | 10 ++--- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 9becb93ba78..b2bfdaaa040 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -12,7 +12,7 @@ #include "kmx/kmx_xstring.h" #ifndef assert -#define assert(x) // TODO-LDML +#define assert(x) ((void)0) #endif namespace km { @@ -403,7 +403,7 @@ transform_entry::transform_entry(const transform_entry &other) transform_entry::transform_entry(const std::u32string &from, const std::u32string &to) : fFrom(from), fTo(to), fFromPattern(nullptr), fMapFromStrId(), fMapToStrId(), fMapFromList(), fMapToList() { - assert(!fFrom.empty()); // TODO-LDML: should not happen? + assert(!fFrom.empty()); init(); } @@ -414,14 +414,16 @@ transform_entry::transform_entry( const std::u32string &to, KMX_DWORD mapFrom, KMX_DWORD mapTo, - const kmx::kmx_plus &kplus) + const kmx::kmx_plus &kplus, + bool &valid) : fFrom(from), fTo(to), fFromPattern(nullptr), fMapFromStrId(mapFrom), fMapToStrId(mapTo) { + valid = false; assert(!fFrom.empty()); // TODO-LDML: should not happen? assert((fMapFromStrId == 0) == (fMapToStrId == 0)); // we have both or we have neither. assert(kplus.strs != nullptr); assert(kplus.vars != nullptr); assert(kplus.elem != nullptr); - init(); + valid = init(); // setup mapFrom if (fMapFromStrId != 0) { @@ -456,7 +458,7 @@ transform_entry::transform_entry( } } -void +bool transform_entry::init() { if (!fFrom.empty()) { // TODO-LDML: if we have mapFrom, may need to do other processing. @@ -470,8 +472,9 @@ transform_entry::init() { // NFD normalize on pattern creation nfd->normalize(patustr_raw, patustr, status); fFromPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); - UASSERT_SUCCESS(status); + return (UASSERT_SUCCESS(status)); } + return false; // fFrom should not be empty. } size_t @@ -767,35 +770,34 @@ transforms::load( const kmx::kmx_plus &kplus, const kbp::kmx::COMP_KMXPLUS_TRAN *tran, const kbp::kmx::COMP_KMXPLUS_TRAN_Helper &tranHelper) { + bool valid = true; if (tran == nullptr) { DebugLog("for tran: tran is null"); - assert(false); - return nullptr; - } - if (!tranHelper.valid()) { + valid = false; + } else if (!tranHelper.valid()) { DebugLog("for tran: tranHelper is invalid"); - assert(false); - return nullptr; - } - if (nullptr == kplus.elem) { + valid = false; + } else if (nullptr == kplus.elem) { DebugLog("for tran: kplus.elem == nullptr"); - assert(false); - return nullptr; - } - if (nullptr == kplus.strs) { + valid = false; + } else if (nullptr == kplus.strs) { DebugLog("for tran: kplus.strs == nullptr"); // need a string table to get strings - assert(false); - return nullptr; - } - if (nullptr == kplus.vars) { + valid = false; + } else if (nullptr == kplus.vars) { DebugLog("for tran: kplus.vars == nullptr"); // need a vars table to get maps - assert(false); + valid = false; + } + + assert(valid); + if (!valid) { return nullptr; } // with that out of the way, let's set it up - transforms *transforms = new ldml::transforms(); + std::unique_ptr transforms; + + transforms.reset(new ldml::transforms()); for (KMX_DWORD groupNumber = 0; groupNumber < tran->groupCount; groupNumber++) { const kmx::COMP_KMXPLUS_TRAN_GROUP *group = tranHelper.getGroup(groupNumber); @@ -811,7 +813,11 @@ transforms::load( const std::u32string toStr = kmx::u16string_to_u32string(kplus.strs->get(element->to)); KMX_DWORD mapFrom = element->mapFrom; // copy, because of alignment KMX_DWORD mapTo = element->mapTo; // copy, because of alignment - newGroup.emplace_back(fromStr, toStr, mapFrom, mapTo, kplus); // creating a transform_entry + newGroup.emplace_back(fromStr, toStr, mapFrom, mapTo, kplus, valid); // creating a transform_entry + assert(valid); + if(!valid) { + return nullptr; + } } transforms->addGroup(newGroup); } else if (group->type == LDML_TRAN_GROUP_TYPE_REORDER) { @@ -841,14 +847,18 @@ transforms::load( return nullptr; } } - return transforms; + assert(valid); + if (!valid) { + return nullptr; + } else { + return transforms.release(); + } } // string manipulation bool normalize_nfd(std::u32string &str) { std::u16string rstr = km::kbp::kmx::u32string_to_u16string(str); - if(!normalize_nfd(rstr)) { return false; } else { @@ -864,8 +874,7 @@ static bool normalize(const icu::Normalizer2 *n, std::u16string &str, UErrorCode icu::UnicodeString dest; icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); n->normalize(src, dest, status); - UASSERT_SUCCESS(status); - if (U_SUCCESS(status)) { + if (UASSERT_SUCCESS(status)) { str.assign(dest.getBuffer(), dest.length()); } return U_SUCCESS(status); diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index f48ab8a47d8..56cac24181c 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -42,8 +42,7 @@ inline bool uassert_success(const char *file, int line, const char *function, UE } } -#define UASSERT_SUCCESS(status) assert(U_SUCCESS(status)); \ - uassert_success(__FILE__, __LINE__, __FUNCTION__, status) +#define UASSERT_SUCCESS(status) assert(U_SUCCESS(status)), uassert_success(__FILE__, __LINE__, __FUNCTION__, status) using km::kbp::kmx::SimpleUSet; @@ -109,7 +108,8 @@ class transform_entry { const std::u32string &to, KMX_DWORD mapFrom, KMX_DWORD mapTo, - const kmx::kmx_plus &kplus); + const kmx::kmx_plus &kplus, + bool &valid); /** * If matching, apply the match to the output string @@ -128,8 +128,8 @@ class transform_entry { const KMX_DWORD fMapToStrId; std::deque fMapFromList; std::deque fMapToList; - /** Internal function to setup pattern string */ - void init(); + /** Internal function to setup pattern string @returns true on success */ + bool init(); /** @returns the index of the item in the fMapFromList list, or -1 */ int32_t findIndexFrom(const std::u32string &match) const; public: From 4cdc2c3b5476dcefb43ba43a00864a8ef0bc4aa0 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Thu, 19 Oct 2023 17:09:37 -0500 Subject: [PATCH 38/42] =?UTF-8?q?fix(core):=20ldml=20more=20updates=20to?= =?UTF-8?q?=20normalization=20per=20review=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reduce function depth - catch another gotcha (valid was being overwritten) For: #9468 --- core/src/ldml/ldml_transforms.cpp | 42 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index b2bfdaaa040..56a63278764 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -408,7 +408,6 @@ transform_entry::transform_entry(const std::u32string &from, const std::u32strin init(); } -// TODO-LDML: How do we return errors from here? transform_entry::transform_entry( const std::u32string &from, const std::u32string &to, @@ -417,13 +416,16 @@ transform_entry::transform_entry( const kmx::kmx_plus &kplus, bool &valid) : fFrom(from), fTo(to), fFromPattern(nullptr), fMapFromStrId(mapFrom), fMapToStrId(mapTo) { - valid = false; + if (!valid) + return; // exit early assert(!fFrom.empty()); // TODO-LDML: should not happen? assert((fMapFromStrId == 0) == (fMapToStrId == 0)); // we have both or we have neither. assert(kplus.strs != nullptr); assert(kplus.vars != nullptr); assert(kplus.elem != nullptr); - valid = init(); + if(!init()) { + valid = false; + } // setup mapFrom if (fMapFromStrId != 0) { @@ -460,21 +462,21 @@ transform_entry::transform_entry( bool transform_entry::init() { - if (!fFrom.empty()) { - // TODO-LDML: if we have mapFrom, may need to do other processing. - const std::u16string patstr = km::kbp::kmx::u32string_to_u16string(fFrom); - UErrorCode status = U_ZERO_ERROR; - /* const */ icu::UnicodeString patustr_raw = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); - // add '$' to match to end - patustr_raw.append(u'$'); - icu::UnicodeString patustr; - const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); - // NFD normalize on pattern creation - nfd->normalize(patustr_raw, patustr, status); - fFromPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); - return (UASSERT_SUCCESS(status)); - } - return false; // fFrom should not be empty. + if (fFrom.empty()) { + return false; + } + // TODO-LDML: if we have mapFrom, may need to do other processing. + const std::u16string patstr = km::kbp::kmx::u32string_to_u16string(fFrom); + UErrorCode status = U_ZERO_ERROR; + /* const */ icu::UnicodeString patustr_raw = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); + // add '$' to match to end + patustr_raw.append(u'$'); + icu::UnicodeString patustr; + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + // NFD normalize on pattern creation + nfd->normalize(patustr_raw, patustr, status); + fFromPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); + return (UASSERT_SUCCESS(status)); } size_t @@ -813,6 +815,10 @@ transforms::load( const std::u32string toStr = kmx::u16string_to_u32string(kplus.strs->get(element->to)); KMX_DWORD mapFrom = element->mapFrom; // copy, because of alignment KMX_DWORD mapTo = element->mapTo; // copy, because of alignment + assert(!fromStr.empty()); + if (fromStr.empty()) { + valid = false; + } newGroup.emplace_back(fromStr, toStr, mapFrom, mapTo, kplus, valid); // creating a transform_entry assert(valid); if(!valid) { From 3ef04339ea9850a45019f11bbd5e427f3778bc52 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 20 Oct 2023 10:01:05 +0700 Subject: [PATCH 39/42] docs: macos build update --- docs/build/macos.md | 130 +++++++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 56 deletions(-) diff --git a/docs/build/macos.md b/docs/build/macos.md index f5ff0409640..20ac70a6276 100644 --- a/docs/build/macos.md +++ b/docs/build/macos.md @@ -1,6 +1,8 @@ # Setup your Keyman build environment on macOS -This document describes prerequisites and tools required for building various Keyman projects on macOS. Each project will also have additional notes linked below. +This document describes prerequisites and tools required for building various +Keyman projects on macOS. Each project will also have additional notes linked +below. ## Target Projects @@ -10,6 +12,7 @@ On macOS, you can build the following projects: * Keyman for iOS ([additional details](../../ios/README.md)) * Keyman for macOS ([additional details](../../mac/README.md)) * KeymanWeb ([additional details](../../web/README.md)) +* Keyman Developer Command Line Tools (kmc) The following libraries can also be built: @@ -20,19 +23,24 @@ The following projects **cannot** be built on macOS: * Keyman for Linux * Keyman for Windows -* Keyman Developer +* Keyman Developer (IDE component) ## System Requirements * Minimum macOS version: macOS Catalina 10.15 or Big Sur 11.0 -**Note:** to make a fully M1-compatible release build of Keyman for macOS (for the setup Applescript), Big Sur 11.0 is required, as osacompile on earlier versions does not build for arm64 (M1). The build will still work on earlier versions, but the installer won't be able to run on M1 Macs that do not have Rosetta 2 installed. +**Note:** to make a fully M1-compatible release build of Keyman for macOS (for +the setup Applescript), Big Sur 11.0 is required, as osacompile on earlier +versions does not build for arm64 (M1). The build will still work on earlier +versions, but the installer won't be able to run on M1 Macs that do not have +Rosetta 2 installed. ## Prerequisites Many dependencies are only required for specific projects. -* XCode (iOS, macOS) 12.4 or later +* XCode (iOS, macOS) 12.4 or later is needed only for Keyman for Mac and Keyman + for iOS. * Install from App Store * Accept the Xcode license: `sudo xcodebuild -license accept` @@ -48,73 +56,83 @@ These dependencies are also listed below if you'd prefer to install manually. ## Shared Dependencies -* Shared: HomeBrew, Bash 5.0+, jq, Python 2.7, Python 3, Meson, Ninja, coreutils, Pandoc +* HomeBrew, Bash 5.0+, jq, Python 2.7, Python 3, Meson, Ninja, coreutils, Pandoc - ```shell - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - brew install bash jq python3 meson ninja coreutils pandoc pyenv - # Python 2.7 required for DeviceKit (among others?) at present - pyenv install 2.7.18 - pyenv global 2.7.18 - echo 'eval "$(pyenv init --path)"' >> ~/.bash_profile - ``` +```shell +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +brew install bash jq python3 meson ninja coreutils pandoc pyenv +# Python 2.7 required for DeviceKit (among others?) at present +pyenv install 2.7.18 +pyenv global 2.7.18 +echo 'eval "$(pyenv init --path)"' >> ~/.bash_profile +``` - On macOS, you will need to adjust your PATH so that coreutils’ `realpath` takes precedence over the BSD one: +On macOS, you will need to adjust your PATH so that coreutils’ `realpath` takes +precedence over the BSD one: - ```shell - # Credit: brew info coreutils - PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" - ``` +```shell +# Credit: brew info coreutils +PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" +``` -* Web: node.js 18+, emscripten, openjdk 8 +## KeymanWeb Dependencies - ```shell - brew install node emscripten openjdk@8 - ``` +* node.js 18+, emscripten, openjdk 8 - Note: if you install emscripten with brew on macOS, only emscripten binaries - are added to the path via symlinks. This makes it reasonably safe to have - emscripten on the path, unlike on other platforms where emscripten also ends - up adding its versions of node, python, and other binaries to the path. +```shell +brew install node emscripten openjdk@8 +``` -* iOS: swiftlint, carthage +Note: if you install emscripten with brew on macOS, only emscripten binaries are +added to the path via symlinks. This makes it reasonably safe to have emscripten +on the path, unlike on other platforms where emscripten also ends up adding its +versions of node, python, and other binaries to the path. - ```shell - brew install swiftlint carthage - ``` +## Keyman for iOS Dependencies -* macOS: carthage, cocoapods +* XCode, swiftlint, carthage - ```shell - brew install carthage cocoapods - ``` +```shell +brew install swiftlint carthage +``` -* Android: openjdk 8, Android SDK, Android Studio, Ant, Gradle, Maven +## Keyman for Mac Dependencies - ```shell - brew install openjdk@8 android-sdk android-studio ant gradle maven - # update path - source ../resources/devbox/macos/keyman.macos.env.sh - # optionally install sdk images - sdkmanager "system-images;android-30;google_apis;armeabi-v7a" - sdkmanager --update - sdkmanager --licenses - ``` +* XCode, carthage, cocoapods -* kmc (optional): node - - Required to build keyboards using kmc +```shell +brew install carthage cocoapods +``` - ```bash - brew tap homebrew/cask-versions - brew install --cask --no-quarantine wine-stable - ``` +## Keyman for Android Dependencies -* sentry-cli (optional) - - Uploading symbols for Sentry-based error reporting +* openjdk 8, Android SDK, Android Studio, Ant, Gradle, Maven - ``` +```shell +brew install openjdk@8 android-sdk android-studio ant gradle maven +# update path +source ../resources/devbox/macos/keyman.macos.env.sh +# optionally install sdk images +sdkmanager "system-images;android-30;google_apis;armeabi-v7a" +sdkmanager --update +sdkmanager --licenses +``` + +* Note: Run Android Studio once after installation to install additional +components such as emulator images and SDK updates. + +## Keyman Developer Command Line (kmc) + +* node.js, emscripten + +```shell +brew install node emscripten +``` + +## Optional Tools + +* sentry-cli: Uploading symbols for Sentry-based error reporting + + ```shell brew install getsentry/tools/sentry-cli ``` - -* Run Android Studio once after installation to install additional components - such as emulator images and SDK updates. From 14e78558f28135f3585800bb31245bd336c06f1b Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 20 Oct 2023 15:03:18 +1100 Subject: [PATCH 40/42] chore: Apply suggestions from code review Co-authored-by: Darcy Wong --- docs/build/macos.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/build/macos.md b/docs/build/macos.md index 20ac70a6276..aa1eea34bfd 100644 --- a/docs/build/macos.md +++ b/docs/build/macos.md @@ -39,7 +39,7 @@ Rosetta 2 installed. Many dependencies are only required for specific projects. -* XCode (iOS, macOS) 12.4 or later is needed only for Keyman for Mac and Keyman +* XCode (iOS, macOS) 12.4 or later is needed only for Keyman for macOS and Keyman for iOS. * Install from App Store * Accept the Xcode license: `sudo xcodebuild -license accept` @@ -106,10 +106,10 @@ brew install carthage cocoapods ## Keyman for Android Dependencies -* openjdk 8, Android SDK, Android Studio, Ant, Gradle, Maven +* openjdk 11, Android SDK, Android Studio, Ant, Gradle, Maven ```shell -brew install openjdk@8 android-sdk android-studio ant gradle maven +brew install openjdk@11 android-sdk android-studio ant gradle maven # update path source ../resources/devbox/macos/keyman.macos.env.sh # optionally install sdk images From a126806597158853ea7d6615cc84121ada17afbc Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 20 Oct 2023 09:34:16 +0200 Subject: [PATCH 41/42] Update core/src/version.rc Co-authored-by: Marc Durdin --- core/src/version.rc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/version.rc b/core/src/version.rc index d129fa52f16..af2a12eb587 100644 --- a/core/src/version.rc +++ b/core/src/version.rc @@ -23,7 +23,7 @@ VALUE "InternalName", "Keyman Core" VALUE "LegalCopyright", "© SIL International" VALUE "LegalTrademarks", "" - VALUE "OriginalFilename", "KEYMANCORE1-0.DLL" + VALUE "OriginalFilename", "KEYMANCORE1.DLL" VALUE "ProductName", "Keyman Core" VALUE "ProductVersion", KM_CORE_VERSION_STRING VALUE "Comments", "" From 1c639e80ea2aa1778ef6261244345caeafc03670 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 20 Oct 2023 09:56:39 +0200 Subject: [PATCH 42/42] chore(linux): Update packaging GHA This enhances the packaging GHA to be able to verify the renamed core library. Since the GHA packaging build always uses the action definition from `master` this change has to land before we can successfully build the PR that does the actual renaming of the core library and Debian package. Later we can remove the references to `libkmnkbp0-0` again. Part of #9733. --- .github/workflows/deb-packaging.yml | 14 +++++++++++++- linux/scripts/deb-packaging.sh | 15 +++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 6afe34aa142..42feb8afa17 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -242,8 +242,13 @@ jobs: - name: Verify API run: | cd linux + if [ -f debian/libkeymancore.symbols ]; then + PKG_NAME=libkeymancore + else + PKG_NAME=libkmnkbp0-0 + fi SRC_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-srcpkg/keyman_${{ needs.sourcepackage.outputs.VERSION }}-1.debian.tar.xz" \ - BIN_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-binarypkgs/libkmnkbp0-0_${{ needs.sourcepackage.outputs.VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+jammy1_amd64.deb" \ + BIN_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-binarypkgs/${PKG_NAME}_${{ needs.sourcepackage.outputs.VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+jammy1_amd64.deb" \ PKG_VERSION="${{ needs.sourcepackage.outputs.VERSION }}" \ ./scripts/deb-packaging.sh --gha verify 2>> $GITHUB_STEP_SUMMARY @@ -254,6 +259,13 @@ jobs: path: linux/debian/libkmnkbp0-0.symbols if: always() + - name: Archive .symbols file + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: libkeymancore.symbols + path: linux/debian/libkeymancore.symbols + if: always() + set_status: name: Set result status on PR builds needs: [sourcepackage, binary_packages, api_verification] diff --git a/linux/scripts/deb-packaging.sh b/linux/scripts/deb-packaging.sh index 62fd389199d..00eef3b1cc8 100755 --- a/linux/scripts/deb-packaging.sh +++ b/linux/scripts/deb-packaging.sh @@ -57,14 +57,21 @@ fi if builder_start_action verify; then tar xf "${SRC_PKG}" - if [ ! -f debian/libkmnkbp0-0.symbols ]; then - echo ":warning: Missing libkmnkbp0-0.symbols file" >&2 + if [ ! -f debian/libkmnkbp0-0.symbols ] && [ ! -f debian/libkeymancore.symbols ]; then + echo ":warning: Missing libkmnkbp0-0.symbols/libkeymancore.symbols file" >&2 else + if [ -f debian/libeymancore.symbols ]; then + PKG_NAME=libkeymancore + LIB_NAME=libkeymancore + else + PKG_NAME=libkmnkbp0-0 + LIB_NAME=libkmnkbp0 + fi tmpDir=$(mktemp -d) dpkg -x "${BIN_PKG}" "$tmpDir" cd debian - dpkg-gensymbols -v"${PKG_VERSION}" -plibkmnkbp0-0 -e"${tmpDir}"/usr/lib/x86_64-linux-gnu/libkmnkbp0.so* -Olibkmnkbp0-0.symbols -c4 - echo ":heavy_check_mark: libkmnkbp0-0 API didn't change" >&2 + dpkg-gensymbols -v"${PKG_VERSION}" -p${PKG_NAME} -e"${tmpDir}"/usr/lib/x86_64-linux-gnu/${LIB_NAME}.so* -O${PKG_NAME}.symbols -c4 + echo ":heavy_check_mark: ${LIB_NAME} API didn't change" >&2 fi builder_finish_action success verify exit 0