From 7f9464b399c7c7d313c19c463776040c4db17bdc Mon Sep 17 00:00:00 2001 From: SimJeg Date: Thu, 19 Dec 2024 11:51:19 +0100 Subject: [PATCH] feat: Enable markdown text formatting for docx Signed-off-by: SimJeg --- docling/backend/msword_backend.py | 60 +++++++++++++++++++++- tests/data/docx/unit_test_formatting.docx | Bin 0 -> 14075 bytes 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/data/docx/unit_test_formatting.docx diff --git a/docling/backend/msword_backend.py b/docling/backend/msword_backend.py index f59356e2..227d0ebf 100644 --- a/docling/backend/msword_backend.py +++ b/docling/backend/msword_backend.py @@ -222,12 +222,70 @@ def get_label_and_level(self, paragraph): else: return label, None + def format_text(self, text, bold: bool, italic: bool, underline: bool): + """ + Apply bold, italic, and underline markdown styles to a text + """ + + # Exclude leading and trailing spaces from style + prefix, text, suffix = re.match(r"(^\s*)(.*?)(\s*$)", text, re.DOTALL).groups() + + # Apply style + if bold: + text = f"**{text}**" + if italic: + text = f"*{text}*" + if underline: + text = f"{text}" + + # Add back leading and trailing spaces + text = prefix + text + suffix + + return text + + def format_paragraph(self, paragraph): + """ + Apply hyperlink, bold, italic, and underline markdown styles to a paragraph + """ + + paragraph_text = "" + group_text = "" + previous_style = None + + # Iterate over the runs of the paragraph and group them by style + for c in paragraph.iter_inner_content(): + if isinstance(c, docx.text.hyperlink.Hyperlink): + text = f"[{c.text}]({c.address})" + style = (c.runs[0].bold, c.runs[0].italic, c.runs[0].underline) + elif isinstance(c, docx.text.run.Run): + text = c.text + style = (c.bold, c.italic, c.underline) + else: + continue + + # Initialize previous_style with the first style + previous_style = previous_style or style + + # If the style changes for a non empty text, format the group and reset it + if len(text.strip()) and (style != previous_style): + paragraph_text += self.format_text(group_text, *previous_style) + previous_style = style + group_text = "" + + group_text += text + + # Format the last group + if len(group_text.strip()) > 0: + paragraph_text += self.format_text(group_text, *style) + + return paragraph_text.strip() + def handle_text_elements(self, element, docx_obj, doc): paragraph = docx.text.paragraph.Paragraph(element, docx_obj) if paragraph.text is None: return - text = paragraph.text.strip() + text = self.format_paragraph(paragraph) # Common styles for bullet and numbered lists. # "List Bullet", "List Number", "List Paragraph" diff --git a/tests/data/docx/unit_test_formatting.docx b/tests/data/docx/unit_test_formatting.docx new file mode 100644 index 0000000000000000000000000000000000000000..bc5c9d79823e7a7e3202d9454060926722d9af22 GIT binary patch literal 14075 zcmeHuWpEui)9x`dGcz+gX6Bd~W44bOVv3oWA;y@Q*^Zf+nb|QrG4u8AOS}8-ty}l^ z_f3_$&YaQHnvq(qmh?zb77QF6011Ew001O_&HNc_Z4dx}7yI;N|9olEIZ9=nkUHxeyT?gJN7)9`WE}ca;E%CTPL=wS89 z<{k_HczXi_DE_HS62{>+UA)_w{Ckgsd+(A4j;7X5OpL$D|LuzZgZtkfKD|7?8`#SX zFLV*`5-{Daw9<{8C(mR&wTitA1+613hqk%|Tzq}uTUr9uJ2I3UotaIT@^H$K_`H>< zcZr*IH8#}3P^K9k891$M3E6^R1S-K**1Obk=I$&9~MLplLHgR^yF!#i>m#4 z7=C~w$az7cRd29uhnJxzeDyvs{dfQP1{XoR4GsX@-~s?B@0H?e=V-!YVrTr>=6&4y zZNNI!TD8rSLiRGSd;|HJwH*x-#qS7+Aw83sHai4cnc}1t$}6ndeR({yt>lb`lB!J) zahDe5_*{C7t~O6;(Al0JX7Ae^s?>;{#8gY&MK5HSah9?Z)5+zUOpOk1LL#LInX&_e zxzjX%(dos{LjpU3ZYGwEthJ4Q^@&Cza4(u1Ll|5|(NlUCkOz7;lmaL~T{+tU$Vdi? zN+)5Y8th|3R8tr8W0^oHhH~g$vLZ6{ZAm5JWHUmbbb)s`V~?01i`G(DLFZ2Za#2c0 zkY&>0O=ZT)Kw<^pLOnQySPw|_U?ws`@)6@OuVU*-;yI8kFL?@jvGDhq^U`?(RfK*p zY{=4dO`3X3`Z)>h*kttWiLsd0`GGjx^7WFH&XeYAbK@A26i^3-VRBMi&?Iv^0bjnW ze``5ceY}|gc9~>o0Vk5T;G|(^e(bQUsU7RDYjRtm2E**5n2Mtx+PF#`=1z|ngM3&c z-HN3gcN)~i(E?UAa8}8PLU9kg@VHtkyL;m)#YvhZ2-o8p>vA_OyT#9NHM8+Wq!G4^ z3?0k@iopPEzGNZd0fzI~%2R4wy#tfIEwIE6TsjJe^}YB+$EnMw+9Q z%8k@8l2duW6;AIuRB2+T=NHX1#0hHJI^-Sli7n^>c=w7b z{(gRQxfZT-gvnbw7MIM7ABj$@lb|k3TRli5WoA_2aQ>HJA{swm=pCQMo7 zB+U?`3vxd1#|OLYawwzA{)N0fuikT z=dBNKXd-=wY33j@-k?0nvhds3x8-)dTX-j%pM+~fZg`^l)F-yBKTAwTN++YWU!-ir zo9b1nu&_G0tOW%!CN6 zyo3<)ZDPyyY$qvhqq^)yAfIpJt?j{`)P0g|yDP&qt%Kg@lj>M#6N^pKH(0?g*37V! zfvODuF$GV`_D`d8Se0up^<^1lK}6|AM-uAPXj0%op!te!H&{|2V87hVj?{}xaUWif zh+5tAOO&l|r_)d+sy@or(e7NmeDftiO;l;ARNaK#@e*rYWgrDFSoZ>TZC{55)h4-4 zG;nAa4ZXI#qu6fJ+cj-(E==I>?I(flFur~1HLMXFAkWqZQfkDfH9wf%Jcu5@8ZTw=F;fDm%<1!V`= z@^|_&%Gan8{@m*S08lvuC7v?1- z>94^K{E`cc8^V`lZ{0o*@`Z?C<+9+CE(92t-TyLrfR7TwbyQpb5JSBpUM_Na{=v1@ zU!8l?Is2%9y%9sIK&=hUkqrG(3z@!|fK}78=XycB@3b}hMQgL$ZmE#o3MMRW6_Y(n zt2@rj&$oxREA(u1lKC2D;NCstgoa>)SOFvs@idw;z+P_8MJ8^>VoYoU9`P4Ol=N6( zqWz}4gVX?ek2FTL-6!hms=)=OLR|xtVa|pPd<22&=;qP^VEWTcJ)iWUWe*%#i3$PF zH#uQcBwp~c{+LFtw-J|DbDM|Z6s*p_4NpC51bdet0e}f&0086NihegYXA4ssQ>Nb~ z%Rh!-g$O(@^iHftg1$9^n8CLcw}3aZ^ixXN1?gcW-emLog-A&0jVKtu_zVvk^qCpi zoF+^e%WS`3eUBfKT2dCXNJ`SiUnCmWKGx#lj#L`*$9rEK72RcJWpQX!@rOclOGFyu zHf79UEP_zq9zhQZkGAmQqvIlOfew&KSww12z&jkS+zt}y1?iBAp@1q29OzW<2a1sE z^poD1a_wnY_#1uTETs1r;nY&ZXK^FFk;wniVq1YNjL%CP9H+_)|8_4-`<4pO_?L~jS%S!Q4Q?Iv4uWFVa&kaF_ z$ja(orc>|k5mG-u#D8~~r&ZmK9q4q8%35eL2y3?hv~A}KF)ub^Y*l{Hv<`28CRE~# z1)S+p+r#(K3mgBC)#XJ=3?Mv!@neG`XD-xr`L16=4pz0}F&fjzy(Yp;mFe=}St0uq zWX+r#h2b!G?s$)w$)Jtf>-qWmXH}6xR))YU&gl*@I%W$2Hk5nI;-=5TUDG$ePVa~F zo7t*Ux2(a=$D=Ea&X-ra;~?Ra6Z5Jp!7mTvSU#`!bD(x?ZeK%?90IwUgXFsiaEBqZ zDTP8#&&Y#G`rN}@@a?fhDU5~h#4WPz#)1#nbduc5j`hruRHP~xVt4b3lTXdTTmop)x@?gRrroF(S7vz zmY+33jvUR+Nc3Dp=4yyA(^(XoLVbc8@oGuoUM!dz{7F?)TG1(_xbYe+lB@NJP$KyI zJWS_~U&@K9mfI4N0wBTl1)q&Y5jPJ0BhAC-L|a`RIdf&yCa z0nQO#&KNE4kXbt3RHWK`G>W7U78mBAk!|292<#6?fiHDQeQW^^;LDWbO_XLs*GQ>r zk&SK?uYFPlUrRn(Q8}bb4m@%K*O78i)Jw^uDBquE=X!1C#L5(>a|)9SVMg*?USrTyLrhV1`AMxSw`isq&_y zjsT1s>F<)kL&~04eD{ zU2{zB)Jj@~ac(?EvXDR-NlY;=79gd3@nMh6Y|RNu#`-8tO>{Vm)9fq!0*{UH%?O1s zkU4HP@fsfFTNxs^6m`me!g=GdI+zLW+4S@?p~Ae@qpb?8P~6S;dDBk%d9f9pGIOjP z*_&33AEOHpAQVrWzQ_7)+R`|~ z_6a2X%HA^jSR3v33U;i8E!LB@ZG}4?ELogVdp+aiad5qggoI`5Sf?ju2g(6ito=Mu z9=l%m+7xj~YeGhK5b}FjbQ%S?M3fBX^gf)!gVb@4%_FL@;0~id``4x};}N&}81F?O@N20=7s!<79fFrZ zX3ES!ZT|qrkEi(#R*+MA6VJK@?Nn2L09~hRZ?3K@t#MUh(FQWoNKGyceMluEkXmH_ z(w3i~6JpldN?XToFlLyY;e4)gXHCKpE1QOgi2ND2ay9tGyXRD1+h$iSqjCqVv|@Bz zFI3rih7yU^=1dj-$kM1d>hTeXrHQ;(s1tlp$hMQK5HMe?2Gy5aIakq;vU+_fCE=~0 zUZz6YTuAHFz`Uf_mMq8ScdloW0tG6EWBUPV9r%6brgYtZwfGh;$+4e-z4Xj6N3SAT z_60b6_PI@RhMF`|X5RVS@3}_7>?-B1)#nVtyPmqC>YkleSq}=Uq1xNe;8!Hpy=5kR zFmMe-Q~SVh9FKi;T)tw8z6mNwrfQa&{dgHtmjL~y6$?bd4_lf6bZ3rD$|qc~ z4a!YA#dEsiozwJlbXkj)bE{F_1uqu1RxQ8!?|RzO7Hg;T3Ywq;hFj%j#YP-vc?8P+Y!~Co}*s9ey{~5D`Q>$;CR9-3DE>Ub*OiGEE zhId2!tzA^6K0{2ekLH(=l9Sm4di2f0OT zj+k^O#Bb~3#CZA9^mRG4kOkFSpSD{A&gq9=CDWJKfH#PhSHWk5%~A@Ob}$Nf=>Ali zGTg)xHooL`_)t}$@*P!OuI=KY5C@gU>W6VijAWPkD1k8Et&zs6PS@SP-siC?KLe=< zXUG>2HnGNVgthg>wjVeVU#1Ssby?Mp&RKcBNO;*qps$d)mr9L?GOccHiMg75?4ae)Nva`CKlC$UZiNthVN4QsI=Xpr{xL{DUO$}T z3fa=e6pq^Ex#p?8ty$kD^9wzpTL7$ri&VClVGWHU=*9t`hQCT#%CF6qwBCHQsnCX? z)Gy;em2t)DA(VhXzVryIEF%JUnxRz%c|7ckN`%^Zl#o;=sc@oeVe7fL9SrbJlFdtY zh0a7}luzuuZ$v21)PNy6w^!JLD|`A&STZ${G=ZVi4`CP-{b|tM{We)5XP#f#;!QPu z!9W|DgMX3w6Uk-SG0SY1z7Tg7^!fO)I#<2{Odq&+WgJ{)=PsXK(lAfHr7(Fi=g%;n zf6$;(GiSa^zG}T^Um=$N;WpZ;XF$4P#BC#97;UHX3{mUcZod}X9DNb>qTUB5(a4fT z)`1p{B}})2=;b;cet+($J{gWB#jQzyGV2iy-b)PM!@1Xag$dc)k|{kIr-aS8TF2%R z5Z7rfda(QHbW$fS2`jRl)1(G=P~XZWIUuKwxoApI4I<)taEhp>^4j94dc-JNlOFoG z{N_;Ez^ZS@LBcU&5c?d(GunD&$-iA6LMoVAPdHAeE8ka~CaxdU-0b6`lr`RlwrIV~ zHsQVF&dePwW+#k3CWZ%caj`_OnnOT#BY8_J+1Q9Fj$OGLBG)F4>cP=sJF`1*#wirN zJ{zaqb*R2}dR)xzvq2LN2Htw1TU2=N@vYm5d~2e$>&QGp=@vs!74Qnxt{wLZ{y^=k zyKvYBj>Tex#DUgwYL=a;HyK0QJ}ya2uBHiyx0*IZ~?_bvJ!%jU0YmtDGB zBk;0PXZ#apQakkn6k6=>3fXvxQi_VMMI=V_!xdA}ZA_&ojGXCE~% zPJ;j?9+t=2Ube_O9GzWXr_Y#$81~+hO$!ZR%VLybT}|Ut*ilNv2z%~X z+z@dD{O=>|fLu_B8o`odk~gU4bS@N{O1}_}wG)xYFDO|Z zK5}j{ghqbkq}0g5$kd-6Ixh>J^So!OfSuO<&m_3F35pthy>vWe;u+?SZ0#R!3OAH} zY~A9`yGLn;MF!5rImET#D8s#6vrMeXqZQ-#4{^vUpbmdYp2RVa;(ce*?34Ij+eei~ zxpT=DHYxG6UwIniF(}+{sH_gOF_4QN1a8u=w>+$suPujC@*14)RYEDvEr;gwx*m?$ zd)njHNycN9NO3sIk|g5LL@|m#9WAyI*YLS(@D@I=+Ar2_&`z#gRt({j#;heB$>A5& zA@OWf{kzy&A1$U9XoJL~Bu7;g8s;HnrUdn`T>*L+jd!}&c()_Z#ir6Hmwd*jNqkgGBL_eIV#<|O< z;3g0Df$y^fSy-3ytd5{t?QC92iiC$R4O{woXy&v?ZsQY~SlC>L;N<=o<5u0L0mA*g zzLp(!_d*tf8DFuut}E!T$}C>2Zhe%6Dm-~KA4QJYs$cvD=yQaX=?VGz4&wPddUK`s zmc1}?Q}Pv|*lB*Xpu=1t&%vP*&>za{wq=9YL7YUVA#&o^YK^u`wiD`H@M^Lc6anKYT8e!7CUS}x8m$Sime)1JRcp4!3^>Ae$UA|@dfef~X8oX0 z-Opyy+vigp>;^lfKlz5s2Cm4faVPbRi29ke5T`aCQYY6iklf2GFlLCSH{zP9q3X|p zWG!C;E5Pl0XU5XuR}A^ZE~!3bBso70d6sFnYFG#DGnS2)?=^Vf&WJJ?6AC;?_g?6y zuw%^DQkm!D5e_~FNxULo{7OuzSx9{D$Tf-GtHK)nF#I)*u0k08;2)2K~4fqoZB9ZG(4MDPuq14?8WBwjb8%$uK<_ynP2BQ86F~SWD%wxeq3fV z#9aeT@BkSU}RpFJLFvgvS&d3L` z9MJAw4SIbbLWmVXezj~AbXe03JL&p`KRB}Is+zp$@-?Y;2oDE#3=8c&A-pFj*fBbg zbJrj2U2Ar;mcA{UWh(yexS;G^p8ctd=bN)?S&(0=xK%!N?8x~>I{$&`nTkfMkyM z5ic2F&H7$rI3Y;+5PXcE?T;@a_qqXhCzzGoG4!h!`~LAkPC&gU;TNSXQmkVX&n@>7 zLHvCYh#eF`<%oo`C{$a%b07Dnh}!IrVUueDa5IJVgC8`jD?mp@6(=% z1Wq{}n^;Phz^pmt7Zd$R$msyq!zu`-#j{05ZUmp=lXh%w3LFG&2g+V1 z-3n8jIwGhdJhr~AHsfII4Bs*20M@u^{G|9X>Hwz%93%U9R?+M_j8hc*W!RP7T8#Rp zVRhtwN}Ugppzm2jz|5fQ6-mIgixJ5n*uZ6qyVhiiks%mVq5Uvv1BKu)5WDO}f^NBU zM84qVh@k(L{*fIS^xt{r9j)!yn^0-RgyAqmvV&krrbN_s#i8CI8HzdWQHmtt?+I4B z_z#eOEOMZ!Y;v6FO%MpjF+vz$a-KJd`KzKbO862z6at^LHHyf1dU@88?uMcXkZ?- zVY}7?O=BI^ul4d_)btKb;G^?k3geME$sC6Yz?*pvwY&u1ImmMsRqhvJD`~qWj6^1s z-g1*AOlx;QWqz5n)_NjoE7_;DBs1Ir;{bP!M%%BVSg!j&f=RCJHOy?WMW9^wcZ0VL zi4(30Z?=U5Kb}0vf{18vb0SBNLF*a9w3N=Upbsd{j6sVovys6UOh@|e%t8lHEx-qv zEy0^gawOs;1Ha^?p;W7ig430PMDougkE8*i-=D}uYL=3KCIg@LtR<>*al4jO5OT^( zf)!mRW(Is;PwV>GWOOf~sgSW9dBY2<|*}Fy2A4ux2mvA_*6~!*GkC1&a<) zQ)eO>7dp9)Vo0t3gKgs+`FJFAB0pP?BJMaewLlcJo6)$UMez9hTkV?3+NiF=%}2hg z=h9(Sj!I|v{BMz3GBBRJQvADURG2V(|5k^(0{MRl3by&?Wo5iSh-iXZlz}YZnsrIS zyBL<^crRPxPr@Ysy_n;F3s#l)0vnY6ByfH!mFM3=02b|F8$;0*F8;u;^RB<0Y?+Vh zoQdi=KA(N}CtDO~=f@Xc;QsmO%3&3G<74GFZb2-81Un%+zDo2YJKuTVmv5s1LaPlnp@9)E<3-AqomQLf)lQ>M-8oD4b@K1oMx)E^qJTtbp zN-m4(9$v?~8Qc<~xop+vf|69hz&tjgc&$TGnlU&_zRQEojz~u}yg6$==5e}Ob=dD1 z&{)58sI!a2%lzWmH}37TR^F$X)kN8Ehp)Bk7pJx#a6w;7nRaAmlJt~8YGo8j?-dae zGnD2@6sP9Cimc`Jvo4Byc~P{X!A_&pOJ2$GQzWVvZ%hpJ2e(xv$2ItP+R0vGrP`lF zezt0_ zAymIMDc^qS$k5$hq&`brw2%&kJ(H0=cqDa0JxKbf{i1vGmTj?1IyWX;_sG}|INxm& z<04msxK+n!_aT2n$hjZ!vT!xZ*L>Q5>Zh_x-x-P6RBn2eMmY4JSL`HPug?EGYK*BY z>_d)t=nL#EiiMXu;wTj;9OIbSj|WQH%*WHw@GNeReAY5%Mu;waCSkP16x^U<{i=UW zbu`*4VIU1d5p6KT{VFw5_b}rLlrvXG+WFz8(2nV}doTJB1eNDi$_RlC;<8AmV(T&} z^)N2^5*gloawx7z&Tgh9uj5&nmFD>Qu8SBkLhk-hJ_{g{r@y>bp38$88NkzTO|LJoQ$(7z2-`$R27SGX4K&$ueSiNlu-)N;UZ6En{lir^_KCf z<#a(Q$XNE7+IwzBn^zFZ_W8$6XH)NFBi4u_U$I(+3nAjV+m!yZxAV^O6;QZj{Yr&v z@fVGnzyvc20~fUX?2i?t>B5RCX8^ zFEv0ST)m`O3f1<7092qd{Rk0#uq*C7lwZbvCPn7Ri%KvSG4nuc&1z<9WgP@)o&d07 zO=9V$uLq9^$=kSRMf}U~HP#kaD$Qo&g=m!;3IT9^F#vniw&4YniD5KlcC!~&azd=6 z6av|5@awNn3a@w~`4`eQv!Bz!)dtIhR8%%aWvZ+jk6Uk4c`~fe@(`sn|DuzrhFMb7Toi){1pPtlB$siopix~Rev&? zw%JTx0={0`0fn@uj-!K+N}!--GpDxTD4V&n0`puFkh z0HTMdf&(oq_P~?1@$P} z7v^#0y4i#bxLR?(xO}`+HlDQVWiHH@Q{_1b&lnxYiRhm``8hv6pVZkk0((2Qr!l(q zBFDuS{Qwu`8H~FSqQNzr;I-aR|8Qx-v(DSP64|EpWt?B)lM^4dAa1ge5tDJZ!$rOg zt3X<$np~TorTSU!R{_zgJsJf#{&aY|80o)1nR?%s zME}=fhO4QO${$u-GiH?(-bsppEmyR+-Wk^)LL|Y=Qjy*FMsjmRiYs_l)?Z|G@b6FB z<-2h7zcQUPta`PatUNfHf4qeeuyBUJLvVWMC>GbRqMyFla5Nihh4#=GP6>D4A}={Q zcyZX7A(R`CNIbHdhZBY@e|XDC|+Fo z(R{ee_pKIemr1Fbz9A4QQ3sB(@nJ211VyjdnIKx*DQqEIqd^jeL{V_z*-ic3%Skku z$Ij0fcd;Utq>73&h#%#(MU_85P`>#7#9>aVJ~3Y-fR|6zlPvR|xZbmvcX-v1gHl}8 z@4G+Hl*Co`7=)!QEhTb&mgL(Hr=~?It%QET&B$nO|M+CHe`>iq712NvCa)+FLTt5t zo!>4eiq})Ar@)Z!i>ot*(aQrY>--e-x?zmgbyZIv*?qA85J&ku$*=4Aw>!Zv3(g;3 z-`iv4eK#2OU+rP`&L37aG_p4RZT)*Mp*uRA`F)!`=@D`04gpexGa|XXbv`@gafZ|Vf$rgxEC=T;M-8byT_bGjO#enSwfmz z@aW8Z&RThGRR=THhSXb)Gn8A2$p$t=SUxkfxUqN^1-cjtH0FxuPEQRX%GbYQYTH@Q zYxP0^Jk-y+ROF7?C{qpm689E-OV;7XBk>^6qp8?cOTD1>W&uYSpSt@Eag9H)8fCQY z4FPe@9S(+${jcmsbpfU8;VrruBrld z{SkILHuS6YQaxzs+a#f&OK(QWuc$H((5+2daO{n^29?E3>?+)E1x}FuYb-WGwWj@9x{ zA3hRekh0CcU5b=Zvr%+@x0dsFS}V#sP34`NqUdO6@5E$m=lHv^ypvo1mz(llb&>IU z?=B2I_~PR;lI%s3Z2-33fYhA5Avp_3-p|U$5G5_a8BEBx4cmb2M7WEIL8pzZ4jX~> z2-+pT8r@+SnOtlL;u0g>WVGwnjG!j!>S!qmS_(sd#81=p%^f2p6UD5TMv0KS+)G__ z(&RuP_6~CT4J!;?Q*IfLp8lydT@onqw;VQQs zIApqu0in7VX%_t*HXQ$(DADrSA(rA|(o^5y9Kc}&9gZ;(J}X;wHTlCl-XNEKagyPJ z0upNsOZA-n*)y-pN`QY6ozeQ5;AuG6cR~1=hgDlj_*1tlrf`!vQm-r`y>k~ZOgGos ziBXk9o9oNwj2zOaS%V} zt*L6ZbT=)(o9gPj+e7}NsT$hb|K`^I*HYiB?fuG7>9$>EhHpn&62xxh3a?|d0MqJ^ zHTq$w1J-9VnI=coV+=+2f zti_tcW}`%J#o{CZ_g=(<=kPW}H^-E2pmc0hcp2DhmBsGo3nu=BK_L(2xhaS;KX8kGzop&kt%NVVT;xu)LH9g+2uZH^$TS`fLeeT$9ux%$-=YSyRO=Y|L=w((eUN`ItGZ#nmBHmXey(dO$ z$*X;|T#z>zq@3HPmr=LOZFlC?$As<|PU?@mTAm$aci&9=l5?k~M2a%yKpZ3q#8yvc zwVDDBRIfOvJBtT@8YJyIau(USi4mKeo9+KpEa+tiL}c^n2U?LHx~` zAw}6gEBN!7hCg5d3GaRE4<{V{3j8bC`#;dS_o(B)sNa8u|Ct;4A20yW3-?d>|Ai^} zS51GVz5PcQE8_o!1ou}Jf29ljM+N^ok?!|z`6GGYukgR_<^2b~iSsY`zjyZj3jS;C z{2y>R?LWbPiKPFk;jhu&e>CvX|5L-CV!yxQ|2+Qx2MquK83BO*iU$4)|Fcj2BOHU} d5Ac6_Xhm6wck}q|{0tnR`#s