From 3431c345aad31968a025925c719b008e3ddf8388 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 26 May 2023 08:22:20 -0700 Subject: [PATCH] Content Model: Support Ctrl+Delete/Backspace (Step 4 of 4): Finally support Ctrl+Delete/Backspace (#1827) * Content Model: Do not hard code default format * Content Model: Support Ctrl+Backspace (1/2) * Content Model Ctrl+Delete step 2 * Step 3: Support Ctrl+Delete/Backspace * Step 4 * split change * add comment * fix build * improve * fix test * fix comment --------- Co-authored-by: Julia Roldi <87443959+juliaroldi@users.noreply.github.com> --- assets/design-charts/BackwardDeleteWord.png | Bin 0 -> 24327 bytes assets/design-charts/ForwardDeleteWord.png | Bin 0 -> 29437 bytes .../domToModel/processors/textProcessor.ts | 2 +- .../lib/domUtils/hasSpacesOnly.ts | 10 - .../lib/domUtils/stringUtil.ts | 35 ++ .../editor/utils/handleKeyboardEventCommon.ts | 17 + .../lib/modelApi/common/normalizeSegment.ts | 2 +- .../deleteSteps/deleteAllSegmentBefore.ts | 20 + .../edit/deleteSteps/deleteWordSelection.ts | 184 ++++++++ .../lib/modelApi/edit/utils/deleteSegment.ts | 3 +- .../modelApi/selection/adjustWordSelection.ts | 27 +- .../publicApi/editing/handleKeyDownEvent.ts | 21 +- ...hasSpacesOnlyTest.ts => stringUtilTest.ts} | 2 +- .../utils/handleKeyboardEventCommonTest.ts | 69 +++ .../test/modelApi/edit/deleteSelectionTest.ts | 402 ++++++++++++++++++ .../editing/handleKeyDownEventTest.ts | 93 +++- .../lib/corePlugins/UndoPlugin.ts | 4 + 17 files changed, 851 insertions(+), 40 deletions(-) create mode 100644 assets/design-charts/BackwardDeleteWord.png create mode 100644 assets/design-charts/ForwardDeleteWord.png delete mode 100644 packages/roosterjs-content-model/lib/domUtils/hasSpacesOnly.ts create mode 100644 packages/roosterjs-content-model/lib/domUtils/stringUtil.ts create mode 100644 packages/roosterjs-content-model/lib/modelApi/edit/deleteSteps/deleteAllSegmentBefore.ts create mode 100644 packages/roosterjs-content-model/lib/modelApi/edit/deleteSteps/deleteWordSelection.ts rename packages/roosterjs-content-model/test/domUtils/{hasSpacesOnlyTest.ts => stringUtilTest.ts} (93%) diff --git a/assets/design-charts/BackwardDeleteWord.png b/assets/design-charts/BackwardDeleteWord.png new file mode 100644 index 0000000000000000000000000000000000000000..8b9d18d9ac48f033a5d5bf2b3a7b00cf2b2d372e GIT binary patch literal 24327 zcmd>mXIN8RyC#Z?iok0FkuD-C?G+RVC7?)Ann>>|AkrbBw}^_UbP$jbh=PE00U?AI z6r~e7NDW9J^cDhylFY*Q{pNh%nKN^K%r$4`y5uxMTi-hP8V9K^sd4kL0mN!JVd z;IFeR7A~a9!~8S{27H!ua#9X9v4gZDYTsm%d=xibCnxh%mKU&3j$^OEsCnkY!D~oe zH68BUD(}g>V-z8V3Qh>qi1t-l_sdtYE0bd(wO>8FepK6rh_|l0s8=QxIm8sASy`f@ z2!;a(+y!8)6>;k({r#tGa(s>HGIQNRv4uqhAHTtr1JwH^EyhU)-Oqw&B3=ag*cFL3 z+PS0-X1CaVkQipNyJ76-!Q;*YQtuGPw_3Et;^5jHk(y0D2J6*d1TJWLd!1yE;U_(H z;@)J{c+wN-7$jhuG0|7xGQVj#eb}o`tO+uHFs^U+S5*6yr4f`RSnm2|z3iE%A~Eo~ zS=YcD0^u;XVQsImTb6r-Cd_oZ^HXvxyspdE!`bcLlzHp<(P#Es2R=SWG2(^9d~V&n zA~zmLJGX8vnVvMp`&@IHvw#f&Gr#YOzZ{Hn%7=myK`mvqrb%sYr8GoQLHc@`V zaPGvC1DXet#B{4`;t(h(5quiZ?zsrTvdxJ6k=}ItM*%08_+>d~b^HM)PF}2_lQjvE zZ>pB2)Rcu{e#0678h^8YxhhcZ`X{{XJIvzaV2?1lQUlXO_|2||f}oM1qAlR_y0?bK zLpNP*2;&uUr56X2_ES0bQ_0phbUi;U4V4Ry&b7ta+TjnYhV6fYGexd0@UH9#0#?2( zP+LV#f^6@!pGE;`M`db}F><+5;<3{rd#0%Uysm+~Zke|CfsUGs(ER8ZQz64`kq7u&b%||4pb8`#WSYuY~l>1?#H2h4FieNdxxi)U6L8u z6wT#2cE^q7={qmB6p!`B5#PxrQuG@=7AdZ``>o>owVG9E2qt#_^4T+a@5Pjf?qKaD)d zk;rkox5SF}diH9S8jPlkqnRk0DNDgve@Y0d+ZBg?*{$iu6F9hO?PxYp?_e4qTKqDJ zx>(a=DE=zqEwK^iT6<4Ns|X)&Vb>7@H+hX>+|Z;ej(9uFZf}D@ojR&AK{n=)Ptly? zdk5EzUCpmD^K12e^9UkieHnws`^DugaKczcK(7R+v{pq0F!gd8UxO9mMY|c(oRd_` zSI=XKe*N(29*B%mtT-gP;Kx6e)GAW_$I>|?dkdjdUE_exv_+|%vLDtQ(ThukR`Tee ziMDosk=$u3yVk#J1|L`*Kb1+gDzk-jvIW2}oE%6Xbys zCQy9Nf*6?g_cNq1R5m%v55{Wy2Y3UMk)EADohvtab++0j4aLZL8-HL2tWF7fzbg_Y zKvF)k-<~DKs}^$0?}Y!p%$W2^s^Pr1{|x45ES&%O{y=iweQL0}HP^Y)#N+iiwh0n* zlQ_Z)I!(85Xug{|yX+0!T~EB*p-OoWb8I-ulP|X_3tw?tF6_l=u=w%AT%#B5Xv>PG zTb-qug(!P(#OA6Cn!*5r2IaR7TyLW1k{K4%@Qbn95RNm7`WIs?2P{^+Z^$f# zvDwOi7K*lF-_Ky{EhUaKK%Sw71$V%5;2&jz&B?5>znywu{KS6N$slWv421Tsl0$Hn zAz$4E!u4^zYz{|XqRy_}+Fy%Gg98KpCMc%>loRyzam(D5tJbA6_C@M<$sFgVLkW`D z(S#Z>>#N{CcPoLR(2!fc!SwzZ1f$MQ+?7E<`J*q)=1V|ZuT8Q)@WzV_8EX}FDXJP~ zGHx!jeJQ$p{L|S$J;VZXM(i}5PoOmY(MA8(?+7jj>yx;fdWc>0Z8ylCwta2XYC?Qa zQN@(g`QTkxP_T!()kLU!fyvwV17J9#yJyrsXt%OtjT#_IHmE#wDU7|JliT{_1dm=$uxt9JmkGn!pM@PVYklSdO8 z3K(RWA(*g4RAZWU0uJABtjspa>!&B)-rB_K54~3ZNjCpqhO-?i9f?q^W0C(C4RJ$! z_CO>2w3Ah;uFt@_o5;bgTY05w_{q>Ta0ii^+~-$<0?RKYejxBQB*~*I_DRSH_<$c| zNNYZRl`V&3W8-G{)@cXHp|D5(UTMT!&oGok-2$qy<@`>OaoGh!yuIsbm(LzUmp2y> zBG!;#>20x7t~s?1yYrt4vzw^1oe!#ibyIeKu{uyAi!Sx{F>pTzbHy6;OIAilWnKa% zBKvY89Av*UixS3O9TgxA?ptawYMte~+j-re*2lsFo;3S;dfiAR&Gwa7M#hp&Ia)OE zHcqP-jWgwm3i9hLQF;0ds7Nqy;~{8Mla7S}c7&xuc~yWYX`0#IwclLt#%AvIFcjm{ zgeI(_WD-1#6*52^s|iRF_dyr9`PyY{+PnEqN;um)8o92d=CY`p$zQtU8^Y~=8s^Fd zi~4~?BDd2?xs4p+m)DglTTl~iQMWD2&)HVpEa&aTFpWEd`9b6km%M(RNRbJwkojbz zn&xR6ezTm*cK&9$fUV}OQ8or0Rs7;je6ey|w}w+=Vf_a?KlV@skKYgMP9!!r-|-4# zkUa&#FhLq`X=!O~J&PTX+0jlY^GypFHYYi8^Y9q@Fh3=O5gY?oh#{7i*RCcX~Pxt!ZWAC=+5vv3ER-npo4aCpN22M)urWaCsjth7`z2Z8!;&Q=%jh( z%%B`0pdQUG3r&y1oCBLKz4B*Za0cDu0T#J4mW{i$<109v-s8&7 z#A9e&F0UA`t)pWLpR3=J3)5m_ZA@$=+u)&zqf$>#$B>=L-$jJLYCdYzl0yxt>KR>7O^v)L@T%WJ|WRMq=tOzI}O}I_km6apkPEuAFOu|uByqfT&HKtoi z0fKojaeZSx0Rv%oxZShBKSUuQp4xg}*hjV9EPTwbA!+0H&S zY{Hs@Hb`I)FNStYMLXu1U-YRQesC7HbXT-J)s2dL+4JG1*JEkK=h>XT>En-kwis_h zCC+|+X9-!hCI?al8;F?ATxPF$xAQBXdX|g=|NMgeXq6fw$oWw7JSB9oe{8k}K{vNK zvSW4KI?sWaGPavS{ie#s6n`M!-Ue@Is;D~U#2#eP6x+Rf_@&XR^dfk=S7h6)%K`Gv8-j&xpTH zDwXgn_7^SxCQWNrfF$K*$(%_id_$2-UKC~2K5k)oMy#LX#x4NkBW7uxC&dmk!}o2= z-g(rXN1k0m(p*#@RQ9Viop$B4{>yS=17N&Rgcp&^D#0`4d-V?$SBnIze>JyxZaM=) zNBzEuO@cg%q)O$iweWt6+*nZzoL9ujS<&*}8_@PBbYyV&3-het#SukFnEAPfQlTT` z%rBCr6jw6z>`4kn{XhNv^CkY5W;Y)G;44gw#NEwlD(=w4VoI-t7ol|@-dwssQZ~AC z9;Tu~++7|E7@r#{sMni_nFKDe2$+Ax{sH~G#Dr)C^{CbYl!SH9dprYHxJpS+%q<*4 z?mkPo7c!!P53#r=C+D9l?WDA9QEp%xpsHxVzZnDP4-<7Jc^gMHav@PDgXq&uiokgm zNKGmvx-@@7DQ%X^oO3l|JJob&|2D>TR4!1C8SO?eI5kr>_lyn7uns(W)?_4d?eMMW zLP=8RqW=7`=;&ehFOGE(P(O3CAvx>8cZMApr46GRiMk?b7!QOA*4o+GSy(Tif6`KM zIq=u=+>#y}TazIuLQG#@-wL53y-2XK`YAt2S^PXNFVwx8vTP!9dEL5lq=5HS%rK}> z#=(gusl(#Ihl5Ph2Ik$~A(8i^nVMWc(R!{*FLug1Zh?~Wm>7u)AooIG@aI$Zva~86 zrI}eSGpm^(89l(GmYYkXOkbK+Z)Rp@Zp7hb@f1qtz*ip zjeO24AB3^;=T1J_1V%n9P|nfHWS&L3ym*Hz<_v`C{cSV(e#H6fs=Wf_?Z-HYb6B6{ zw8|oe$ATn=$DS73j|j&agZQ_Z0-lxRe68eu#{^;evlVz5y8Uuff&;eox1=S+ttZI3 zAx!)vhBh^Ib*FhQ7o|h_&i7H!Vg3}3IoAUkktjxuM~@zTW-szcwy8AJ3 z24Uw>7C0l+m6g!b1ULCtr=6RNJriGpr_659$R)`=Ra@l5X;~4srzA{1!OA|}Auifkw zW>czBRW##Q+0R!JMGo0G+c29>bBni^H^k@NJ;EJJ-O}iyyG-#?o`jqb0Da$?Kqj>s z8$hQ_3{3OJq%9g@dl+y2Qpz_Q88dleoUR#Y<;kd!v;ujpS z?3uuPK~RWVj7pJsd%7o&e9?GNipVXu|KtqTL~s9bok=~B3wBu@_+e93l{Wx3sj~3W zu{XI`<3Y*43$ndVGQIKl?-;Uy3jPBl82*PLy8kC{ib6;!o4(rUBxI0aiYUMfId6d` z6%$d6CSbMvzdOQ#1Iy`k1fV?J3=9Kc$}&{&g1Pz$(N3Ov{NHmlIi7w5Ys~Fqy5*WTweLv7Xn_m2J^gs(PaeDY zMfzd8;wIs0gUcynQZcG^AQ>C9@HzxbU{*f9UowCc47yPrwI4C*SAnn977xcd`l|?R zi?u-{w72BuoU%sv86JRHaq(^fd6u#|@*QbR>KVPUH~gl~vmi1?cto;R4o#Dj%7epL zMNfd{Kk>%f*hLm(=NWx$p|-iCvqTO&*`N3;2Xg-F7Oel9i{<|(Wj6`%C2XfzlnrE(l@NQY zN{Ur`+np|y2i(!W4OEi=eSJQf9!y;V%dFSr?gMHzeI?ID$!xXQ{EZI&05Ar`Kbooa zIPm}?6_$jjZ+Yo8uV$$us_M_G3jelkFb#I1LLkl2elteoAFsqLG+`C~Fg}y|cOd|F zo!Cu}hu7UDazierz{I(&JYlRe-Xro4cGH_htxsEFmhbO62Xwruj0JOBx~T-kEX*;w zm6DEU=p4T{TE>U7Q1SusG&f&j)ZdTPqL>@mK>?KjJ*I5U#pFy$0(3ghd`2Dr@Oge4 z*8Oo;Gvq-Cmf2GzY5Eze{>UH_K2f({iGy&C86)xb!;mnR5`I#il0r>Q8`^Up0?CNt z1k=LlqIjp66%vdPE?aTZfsVZ$LN4c(W*JMTh> zpZb3RSkBWMr#NQVC4T2DnfyOkC`lA`D{!;LvvHYaZA&Dp@ z>)P~3U{z?5{gAj2tE>R_W1l}{P5vJ-nDiP|Fkx?bJ;&uR#t%fS9t~Yd<;dxZ0ZHASa?dt^}&O>t+P8%5GfHJ+e2keu)eLWipncbzeGkxj#hfLZk;_{rTfLY;-?L& z7JwnU4b4R#D|2)zVbE*W1lTgLlw17gP8@n>Umg2KN^IvUGIH>K{~0CAph7Hu;7klx zD3`pP1fkkbf32wYc*)yZ)PHC;6P8TSaO(5!o?(s?GOa~%ja+h8r^iYpD!jvVv)*@N z{LR;2wCz;w!3+z8MP-;@2wED~#^7$uX)>7nIg2^eN{XgkHSF(2cz=;zt|`HmIyA1T zoVA}TfAs z+r0eK`-6V@k^I5*5Xu*#1^Zxw6ETit+L5eF8ri*FNmM9j|J{40_$jx$q)d^uc_T}X zV7F{K7E{yUD$AT~f4mxiH6NE~$}@pGLXEC>2wRTnne+4CpFe4FzdQq7l*+4Z$_u>tsv+KT6rDy2OpF+pS3n8WmTl)i_MdtH1AGs#ws+4j4GB? zZ)~MN<@?qqXx{p{9P+AR9j*OOCH_bhd;Z~r0L3Ln3=F#esZ)^&sf2^C*45pAg%cVm zhr;~W0sA=v3CDeKj?zBXZ6ko-Kj*OcdGCkB6;BXGG{%(IfM~G~0FI78YOvKJX>f>G z(mBC_mVAN_)$>W`c4~BtpluaoAM;(iKJk>p zE*x@_W$x|D_#(?H-lKPZnA`ce4 z@EPBsBiHf^8M~#VrP)u&G@I?dP6*Aqnypk~y7Dch3ZF){uJ%+7F6+Zs)YW_n!0ha6lyT`Q!D#( zXPJ+5;pvd2kyN=Qo9jC3G9)trvSP6gU&?@7>B zim%isr!MaXj-sU(it^Yj7I9g%n{lGoNEkPQtFvPPbRZuff}5|`WCdqAXj~PapS>;uO$z)9!U*Y6LNNsRO3tae~j_+u<=y+ z2aY5-^@t0HIB%Yd=ED3gd4jgleYy+ti6_rhR_`>eTKrs4n3mR5@@EU!rxLxwQLlWv z3FbYo0C~HBTjf`KN1qMHkPic3cIB{Ok=sVfRi{;j&2kKW;9ZU-ai4zhIw4WBt4|fl zsh6b017ezes>+hjRjku!BULMVeUThE@|Z|~+fJ9_)D6k2ZXwEsE;SynSEI^Q6>aD( z!vQrR4L@8}BrcHFGPy3e$VaE@iaO=)uLvP6ufFfx_Hy47rw{Se0$?@h;bOj}JNn8i ztF;!jB8w_7>-XEv_YUGP>3$*d4XsA1jXy-T(~i7=5(5P;qRS|eYfh)=3B?|XQ+~Sr z(j(_~?~h_LkNcb$E;EAmT)#N{2PeXa&om=@b8bupEn!>aWi#M;hcXDK6$H!+J@hkA zhs596O}Hk0jg|U&^A%|ULSRxAHma_L3(V8dwG{;$77z5Q!o{wW){=z&@LxH-Fu&=Q zXe{kl{k3HBt57vUjRvc!suwnADB-#1e_^d|Ge61bOEdcsiOeci2;x1!KZMZb3SZM4 z0fvKHzWr(PS9i3P!@|eMlc%t5?iOzV(c=@I@mh(fbHo>yUE+(J9JqS_FLm8idBe%Q z2N;QKW6YeBJRd^s()^3)8J5C)d+qE!95Rv4*HQT!Cq_(WqUWE%pt9?BsS=X(rTO^{ z@1=rQO~=ibT(<+}o4o4?Z}x6@eN9!*C@O51DhZunyJ1exIuDp49lCK?@;#HyrFqR1 zAMZowQ-+gP@_&MDJ_yM7a_)=(0z@#ujuY}h{FZAKKw)G$x4xyIk&ceYsXT9P1rMYC zU%Ca-^!uU@tp;Zl1G`1Sy&TGCxrCzfCw+p9ymIE5y{t);aTGv?b|Nr-X`v++Wjnr_ z?StdPXxptlsgmo1M(m%UTvz232vWPtNnRRVrWsX7A!v1DA40z*g)6-$O}LXF~VFIUJHu2>EE0byi$K1k@31%7S_Rqdp`bFaazZ+8GS_EX1C>vRt`F?a}W5} zP9|VBk!#7aR4%qfB`e%dQXdDj{ITaTWIR6L^g+NS6E_cZ@^F~q5-c1dSy8&b8!%U3rxgFG1bxqq1y^dgS zvsaZ6waenEY>2X)g{EY>g&VDSKxU1(%rDx0w6ReeWUu7EQD3<|uE6A|l}y&Ow0@g< zmquIj(UjXnJ8!$Y)p@#!o*VSvTDZLuU*EhL9WU?QFx+(`s0c3`CZy_{0)8oz`r+RkIgICQ-7-+=|ig?=qM0aRhyXW4J_Eh25s;klB_ zPsHl|wWNf2q6M!bfA70F7l*3o8;H-tFTMPJX>{2|rp=jdDmI$yE1KD2C_n5qqhxZV zmaa)l16N3TFQ)3rM(WXIR*A_gU*T9A1dT$iALfdy6>_#Q-RbJ-4|?CjC|qf8p?!JQ zQcNzecJWrWzeW>_xFwQ}jg3!s_E#qM+Ax%vVx2O`+!wh_O3cKHc9sS6o?vq$5#~w>Y2sbI@G$lk#dG&dc3Rl zz*I2++Sg(gc^1fkE4VG=1s_4Ne^qB|+m z?usb$qaL>Mn$x3{ouAl`MXU7nA1<^98U6-6kEa3y14pWS zYwh1vbc5D^)Bkvbw=?EdOetA};^N{Kq`&@|NROeOh1aD!_ky}@I-%PsVl?IF{?6B= zAUL08EB<%Y;XAsjG~&2V*Mh>C%W^2<;b$|k)+zA|5>G|-Vbu;ntEOHJiY1eVuJFa=g z9JZ*u&OsWMsHuf$0<@XCaK1{)&kaElN-nCU(R(L7Qvf+=U5P5j{G0q%Q5tYL<;@a> zDLy!Guu&+172$Np!*6fRP+rT&T&-NkmP=K!`a3GyKWk6JFoR4`EyZ5{yyF|^a&k~P zw*3s{Xl&$ufi&eN$nxptrUl3-#?fXkw$xb1Kfv*sYY!V5g;33y3{lLO0X&vp>fkcHbT=v)1T{h?1 zNP%4@cNNALIeUIJqKpZqS?^BR^rR-`KqqD*nLfzq#=8%>|j1tWP5E1_l;0M1Pg~q{WHzf$NXRVQRQf1{L)9}Zh z633*(){*4ufnLZ(`$#U0*+~0op^5wcSjX|{pKoxz7hO-ANbHe@ORtbFFuz%1lg95I zx^KE{)aP%xdeL-@eK}w_8o7jvaL#B-e6xDa5D)ginpkbIw-BSzm8#8=)xhzWx_d&x zan7+TN7h9*06$|bOfA*pto8e^s(fkXb+M>cm3UswkD2W-Gnlyj)Qfz)-IUm%IAjou z%~utVrWsct==MdZKXr~m#4xe3D9wSsOm7W3bp!C!+`YPl_d{b;ErGYOl)`*}ZKVc0 zHx=$t(qdCdmwfG}Yz#>fS{Ql$rL}tlHSl3sH$`3|pOW_JPKiCO&_*mt0)BK;)PQTj zA5bvnBJv`lqX+y;7L&a>1xYXI)b58=f-gQARdXRVcr|rMGYCVB^Tpd;aYOx~OP~EB zH^m?#Q}z+wMqL0^aqDs$sVH_(k7MIa_`51msrAA&(X_|mvjR=Met``qQR$Fq9>q(| z#eHvB(GrlknV=zUK`y@e4W!In*)*+k*mAc8t>O%?dyiZH!R(cp1Rrnd-a|XKdt$=G z*t_M%F8N7d0j>}tme8OBp4yeX9oW_TmAmnKeMv;?X2TNkq@IQ{X*1mN+HB{0xl&(1 z)0^l{m%*T{SGRTa58K(+%%=-Ck5Rv;V8omCMkX8MUr#IaF_W0C3W^WGVh8>H{5<)J zDm7#yf$mt?30oK+%fxP_XJg)X{y<~n9BsYC+Y>J~Zra5=qj75q#KE7N+B6K&Haj0b z^|*oM#UU$yw`vpd?5QKK2ZroG3@HNCVIPTVNEZ&87@2UBLCfciJ*5q3PYo2RMvtFq zr`FZgP0U?fUX#;19sIB$g?AC`MS{-WDc7tHY1pboG_fvOBAEfl!z}ksV9Dqoh>}sS zZiV0gc9KO!;S3b95`Y_dkUM@_gxQBrl72$o4{S#A9OJ78T)}=Y#T?&SA2GeVHYHLO zCDiOk8SK>0Yq#!dj>r56crW?L|BU1UOOl;M@pbFkEP=k#GTMXqs^C&~SvXvLenCFI zP$5bmsUFb&43L&^F2S(AF#~c($607e;QY)xW>)s=`_9>&621{;x22)VfnBMEm1Bnv zW~mFDCUA)($p%$r0oAPAvb~`Z`Mz<%b?2GlO`if}{-+x(c&8iv@Jq`wGiyLf?LB)` z%T}`1c}m1mKkpT;YWgC;;8-_mp47I?nU-a0zY*{0@o8UFx6y)=HqJoUe2H{@N42{V z1X=PwLPu;$=G&dsWI}Lpan9&r^;z9A^MJ7cJ13utg*zqsH^#6`<>7oGFap z4y|K>n2$a4@J4z3Bza_b>`g~Jx7JIP(mglTzBg}v23N<{AvG2*WX~<|`u|Ee<<;=@ z6hcy4FG+jhyHigxM_%K0u3%S>kW7uxw9_K8ayw?1#W1?R3_zC4_)#I=k_&ctrXk~V z|IBoBzrD|N)Hk1cn9H>A03ZNRgIJ=Fz0YF5Ya4Bn0{1!S>y9T#%zuK;m!!Jc{UG~U zcCIVivKo}13pmc7sZnPlcX>@c`|{_8t(pXi|E^n%mLAUIv~FzW?KKH=#w;g_-)|O` zCtHrofgZMOErmJGHmt$!@Qw_fU8$@8^9ioSFS9iXYA3_7A9p{t8j&Q|!n#3AXytidJ*671 zcNr^2DzP-gFLo)(4dh6>#cW5*ayjcnk|I2+&(Ey4bZJe!ai+5JqjgOQZ|W0#U;Bw3ghfQxwndG3 z;`F!m`YVEdKP?AELYs(lf&Sp}`!ll~T{cOcxyJn&r`xG*@np#c@49u{73}tcNop&j zUN*a1NFI4c-=fQY)SiYLzw%~kB=EyXupH@5VTrv-s=1P;MQYJ7m*Pd#tP>;<5ZsK? z?!X?%TFE2$O%0P8#;z*Jkh=Q(^d88KH5-v$XWxKpwyOTpeXKxiFyw6W3p7;xoj%__ z>nm)YDx8L&vHj{q0-}~Z;te!dq1f<#>zht;9i`H*vft;YP$ToOZM8 z*g()Nk8ZX{$&?`~bz??2I{DP)`m{xe?c=UL^1QyCzmPZ=Op)6r8;V4rghiQcA!(m) z-%C0CXYeI$0hz6X6Lx-_+wDj{F9oCdS(tcDHg-v#L@2F`>^w7jNHa6yU_-Y?}+l*Hn zE~N#LfX^9eGyd?yDI<~z@}q#vlR$#sx4Dsq)om45Lv8n0Qf2a8y0bkai;2_@dU)*RveP9Eq?DGF4@QK0bhP6`gc}2QG;olR)c+c z##0Z~;AwNG`@DHw#7360HggTpEVb7C5B<+@wGOGU;GusWhiR1urz5YG9GR&mZbx6l zuj(3@H&t9FKbnz|k?mNzLdHkNik%&_(qro@>xR6H;3q|O5BpFS z+1Qe=UY;yW9gbh>7uOGJaH>Xfw_`QR;8o#3`Qa1h=2pUi7wM4ZG8caew}p|s!y@uW{zJ}e!YHWKrf{nTg+IlfJ9VY%Yej)kR31|FCOTEBhCs%j43p8PSKFUd>MAF>$7a-uN?^soZp?B-% zQ~U`1T6m#=Q%d@*cF+;fIYV8{o_?PI;D*%|YXWf()0LZ%O?SmyQj471a!JE{0eM!z zlfCtDG1H{hIg3=&tFR^I`_Nf*&MQeKT~tt3T^H{4GGcCT5?PZ~B}N9|`H*7}ChzLI;CHD~x&?(wzUi(@-4@fO8D)S2cb`q-8YzA8 zmVMU*IqPzZtX~MUqk9*C0|SMY#?dV9??6Wt;RI4P+=>dBqa!18!bm!trpWLVOf7Xu zLM8<&d81X0_q~D^<>{z$u&jtv+yb}wB+#NWEoA^xSc$|^wYNB(Y=*2awrxgwq9v2P`1m?@I!$%4twnuc z-vc0s2|( zttBl72-;PyVv>5A0mFR}I z-1*5>RyRl%o|e?~FX??}G;H2PUw5OoodjE|f;WQM@!T>E=b~}Vi$xOxK8L=Y3|lsl zr;E0d_pulWXO`#@Aigq7^>}?<34YjBK(oJ;6c^a`sckbDJ8|y^Buv4)+~0gp-aHDq z^x;~o@#2$+i_{9-#lLp(yO3o|fFPOm2oi+ciA zlWf{B+eGbR={@>FkPcwwwoL7dV}Z{@;5wY=YMp!oqaa zlEPmrP4ss#JJkfVIB9K)K@)CJ?hNGJfr%Gd^U3O^1cr#i(=X-E_aHf>`4(6gpozqo zFOtiNp8kv{p`+YOryLf2gG%?zn>F8M8x8C{i^cAsfTAzMgOQ|ec~x92DLs$e}HL%>z||7Y=UwAx|&W^{ufJyI=~94i)yQ^ zNxkoB-aoU3_9cC&OS8Gd%3$9%TRHvpgG8SVgT*fpGvr0w4vy4TT46<_YbFXH12LZS z5L|`eYNAe*KN1~!Tn1^6mQ5zm95%-UdCk4|XdM&^*x`R1>h)chF`o;XFj)S1HyKW4 zVu>vmV~XS6=zCWZAFJCUwzp+KjSe7sex7k}$!1|ixmJx#ZD*M9%V=w9p>o{bDkFmz zQWezm&ev8a@9qg!i3DiNj9H9L)!CQ&H7nS&jGP;xm?q>Ttx{*hhizXwuj+^)2A8N6 z>z=t@)Oy|EdqE8{9UeOR(-vAfI@>Dk0AGKX%Zb)y#c(TdetCR)8iY^51j!uQ)Xv(P z3GF9WHy;xB261hhK9+KvE2a)`@qhC;x4Fm4Q&!DsIQo=Q&-IUm?x(CIJ3JOslgKiO zDY=|{&Gv(_LrDZQH5q`t=5&u_VT4T_o1M(GyppxgLN9hJyWk=~y;T?LBx64=n@g!B zF5fpT05AllWxBg=bw~!Q$JN*$MK1y<*HhoAnv^2$-O57%^(7>lRl}m%ejx0fe=8^ zR?%A@mB$12hKw@2D{-BSF!9=IY9A@>qWbOVXoHPX{n)wEuMvo}7tWt|3w~O|r7L!P zTNP5yfGEoLvy)KA!-F4w6IHoay*mx|l?R`T#rl|iJphdx1Y}M7Ye6K;dUR;|Q##t= zd|cJr>BcMp=I`F&nrnJ9ulEtpc*C?!CCcO7oF`PnAw)F|*eC;uM6P&Tadi#(tHd!y zS<){@y4RI$g_{W=C_Z`5Zu0fq12u2|q!_wGxUTfcySlgIcrRaqt_96Ao*c7_at8Ck zgr1|KBs4*34<7WlYLa^RdZ(6CmCZ-?mvSUf>rUx1brje0Ykmbt`|tQ?dKML!lg6un zk9;cV+s}2);un5dbt2(35Uabxww)-;r3C5f4ZJxz;3syObgO@c7h#(=&=pB_N9+{O zy&QsveA^2F9U2~V_Fj3A6=6XPa$bSNvZQo2nKbBS10ttKU#9q(pD)pE%#_$jbsu3$ zM|>8(oN}JcB1u?CwL_%(^P&bOk|)8fCYUhAb9Uj+a8p}yFW;N$HSah!0c{irV5^K( z?%8)xH+Wa`$Ck{$Nv)J{(;r}#4Yb!a7VbpzqGi_>H+Z+Jvo~`qlh{f+QOu?Xc2wHQ z3-i2u%`5nf0`=VUnJwMZ&u*a=jwE9{3>3=bwpVO#;=IF_-R;KPun+v~^npnuE1iCz z<)CV=-+I_eLcP55+SXLFS#}^1>1@(8{bNdG_T>t2T#}@34+W_8`Y|`l1bI1eE#FbO zUlKdyU#pV%Ei3C1k7)~Tuj>Q&vXj|0Kmt=+a_xKtw(4uETVxm1f!<$30S&Q(&>Ek0 zQ_*HJYOeZvhPY3gmLT}~2nH(==uVv5n}Mt6in@r)4+f@R5(P;4p@ZF9Yw>G{jNFcw zM3`yka-K=&%Ugj_edS&j)g@mH500|~M-T)%%lQr(NnZh?rkHkSFHCojjI6k6g5CDM zjx_f1S|l;L0Z7lwoz?18ZLb85|4cS;JDVcEA9gVwSlW3qaA)?5Xhmcp4|KMmkkazn z-|Y)QvYW;+Hd3}CvD3qW>FZYJqRYJeR2sJmzv0VJUWIimQ=Y5WPo7t;HW-`d&Jpouiap&%n&C$0q9Ghpna`AE9>?3 zsrDXp0NTX#$=!u-B;fP=Z8fuLafrLulF43Fu2v8t7E)NPog~xRrU_L3c%61RnAj8GfO_qwZX0H_pV$k zQgnvR5Od^Kq>763BSH1%|Cr~!8&=Tt%C{FBKoBbbvPY|E7jF8hm5*i81(v9Yj$VMz z_lqIkKipZ0*R*n!2uw3EH8IsEcz+N~)h1<OwkLqhX$PuFiMplzdOPYs^lU_XnvzP8g#cOvi&~ z$sNEms){NzKs&J>dJb4Uo$D5Sf7-1SlMbaHEouBf#S$5vKMn{NQ{O?%7*?>_!5^6iP5w4V#4J+R5Lg5tICEI?}z&%&88fB#piI!8^_NJ9xA zEr^x$Cl$F<*Up9yFg>mMcO3XrQR^%JLV%a7!kKTYH7?&B66ywO(X)Zz8~~K8fsc0K z)N}2J$z5Gtt(K!uM8$T*X3e5HukWFAJ%J?B{CHa{8(1cceLOCTcguAC!Z}_D*|^=$GqDkL1C-GbBc8;>*v_A>%lk;Q3e~&t6YbY z_fihp)~v32P=CaRmhGNuj;kR)&<0?cB=F2)?0f=&w}P?Q0uW``ReTHkQv%-;SBL~I z7s;BGE<}1wWNQbM{i#^%8h^@NA6V*| zsT-Q77ARHqlCbp5f}V=q{sMFMg~=ntgH*$~sl3&czH<)ocANHRAG&?Mqtk>m60i1N zMDE@-b6>x|2tm&`7@r2x*H2Om*QL@jjO-bnY;?bD`cU~6&6CdtVJhL?C&vYL)4^`3 ze>vd}K13Ao`C6O0M}BtnZXcHW>xuk_qV18z!67<-B|CiwOn;MVWgwDI1RTD@r~dU) z3n&QlE#Ak&`CDV*${6gwUVc|ODe*UbG2gCH;1b9~ik7uszW^^MbAS)bMT7!P%GCb@ z@__$O7_a}env4IWQ^Wsb!g>_#Mn{s8q!bYD&mCvj7m9QX>#_Ov&Qc@aPD`vLGO&jL zT@WE3wP|FK^TS_{zNf*zjVopbVg}zgy>eDMTrx*uP69Uh_9+-RgX2QK+r*VO&`t4U z*7_%LF?|pq5nzg`eSvC)S=2=FrrwA(8Mn962Yg*{UXAkhV^W~M7q8%6;Z>lbY;ud{ z(ZA%KI6C!I3lQqNW#-GsTg=301?#e`%iD2Sizn6Iytu>xd`KIp=5N0JKQYLk2p~+I zsgnnZm-dO|Q2w*~=UMERo(~SB*w+lWjZN<1OV}(F^OSOp*p7TYzAxX2gVTn#-|&gj zJ-1Wmms0k%CGgmw{FEJt`@wk*Jb&wHo~73Vc?4+5 zh;r9MmfwN|UiGer2Y&xbdzG#t^ZS?Ut*#Gef7b;ap02|8SLzwnuuH#lOd+eMiP4e> z>_7nGmP-XSq`i}woWoC$0^bLPdQ^U*4-*G<63Dr62h?q$P;i9G8W7p^WgM1#0W>(bjQ_L4PuOR` zp&NO;Vle{ywdA^Mc3MfwQ?K;>e@x>|Q2bjE658K~&2IATw=Af(Q&9b_SQ*wO?ZdU; zFcH@MtSVvR;m(scV9n%wQd>6MHTZ%hjMqF|7~2BQ7wH6pt@A?tzZ?xv<-Z8DGEy@0 z1)q=s31WX2qzceS!~Yr;%D>57z;LN8|J=Xsmoq4X+nIm(4|$IX41YlMz&{PQo4@($ z`!c0{p85V*|CiDD|9tXCPnVSqDS8>b*)U*K77&a^f#k`~Wr8&KwGJFY1sbD%SKUcM zp}KkojKWb_1YN7y2Q9h_u&yH7Co@G}pl{z-ooU>G(dzBP`TS}}GRe=U(6!~YzovYm z^*qi0>X|FQ!~XaF?NS>YDue+-8Zc>N?@Lcv-17_v5FoTuzx(^_BFZ(fPE#Z!`~Hi71py`vAiL|g18RO~ z`h2I1Y}z|`;`CD>IJ@8Sab2B(ffbGNyi^x@JD~R_- z`oHk&!BVSn#|f{f+EBXWI3+gu&jFshK23f|a9V`F|IV|CX=VQL)t6BNFE9(tg`*Qv z?f;$!UzsOyfd4e;nC=9)sV!GgtY(_MqCGSfPCh*w_gBycRLB|$>~c>~wnypgYgaA2hiNi+T%GcK-`HIWbTVh;V`M zSg1ZsH;i2F%NNj!np17>7+a~W9W?hBC9)j?G%c`KoEv5^QP|H9xqq^V_4|O$CCkc@ zpz_-P*2j5=-xrH8JF!0L#i(jpM0M5G#e#}$V`q{I;r1RWfj zfJqD%LQw=uA{ZPDMLH4)U_y(4p(Ohr(A~XuXZI&0=j4>{d;4?ur6oDj$tn%Qji4tz z{p5T?h4;ZCzg5_Sa6d}ywf}SS8s?Ih)U7*q;*(+ao%EuwuKJMtzRA2Dm^GBH?HDPF z#7nP{np#3Y-VsJXW}a44`=c-Z7ai`JD_YBa3QzS}8B^|#$J=x%qIj@j60+H)2??uQ z4org1_W?z^5qbw-B9k~uVyesFO|uMjt&BmudryUY{*-aZzfJd|Q7Wm6KXDRDMK`T6 zcd^#mP|58HQDRQVw9}l1IB*Gjzr{E46f> zZeuTwLB>~nK^4I5Dx-xjPh(yP120QUA+v3zJe~ZouR;5km?Juen4+k)W(-0TO?N{_ z{k;BQ(u}oW-o#kj>0&)?S7P{MwC5s9#DU4qMV|*siOwOt+BK=af_?@(a;Z*RKBLAM zgy91|6PXu=uM>Vw#g;o5twFKmTE2X5jjx#A`X3mPSUtRCN%g`63{_y5TibTS%fMd+ zRnD8Gf^pRb`2#@58u{li<5)3|Oa~RqmEZO|XUUc!McyZ0MifP;N!fKaUlJSB<@{`L z*n6gWpHu&!6lx-!HbzH{@nsOClO>M@#Ft0A`p>Mh*oN}$JAHEKMPOzfRg@(s4d4u9 znBVzDnX%i>oUp*N3uwRF%k@ne=hvkVN)qHrS-Tk#1`9^ja{Nuys7M<~+L<-8LMHVYr z$^aF+^@B$dg{aaON27EL#!biHB*~5(`U#zoZDx=@$oR9li^tdizv{wpB9U) zLJsHJXBot+f9Sn0WsUh}SW*1EdvgU$KZj?Vj)L1x%%=t{jpvMK2D|<(<(hAI)dPjS zsSz~vSGZR>UEj__xT(L`S649-~tuyWaY()%Gxrk~(_)wTHeax1MC>bOUZ^e- zMk}vf=xMvQZ|_k<^GovTg-Nn()%3yOgPTt5QV6QY3ocCEb}W4sEGTDre2sD4Jbl&z z#vEEE@ZqyP9^U_idzcdGx!F=FoqW1iVz<5v18h|9RJAbt1#P{7#DaqUeL;y7D& z0qHj{-a?@X-y|mFU7(_CKTO!-2eY5U_&qDq$;G9#MYYqP@)cQn!2B88DRl0bLSM6Yk64gR?n0>vUGe(}mE}Eo*XzS6OexAW3xF;;;{w za2xJUj6%?$5~5Sm(!NefOjmC)DZz!HoejiF+3ndD_gqs*jFOg(<}Ml;;z_NH{?^HM zH0MIx`;GXk>T8|T3x@%zQ5~!ha|LEbT)SI_jt$~-C)&g29pyPeTVLqu`=_e z&CcXREJUsr$7SUmw&<$ z9C=cu)(s6?SeSsRk+k;-4E%b=rzN_Z>ivK=eLcJgRSMc;*`ZEq*Wg5`8E^$gF8_q& z5Z)ichX2-`uG+x=(d8Af|211wr#$FOOW#L_Z;^8aH%)t*U-cV^kbytvjZlQd;}o3S z8Mz&WEgKZJZL^bF56~YpHYNQj!%oMena2ei9>h`8TV!O~gY?|?-v3tz zQ-mxH?7f2skx19=qk!8S)CX*ZC9sg_)8QAvQ5`r3ifLoVXRm61E29G_q@LjOqs#Yk zsN%cLd3HxsM~NMqvxH9i(U}V=iCx(Tv2LRj6D3cr*G+xAGTka| zwv65yxn2Zt8JCcdpgEPa-tx4@_pT1QQ~`y;K+LD|7wilaqEtK-PIB%Eqb;v|z3}8i zPfN5zmnTXRr0%%k^0oy=a;(a|2A zY%FZlp$%TEz@V}LS(dDG@L-?#QRr<38%+q_9rep05u`N42w&sB8d3BnLn*KVVE}m#yvJi;0PZmKC*xirEk8Q7_ru(f+N^ zFh?*&3^=i9%f!Y9K~>p0zTQe9V<14`8qrD1u~3Q8yXI!&xZm84 zt*QDK)_WUSe=}fwZGA6U#Yh>TGGX@=ww>z^>b&!&zFjIcHNYXcr1j4~%66?6pKtIQ zkz!DXMubPN6WIyNP>G<&w-|rg1;U?`xW~71c*D(MH@;RDYc)MrdQq(#9W2tZi6bqh z2M8F>Zm&)tD?(OO#B*#2=Nbxr;_qm*d#G~#x!eBDGNhchq@SnS!?HX3ptYmbI07 zG1a-6YF>}f$2EWV>9=iS(y)!5CdA+!rD-%C*XGr5NL?jf&}WAcg0y6#{8M0<_~Z@P z1O~ks(3m`KCe5k>_F9kaXu%K}l1A?`&ik67Z%#pP7Mfz78;RqiA&Z6Ra-)0;SU;m5 z5Kj8);h4DN!5}JGxd^burf>`lFU_jo!y(yc{AF3=e9UX?a;R_cfS?P%$;=5hF_QT zf^1h|D|+<$$2=-2+IA3fRr^>l?6l;%%B(#8?-Xq~2U0C+1O3^+bhgQS3HOCPl?~bV zZ;VC(`O932AJMO&_?aexSi+`&2Ay=mo$%H!zKKt6b%UwX(H^1gdrPb5!;Y@svAMV7 zW1OY|gvx6O*)})C#tgSzllV44#T zGJQH)3+#arWh{yf3tYiP{w5OJuOG}$dJseZA6Y6=8ZriF^A;K=@gu|snhuH!QfD!i zf1Q(3a1c{2{l;zXa7PqHb_SHuj=@&!@ZLWL zs#^fro@g1M=-c7a0r$N4ft&Ib$w!qGyQ9cBlcXvTw>h`88=v7=JZCzZ>;^Y{I@qvh zKSmk42zmTkklxqa1ow90(?n!Mfn);kyLQ5=ecvg7Khpsluz1!x)P+!ut%iu#R8RyfuTyX*xI03%F+Um2-ET zU-ErLSkOXc)KUpES)#O+@OASa+~$7wY0qGkCxi!A%Lj@yKFtghV>{pq9VS<+gPyE; ztHp?{oD21Vd*D1#U#dm~v8^z-X{A~@`{EQ^{^)2oOj*HRRfyGu^?w=6kma9#k=nTo zgI`K%yhH;z0ZSzH4jgsl&4%`5$7S_{uVDvWME4#C^IimwDsKoW;#56kF)Zul=hmZ8 zaCI3Itm)Br&OzO~C`uXW2IKS?6!5wH_32b5lW0V*zeF6|taNZpF5=wdDXVVq{n@y*B%f1A!>CVx`-Ef zW__OLcYm*YzxUoheEC{tJ~MM>&N*|=`+X)%LGC3C4~_?cKwz&V#grhB>y6-_5bkyG zm)X#J&fvdm4oWXYAjLh@E8qvVsj#dt1X31_f3A-Me&4i})Np`62-`6Ku65ev8bKiT zA6|(GtGMcJOr7bGnl`hY9^6RQD<3H|dc^r@jMJm%bJJ6YIEJ2Z#oG+GBA@>-ihSJn zmX#XIVw*fRm$Fgl3l%P&?yZ;zMn`oQCm!^qu=pQlky02PLE7?(^T>d#GO3kbe*RkC zWuxBAA5~Qyue|J{C1v@k%aw-^%IVnEYQrFBxwQxZb)p50u@4YPFEJeQ*4ox}{oshO zWyhN}HFobK*-JewZ@PsBscL@67nVRMBqIa89YGY;T-BT~laAhANGbRZgEWdFA?$n( zfly82KTyatJpw|RY$k#wYz>)(i(vWoePTjj8od8D{P?GV^lt;(-v-*h4NRhczaaeE z!1cGGnGQ2l*gp-ppdm4_-h2OTZ|SYrgy(;I=z<<%hKpm9MZxa}c-)m8o;UOLms`tU zY&BOZ^=*4F$(bDwIPyd)(KJnNtZtW@O2`eN=`?klJzLHB*;Eg6RS>;GiJk^B2NkNH zVyHw$U|lrTUm$N(aAA;J2?_3Mo7B@zi#@wm_byUJ0?<##2M-Kqwn>xjOOAX|b`M=O z=ze3bM%96PDhPYrL+$?)rJpPC3f@EOilQcr!-J8zi8617|8HP@la)|bZ!r;B5=`Xmyp1t>R@Ni?mzvU}RH z!X~xJ$GB`aq@>l>LCuBP=1!xE9`c=h<~1MrfUlxu4j+{pXkHv~7jLAT;C)4p0f4!OuC1&Yp>z z)~l!QW&34jwqy2nrD@jER54S=M(Lyxm%d>O5JY@;5l}#GA3SE{C$#`4ZhH-NO{loZ z)bls$5t)lbV1~u)ijqyfJu+eIL4sgDhi#+(Rt%oA43K%R;vi1#i8tdn5hP^xIuP~x zLeE@DRAsJrQIM^SjDU3a2Cr8}eB@ak98RDKj!s($2}y9-w?De{_vlN$gCbuUCnBXA z_2OBUfMxp|Yp%EJd=58I*B}Jf zQP)Tk3)((@9{MIhu(G_cIr*UhEbI+7F6><`tw72pVv5Ey#h=5=E_c(kAo{ze#z%05 zn3JFrR#Y2x&1LGRlKa-tq}wa6=E-1TEUWg9Siaa!`}Q@$a;2VGt#cgvV9mGa5JLBM z-}V-cDN@Wt@CFgx5V?*KMCjgpZjzs?2VBj#`9!ffY^{i^6Jz2;U}c1QQv^omEBo~= zWJTkihd#yKI)`_2%;>^ThfXS6IA514~<_fRZ2?|K+?#;Y;=S3tTT_wZ&`ae zWoG5Az2q*h?Ad%D-NFj@`MFsFgAhN*bb5cdL8TnV9-}zfAra@>4uNP2A$_sS*M21f zv$71-0wz#0rr0SFL&pK}l>l4o_hjY~{OV9;?ievO#kCz1`6Lj0H7<Er1L1C&Cq9DllN5YnLGs2jtiv2sVW~FF2B{Y5OYYX_JgfnCT#9^NrbJAI0Owm z)`M`{sJnkUIuctm;v2?7iAb;@%Mi?0Tk%eKk1F40FFKdr0@9=LF@4|87nA6RM%uC- zKO)5lOa){iUF4lnn!l6k#@4|E>;3hj?R_*PjR@1AuPgbUqUEgrXD|e^kJ;C+GN`%O z5tr)_G%$q+c$^z4xG%D=L4i>@!GXzn)>*%kclCDAK@lk#i>F8PWjk`X4k(2L`=xzwaE(HBW z;}yz}9J`LbFAdX+5cI+eg!)eshxyU?m2gz)l;HFCC54l7Q{Q&v{A+QxYON?gI-WqQ z=8I}0imlM&%#NM&LH4Ap?WMPnEz|JpzR&0owrb^Qp?6eYE7%b+)eI+RR8PjpP6nxo zsu+wT3VH5%d^rkr>6v9qWzKV)d~Uk!Ftzc@?@sxR>+(B2D?S4!Bky%JR+>c8YK2gK zkw6WJfJ{}q;Mxmj$KFUubJiT*mPY9}*?gH$ou#sk5N2^#xN4nWdfLz5DgbLn3zcvR zwv>D{-0@&3%3JIFQ^>uZ0D;^WLe3VFVs2E|*pn0}TL-$-R?5aKsH8@SL>V4 z1o@rFo#rr6`WxXRt{Ovw)jKeLcm4~rdM!8<5(*5j$2<=smxv}a;<)C3g6Gd}3e;zf z)x$b(xffeW8#j>?JuvH&e3BBJB?|7$(+0L8JsLAqVd;*-$D5lAFi5r^*z${Ad5x|Y zf_3J58Xf5&)2?h-b;3WsqGv5yy!6b(`;}1o-n2H-Y%uAQH**dvu~-??HMof%o6KgN zUc2MofxnP-QAVa|R%G-H&FL!xj|)N3ukLa~*6hdr+3sY3b+-Bnc!|hB@Lr!t7uzv9EtgaJp z!_tgiq<2ZzQPA_W?V}lYaY?aXPHUJjbNDS@r|X!Df__yA50o-%3N7$i5i0KHUT0I^# z3SvJQ?V2Tm?dlMb!aXnW@Q6CV4O;){^XGk6{)>wXS(fCUQxoQp0>%BZ=kQD>r%km) zeIp|yOUt(cFo>@f*a!+7e<6m}bHzx%-(;RSDy4>Sa=Fi`MKzTMJE9!qA_2p>CtHmk zGmnNpO+3Chm^u`kC;${m@WaD36c}r^v|2aBvs} zk+il5l3j})f%HpsGhf%5*ivef@Ak3JE;TswZzF5VGuFZhr#MkhcJ7RlsRpbx#0ev2GwJ-7As7dTnIIQA zJNNEQiL~Iu{8%z_3Y_f6yCfx^T&U0OO2g#n>~vHH@~F@oflw#Ux+U|fjEG3zGFe$!)uG`# zbM6H1hlAFf?hEppL|lWU0T)G;7luVM+%TGmXQBqx!M05Xdq(_i^xReu9#W%pw8(XovRB=oNb235bSc%~VPf!1>Yzh8oWs1^HNKP&y)3&jR zt7+vQJ}@6?Z^zbRhd`R>5bZ`jn;VTEfRx{yk91w&MDg}`IWJ^xi^V6gQ2xDF(?hJG zQ8GX}t=hU6WnNX4#a&l^%N+>B(GN{5;K*s4nNwr4b@QVmr)1Si8hX$_zzia!4op#m z2TPB-_?y%7lNV+pYF*WM{!?L8N)X=^Vt9HW81q+8k0y3hNq4D=4zr7Z+ioDs1BmY- zXyo8LTaUMh0^3FrO)Kz|+5Wqx_H7mWY_!|a3oS_qWP=_dcE2Q3-)GYb9FF>u^@U|g z-l42y6~9Jl7;E)A5Jib1Lq~tDSIqc);5_}qM0LZ^!U4vrlLt=$j&<*y!#zuXV7bFBQe&!9foM2iNduZKMaVFhhL5 zsSv@io)1&BJXAIJU`xv7a#!yA8qDRGRnM|u`;6+^ebgc+SB)qKpF$G+o8r6VuU+wM znefdsvCCWuaT~tVNKb=yRYNB^d3kUFK+qq^QGQi3aqIImw`P<~`31RtEef7LQ?0O9 z)&(jHhPzpr7LgIa80^5desXx0v=}dnq zqHqKuENkP{F(cFu*t;2%@uITdV&_%fw4Ftp5=WTVptO-n@v3GiUofF{# zO_@&~xXRuux!+XY?-7*yfq7(fG@-H-;;B*l$Y)GTlcR;uC8FQm!#mxFJM=z_i#&Qp z>%X8QiP{V6QZHCu;j{L+wnQQWuj{D@R#VUUd9z$p(3aDBkE8NPwBks4uYByFbJwGE z0;O_i73Ofljj0s41uw-r!f^rNjmpBgc#$^g8Q+?MFFfyf;DhNm4(>kj-t>3#_keq% z{B%vdz4aJ6=Z_5b9*@*1Q7=qc$;b%*SguZ5&;Z9M@;e@>KQZ`j>#j% zbW(E~jFc6=F=;%9=a#hCJZ>TD!O2|mfH!7Qg3IhA)4qR1i%#I77&7gjL3Ys!l>2<^ zT=8#depWqxd48{rIq_2oXXqJs^=hr=e)6^j*lO#YE-i(=S4=X^y%V$ zrWP_yu+AR&dT@a5LX2FXMrGgh`pU>{glSri{)?mNTN|v9&$u6X?UNnpBHgFZlh;eDEZ;?j}*-9O$~X~et&5U5&4UF-Dm8pUJ+@} z#*(1AAs4&Ea1sgI`^?YDpY|KVGk5QrvX9lMAQMtMC;Fw^lwQ0()^=a`l6%DBzi4Cw zccqD=y+;76{eZBQHA|LPExVuOL@O=O*;;XpLly}M#mYO;_FToj$%e4i=3OA6y9QE! zF?kMFNy~H#w{kPKO|gfg$-Sw9rqcM3FbpJc+oXxDTBY_mw~&EU8k#Qo@mC25V%Y7- z+dkV&w5>kbzisF{IT?gR;(AapfPC^saeco92M0gJ|N3n0LsvS#SY9YgzV2mxi{sE@ z#pmG?LH^UTtB;%fBdnSuoav3LP;{mbncFf49ZM{Jm(yrN$_M8R41mD3p~) zi{I-XNNpA*)4stHFJJDJ1UoD-Tbpm)^pLl?i)nv?fyXeJo^SBt;-V$t(JHCiK$lrl z)0pPtS`$;#vH6uQU|@7S5Iq12R4)8=$@aW|co^Cnbcja%S9)Upw!Q?d2!{H}yV-U= zMs2%B!lfhK-Q8tb{mB}CVL1!{hqN{@n0MzoN`}4ceRczSw_H5@#_ho1i@KUgqSdZqAQ=~w|~vVX(&3&-v} zp6OngqM#ekeXfcREG)kCKy#M$?d{9You?*}79u&={=FCV=f%3w?1L!}V5UTk3WPVL zgMz~w>0{f((xyGHUiV-k6o@G1;D0oo3Eb$hb(`smT;Wz1eXRBNxbFd?kWG%6W{aih z9rX24fEGbuaM10Dfyu%M|CD=z@VKz@N%Y`n8~Ph~f0-Hh7a;o<0`%vuxqIHV#pQAsxQ=TI5A5oY`v&7dQ0eN* zRR4o}3L52JDB&0H7mq^ziT9!GFMvAfC}PQUv$OGJGUnMwMlz*7%oUUs5jFQHaa_M1 zUOK0cdP4n>&`te+Id=xko;cTP^qn%G_|NFe9uI`zO!%M|ak69a- z;~4OPiZRtHchylKc~u?=;;}<^8YY zYbY(@&BOXOQyNV@2s2BU?&I1`!DX(m57XQJ6DL@d>VnAql!bc)#jki^A;%_9{(&WF4>-{Z1H21ma6bfcxdSo8Fc0RG)&CotMY>rM{*N4bkIjE#;^&seEnjz9F}x zW?^fFTetodguOm5FR!&#xNo7jjISb`I(38WEC2G`M)xqRA*hLAJ(eqmY_)zs@~eFmp<304tV!^OTT~I6XVV?|Oc9b|wL*gu_2%@7cQ= z9_I=Rs3D`XK!|<@Vu>*^(V=7IOJ^!!5g;jhfa`_yBCc^%OxuDb_PoBy;n-r+h?>=l zF2kb9n5#9S=;pIj)6uUF06+-g6hh*{!dh{mA)VgU%zNF@yU&VJi=Um0?_ynD#t&G! zkznlS4jm%UxBU$BGJJ~$Uw0s|#UG1SR3SzMJ zzWJK+8%Y0$Ns*hjE$b{rxZd9pGI!vTA`4KAplLooerQ<9>Ign^O)gkIAMha?a*Fzy zy{1hg@SAPQKxu{dY|r(+KXM1A#GCe-*oa?HwX(M3#8Zg#EQDo@Y-T3uebe#0c}~r5 zKj1`Pea7I+Z=i#go=%w$Th~6?Shi^R>_!mOSk_-wfxQ1Tqm&R!3KD~rNeG=0(@RJz zBu0j}dvf=MTn}ssOF!)`pdE`~D_)ooW3>+eWXY>{e*tR~^q9cz&WJd)(ItTAp-}|D zT-@P{XY}L;2yCK|5PMM&pINql`E?7fU34L>y6d-=-HX|oFHK-SedHR9a@*N`w75ZLLj55M3q*p{jwbH7t5J(Iy+1@QM|sjG|J zCkz1Y`;8dxS&0W*i})zjnN>^!e=|iiEh_pN`I@(kx4XNZG4r>y|K0E=?Aqtv&Pm0F|BUh~>{S5NnccMx^F#)bVml1MN#0KgyrP5$Py2oen}65`88 z41cTqU$+|~0d@ddxDU1-vI@WmIe>Ox0}4Ti1{d~?DG(Y7>;jUDg=*yuLZ<<50wHvC zjz+`Z4b;CLGbq|Jkv1^@zdBM*Eq}}bS7v~k|L5Vg$AnjVcZrOXD=HQ^K#cO6^kSPB zUWw5*$49R_TB^^5`m6W;p89_iQ6@hVY5TR#Ij{sY{Ws}9i+|gLF~|1(IzPbvoqB}K zxC)N6V+b=`7y#S>^*0a*HQ1c|B8{YTdk6!N7xS>q$8u-d7N#W1&Yw-QnRltQG&#Wq z3^6vrgSEF~#z|vE&9$Zuu$J9ICWAzjwv5#BHi;Ac=-t;PB8N_M+)Q z(9^x9y}LQfM?by^1I!tc_NyJ`m%zmHKoj}>d&Q!zZTT%U1!i_-uKzg*=?W+K38P<4 zy>k7IeUu}ai$;4c&81e8gYRhme)kMPP{r`7Z48Sorma;8g}P@WxxjuacSN1~g-g?W z7WNtv2E?ylFEZMJF@F6fjJYt=WD3xw&AfSzjWv}b81=U1AO<P&=e^_lhYjOqKN(B>N#yc6?a9FK*E`C3HhssyE{uG z&WvGasM+*-_>;f{B3+E#wt!5l7dY4_b-yQzdO7Af*M~s7F3-9kEu`@K`C$Vewi||M z5k&mM?b#YFWb>~l{JkheGrqkU`SiT2CJgw@hhNcv0$BGe?NeS31M5~2#Ll{L_yt3C zf*>l(kb?1lL?au&H_iU~UQvE4^e#|{fK|Awl(64;03dm8E3Z2KP#O?fq5s7#v4Ccu zdByIdVmRxHGs8r-o&X}bCDS#QqSxmNZ-dZZf^J|%t6TqUi@FI=a0vV7m(e*c$#r{X z^%DR>{-`1jBjDnUkz|Z7eI=y=gFyk`&FXpX^#10h%p%5S=x!_bW@6p4=0Y}a#gW`x z_%Jw0*n3QnR7os?#rBH^9&U};;P?I*Dbxr&9;G-S55YN6SU~R!7yuNsi%~peHiHR^ z81AY+j}{XA>T>y;)LDk|2Dswr6@JTx(Z_K<$XL9Sic$+1#<83vLitfVB7^})`~boK z=KdQ9P@Nr@G81-ySrlc^WU%j9MyX-HWMivMdy1JK4lh=z$)aX8z5j*)(^ahKpqSIe zsK}{N!5Ov4@J#{(-o)6dUAjPt12$BpNA>lef7FO%Vh$h2eWt(3$G)UeZ! zT-guSi`Lu4wSgdQE{2$HEFw_0RXcibOE>c)WJ+vw2BXZ6NgY&K948DWJqz zrt8Hh(|wp;8&U!N^7a1T#2$U++kWQ>7wx=;9i;C*2UIDGxKY7@|$G>(s=wL^n;16s8eKUHB zfJBNQhH6xj@*SmPHoz#-gkTb2zox7q@dh++)_5yCh=)Sa&K_~@{x6YtkdsZ|(B@TtA67>wR9*`+9 zDZ#zkGwHiTEm^&>h(YN6B2m%C@vx5N)c%~6&6m-3wK9`HBauD4Ym=`@)ehL4uqVM0 z#U|~(qnsBXK-1|o|KJVz4OGpFe=u;5{WlKK0Ec~>x0hLEx2*eqx=*FJbCU#;v~}uH zI5``dI!q!d;BV`Ud;5%4yD{400Q}hED0@1Zu0fNXB;XdN*)3)JSH-haCIbd>ZOr74 z`y;3+Kdo;&J%9$F84;8x{ByZ%DtH{243zv|8X3pNqs(6J13Xg&MDgrMV~37$BjI+8Phq&A$8%Agn&EjMvO*Q;_WE zxFct?q*wZWuK?n76n7a^EeL7BX)?*#{u0jM%d1Gm0wF6bDh_UH!aPpQ8Ugh1rmZId zv;gM*Z!wNxTeyxXIGypQ1DiLgORQy9gQ3lrKaXWs|1JkAI3{p!@TuZiJ6Osf4CYCr z!@Fm>N#p~_T{JDwscH<}+~8WoMu%_`#d6_@&q!u^1UzmU3#ZP+7ny6{Wx!X14;0$I zNVxhIa4p}&tkR^u`r75KJ0I&&!c*!4exC@bo?`N5jBHSF(-adDQC(|OsQI*H-6oJz znNymd8i|Q5A&8pesDA!LH|`qGB;=o@TMwQ%iMA!uIC)Mv!PlGt-VC&dT1%vuP#zTa z09aQT`K4Y5Q0sgrr?wsW-KNiquBr}?OD-J?QBx0=V5`w;%axj@_a~nY)MY%%utv^q z!XpNQ6{WvP2pM9&>K^|ssO4}9(jm9P)QgpUwimpoEyE`CJYwD0ra^W!*AjVwbdxl7 zUprvWWU)}f3?MaXuHG&>=m6l=h>2dFwbtPhK$-gHybN(E-f=(0?9q|2s>=>NA`qIK zK!>Q+Ji8In`Hm{R?d;mM+8br>^Crx{xv1X3aiB*20wGj#!hHnVpN>b&ba9NjXqqQHm66s+jDfO4rMb)c+8<-S3D}GD{B@fC0!E+)HJK{`i%fYCH&pW7Ox8 z#S-UD3}X=@m~?bFi>RA%a+Bzje8Nb%{$DOCbiGHP}AfMW6pA>#!q|PN!xh6)_fx9w<@VAC0_en%OVO3Ze zyB&rmb_yf6Nt&bje^j(JGoP&iP{53yVDN!*n@l$+m7Z_EFDbq!s&!^y_P59Psg#)U zX)1oLU)nUZn)skBDnzXl*yXsBi~93=BLP+FAqtP;%abh%hDTSe@ z`H~2k$B8D&tq8ALrd>(@@V%6uqdUTTP9GY4lxmh1W-JmN!(JI+4ZUF5v_$VUk_86m zJ_bpevx)RswVAJzpev7z#{bS2mzl~#3`zi$n5D`Lm`b9eWizK<3Gxqw7VikvaH;2o z%4}X%AKARToh4|nZOM@y2{jYn7MJ*#T|=AZgKtxK`Jy%{JmWMzw$rD6cN=hfTq@QI zUPoeU7oAB&noQV|b3%!XLC_U?JaQ%E_s46Qp>*9sZJn%@Xfb9c2PF0sfgSytCHlmd z0W4FR34xZ#Y1xo>h+cAQD7?`RX!?KALh&`_z+lFh=OJd1En+?a-pB}gSEn=`0FZ9Y zh#pKs);YV3CQQd2IC01|+VoMq0E{SISb7r(4YKF(0rW27DS@@)=XV zFPr1nDR`Ojv+rdFLWZbsfC~mYvtwK5y$Lq0BFPU^0kDgnof_WX%z4vxILB z!0;J*dWW?%%Z^c$faaB`q@8qHl+%vdNau<5zm60CcAjYm_leS2wcJ z83>~8S9=wv`BtDxr%Dxb>D|}>toW;f#JFn0kOIG-o4Gj$zao>;-?&IBYahtYm6#i# zULn&u)|ML?__VpIgQ;q$@TI6K*s5rmtTXsLo4wkmq5p>z10Ye`cRUnwO&xxd73x(*2Iw^`d1O)yF7;1nnq_Q&)d;vw38NcVhTj@NNiYE1Lw=P?bH z(*c(P$1Es^B`^uk_gG z9TDdRat+J2JTCn1`$*?FopCnSUledjPlo_a6ksTJh2={CFl^{#Le6_#ACaDJ+UKxB zw(PV#w9GYD@KTy@o}u|AUJ`AQIkKWIt4Opo>>-bNvt(D!Yr2Ji?X@bG!-rPS-fJny zvCZe(q^m}Tk0}PBg*MzrE^avYsk#?1E!lUa@Gle+=Mtmn754A#EA9L5m+#LrOMv=_ zP}~cu=BqUI(&u?)Gap*3Hw;Q5SE>u|-LN85A`Vk`WH(0A+5Li*g#8j=vzpMx={%46Ru`35-&w~&g2QtoVSn455%G#e*q$~4R?ow9b8B7P|!zSb|J%_Go zkcJkkS^wgTQMNDmrnY6R_>ixi6pp0#ID@W7ou2nEs+$A}1>J^U7iW`Zld}=F5wq!> z=7r+Ih@1UFME#psFJf}l)Lv_oikah#OtEr(v`YYKtYo9T=o8E?_G`>|=1m^xC#nf@ zyBoWExyPfM2>Uy_uC~>a9PPzswpBocraD<-U|;~sh18Pn3#_fKZDn_9dR3WsFH(mS zCc88kOiG2lK)ikXHnit7BDVXiCzy&NGn^42DGrwN$CeAt(>479)H@LqMt2&zApU6E zvdRye!)S9s9YM=8dQHm%tN^01iM6lTDEg92Kl=a!8eXfuEBE((K~T-Gn(CRNLOZL` zqLb*z@adpwi#%3VCJsplU5XHUDUaceyiQ?)@jFyh6%kH#I17sF!J+BR<2tql!`>|o z46BjbNRhVob}=1M<&~oBtSC)f9mPOsUNg&jtzEY-?KEwQ#AE9>DiPygFkLs#)nR-M}xu*Fc_sxyvd`-c;EO@Agj%dQ=`3@8(kYhl|h zD<y$OWA#^V|qd}HBqEOP8LpI(rqvxmQLP6EGvq`w0z-Mq17ERP8z z>@Mp~7i-{lFLh6Dd_W- z2G{J`{Pc0D$i)rKv$3u?q1YJw>1-8Jv*0bcIIB_Tr{`Grm59hkxDYQZ3+_##h1kX? z#}!qk?^(1^#5<1vSqMMXY<=(2QTDLSYDsgJYA|f*vniHtH98*PCW}>JE3c2;K2Pi3SA@T-{4K;f#rvfIludF-r|_ zaTN2|>;e|&Osro>$WOa@SG5R|C^Iodh4&F+gURHNxJ27L|CVUpbls~)qvrayiEu_f zm$#Jkg+Gma(K+`h(cMLQxxLiha5#3Y>W-K1aB(a3VVw ztCLiM3ybWY_7MFJUlH1PKH*mMGxmMST$WAlGF4_xt+wLbdG;TZkFC0k>}qG!h5oJQ z`;0VeG5yj+5&?=nTQ~7|$U}%4R#hnOA!KA^VpK=&R7Zo0>}HADs>`&Ef9=Ep-M(&^ z&L?#_z5T-{KQF17$uEqRqws>_ZGw2wezf^zd?I(H$e0mmdvaa9-S3?NK^qTa<)*6p zFZZ;x%wqQ@z7mdg;;JaRbu|^&Iu!!)@NdZ=1W>!sif{tQKZeVIa(3Y7tA=x5x6xQ@n_0p~!J^NBRmf`WQ8SJ=3#RC$*sENsqjMSKwVza-@9h09 zkG{p|{(aLiWK>M!DPo_9Aa>e1j_a4-&-zjOtVz^fU(4O>n^`=`FT>;cX)7AD-(T`u zUUNhM3>FY1qqy6%A&R}R;e2C6zh<2>(PKkXo#aA*gUeSYfK4Zl5!N_Ugbb%6r>x5w zxCGo7gk|L9;#4)`_|)fX?$Tt-UbU>p`&=cG2Ys!GXVF_{eCL%QWHs)_E!Nv)kK9fH z(1x2pu(Ep6ZZR)n>S38mUs6iL;_>B&U3$WOKMTuEcA{&leV3CHfV8p1Ca^I#&(9yW z4JipxrYIhXJSSc-?j{#SDkK{qb8ReV9Nz^ry(u40uy)|8EI0Pe1(k$qBjM5;glSf% z4(b+8(}#qEH93W6QSskjD5Vj4`xND8 zP1;tzc5AumW!a}uaymUj3<#f#3x;Q$6}7SWcU>pf@kuX^EpXyBZ;0PRe;?&(a%eEN4b z)oBj9W6!e%`w`A=W``FtWL35 z>id5e#kzQ58O7H@bG!1Ep3=(eGM&p}JEqfmY3Ahcc-u50`G~Y$BAO`z&pEG$tg4_2 zDfTfSzH{Zs;f&LY-#=cQ95a)Nnx~gVI7#cMG976zn0h=ZFX3Zd*r;nU*I0E>T1G9m z#dXTl!ZpVm<27Qq+EfNf;#KO0HxTIY!#%IBu|N67mSUJ&D(luR{Tph`KmzJnH*qkV zbbDvAE?degW2;h?hNmUV&c$N9 zRR#@J+dr*3qy-fd(w5uEf7l2k)4C}lbWXB|fD3)z!lP}J9W8cO=1?)uGZ_ZcOnnN9 zOq&x7AF>V|n5(bfI0t9P%8RIUarW{;TpneFH&yaZjZA`yq$mElqXZ?B2~MZ!=TuQ1 zAcY{+&X4m_$&*VdcsC{NA&f+t?M=rk4MUXg{80$*5WkzZTP?biAgf8NNnQPB{69vB zo&WYe*^SjUgs@}Xn7=qkwwwUXOZ6s}Yf>BMPi)pOZ5ei-AbUbemMv2bxZPY9+>3;r zceDfrz0qCd@>ak?&m3$jZm~C}OgA?%$naIp~Svec_@Eb*=K<~gLEb?Lf?WqE|;)z8-qHomHYbKT_Cjtu5| z%X6q%txCM!Gi%)@srtM=S2i;1#bte{6nCG(5Fk0@)F_mYlr%KmEkAe6n!=rSKYUCS zt^QnE`7L`mlZC-(gLSC*mo(E$SQB2lP{m-!V~Z!%Us_9_U@n5eQBuB>XdNWW6Ank7 z02OH`Zd`+l$6I4$IF6j#LEf9>t(K4N@}(DwLFQ5#G?EdDvxg_&_>x$$TzW}uure${mvrc3y? zN%!HCW{qzaTk8c?<5>ZkjVOI{b17o7sGGb>lWN-Ex?^pN)gSAk;=H-_N^SkW**2-qbM`d0pS5#8jRq`^Lmgc8o?byEG zx`j41oDuU{fNbdNyx}vf_?W}s;M3s6U+LT$S`95}74>ly1^f8ib~{DYQ94K~OY6N0 zS;HV}DD9_Rg+rktxw3H@_sI+FF#5pu6##+Xd=5RD74B7Uz&EmD83M7DL@mnr7m`a5zPk@JEh7=6l+Nn8K|xrq7L1e=lZW4@%t?;2~t zR(MBUamB;i^82Z>jz8d<`5;YlQ|HBpbr{dTH*kx;8O!Ltr}!3>(i`oM%;?$5@62#k z<-w`_2bmf@PYjwL;UE1lL6=l+a?pD1?#pZ8zo)!DyqSMBQewHXv{`0%my3_8Q82$* zs>WtihnKo1T54-X|0hS%3m#(bLcl92IXth2lFHT9jOnyoESDCdye_8pC#pW{L^!)d zvAEfwEQNDff1~wroP#D^JE^ z$s5RS`uo)^Pbn{-f)N3KzH=Y5$3E4GSVuZH-LGf&i_IgXBOWKb`j#q(u~xoOe?{cG zW;YGT(7FngGG)rTVM{K%Xn@M;1)<$_901)Gyr!nXZZO>W5l(tv>gThy6`3B=L)wOW zTEzJd>iMbsZskTuMnF|8hf; zt042I_>P|DYp1+;N~m+Go{o5Pg8-A3QAXazv_~6YBg$TSwG5@{?Ghe+O)cZV770-C zTb{j>dUB!uwZSK%J}3>1K8oDBahn=`U66K*&0J4UZ;x?JXZzyI=aI{;TU5ulHTY2xW#0V6$orq48?Esc!{@k)$eYVD^1v`(C zuhPZCyl`Ach!1JJp_5b*d(6{4JtR=H7WDIr7|a9S6U(>JO=PSmxo&K8N8>q-Kls zmV!D95d3hJRC+DW80-}lHWf_Gjv1AFvH9VB)9>{3ta?{#oF?&hWqEdNgAZqv)c3uj z=A|PJ3rhI%(uH`i)?rY)Zp+2-FNT>>quUt;cq8m<>}l9SEuwh=ts`3t__y~12+q|y zKv|z@UvI+Yvo2l!)zg`zzy+V%8M#+Yuj=sQ?Au(H;D)cQq)@`ERLdWq`VA^S?4G5u zrrk*DC6+?z?`*w$)N@5@ctKM<^YGlQg%Km`WvTc{i9FF%t=R{3DPM;>h<|{+y+$T{ z{#-dqX07UUS^s>2(zJoWd1(|crg$s!=jKgX^iTD0oaQR4HxAuhNAI*W^=@>VkeBOS zkF5-uKiOOI*NF!&J{d||JgNNMye?Hex1CWxv-&n~vTTYi$le??phJ|VKx>xeZ#MH% zoe#uby&V$MqoE3aXL*FJ`i4l{;!FoTs5=@Fk!=P_yWe0y11$k>HM~PqS_L~^`-Ie5 z#e$w7w|_K4i^r^Don3x1Dyf4nw#~ z^M^|PSI+~oRBhjMZfmVQb~dUg;xU_Gb>()zy}a|CP@#V#A^60rmyIFFq}q>iR4Ilw z(p}F!wEs$~|9zq$t9}!5=g{?4n_n9#^!iKw2Pdart_$jfleP*PI(=mIar9snPJT&f7W{=x0Uo;J5^4wsksTeYBx$tSagBUb z3frfv>vfE$kMDkOexyarydu>v9Sk-1%7eu<#kCfcw%e_X6o7+KdA30ER_65DlNTwU zkayq&il22d*^ozKo_*?_!S@~nK{d4B?H?UExSYF+1)=Hw{$>n@CfYYiCDB|Yg6xoG zLFhdgXp|`)ynjp2we^u~-c)-yR3+6px3}w|1~T>)+x9s*-!z7fTDr2Z;)NfdFES!? zu^W`@Pn6Ty-=lPEfIexT6`CVn?`H5y+qqcIhH7j@ zu&6d}Mp<3o-zcL$NI2U>f`=Z?xHQs}6b*%6zxL^T&Mn7dd8H-yJQvXjY=}ZZcL+{gNmFZx2HX-m@@^`8Nx2 zXQup(&OeEY8b?M{bB*-Z!etI|&- zVg&{}YVaT}33IF}kIIm(WVs`<0}C1u0;OGjGyO+AL1IRmw1z)LW5P$%MIJ0q7>edU zJ>o!o-oPJ-@BICvA1!7Jn2+2(&qC5hUT|jMw*ci2DtKe32%mO@4Rd$jRN2h5Wf2*51jV(N!q>~foIE#_ z30g`QD7a1!N8jbkOjcAK1vcf>M6)n3{;$%`GOmlZTiZiOgOs3@ASxiKbeAZKNJuCx zf^m&B3;smq=15;gzPooeLwfR_kN!J;r-?hXXby+%oTI3b)Ltv zt3juKyF2y@ZE)U4y=qo2UkA#woSA}rQIOGEQ6OMpkDqW*t&45U!&t6pd^K==j^e7Q zNL-|S;Oid80}N-)3xv6*Ve!=#BQi4n3xr0p1&OJtgWo&mzHunpgx@EjJhjbG&tYMm zuS!ChOFy4>PPbY;FONzOzIj$Q4wn36eN)DLp9KU)M@RQ+E5g_7g*B~vJ8UFe-yE`} z#2%_oZ)gq*xQY=7HV64bW=uoX_?0+irJMpgTXhsE(+=iH)aaqx`ZexKpp-Sl!>a;U;^4)!t66 zl)&o1You>(WwvY-h8x0JorHO^%=Fl0?MlmUj1M;H%bBX4ejnDJ)h`}nbai!ok^3xx zzYg6uq%^S}Xb`I_jFas`bl&Q>2ZfOY-{$slojhezS2MSi>RDnp#m4NqQ+d~I_@`SQ zL_La#_@d80kb0jkj*5AQWF!jbhSLfsCm|=9wg6s8hWfj3nmhIEC-~p>^K>?8&-Sa1 z6Tz0n(k!W2VZ_}OjH^|mG+Qa^rXFUZsnuVMZ49D1xXA6f*maByy`*y&irzNiM|E>c zlP)V(Fl?uA#j?J*+lS{7ZYhri*^OuNloQgS5nmbARO3xt#>E};0%|h5JR9$(3*`dR9cQuCajf;s6{8?ry?4Zc^jeN zyU;z<9>_OEoWCW~>ePKjt&*~mV;CVk&0p5v-I1u5|7c`Hik8VZZt&T25yst0HCQeB z>$Qh{e{tXuZ&sugZ?wIzNfXf;d$5{Vc2go}u0i=rJBO+e$?W>CtvZ~wszSu~>1?S} z(Q9?-$+NV*fo3x1UB~8IWRtTfPMMqL*T(U;fUSO?wtRQ_PyT_1Q6At!~CMy#3=gzGiI60c%{_ z1gU#Jjug*?Qc&x49nMYTawbkYdexo}q=e?y`912Ek@-%>de?+69K3QYfz4}xrZqc5 zhg4juW0jZGv|c0x{P+Z3u<5BMntNf);asKL9b+xmDYm_kIw$Q|kd@ttH54AGtTno} zFCNxuu{KrLb0K3bhS<6Qcg>&y@erId7*>mK3?)Ka*RQ%N&+>t&FpCu7>Z~Ou}MA;yN##;QqTH<8T&xZZr z;;h~(HN$oe^FsR|WAPy6LA~!^OuT(=Z%uCaSp|tM1Ho zOD*6F1i9sZd8ni}A3*y9_vW&z&gbchnP25NZOcL`DzRmvw02)a{QVO3%)N!{EL+Qq zWz?}ZTcZPnH7Mo@lt7zxxBHhfE;+*gP>dYSCsh{oUp9N;|0bKpte{VmcB4ed;kK4**vzGGvAvx6SsBO8 z%IfV{vHT-x49o)`)-tulh1Q+EV#n&2xS(w;TL(#O%Xd@va);kIBo(H|c~}RZvg&}y zE_C;(qK72?9o$Wl&g!{tCSGnGJLB)lvm+|gL>}z8Dy<>A3)f`=aC#+q)LgZHRbI(2 z=c-m4cgkX@Fstl~&~uvKc5)?b+cm^q%E?{~>Up~&i{&4bc-Q=qv7|7ThDLN@uF`by z+2CUyWtQ^Pm2GCu{B8TJ-!-ZDbt^Ff8x58+h)2t>D+K54K(M>%LF3k^TfR;Sw!;p} zHG1b*^&npW8QP%^9wOmbn-0I9Uh0~Z<7@(oL_~iH9a=R%b6OxsZ7ER7E%oR4ads|X9x4O zPBQ$OUXh!Np~Zq%P@!pa+sR{XTq}Ox%rGB>4hbC%+YKZ=pQNiGkO-z!iPc^`dK&q+ zDZA;rw0T?94pft-H5!ZIBp022p~`|1P0=oWXLh~ROUiEFMQr-{9LLv;Pg62JI5O95 z{@h{?=z0?`y5i?su&`__E0V!^v#{~ylYMk<Aj{b$I&+qpCH*d_9`?x;vrFgPKpWJMSpu5>D%c(keV1L*DGsNa{TEo zDu)|3#mYqiPPH>y_Y|beBowvpgmADc9LHwuf6OYT@rr~?*HG9=%)(Y|m zQ*0f|X?IVP^W-lJfYx-`5j?)R=}l#1F5)f~Z0_Ayno~2>XX)+2LwK1v+YArc}As+UtVF7Vwd;flPMPK@ivv}9F;~gTS*-Z38&(4oIEE_=jfaR;W>w3dr|5k8%Yrz z&s%tQPT3jlGFa)^R;HG<8sl_3rhcSVcWtoZEDfK?*=V^I=GRtX;coj|ola;PzH`w8Lwzgn)Ai zbSl{l9AIb6sM%C_oi6=-eL8BPh`X+5vm=v^O4A|TXJWz7n747Uy&Z3t6d7H25fdnj z7`6F&)9W!BVnfj>0z)lRvYX_~eoamSl$w2geZZTE<1Zl=(aVUP2%CtWpx&?gCa3+& zscdz4u{@irDof64&us$I>iesy3)PL8M8y6YR*=?Pi3r^5)l9z4RLBY(f`Dxs>rD^dIhFMo7&ODW}2rzd1kXC=tt(!8dwT5w)-yycROBf;58HCsLP-H1u} z7V8E35jmE&W0vb}g!`PsQ7^lla$5&X$&&BLMGV^%Dn6HbF8ln-^AL}EoU7Vwl>r>X z^q{r+x;TGNY;1bIki-%+3u`iQ<6Hs)RRxaa=_2V&29}m95M*st=MJ7=W{9AC!6~sI zeUY!*l8;wBAXOe36|xz16Dd}~I!Mb+s2(sc>Uxj)*2Jx~@yqh3q(--BHe2PD(vT8R zl!-Hp$)D+3SbG&n6q2!&c{LKgZJc_dy#{K}G5f~+FuR%fELHAfwr1Lpv?i5)PEL-i zsXFuEctII`2iu+6i|l)T3}`G1KJyi<`|$+IKvMNXJ-j2G$vgg`t!-&~S=+R2NXBgZ zR8F01MSfc4x934AYJ5_eFfvh5$Re8x-Fy7Bip9mn^z?KP3D*vTzGK&kwVn zvfS-*>*C8M0IThGNqx$X50paP&kgAMYs#7KB_$=%($ia6HM7^5sP@n&SN7$3_ArH`+q4H`1liH#*RJt)p056U(L)UBm!9agi|+#+@3a}+j|wOyYV zYS)LIG&@1#+jHFceGpYsbMry{2Io;j_sV@NdOx}t8Toq-1k9xz?0Ya1D;GrN%-Yi3 zn>okV-hKbXeqO5BLz(6>1HOQqdzqbM?iR5ZL;W_XscF{^zSbn+IOq*5kkJ_3oLtB@ zWJbQI0@_mFdso5EyRREz{KeP!?O^2WjlJ1Zv_n8o^G*c65SXI(T-Vpx_Pz6ei@r!v z?EU9|?C>?J5D@P;3xMY#4_edMZc&}(SUlK6L^c!wh(MK4nwIqa&e=px|uzcW>a$tfTw8^H4R+Bm^4aUr6n%N z>a`1C!MVRKesR4ht9MIEsdZ1GN9^V4HwStK)?fs=FO|+DTnN;;tFd<1O;2TaxBQmz z1Qs4B;+h9|&bY0r@_$R?WXs#1nAQNRFZ=f(Q%f<)0l_IMKdsHQF02uXMx|)%FZe`1 z0Xd`aK{DIrYW`lM(K9OJrkzjQU*Q+VmT6OWmgEULOdZSaN9_mfQ3c^7Ufn7p9TuCLK5y+xG`mYv%J2yl7A18w;SK=QhOPl_D@Nobb#BHO`+;%~PXqNb|bAQPAnLhmW{a7Ku zYDY3vd&IEJzK2{q;qmhj33Bj#qUTxjvv=_tU`G7Y)Wm;7-u*Sb=rz7BuB}D<_q`|j z2mWZgmOa~;eWDjx_Ou0Oe5tLWU1E}>O7|_J<86AC4wa8A&G^BAgw74#c&7ge@j(iM z61gl@{~s{9e-4T1jvAd{r>~d#UnTDd9z0m&%>Rk&_;Z+^DUT_<6n|(WLIz|qAi2F!oG5Ftf&(slv<-+j5lN&6r0~FD$O4Fo!+vH; z62kP?g+^rz5guabs@dg)}%Y&5L7!zcys>bV`(%5=iIuqhoLfxIaiAJ!R-tl?- zPV7@X7Z<@Uc9F+#EyTyFa$D47-_B61p4k_7vhlXz8|MZ5;zmAfJ-SRZei3WM~XF2PE2_zWxu)@-Xc#|A0u^5O0bX|0{ zL(*U+Lf0z=;Ks&d z1A_7J+G#OfyBi_;o=<)N23BnFqlU@vjpoxgew94tBsvLBRus@H&GLsRJ3{|eh$EC_ zZEue9f!|#i8fY^e41G~l>;Yg=KI7a&@dePnh1Vzx0lW6)lRvE!0m>C)J24JQ9)LD> zhdh-5xawhFfR_5*yg>zKIluHjx9(sFFoP)CM_7LvNddE@@0bDUpU05$;5RhZxjDfWhHK%>x6|x6YagxlIK;5W0PVPwg{tKHYgt zz?TXK-OS9DJ`QOehREFJ=H~qDzB`jQ=4|;>YYaZ6+^hJXJW$5_;Cbx%znDs{v-@K( z^FzCRz(!NDd))FbO9|QCzbz$us|xG`z**v(t7ydhVEg@XMOKF~bqc6a*i7C7f@%m8 z2?pTh%}U~b!BZ2K4nYJ{Yi372qHa9?Es*Yfx;zAOw2q6PLcdPflB#fix?uT^l+|ST zvdJxl`S$j9l=4{!vp6R7i2p=pas>1hF_A=8kyN+`Fi6Fo{y6tq6=*}&BCN>`pDRa` zVdwR+tE*fP3&A@MP2z=S29UM<8(OvA?Na z{%R&;Gb-Nr`H3YU617PXB6z3Kw@LJ$K_dmQrOxQ|`2LtTbeuGL5F&~@Y$~Ng&_*Mh z;|MAFQHKDMLEuSn1wlF~tV8E?j#+PgsT!veRYa=&=xdQ=6Aa|JKR7r2aqxU-RzM?8 zT<;i8_!1TWWpxaEisZsY@Q%Tg!6PoEG%v`1saMT!9m1r{!d0g=I$J7&An@h90MiVm z3bUeORnfPId;8*@t3@_%gr6d5c!wu4$`bSPf9#jskf>w%QaY1oXaK^H1?Tf@E?OE!nYTGvgm-%rN?}Ox zfVCAk=TZv0;#jM3w{@1C2%I!2N`7M_7|;s=6L3vw(m*5HC&Habm(to+;uwN`)KUmF z-M)505QXpxQ92A`B2tE6e#Mu;x}l^rSw2X62oyaGlZsnjL>PU*box1^IT?l$s@3_nCMV^m@>|6iT!A4I&-=&A*KAylgz(_r8o#V z@NFgw=ymm1YdfyA|9PfQMFD=T6!3mmkCOe(l%YQV1Azu)`1IogCQQ+_bVt5w1Ixb>V{jLouO!az<$; zzV#)L)D_a}!d(!T>Ip3;5qsV%khd7okS-Oq zlO^ze4V$lXptrgUM6WZpUcIkpE}-nP@Uj4G@5{d3EvG@mXJ};f`SUr7z`)Np(l=H; zfEyYmPfGI>%$$1twc3)OEDAg*RKYz`72;^gguw`S3EM9(TV_#K1}z2_?6oUGZt|uG zlIheKU`ZLum`=S1{c;UACAAoXlFBI)=a(c9K`Gtd+$xhh=Isy2qnI8RC9i{PUBI*v zAi)`&oNN|-d)~rA@m5V19{>i6x@}?q5WbENK-mAz7|q3tvW1)VvBtqdC0O+mpVy#Z!6n z{>%Q1e-=ZwwI$+5o6W<*Gn3$}%cUqHv{J*2BpWd*Bl3Z+ z9e2W?OrAU@U>B43tybGm%GUk8^D2Alz)$hz@()j|N5 zf?KH$?$Oin9!#vU-*7cYVRqxX8qlRM^Vw7Ja#d{N?Vz0tZRD^qVB| z@-`_`199SwebT1M>$Xa=>!lI*??2oB9g_@QaYX23ND)2yR4Cs(ll0;mR$n&5+a%>6 zA)ZJL{G#G@S$W9}k3plV)D6?+iiHas6^Gu7U4VHe#Y$I5>$Gw5VSmjb=bR8)JpN?m zDle(f4AVPm9=}L4JR-NqpzdXpSk2y2$!!6kq~Kd$&aP=;P~HZ`OD2{3Qv**mksIkT z^pook%9RL+3DI-l$&(heQj0#cr92{|!LHwcbeyFb z22yGC2sjR>DkiA4!R2`ti3<+oA^M0pz2_MvcJyRs0cyvgGt)Q_r zBi$!uu%0}-bKGLMRNHn?up$8d1gwcCljzhRhrg{%*FmwnGi&PzqZo)?2A&|60qGvl zH$`Y|Z&@PUr=UoJe@=XtBCg^I8jalBUn6i}`a?kA?d{JaB9iE(KHz5bw%*CZW^HNp zZWu0t!m8x)n#3s`wRDxVwq4ySsx)k;8}N@V6?(N*l2V8Fnu#prkANjuZ2&xEsJws; z3kk3_Cr3)f?&OsXWJ3LcO&~7fe|{IklF(5rx&3`XcpgIJ)$)JpDa=@}4nMJu;culI z_?yN#hkqu^CU34Q9kzImAt<)Uzo^iTBz^R3CY-m$r9dvyJ$*7)}z!D z(t^Mzcf)xW77PDCM^R!sowIG!1Q#idhl8*X9u&-X{|`!%Y<^in#Www?3{#*72ZFRh z4}T&E9}w275C(5E)@&;;egz+01Y9+KPznjcir&d@XF)F0@5!w+XlB#)&(y@&BLbAz zjv;lbYLZIZ13s^oDcHdZhdNeEaBD6D=?MUfEsyg~G#{iTXl+(<)Gvv0#9N(UbT`-8vpxjRV1>=3hVV>iRVz zkOnHOw0s-UV|wq$Ffp;xSjc~jB$WOLF8S7v_8NkC!817{ATBP>$EP-LosG>HYIrR! zZ0Ym4`BggYG*)%_zGL-$Tq4OwF>B!I!x#%5c=EBmUEYiI`iWi#x(PhA9s`n=2K9H| z^Xe*jxL>-s>*5{=B_~kgDbgn>rhbO#D^Q7k34Cj$WJ!W5Se{V8W5GXOg@7QI-1oRH zN70hW@#WRGA$L+lg%RWx?1e%~Y+THL30xGq2;O}GrM)f6y?bANK|vCTG?v*Ny%d*4 z2#zY_`3HgvyA*dOtI=ey&tV&_5kiNwoJ3!?#BMdf(eggrzs1a3de7sV_C!Iu^9zVj zTJlvjCNR*MD;Y*oHOGldQE-e{It1+++W@>lHp|8(^a0k^EyNq$b~U_b`^r|dBnbtA z$g%hbEX}jV3{~?bt^OyWQA%RHYvpt`iv76e$K9FIx=XlPMus3*Fy=2i7v4QvD(Q=J z9os9DUU=}X6u$QZ@BWulNehG&u_yM#iI-9BW)LQJ(rWRPf3(WjiOzWpL>V1wl~C~k zV%5jCFuSEz+^`$in1C{iGuX#7dAuwym&wJ2X@>{as1PS-e8js025kB{ld4*g{#xSK zdv^HCa-A8>Cw(Jb`b{}dJSkMHRpBN50bNgJmq ztG~_bviU%e76U<%Y4XZYvPFQw9KdNEZw9i>P17Rl?qAiVs=7|e&IYJz-KTR(dn}6a{mI2GSD&Mo!n)9ezappX3#1? zols9|uIsMAtMp)}=T9!7jAd^Zcz~~Ek`^n=x~>TE0ods4hj{%G;qO2_+xfYiQ9Kz> z&t5sNXR9iamN6LnVgBAA8Ua9N&fAjp@_z-$MU0w@_#h*hV5kVsEHUB7EqY(1u;l(trHl?9Q!G7#a)oO^)+I zq?h*O@L2v0Pw)lG&4Nk#YjsyXL-l-m}d7rlu$pl#QQbg|&$7&&AoHo!|bSdv%B zV&OCAT#`M7ASicrO8u}F9|`S6?}e^1_?&|hf{1K5pVb)cbjipYNihn99J)!17yv5@ znK|vmn(lrX(Yazrohvwvjr75~cN>;Cvj!4LfR-s6z0|`;csStRg%>ZseK5~MJ0u4~ z9$wW|hj|MGNr6tm`UEIp%~K3=xb#|fuFqkzgIWP6r)>!kZ&G+|@s~q<9>|(o2VJ=; ztb9GKrWBA`ByQLU7rN;9c3-NqRuZpThU6R+4&^%=lfJ= 0; +} + +/** + * @internal + */ +export function isSpace(char: string) { + const code = char?.charCodeAt(0) ?? 0; + return code == 160 || code == 32 || SPACES_REGEX.test(char); +} + +/** + * @internal + */ +export function hasSpacesOnly(txt: string): boolean { + return SPACE_TEXT_REGEX.test(txt); +} + +/** + * @internal + */ +export function normalizeText(txt: string, isForward: boolean): string { + return txt.replace(isForward ? /^\u0020+/ : /\u0020+$/, '\u00A0'); +} diff --git a/packages/roosterjs-content-model/lib/editor/utils/handleKeyboardEventCommon.ts b/packages/roosterjs-content-model/lib/editor/utils/handleKeyboardEventCommon.ts index 811d6c8449f..cb44e86c172 100644 --- a/packages/roosterjs-content-model/lib/editor/utils/handleKeyboardEventCommon.ts +++ b/packages/roosterjs-content-model/lib/editor/utils/handleKeyboardEventCommon.ts @@ -76,3 +76,20 @@ export function handleKeyboardEventResult( return true; } } + +/** + * @internal + */ +export function shouldDeleteWord(rawEvent: KeyboardEvent, isMac: boolean) { + return ( + (isMac && rawEvent.altKey && !rawEvent.metaKey) || + (!isMac && rawEvent.ctrlKey && !rawEvent.altKey) + ); +} + +/** + * @internal + */ +export function shouldDeleteAllSegmentsBefore(rawEvent: KeyboardEvent) { + return rawEvent.metaKey && !rawEvent.altKey; +} diff --git a/packages/roosterjs-content-model/lib/modelApi/common/normalizeSegment.ts b/packages/roosterjs-content-model/lib/modelApi/common/normalizeSegment.ts index 3d5aaccb20e..36bbf5c6913 100644 --- a/packages/roosterjs-content-model/lib/modelApi/common/normalizeSegment.ts +++ b/packages/roosterjs-content-model/lib/modelApi/common/normalizeSegment.ts @@ -1,6 +1,6 @@ import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegment'; import { ContentModelText } from '../../publicTypes/segment/ContentModelText'; -import { hasSpacesOnly } from '../../domUtils/hasSpacesOnly'; +import { hasSpacesOnly } from '../../domUtils/stringUtil'; const SPACE = '\u0020'; const NONE_BREAK_SPACE = '\u00A0'; diff --git a/packages/roosterjs-content-model/lib/modelApi/edit/deleteSteps/deleteAllSegmentBefore.ts b/packages/roosterjs-content-model/lib/modelApi/edit/deleteSteps/deleteAllSegmentBefore.ts new file mode 100644 index 00000000000..2586e2b7282 --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelApi/edit/deleteSteps/deleteAllSegmentBefore.ts @@ -0,0 +1,20 @@ +import { DeleteResult, DeleteSelectionStep } from '../utils/DeleteSelectionStep'; +import { deleteSegment } from '../utils/deleteSegment'; + +/** + * @internal + */ +export const deleteAllSegmentBefore: DeleteSelectionStep = (context, onDeleteEntity) => { + const { paragraph, marker } = context.insertPoint; + const index = paragraph.segments.indexOf(marker); + + for (let i = index - 1; i >= 0; i--) { + const segment = paragraph.segments[i]; + + segment.isSelected = true; + + if (deleteSegment(paragraph, segment, onDeleteEntity)) { + context.deleteResult = DeleteResult.Range; + } + } +}; diff --git a/packages/roosterjs-content-model/lib/modelApi/edit/deleteSteps/deleteWordSelection.ts b/packages/roosterjs-content-model/lib/modelApi/edit/deleteSteps/deleteWordSelection.ts new file mode 100644 index 00000000000..e52a6775b5b --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelApi/edit/deleteSteps/deleteWordSelection.ts @@ -0,0 +1,184 @@ +import { ContentModelParagraph } from '../../../publicTypes/block/ContentModelParagraph'; +import { isPunctuation, isSpace, normalizeText } from '../../../domUtils/stringUtil'; +import { isWhiteSpacePreserved } from '../../common/isWhiteSpacePreserved'; +import { + DeleteResult, + DeleteSelectionContext, + DeleteSelectionStep, +} from '../utils/DeleteSelectionStep'; + +const enum DeleteWordState { + Start, + Punctuation, + Text, + NonText, + Space, + End, +} + +interface CharInfo { + text: boolean; + space: boolean; + punctuation: boolean; +} + +function getDeleteWordSelection(direction: 'forward' | 'backward'): DeleteSelectionStep { + return context => { + const { marker, paragraph } = context.insertPoint; + const startIndex = paragraph.segments.indexOf(marker); + const deleteNext = direction == 'forward'; + + let iterator = iterateSegments(paragraph, startIndex, deleteNext, context); + let curr = iterator.next(); + + for (let state = DeleteWordState.Start; state != DeleteWordState.End && !curr.done; ) { + const { punctuation, space, text } = curr.value; + + // This is a state machine of how to delete a whole word together with space and punctuations. + // For a full state machine chart, see + // Forward delete: https://github.com/microsoft/roosterjs/blob/master/assets/design-charts/ForwardDeleteWord.png + // Backward delete: https://github.com/microsoft/roosterjs/blob/master/assets/design-charts/BackwardDeleteWord.png + switch (state) { + case DeleteWordState.Start: + state = space + ? DeleteWordState.Space + : punctuation + ? DeleteWordState.Punctuation + : DeleteWordState.Text; + curr = iterator.next(true /*delete*/); + break; + + case DeleteWordState.Punctuation: + if (deleteNext && space) { + state = DeleteWordState.NonText; + curr = iterator.next(true /*delete*/); + } else if (punctuation) { + curr = iterator.next(true /*delete*/); + } else { + state = DeleteWordState.End; + } + break; + + case DeleteWordState.Text: + if (deleteNext && space) { + state = DeleteWordState.NonText; + curr = iterator.next(true /*delete*/); + } else if (text) { + curr = iterator.next(true /*delete*/); + } else { + state = DeleteWordState.End; + } + break; + + case DeleteWordState.NonText: + if (punctuation || !space) { + state = DeleteWordState.End; + } else { + curr = iterator.next(true /*delete*/); + } + break; + + case DeleteWordState.Space: + if (space) { + curr = iterator.next(true /*delete*/); + } else if (punctuation) { + state = deleteNext ? DeleteWordState.NonText : DeleteWordState.Punctuation; + curr = iterator.next(true /*delete*/); + } else { + state = deleteNext ? DeleteWordState.End : DeleteWordState.Text; + } + break; + } + } + }; +} + +function* iterateSegments( + paragraph: ContentModelParagraph, + markerIndex: number, + forward: boolean, + context: DeleteSelectionContext +): Generator { + const step = forward ? 1 : -1; + const segments = paragraph.segments; + const preserveWhiteSpace = isWhiteSpacePreserved(paragraph); + + for (let i = markerIndex + step; i >= 0 && i < segments.length; i += step) { + const segment = segments[i]; + + switch (segment.segmentType) { + case 'Text': + for ( + let j = forward ? 0 : segment.text.length - 1; + j >= 0 && j < segment.text.length; + j += step + ) { + const c = segment.text[j]; + const punctuation = isPunctuation(c); + const space = isSpace(c); + const text = !punctuation && !space; + + if (yield { punctuation, space, text }) { + let newText = segment.text; + + newText = newText.substring(0, j) + newText.substring(j + 1); + + if (!preserveWhiteSpace) { + newText = normalizeText(newText, forward); + } + + context.deleteResult = DeleteResult.Range; + + if (newText) { + segment.text = newText; + + if (step > 0) { + j -= step; + } + } else { + segments.splice(i, 1); + + if (step > 0) { + i -= step; + } + + break; + } + } + } + break; + + case 'Image': + if ( + yield { punctuation: true, space: false, text: false } // Treat image as punctuation since they have the same behavior. + ) { + segments.splice(i, 1); + + if (step > 0) { + i -= step; + } + + context.deleteResult = DeleteResult.Range; + } + break; + + case 'SelectionMarker': + break; + + default: + return null; + } + } + + return null; +} + +/** + * @internal + */ +export const forwardDeleteWordSelection = getDeleteWordSelection('forward'); + +/** + * @internal + */ +export const backwardDeleteWordSelection = getDeleteWordSelection('backward'); diff --git a/packages/roosterjs-content-model/lib/modelApi/edit/utils/deleteSegment.ts b/packages/roosterjs-content-model/lib/modelApi/edit/utils/deleteSegment.ts index 418b545324c..39829c91e14 100644 --- a/packages/roosterjs-content-model/lib/modelApi/edit/utils/deleteSegment.ts +++ b/packages/roosterjs-content-model/lib/modelApi/edit/utils/deleteSegment.ts @@ -4,6 +4,7 @@ import { createNormalizeSegmentContext, normalizeSegment } from '../../common/no import { deleteSingleChar } from './deleteSingleChar'; import { EntityOperation } from 'roosterjs-editor-types'; import { isWhiteSpacePreserved } from '../../common/isWhiteSpacePreserved'; +import { normalizeText } from '../../../domUtils/stringUtil'; import { OnDeleteEntity } from './DeleteSelectionStep'; /** @@ -55,7 +56,7 @@ export function deleteSegment( text = deleteSingleChar(text, isForward); // isForward ? text.substring(1) : text.substring(0, text.length - 1); if (!preserveWhiteSpace) { - text = text.replace(isForward ? /^\u0020+/ : /\u0020+$/, '\u00A0'); + text = normalizeText(text, isForward); } if (text == '') { diff --git a/packages/roosterjs-content-model/lib/modelApi/selection/adjustWordSelection.ts b/packages/roosterjs-content-model/lib/modelApi/selection/adjustWordSelection.ts index 90d8ef36766..529575ae96b 100644 --- a/packages/roosterjs-content-model/lib/modelApi/selection/adjustWordSelection.ts +++ b/packages/roosterjs-content-model/lib/modelApi/selection/adjustWordSelection.ts @@ -3,6 +3,7 @@ import { ContentModelParagraph } from '../../publicTypes/block/ContentModelParag import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegment'; import { ContentModelText } from '../../publicTypes/segment/ContentModelText'; import { createText } from '../creators/createText'; +import { isPunctuation, isSpace } from '../../domUtils/stringUtil'; import { iterateSelections } from '../../modelApi/selection/iterateSelections'; /** @@ -93,22 +94,23 @@ https://unicode.org/Public/UNIDATA/Scripts.txt \u205f​ = MEDIUM MATHEMATICAL SPACE \u3000 = IDEOGRAPHIC SPACE */ -const SPACES_REGEX = /[\u2000\u2009\u200a​\u200b​\u202f\u205f​\u3000\s\t\r\n]/gm; -const PUNCTUATION_REGEX = /[.,?!:"()\[\]\\/]/gu; - -export function findDelimiter(segment: ContentModelText, moveRightward: boolean): number { +function findDelimiter(segment: ContentModelText, moveRightward: boolean): number { const word = segment.text; let offset = -1; if (moveRightward) { for (let i = 0; i < word.length; i++) { - if (isWordDelimiter(word[i])) { + const char = word[i]; + + if (isPunctuation(char) || isSpace(char)) { offset = i; break; } } } else { for (let i = word.length - 1; i >= 0; i--) { - if (isWordDelimiter(word[i])) { + const char = word[i]; + + if (isPunctuation(char) || isSpace(char)) { offset = i + 1; break; } @@ -142,16 +144,3 @@ function splitTextSegment( textSegment.text = text.substring(found, text.length); segments.splice(index, 0, newSegment); } - -function isWordDelimiter(char: string) { - return PUNCTUATION_REGEX.test(char) || isSpace(char); -} - -function isSpace(char: string) { - return ( - char && - (char.toString() == String.fromCharCode(160) /*   | \u00A0*/ || - char.toString() == String.fromCharCode(32) /* RegularSpace | \u0020*/ || - SPACES_REGEX.test(char)) - ); -} diff --git a/packages/roosterjs-content-model/lib/publicApi/editing/handleKeyDownEvent.ts b/packages/roosterjs-content-model/lib/publicApi/editing/handleKeyDownEvent.ts index 1e71a1054d6..3cb512bd938 100644 --- a/packages/roosterjs-content-model/lib/publicApi/editing/handleKeyDownEvent.ts +++ b/packages/roosterjs-content-model/lib/publicApi/editing/handleKeyDownEvent.ts @@ -1,4 +1,6 @@ +import { Browser } from 'roosterjs-editor-dom'; import { ChangeSource, EntityOperationEvent, Keys } from 'roosterjs-editor-types'; +import { deleteAllSegmentBefore } from '../../modelApi/edit/deleteSteps/deleteAllSegmentBefore'; import { deleteSelection } from '../../modelApi/edit/deleteSelection'; import { DeleteSelectionStep } from '../../modelApi/edit/utils/DeleteSelectionStep'; import { formatWithContentModel } from '../utils/formatWithContentModel'; @@ -6,7 +8,13 @@ import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { getOnDeleteEntityCallback, handleKeyboardEventResult, + shouldDeleteAllSegmentsBefore, + shouldDeleteWord, } from '../../editor/utils/handleKeyboardEventCommon'; +import { + backwardDeleteWordSelection, + forwardDeleteWordSelection, +} from '../../modelApi/edit/deleteSteps/deleteWordSelection'; import { backwardDeleteCollapsedSelection, forwardDeleteCollapsedSelection, @@ -29,12 +37,23 @@ export default function handleKeyDownEvent( const deleteCollapsedSelection = isForward ? forwardDeleteCollapsedSelection : backwardDeleteCollapsedSelection; + const deleteWordSelection = shouldDeleteWord(rawEvent, !!Browser.isMac) + ? isForward + ? forwardDeleteWordSelection + : backwardDeleteWordSelection + : null; formatWithContentModel( editor, apiName, model => { - const steps: (DeleteSelectionStep | null)[] = [deleteCollapsedSelection]; + const steps: (DeleteSelectionStep | null)[] = [ + shouldDeleteAllSegmentsBefore(rawEvent) && !isForward + ? deleteAllSegmentBefore + : null, + deleteWordSelection, + deleteCollapsedSelection, + ]; const result = deleteSelection( model, diff --git a/packages/roosterjs-content-model/test/domUtils/hasSpacesOnlyTest.ts b/packages/roosterjs-content-model/test/domUtils/stringUtilTest.ts similarity index 93% rename from packages/roosterjs-content-model/test/domUtils/hasSpacesOnlyTest.ts rename to packages/roosterjs-content-model/test/domUtils/stringUtilTest.ts index 57c10a5ead2..2e2d1b8ab08 100644 --- a/packages/roosterjs-content-model/test/domUtils/hasSpacesOnlyTest.ts +++ b/packages/roosterjs-content-model/test/domUtils/stringUtilTest.ts @@ -1,4 +1,4 @@ -import { hasSpacesOnly } from '../../lib/domUtils/hasSpacesOnly'; +import { hasSpacesOnly } from '../../lib/domUtils/stringUtil'; describe('hasSpacesOnly', () => { it('Empty string', () => { diff --git a/packages/roosterjs-content-model/test/editor/utils/handleKeyboardEventCommonTest.ts b/packages/roosterjs-content-model/test/editor/utils/handleKeyboardEventCommonTest.ts index 65a7b78db5d..b09b505a315 100644 --- a/packages/roosterjs-content-model/test/editor/utils/handleKeyboardEventCommonTest.ts +++ b/packages/roosterjs-content-model/test/editor/utils/handleKeyboardEventCommonTest.ts @@ -5,6 +5,8 @@ import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEdito import { getOnDeleteEntityCallback, handleKeyboardEventResult, + shouldDeleteAllSegmentsBefore, + shouldDeleteWord, } from '../../../lib/editor/utils/handleKeyboardEventCommon'; describe('getOnDeleteEntityCallback', () => { @@ -233,3 +235,70 @@ describe('handleKeyboardEventResult', () => { expect(addUndoSnapshot).not.toHaveBeenCalled(); }); }); + +describe('shouldDeleteWord', () => { + function runTest( + isMac: boolean, + altKey: boolean, + ctrlKey: boolean, + metaKey: boolean, + expectedResult: boolean + ) { + const rawEvent = { + altKey, + metaKey, + ctrlKey, + } as any; + + const result = shouldDeleteWord(rawEvent, isMac); + + expect(result).toEqual(expectedResult); + } + + it('PC', () => { + runTest(false, false, false, false, false); + runTest(false, false, false, true, false); + runTest(false, false, true, false, true); + runTest(false, false, true, true, true); + runTest(false, true, false, false, false); + runTest(false, true, false, true, false); + runTest(false, true, true, false, false); + runTest(false, true, true, true, false); + }); + + it('MAC', () => { + runTest(true, false, false, false, false); + runTest(true, false, false, true, false); + runTest(true, false, true, false, false); + runTest(true, false, true, true, false); + runTest(true, true, false, false, true); + runTest(true, true, false, true, false); + runTest(true, true, true, false, true); + runTest(true, true, true, true, false); + }); +}); + +describe('shouldDeleteAllSegmentsBefore', () => { + function runTest(altKey: boolean, ctrlKey: boolean, metaKey: boolean, expectedResult: boolean) { + const rawEvent = { + altKey, + metaKey, + ctrlKey, + } as any; + + const result = shouldDeleteAllSegmentsBefore(rawEvent); + + expect(result).toEqual(expectedResult); + } + + it('Test', () => { + runTest(false, false, false, false); + runTest(false, false, true, true); + runTest(false, true, false, false); + runTest(false, true, true, true); + runTest(true, false, false, false); + runTest(true, false, true, false); + runTest(true, true, false, false); + runTest(true, true, true, false); + }); +}); diff --git a/packages/roosterjs-content-model/test/modelApi/edit/deleteSelectionTest.ts b/packages/roosterjs-content-model/test/modelApi/edit/deleteSelectionTest.ts index b619e6960ea..87f93ef783f 100644 --- a/packages/roosterjs-content-model/test/modelApi/edit/deleteSelectionTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/edit/deleteSelectionTest.ts @@ -16,6 +16,10 @@ import { createText } from '../../../lib/modelApi/creators/createText'; import { DeleteResult } from '../../../lib/modelApi/edit/utils/DeleteSelectionStep'; import { deleteSelection } from '../../../lib/modelApi/edit/deleteSelection'; import { EntityOperation } from 'roosterjs-editor-types'; +import { + backwardDeleteWordSelection, + forwardDeleteWordSelection, +} from '../../../lib/modelApi/edit/deleteSteps/deleteWordSelection'; import { backwardDeleteCollapsedSelection, forwardDeleteCollapsedSelection, @@ -2516,6 +2520,180 @@ describe('deleteSelection - forward', () => { ], }); }); + + it('Delete word: text+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText(' '); + const text3 = createText('test2'); + + para.segments.push(marker, text1, text2, text3); + model.blocks.push(para); + + const result = deleteSelection(model, onDeleteEntityMock, [forwardDeleteWordSelection]); + + expect(result.deleteResult).toBe(DeleteResult.Range); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'test2', + format: {}, + }, + ], + }, + ], + }); + }); + + it('Delete word: space+text+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText(' test1 test2'); + + para.segments.push(marker, text1); + model.blocks.push(para); + + const result = deleteSelection(model, onDeleteEntityMock, [forwardDeleteWordSelection]); + + expect(result.deleteResult).toBe(DeleteResult.Range); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'test1 test2', + format: {}, + }, + ], + }, + ], + }); + }); + + it('Delete word: text+punc+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1. test2'); + + para.segments.push(marker, text1); + model.blocks.push(para); + + const result = deleteSelection(model, onDeleteEntityMock, [forwardDeleteWordSelection]); + + expect(result.deleteResult).toBe(DeleteResult.Range); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: '. test2', + format: {}, + }, + ], + }, + ], + }); + }); + + it('Delete word: punc+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('. test2'); + + para.segments.push(marker, text1); + model.blocks.push(para); + + const result = deleteSelection(model, onDeleteEntityMock, [forwardDeleteWordSelection]); + + expect(result.deleteResult).toBe(DeleteResult.Range); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'test2', + format: {}, + }, + ], + }, + ], + }); + }); }); describe('deleteSelection - backward', () => { @@ -4155,4 +4333,228 @@ describe('deleteSelection - backward', () => { ], }); }); + + it('Delete word: text+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText(' '); + const text3 = createText('test2'); + + para.segments.push(text1, text2, text3, marker); + model.blocks.push(para); + + const result = deleteSelection(model, onDeleteEntityMock, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe(DeleteResult.Range); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test1', + format: {}, + }, + { + segmentType: 'Text', + text: ' ', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Delete word: space+text+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('\u00A0 \u00A0test1 \u00A0 test2'); + + para.segments.push(text1, marker); + model.blocks.push(para); + + const result = deleteSelection(model, onDeleteEntityMock, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe(DeleteResult.Range); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: '\u00A0 \u00A0test1 \u00A0\u00A0', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Delete word: text+punc+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1. test2'); + + para.segments.push(text1, marker); + model.blocks.push(para); + + const result = deleteSelection(model, onDeleteEntityMock, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe(DeleteResult.Range); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test1.\u00A0', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Delete word: punc+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('. test2'); + + para.segments.push(text1, marker); + model.blocks.push(para); + + const result = deleteSelection(model, onDeleteEntityMock, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe(DeleteResult.Range); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: '.\u00A0', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Delete all before', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + + para.segments.push(text1, text2, marker, text3); + model.blocks.push(para); + + const result = deleteSelection(model, onDeleteEntityMock, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe(DeleteResult.Range); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'test3', + format: {}, + }, + ], + }, + ], + }); + }); }); diff --git a/packages/roosterjs-content-model/test/publicApi/editing/handleKeyDownEventTest.ts b/packages/roosterjs-content-model/test/publicApi/editing/handleKeyDownEventTest.ts index a618da7ceb8..2142cf2b311 100644 --- a/packages/roosterjs-content-model/test/publicApi/editing/handleKeyDownEventTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/editing/handleKeyDownEventTest.ts @@ -4,7 +4,12 @@ import * as handleKeyboardEventResult from '../../../lib/editor/utils/handleKeyb import handleKeyDownEvent from '../../../lib/publicApi/editing/handleKeyDownEvent'; import { ChangeSource, Keys } from 'roosterjs-editor-types'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; +import { deleteAllSegmentBefore } from '../../../lib/modelApi/edit/deleteSteps/deleteAllSegmentBefore'; import { editingTestCommon } from './editingTestCommon'; +import { + backwardDeleteWordSelection, + forwardDeleteWordSelection, +} from '../../../lib/modelApi/edit/deleteSteps/deleteWordSelection'; import { DeleteResult, DeleteSelectionStep, @@ -87,7 +92,7 @@ describe('handleKeyDownEvent', () => { blockGroupType: 'Document', blocks: [], }, - [forwardDeleteCollapsedSelection], + [null!, null!, forwardDeleteCollapsedSelection], DeleteResult.NotDeleted, 0 ); @@ -104,7 +109,83 @@ describe('handleKeyDownEvent', () => { blockGroupType: 'Document', blocks: [], }, - [backwardDeleteCollapsedSelection], + [null!, null!, backwardDeleteCollapsedSelection], + DeleteResult.NotDeleted, + 0 + ); + }); + + it('Empty model, delete word selection, forward', () => { + spyOn(handleKeyboardEventResult, 'shouldDeleteWord').and.returnValue(true); + + runTest( + { + blockGroupType: 'Document', + blocks: [], + }, + Keys.DELETE, + { + blockGroupType: 'Document', + blocks: [], + }, + [null!, forwardDeleteWordSelection, forwardDeleteCollapsedSelection], + DeleteResult.NotDeleted, + 0 + ); + }); + + it('Empty model, delete word selection, backward', () => { + spyOn(handleKeyboardEventResult, 'shouldDeleteWord').and.returnValue(true); + + runTest( + { + blockGroupType: 'Document', + blocks: [], + }, + Keys.BACKSPACE, + { + blockGroupType: 'Document', + blocks: [], + }, + [null!, backwardDeleteWordSelection, backwardDeleteCollapsedSelection], + DeleteResult.NotDeleted, + 0 + ); + }); + + it('Empty model, delete all before segments, forward', () => { + spyOn(handleKeyboardEventResult, 'shouldDeleteAllSegmentsBefore').and.returnValue(true); + + runTest( + { + blockGroupType: 'Document', + blocks: [], + }, + Keys.DELETE, + { + blockGroupType: 'Document', + blocks: [], + }, + [null!, null!, forwardDeleteCollapsedSelection], + DeleteResult.NotDeleted, + 0 + ); + }); + + it('Empty model, delete all before segments, backward', () => { + spyOn(handleKeyboardEventResult, 'shouldDeleteAllSegmentsBefore').and.returnValue(true); + + runTest( + { + blockGroupType: 'Document', + blocks: [], + }, + Keys.BACKSPACE, + { + blockGroupType: 'Document', + blocks: [], + }, + [deleteAllSegmentBefore, null!, backwardDeleteCollapsedSelection], DeleteResult.NotDeleted, 0 ); @@ -145,7 +226,7 @@ describe('handleKeyDownEvent', () => { }, ], }, - [forwardDeleteCollapsedSelection], + [null!, null!, forwardDeleteCollapsedSelection], DeleteResult.NotDeleted, 0 ); @@ -186,7 +267,7 @@ describe('handleKeyDownEvent', () => { }, ], }, - [backwardDeleteCollapsedSelection], + [null!, null!, backwardDeleteCollapsedSelection], DeleteResult.NotDeleted, 0 ); @@ -237,7 +318,7 @@ describe('handleKeyDownEvent', () => { }, ], }, - [forwardDeleteCollapsedSelection], + [null!, null!, forwardDeleteCollapsedSelection], DeleteResult.SingleChar, 1 ); @@ -288,7 +369,7 @@ describe('handleKeyDownEvent', () => { }, ], }, - [backwardDeleteCollapsedSelection], + [null!, null!, backwardDeleteCollapsedSelection], DeleteResult.SingleChar, 1 ); diff --git a/packages/roosterjs-editor-core/lib/corePlugins/UndoPlugin.ts b/packages/roosterjs-editor-core/lib/corePlugins/UndoPlugin.ts index c044ed6c168..ff467d5719b 100644 --- a/packages/roosterjs-editor-core/lib/corePlugins/UndoPlugin.ts +++ b/packages/roosterjs-editor-core/lib/corePlugins/UndoPlugin.ts @@ -166,6 +166,10 @@ export default class UndoPlugin implements PluginWithState { this.addUndoSnapshot(); } this.lastKeyPress = 0; + } else if (this.lastKeyPress == Keys.BACKSPACE || this.lastKeyPress == Keys.DELETE) { + if (this.state.hasNewContent) { + this.addUndoSnapshot(); + } } }