From b4ef5d3cc3ae414e5469b92d1cc9799f8a4c85b6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 15 Jul 2024 11:33:41 +0100 Subject: [PATCH] Fix HTML export missing a bunch of Compound variables (#12774) --- package.json | 2 + .../e2e/chat-export/html-export.spec.ts | 132 ++++++++++++++++++ .../html-export.spec.ts/html-export-linux.png | Bin 0 -> 40662 bytes src/utils/exportUtils/HtmlExport.tsx | 132 +++++++++--------- src/utils/exportUtils/exportCSS.ts | 90 ++++++------ src/utils/exportUtils/exportCustomCSS.css | 5 + .../__snapshots__/HTMLExport-test.ts.snap | 122 ++++++++-------- yarn.lock | 5 + 8 files changed, 315 insertions(+), 173 deletions(-) create mode 100644 playwright/e2e/chat-export/html-export.spec.ts create mode 100644 playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png diff --git a/package.json b/package.json index 4250a9d1ca..14635fdeee 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "classnames": "^2.2.6", "commonmark": "^0.31.0", "counterpart": "^0.18.6", + "css-tree": "^2.3.1", "diff-dom": "^5.0.0", "diff-match-patch": "^1.0.5", "emojibase-regex": "15.3.2", @@ -167,6 +168,7 @@ "@types/commonmark": "^0.27.4", "@types/content-type": "^1.1.5", "@types/counterpart": "^0.18.1", + "@types/css-tree": "^2.3.8", "@types/diff-match-patch": "^1.0.32", "@types/escape-html": "^1.0.1", "@types/express": "^4.17.21", diff --git a/playwright/e2e/chat-export/html-export.spec.ts b/playwright/e2e/chat-export/html-export.spec.ts new file mode 100644 index 0000000000..947aa2c1bc --- /dev/null +++ b/playwright/e2e/chat-export/html-export.spec.ts @@ -0,0 +1,132 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import os from "node:os"; +import path from "node:path"; +import * as fsp from "node:fs/promises"; +import * as fs from "node:fs"; +import JSZip from "jszip"; + +import { test, expect } from "../../element-web-test"; + +// Based on https://github.com/Stuk/jszip/issues/466#issuecomment-2097061912 +async function extractZipFileToPath(file: string, outputPath: string): Promise { + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath, { recursive: true }); + } + + const data = await fsp.readFile(file); + const zip = await JSZip.loadAsync(data, { createFolders: true }); + + await new Promise((resolve, reject) => { + let entryCount = 0; + let errorOut = false; + + zip.forEach(() => { + entryCount++; + }); // there is no other way to count the number of entries within the zip file. + + zip.forEach((relativePath, zipEntry) => { + if (errorOut) { + return; + } + + const outputEntryPath = path.join(outputPath, relativePath); + if (zipEntry.dir) { + if (!fs.existsSync(outputEntryPath)) { + fs.mkdirSync(outputEntryPath, { recursive: true }); + } + + entryCount--; + + if (entryCount === 0) { + resolve(); + } + } else { + void zipEntry + .async("blob") + .then(async (content) => Buffer.from(await content.arrayBuffer())) + .then((buffer) => { + const stream = fs.createWriteStream(outputEntryPath); + stream.write(buffer, (error) => { + if (error) { + reject(error); + errorOut = true; + } + }); + stream.on("finish", () => { + entryCount--; + + if (entryCount === 0) { + resolve(); + } + }); + stream.end(); // extremely important on Windows. On Mac / Linux, not so much since those platforms allow multiple apps to read from the same file. Windows doesn't allow that. + }) + .catch((e) => { + errorOut = true; + reject(e); + }); + } + }); + }); + + return zip; +} + +test.describe("HTML Export", () => { + test.use({ + displayName: "Alice", + room: async ({ app, user }, use) => { + const roomId = await app.client.createRoom({ name: "Important Room" }); + await app.viewRoomByName("Important Room"); + await use({ roomId }); + }, + }); + + test("should export html successfully and match screenshot", async ({ page, app, room }) => { + // Send a bunch of messages to populate the room + for (let i = 1; i < 10; i++) { + await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); + } + + // Wait for all the messages to be displayed + await expect( + page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"), + ).toBeVisible(); + + await page.getByRole("button", { name: "Room info" }).click(); + await page.getByRole("menuitem", { name: "Export Chat" }).click(); + + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "Export", exact: true }).click(); + const download = await downloadPromise; + + const dirPath = path.join(os.tmpdir(), "html-export-test"); + const zipPath = `${dirPath}.zip`; + await download.saveAs(zipPath); + + const zip = await extractZipFileToPath(zipPath, dirPath); + await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); + await expect(page).toMatchScreenshot("html-export.png", { + mask: [ + page.getByText("This is the start of export", { exact: false }), + page.locator(".mx_DateSeparator_dateHeading"), + page.locator(".mx_MessageTimestamp"), + ], + }); + }); +}); diff --git a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..99af1a757ba327b750e7859f9e8436102a2f8d7f GIT binary patch literal 40662 zcmeFZXH=72v@QzjCnzdS1gQc7(xrF!M2b?SOK8%i_ued^AR;2YOYejpTIkXtQbX@O zB#{yVgus2_+k2n$XWw)7+54RPK+zLI^wvJ+zhO3Cf4*w+RRy5xi7*rt6!wJMUwxhhgg3L$8}2 zQ4kWb@R2FZ1uQMh3PKtT8u|r?V)F4d!efGT-$n=112!3-aIFxKD~@a5u#O(6H%6hO zEyc8reIs^o{<5d5a~>_5z)t`^M{E%-VPO4~<_0h<0l|frP1yY93W%gzg6o&R=l)G_^YZ!PJ;9yJ=l5%bG?&jue_eZe`TYC#f3$-S8!q)q z-ubeJFBE8~)E+Q4*QCZn^ZEiVRYmYU zdHcGnn!?=ZIl|Jpd%a2CO>=XkBq08eyMSHmWdj0jp}mp`Yd%b;DguXpo299qD0IO*+tFu_9HapcT+C~`Q$6ybvtgY2gTpOcZYPlxt!A7lGb|5q}cEIoQ|wc^@AqeJ zZf@0^gLpsW>D=Zw8cdz?_Na)xZuGZ^hjQjCNGYEkc!^Q9&0Xfu&&u0b&u{wq#s#B> zc0481H8?_D-Q6kV)8v@OSEFQ=qZz=vMnxt~qJXB7_eOdyQgO+nzH3{M*4KyD+V1X~ z8Vo84Pg)${8GQ5p3W|ykiI6r5Ynz$;_A8683&t44U0uhwD&@}V8}$YrBpVu*=!2&l zos?Tz?_Ub>$$i>b-Iu}N?SDwJ^E>D3lQ3&*>l~q`%SYa(h`KO5^g}=vL*(4wwvuI= z23_DrMmRJwcE*|tW(L)WW0I2UF^P9gO*5WhcaA3l3A@xS9Vp6&`+000 zZqMrw3jk^ZMm=#n-z)>wzDg|$Ba@Jj-~|ceT>BrdHv&_WWqCh=b19IUFU$`rAH8TdsKM%LtwW)Sfo z#180xrIp~};W=As@3gV8nT0rWnYE;86do3eoVUO9TDY}N z_0X@x{NpJwgnalHDkw@>xu?7Pp-}?2(d58@4zanDlS%ED#ciSY@bWMzNl7lSK5DYI zdbHGI^BW+%TTKr#;xU_%V6%`iqYMK`RgH9oOCrS&AweM%6O&M~5dndQ*%}wPK5>?Q z3H~fFCp()_+V@X3WU|l>70ngDvl=bu?&hYUsVNsC`+|4`6D{lWc2U1fAAvb7F#xAa zQ#}eTTrp?}JaLyN_V743Le3aNX}!1isVs}M^XB4msJQ)aG506(Z*FX`hyK~ydoZi$ zXgo(5bhX$JVZana#FdGNtUKpAjET)%T)=@fHC6ZJ*VfKX#7jPYgu2a64(k)geE&|v zl=*Y*wS$Ah`YSCC4(kn3NlB$VZBD63Dvux#(d1b(3TMsjyx0;Od$6Tv9t|yQcoD-A z-DqXU8TuPxb{J)mr_x|2oU*&S8-UFYC6g;Noi4SbWLB`y{^1^xtr*G0J2W^*&%m&{ zx|)MpjE;_m1@+c5Pa|a(qHbJ;eRnvQZu>C zeT$9++DJ)BNl=l?TUpuISXn*!{PiHSdxo zQj2}JV*kOG5MhtQL@{UR>z1?6$_yT!xQP` zUs)?UJ`QD7js_G&DtESI=cN+&Bp7Wv{#nOX*lx6531crG?f~zw9Xfx!V>;eB^_sa0 zIqM^fcYL-e3(Xu!ruY>}!aSUIi(by$0&MCc?D*>9VHEd?FDaAg+AKO@C{3imaEzUg zZ+I+U12tOK(;bz%YOzXBzl+_S=Ln^db9RhjQ3PD;Y`Ifv5RSBYA;+iMMjenTU_YkW zzI$4%pP@6ymwUENF44~B9XF)(%@0Yx;)V96r7v>b6BAxXJwmXX8BHN_jlg8-#7eam zGDJLkfBpq{qbFv;o8>aLuymKv+`Ww`hs>B41sW%9zkW&VPnP}uU6L;8MI-B2k(0AJ zQ%fgIdl1jv!gOpCQVYV*nNjSF7kz9EVFgrk+$(@28N99;=o46^?HmyiF=jJ(UtYHi zWIgsNlTO6m_f%~u1Bz>IZhpXb>^V0DiR)HP;GsP@dJ@%ObHHz8X~B5t3&njyrlzN- zn*|SEs%&ZT8D?rWX`e$z;sCEgQdOPNS_)C214BYktf9w^i;dhw&e9O*%$(yMl9M*F znF%bC6-6i3id!IRi4VNDyJ+$oeu>-(&hTp0H)dwqtDd2EIP{5RAnWnh#grO?&sO5; zj*9lga?LJVwS{EZCHzgoEU9T(H!U%nRDLx- zg6ZV_hi`Ifjt{{&aHO=47}MsdFsE3cA?a-PBs-ewvX{SY{-SsnM)ryRdD~3n?75Hl zy7>%Ah6UPU%u6Lw3GYxisQcFl+hxmlgSWW|xCOPVkNBkBJlCfOh3AD*45y7m8FZWQ zP3PcxZ`!M71)Ng^rM<)BW-|T3%cMH1HAQ=%EDCQ$r`)f`71-!epnHNFcVy6o0h?*?GSy)XLCHiqs3 zLDc5zoISFch#v{`YDj>;GKHi&gs;04x_FVTvKuzWE!_99o{Z2&gIoO6u}4=i<{Oo& z);>-V4V+i27x+golj0U3LdT*uA3=RNL|a$xy*7W7VIah1Qs{2=FCr3KZDpu`1|ASl z{3pcC=kOvp_pVY7C8S;LJt$?LeeY@sYA;6#(lXK>R`At7 z;>t7)5s;Cne#CVpDbuiALh-Y;vTOe4dfo}Lz^4y|R}rQ!)sLG?zn_a; z2+n05pPGT_FK6~$4tTvrRCF=Aay8L2W`c`>FIQdeE06vQjS-bg2q2sJ)*A84HjhGx z+b*j9rFo1nLE;hj9zXBB%U8WjFR#0_{-qmhIoVSUKf6_^P#X*RP5l0 z-+(OsYBFGXfM)@mM%e0RM3ZLeJwZRi3o!=_t~sY~;A+LDF}Xr*^5Mg=DfT?-(zi-0 zb$ycO25UsFzRk|;Lg4+x8E*B)X>8I_BD)1zQfBAykEG-m2zWxb1RD%F7cKIO^~?2( zG|*xq|Ei6b4DZTzd3#C`M!C88l0b!* zL#@Z2)y}peF1|PvTrD_+?wTuOl@P`tdO^CC_wXF^xyWs1+_&s0aZ|Ra_pm4%)WpFH zgK^l-6HUbL*7#6(U%clTkAcc zwvejIN&#oH7K+i3wx4Tj_n8$ypqWW0sg6!Cda5$d?BW^q2YxQ6@=qpo0{cia(K4_R zq>ij(bX0pfGcD~5=`*kqCvA4XszN9;I!-gO>PUz5UYEsqSE5K zz5h!!(V)z<`m~2iulV>Db7^VmIgdNn5){ipcjqQ^s767^jLBnmHYgCJ_#R{Au#edp zWxUU}Dpz&n&fXjrl84Nd)>Z)#^YKPo7GVMG2@_gbncwLA`P;WspD+%(ToWZxTW0lV zl}|v#G4aXPyU56|fLw$ZK#s=7nZehAt--}~T_baQ3_FAm@F@pDHvq}RrDIWwL82j9 zWW{cli-OBTm%jIy$l~U)radMzMBj&gYwEPacFcbiH{R$8T0{p$N7aN_CuW zZXX&_mk@UEs^Co(3#@c)AzK;XZ`B_KM@AkLGbJ)jwFVqlw5wFZHu#&F81Owq#qm+- zM6k4vY@ylW!pEEUUP=D6`UM1{x0olvCU}(8YJ9pPJp;sXp~Cv$(qQb?KQ%Ox`_j*?f1Y%r z%zg=A{+Ru`5{j4}Fb*K4Fgw$$2LJ)eGyw2^4uihdRRT+g$@%JMup1dNfop~ChpFIJ$BE*w zs`+BC$b_;PfCrn8{LC10Ap+zFs&zR-ub-FYa~i7e~0nV<{-f-9@F;h zSvzlvSczWA#(V%aE`in~YLAPbjeVoN1y7W~hu1%uYB4b~Fra4cij01KuC%N2Vyguz z6~ukAg*m;K-pHxbPIQlc7Y@M|9LSQ(kC&Pv(<5R-$p9J@4mJ}TX1;_c$C?&5#a-|? zOeF+DEq|ZiwA8ySrGFR*`hB+NUQVONv!GQm2?_b7al=kq-=B3O?=j3gG&{b3|2|LX zR1Z9uG*C4T}6nR6V+<6zLqu{(6KGsjz1P0C>)FIu(qBP}JH ze8CKzg5aOR0efz}i{T;;GhL7$(Sp~IGj*aUCJBqAs*y^|fYo7xhO1EX;`WWDX1@Rf zVdumrT66VdyU}(F7Qc+6?gIY8BeP`WZD3%tM+lG6(TJqIwJ2t$)^*(yWfOgL<~C19 zP9DwRA9GkzT6+%g#EdS7MJ5I2+kzP?H=qyBx4uY7M9WDV*Mj2bnC4YwK>FzkDO_s( zb92oOiNgR*DO%1Iay3OfMwKVa@ihoUL;YrUYKrDUG$tkGs2_6a@s^mP*nazQuWY@Y ztE;GZB}z}v@KtJvcQDhtJSwVIfa9Waa|BU4IyM$Y#u;LzYLwa&a=UkGzfW4f)VGp) z`y|N?y*t-nH0|x~&ShGfl$^X{W3@Jx;pWA+lZMOL(=!EU`_~qCFD%qEx&g=SI@{~a zpwpKqGdcgOsYz^L*k)#DH-j4z8%}gum4O)%TnOBFGd9rM8|WCVU!o`KSXvf;CqGav z$Rc4PX@&m$t)%450Oh-W$g9qScN$(15|5|M-(l0ahSeCEY z`weXT_Nc4mpdZ;glCD!s>epGKU>U14!g8I0IOk8J45cbzq z4hhjD?ss!MenQtjTO390#=m)DmzEv)*n20aF=#GGQ9p|`Yf#fe1Cm_ zv-M|5v#+6_!(OP*0x4-LYIQXTF^RMuFyOj1Hrup<0DFa?!oE;*dmq^3lC2bEPA0f} z2GlU=j4;&K%2_;jGW<<;teQXLSqHbka9cxZbHe}LB zAE~j61so2S!BNoO?a9f>Y8wqE(ZCuC!^A-RkQ8M%bX+{i{z?zMUkM!jy}j!IqsLo} zB%1M@dKBd4{p$ED696pht1F6`qz@%mcBc>Q?CeNn&EjyL{QNK`1HS5-8eddciiiV+ z>*-fA7D;xSjwZB`k9xDGzJ8*; z`sT-fld`FPf^+{Gc5^6w6=BpjI@$q%N-so$l#gSXNmD|lI=OO>I^856NPKazH6}JT z7F5Qko%;gs@3lFY(cl8pSl3$Kq!so2^T+%nz-&*v6$VJlp;VEPx55Z`IcgECxjl;N zN8_8t+&W+m!H0WC83Op{)YP_7N~`PZhJzEBzxw4YY;7U#ClHZhgX*f9n##((y`!W3 z!@-`4!SiEQwjAKBfnES7?QJAA6H)BftDTu#^i}iA3Ken&>I38y`}t7{O48@$|3neX z7B~L~sqH@$JeS7v?tT*Ce{oX$pMd0HyX-cg_wcuyb-gR|%5feM3S+y#8 zr)MSTfq2d!F|N}YOsXpN@QD2wWOvR-LYRkF-Ka`o50yTnU#_b~9FrVhYllD$Fq;?I zffBPdkh8)ruOK$ZQN6zw0jhH{+Rw!whDuKnF2>e48`s~ja@ssU4(%F6IY-fr`?=p} zIrjH^?b{V!vfQJQku2s=MaIG^bTAw5ATQr3LaUB=aLzDW_bTDUg;D`LjC-RUY!2m>E!TaU2!JN zA$`^e1R`ceDa|j|cwt>ym6MBx`r|q05w*kRe2QUYtUa0!f8%No+7m5TS1_k(giy1{ zFW+uSQ>VqHt>{#Cv<15q4q07vUXq^g0>4YXn5*~W-U(LgY^#?dH=h^O z*ijvr+#tje4Xi<-Z90eufTe>X8wD`aLOp1|ZMEEec^<<$E?H)`od8Sf5Xq94osFt3 zH~~J{XT+6Wn=K`9=VrO!@x;ub-Kp~RXgQZgYAPgB?|evBMwk}!GbT^VmyzQBWjS;MlFKgFF?kp`iSpQdXmSY?J z$`_x!Oia>vc?P5DN&g&R%7Fota`;eyb*e`SZGP}-+-*B(=gQg{sr?uqpBGi@1%pe< ze2pS$CFcw!D|an! zNtK!P)1i*+saR>#accbDtq8^RFVfSmQ!_GB@_9E_N`XXe&8q~;9_jF?FW)4nJvn_= zyK)}`((H~VW$XsxwTA}Cjo>`}2#fN5rNNIt%I;8t#2wD5R~72M%`0^@4_Jz7fliU` z!-vTLc&bFnl+gjB3@b(^;oa;YKYp26S$(+};f`0ab}k>Ww%wGSL0~lGT&5n7(X1g> z^=hj$C6102nr`p-yr+>yI$AXWq zU^orFT^0Y?5^UNOlN2{sVOrK&!@0o{dW^(A$^1eUCsXORH>{Wr)F^0SEgOC8OL7;9 zNLRJTYBaskcJn}`$(N-q z`NR422TxE?Tz51!J6n${zQkk1qMVP=_T$%1ip=jx$?Kn(MnKz37*5N>{y*rjyisQm)5k~SX}yDsY4zE(HX0eIy&>;+IneZgh)`Q1J2c; zwc~A`bUscA>yu*4K(H}(Dgv{kYjA5~0~zq+hYsp27~NBwNW5H=s*!L+I+ zOTdPQ>z<{jAVFdQRp(2DGGfltC#`>}R$-=pMK;_EsD{{{RLO@w!!JzLd%;I23sf88 zx8T*d2?sH66ke}PAL6`>M(MkEb7cCw9cu<=;^&=H(=_{AEC4wd;EF?;$;<&qhYM!D zKNADQs3whetNy7Fbb~9W)xuyOj8P2m9ju=ZXKS%bng~eYguM^s$EwoZ;C4j?c@j}& zkLF)3BV{>Cqahua(lbrjX^0KJ{6ug@^rG>`RRZ=A-T!LI=KlnsytG_`|9wTH|KS<@ zv2f!;A($BA+t^Y%S|G&TZ^%BOuxbrXNw{44&58qr0?*CxKJHFMe;2-2E(3Fg0-FhJ-u=J3F8N@y4;V8bPcMFmOzu7KL4Q&pVV=XQ z#DS3u?z%xhad`*o%DuM5@HBdh%kC})y zBj7ZFA)qJZ>4AS2?`j6qq~lzjLo1oZzVbmwN2b~GOSipSKsY|Rxwhsf{OIY{7=KHj zvSFA;q6`q^hIZx7Nx96Q;~|UaTg=0MnWP9^lmt>=vhD9e#QmAcruOg}w;4Zgj;Kf+zW^6g+#O)qsF4;?Qz^cw7a zfAR!(heIP1>VeJpC+XZK_dQqjXKlp8l5VFE@6=58-6S`QnzC504CRmfpX0c78qJ{~ zDJ-Mfn7xyeqdODc>fGQu?x=7L`S1Z_rQ1OHTHJkG+X7jc#sqF15E$+<-ynDFUT6yD zM=YP)EW({iFPatPEk@&#GlQ$QLhOaAfk2c=g8b1PAfL2?DQ5`e(iaa|-j8@!&D1P( z+LI|Z{o1G?H=mt7?=c%27CjHmwmp&N2BhUU*NEuIBa@<=&5uIJcvIu^-gZhTy!zWYkou6?`)BLc$mV63U9qjBJc={&#j zZNEQHMJ3q0)|P^u*&-OT~#9H#*kJa@a1mTZ*x`sAmd0 zFO>Fu`*u&RZ1%@ygZVN!01_pa%d(g>Q>SWmQu7839BQ|_a{$l|fJ{L48uU_gty=R= zP4}MI9rg87^Mx5V2YPL+`Pis2UI5^=vZTggV!RWo3)gU3*jmsccEc{6iof+6s%oPrE2^3n|d z4t_!Vj`ibt)fOZdQlj(na})E(gw(NgPO=sNBI}PsRTeVj!`bg5GKj@fZL3S*<ZWj!uE>&!2rqLcE(KwPy-NfAcor+=^MfmU8s^k|jg>M4xzfilr&0EXRW zZsv*`7@p)P$OW{d2i`H7n@}Xys!qvFPM(fRg?MCUYZn1cbWZ)2wOc{YB0k2^fPs)zae{AJ^f)5(*j z2DwwxLcgO#ZY?;YDB~+3pcbh>L`c$mUrpP+Ka|vllL;to24q^ib#nZ?wWtWRx^DwS zkS?~|p)bbrFRCoj-(*_3Z~)8oLq|Iz)#weB1o zjao=`fG4jJ-8$cXGr0rzWhB4a`+9w5*sq_Y4bUdR64guEcV5ZU-A+JVlPRn? z0;?04lHh~oMQP{1*iG-^NX7e=^WhGsNvdT74U%i_%hOBx?SOr5dRpyELn2H^yXMYH|s*vW+}2A!Hlh|l{T6g z0lQXjkSTf|XwH?bC9THs?P_X92RlnAAe>kdp{px3ZJO>Y>NV=TSvWK}Q){1+5hLJ$ zVqp&b2sJ?$sFnK%=xqUn(n=mTM#7(t4k5sLb0gT$Gh@HhxWJ~CPRs>%Y?T!iuh%p= zq{{`ANcINlm(l8pIn7^2jJ3sjepUIlK$=zxg4>^Es;b6p(5mA^hrX&N9&G)Mu(6vt zoX+GErMbhVa-VLl)%M)^>XOs#j#amNP2&JP8Y`4J(mm7eJ`)o& z%or_~ZuE0yh4QQ1$3q{}s}iU4gU9NI!;FdxXH|&m**l<7q>Bc?=*7heM=3o&y}H#E z8!Ps9Vrxst#>A{=#aolnoyS?s#$?eR3BB0h3;SeZ1`w8!`FAfi5SQ@a-JQoK*PL?y z{j;At7DINafj^{=0to)WF!Bp*zc2qpQ1m|vCO%wI+Q$zcU$*`pIxf@(00bKmP-8&* zUPVp}XXQ-XYp1D^UI0wM>}!PA-gJ3rE&_&qOlo5qU2i>i(3ui7>y4zAS37@e`ug~Q z`~Hn|zJRX!zFQG5VNQOCBXlc1Sd-X|Z)-H|#0<|14Y>7h=%V?Uv)f$GXIVQ%-(lwb z{)*4#QPuW%?QUDvXBWQhYEe#hOyBdD0!09x2=cx;JUp3sxAD+F@dyYsW&J#NaF$T6 zI3c?bb#&KuwGV26d)HJt9G!L$$3JM+ZHl)}7Tv{e zvzhZ71-GPZ(rBCzR_U7~O9RXRZ|VFRJPAKWny3{#kx#Ww9wiX#Kzkp9+%8qkY z+!AcRrl*TEqsxJ|lL(BF4~n#fz8H=e@UL&3#(L2qx zH|FaEaJ(NeNCVwFm6j1oF;%97NOJ(bkvKj23V3r zd4Rc5*LcYwQJ-nlx6*wIXjaE2_;e`y;;#&nd(uqCGxJD*a{@T5p;TeV*O1AsA`jU> zv=hK6$cGo=1TuM!?b|uV;>_i|_Xj(t>J?QDJkfJ?HX^OIU+c(L{1w(VoB~FL1jlXc z0m{`#)FC(UFTB5Zldr^~xnrOm_Q&&^huTHDDmZ}VGAYon1vp(2<~uh#bGI+(rof-g z*wCB!$ji*@tK-M%gtEZR!F{vMU=AdMpxmxZ?MdeKANOPZZ94M`^u)adf47>OL@DW1 z`^N)om7dMbw@c(Rn+Hh7%nKKPkBd*LiDdxp-Iem)Raz_Cj_n50fzu-+%LQIW?wQ#m zZ-Htq;O0vAH~R5bAFTSUH?^B5y_mmiv@-()E&C`mJiHkiaIuW-i9S7C<(w!!E;qf1 zBwGOxP-G%_IPGlsYu_qUS2t&#^IUT+fJ`CEch0Q3bB~--_)q=AOu=HNg(fBWjHtXu zZ~Fr!>^J%FCM+tEPtP5|S|4#(fO+%LPvBE8!SCRhjKr{+LU{aM&gb)*VAXMaQct7* zvUYF1*2e(oqy&$&MNftqN3+P`svTvMw|%weiG7)<3@n{C>J&izfS@_PF_;$EOt>Um zjF;l=lM_?Oa8-EHGU0 zWo3*qbuxjjWD2M^-%uYK^$C042Y zSr|?mu&ZRLW)Uf3Kc4iqi&kV$Btn4Q{wvfH{<`wRBj^iq1y(K<+eDK$vpDlWQDPF7 z_9Ux|)=1K7+hGZh%;~+xW^7vWOPHgx#o%z*&0~Ll6XDODPsz0e=9_M8ZEsU(xxRuQ z=jt{XYvq^+kczQX0e6qM;ukWI;k_MmHMzN8!*`K0rFZehZ>?S^pqz(c2PEN7cx481 z(n&JGnhMWMu1{&OWLwMzG#Vx)Q^Z6kN`3BZNW}jvr(_WpwBYb%KqA-N6_a1YrKMz* zqNqcFldo#HdIB4PsU*~eXR_ZAp;>zYccW3nExB3ioTKhcl~jp-*m zP_Qz0wz0DVE`6jiym2eqz$HAiP%}!Jng&s7ajSZS$!qDe@1wUEC^NhC>nRh*`(VnJ zv!^g`kW#|$-rv>fBuy(bJp(eFY*F*;@GP0swbW!$el9*Ou|3S5zv-(#OVP$h8kVi! zkP_hP7j%I72nZHfculUQQ^p$MEth;Vw{rS4M-Q65VIySRAm#S0k9n=VFxrvk#7#z1K}S&}EW>If6JV&v z#!VTqBgyQzuckoH_c=UHC0>DrmzUexo+ZsKl zAh~n5So!s9-9hJ)&DaUsbH#?13`ha4Y*t`Wzifr#$L4UFuEEtsAc8-dIeE7#O0~-u zHnv^y%Kiz`R1Kak4(m4NGu`z|<}Z=9V%-*Os|&VvwyfquD$K|9Sym7T)m$pa zu+b`N!}5sAr)@CX!9UNUq`pT~{_YsuErD&08*KlIs(gH(_Dk3LjzwiXuv-f&D~eSn z>wWDLh3f%c=AT-CrM-HFW*LjsSZnu&kk66@<_Ni2c~;h#Em-^?mdd1b5ZEm7-8Z9< z+0BvRUtOBa1G+f!Cxg-7QX?(xlAkc2@R9#wf9x_<6GfB%JRtfHB1TW}Avs$?KGPj3 z3@9csE+IL!_V>g{QE6$T|7x9lR@}h)@HeMe?a_WVig*>OV3`kP)0IF%CXhlZ&+ygj z8f#oee}D0g$)2xqMDDOMTa)-S&`DmdHJO&R-b6WGdD@5FZf$j)G4cCgO8}?9iF1R( z&eofP%0IO?jamuJ&~Z%<_aZn&CgnV6KAF>azr|!aQFGt~W0Dvw&9&SDEs7xqO$Lnc zUwx*^%C;BFl!U>u3Z573OujM{m-f-t`XOPMz^KV+;ciH!=&{D@{`89b-U0C4Sdroh z=K!RQu5xB##%D(!;i^h!hKCFWIqYU;AscZK0rx&3XMDQ4x{hO`dpso4TH5FHsHn0W zL@8oovfdmz4FCRgxKkIj`96=kRYZ{DE4}0+Y~R>#uoxfiY%E}!G@*Q^4L*jLl?pef ziC0;F<09qxW)Th9{XRf7ntVZY)Qy#P}*sGdxZ{; zH1m;#WDT~W&R)+zclg`UN8IN?$caIicKap*mL&Zi3du3=ML0S2+w*00|R8*rv6`Ej_i`0WlEfV<3 zfYJ#lV{&U&_7i5YmhK)p`{BL)^HomDC8j#pkL984kyn@;_~#acH8TGc>JSj{X60f3 zxivuWuI|~jf9!(*M0o4!zgQ3uy#4EO+g0}jKdHX|YeW4dD3V}7u=(b#t5<)kzQx>e z?awRy&6f%b^pHXB1Zllj(0%l>$^T+d4F0^yQKKOa)4yTP0UI){=MmyoHmS0NWvv## zC`YovEgA#9UZgG`)Yvnn9kkqDZgV^2=UNY5Sggb8MB#eN8pak43ZvtME#y8Hlm_J& zSxV*#y1O$MxT0Zp7e~)9#b2?uyxLFVF}tHQMtDMl#Dk>~rf5bnN%^DGQjXz}bjUIP zRi(|jVvQJ~2J{EiZm?z4c4ekXB{M=SYT6*=9#0~7wk74~yE{I3eO)BfEKqc8WTE`! zs+0fAKDz}|ROCjmwn5lx0e@Vg>czs(6m4ZD*h3xm@mlOq)Nt49`rvhH<8Plrj4`|Z zykaalOdF!I|H${Z2B`4{NL#zoV>03tv76!Hz8z(Ell_zy+s%P4Za=%VgA05rm z4Zk=b1D3CK8Lsnp7w8QbY6g1+AEGMRG zn`GcYNTVf;`ThH6_Wt=L)t!YtM3Wvo-XHj$JYDLq4cF?t>0jd^;eO@_I+giku4W|6 zB;{UV7rCJ)Xo&1sJYt-Lk8x8*9vqP9aZLcnMqa$rbm$4W}lhyL89M9X6S3 zQZ5UPs#&_{!P_%XoP_Fw!Ba`km{)IW0zzJ6*rgdY`4TFsQllhG&|X$B*tM4t07}1rOPi@S#SB)jeOJ zqw0JOdJL-VG+I#sI7EO`MxWQ@cNVm#fn+&4bB)(fIN&7lpcG_u-iRYh{SN0_b8q1E z5n})Pu*LWM&b`EH^q+|6MJ6cli(A8ES4xY`5XAY!Yb!fqRSBrV%P&^RU+_Af zO8(iV!JQabcNXbKQFPcB+q{!A=KB`2)g2WGpRJxBniX&~7^%EvigMoZ9G{qe-}&?* z7Iop`V!C@EiJPu2Q+zcKL`;I9$)y_qoUG3 z-n7&D;`F8uq|#o-_wU~DEWF4~v-ySRJ&vVDV8c4sz7NwrD-@JJQ1)k{7j-UQdRA?0 z)^B(pr2+y1M{jD*dSmb2OA+&^O08a9FM;gP20TC2)KE8jYP-|89tqf1pGRW&TU`0| zZuy*e)s4@=0%E*Eh?${Tk@DFYY9&0!Wgdm#99bhw*usPx-5*I9wqfUua@X0PXWp~9&Q-QE$L6v%F`Kg+Mt{Rq)(BHpN;XeJi zh%BcbpTYRssfEs{aGt@zIF&qztrQThh*?uU4rmf}crHgx`;a20DfQss>>7)TDs2Dl zX2&LiQtK6*pe{DNaOx<-`GHNo0)UH#mt9fTa>ot@NVb0sL7abA((LcKyG(xV$wxeU zPbfK*LaqfvnN*?VzQ4$$C3*PC&CAVd6U#HGW@19`E6v#XbR(ZvMa5xZjYn8;XAAjK z;oFh7dqcL`V{NpcsOT3}wLZT;;REYx^kP5K2lPr1zjsEwZf(&}QSv7aL_A^VEAsy` zuhXT{LWuD>n8PK`y{3qXv?(A7v}Dc!J7EIzsL=fQVl^V zK*VDU{S9o}* zYaACF?rTZxCf~w9aNF|JDy{~VJQkM9+~&M6iCO(ePv3|=*0%Fk^7gk8#Z?xo=*w_8 zQhWk?@OZtkwzhcwen~NlFf+Vg!6GV5A-A9;NA`WAWQa3&d@(J)`b3W0t)8Bx*wnlu zX_>myWp^f|rareOwiQ?IuU|Dw+hhge17{}3 zQv!i}t0pY*)8Nzh>G*3>xR;Mdp`X7o$$HyNEd}pDcPn38R4P1rT3Gu35ck$$Rejys zFeZWu0xAsx0!k|-t+15_>F#c%bAv%CA>E)fBHg(`O1e8XoqMzCj&B0~e$R8xdEV!| z=e^$V`>xABymYO#=bCHIG46ZZV~+7tSAgS~GV`oTl%5VcT%dP2hXJA#kZlhEOS9k0 zE??mNW&zbV-<8jK5YKzxyh+m}NU2(2tr&2$l{tFLOp9RU#>Gj=^~4mg%(ExX+#sQQ zXhR=-8tE&nRPd_USFB|t_o~4f@ad@L1q(=|mpu9TI(h=}k5iw0N5 zTyWD{a?Do;@QYh8G8O93d`CvI|0%zd)scCI^WgqNi;)`Pc&!0#UCQ<@W_>LmJkr?O zhc*{njVQa^j|HW|&lB*FI8naR$D90%29-N<&>~tw3KIFts^Z1S>guuygIhMM9Dcca zzUc{expV@q%+>=$UcJg2uj1HO|<0$+qMmz<;A; z)XQ+Z;*7sWU0jh08p{VCvj;tEu@CdII*$NMd5AMimiYh1E4p zw?F#M;K?P<@tOU?IY%3{0zUs>mYRG2f?4hy@%sH%HrCghsA%%Y9q}89mZMqYA}BF~ zp*ryeNO=&obmuN^^Q20rRJ#!a(3w=ray`LgReKoE7(a=ro; z8%yA5jCuv3qx9A}=5})5s_*&;$H2fu6wMFunVq3%uU)GN)f+qh6-C6ycB~BPFLiud z4yn*ZI?{!=_V&8Xlm**A;J^TL$ehw#0>_e9RLK8`9BnB*Tq3eDwk>=J;5u$*|C>IG zq*Kyrm~u^yaA~Ldn)q$wkFItTw(G;owvk*UfnMdy`#q>1j!(ldfp5SN%|s833u~E2 z1raK`lkJ4#i=ta~)i+Bm^15wL@5}65w=x_}QhFPyuW+}&4ERfL;I)da6aPRpqd8<%b1 z7Il}zqmK?3txZs^TcBSgkDvvd?>@C*0@&Hf)ZwaIp2@8OEo}qCdElScX5brtUzYyA zV!6Z9xlK3EFo-!UI+&lhc(k+ZHY%N)OSE<7)By|@98ocPCaZFH0UAzD>LA~rw2E7yb_ot>`YU_T?4bm?D=sE@&_5AN|FLvpui> zb>UT%1^Rl#G`q;<<*I^NMxJs^23`&$ZVR!iCQ7I;n2(dLijEE(8P$}NjT+bj2CT$^ z&Essj`^y0M&Xo{SS1eKi_XOXE?mPCXO^R^~Jupj+1?`+dSqt1An_{%7smKvaz&5pB3)c0D$e?<+mpjCjINo+bbPoR)?+^ zf}m$p&*b$NJ7T+O&yE0{HAB(4(@e4%YMpT9spt3xN|JD7z<1PSiua%!BN6~v(gfgZ z+E6_|)LF~In=0~m-MB06DU40rpX(S@vCFZ3oAD_lkL$FU5Y`3gL-P;-@e0PLnZsPG zkP~}{F0S(JCbnyM?g6f|p|XfkWyf8V^~!jvjbDRjS8k2tz#%z}Kk3>E;%h{({S$;; zv`BxFR|{}6$Wdkb`=9mFJDX`!0moD$&f06nnq3{@g$UZL06x$#W8=sf3E(x$olqN2 zD#RQL5I1j~!@cDZ>Tk(nsod7v@;0X2^(=>nE_&YB1*K+k)uE%3ra?(!G(7*2L9*a5 zs-1Yiv(=jBZ9?VxJoXPtsc##pm0CWg_1qhK}TS{TG@m2EmcZ zXFtUEYxqyh&l=~vcM*so`V8@%N7y8MCb|qH&%a6Q$ZVX}(6OstAd$ z_M{SC3?|2m8xEe_jV*DmsN`>T53%9YJHWSf=GBxnbpfAY=ejaY=#Zr>&o5j2U*rNU zY6Zq6QiX;fE_G6I)ct`=&Py;E-D7TatXLElIWag$vo#4$LNc`JuMF(itNDgcyJ%ZCZquXT@vw`9G~1eo~^(M+Wr;rBID&M&%~9iFt6V%Op|u;i*6pR8^2Zu~rY4C;=gRz3(z_wP|<0JM&r^?w(|l2iQ8=4-(`B z*k_G;WvinfU8(SAZ9-uIQa_8{qnzr^5NY0_X<~+Wjvub&aFEP zjP0L4i^s6fW~=27Pjp24#*vKLl*fJeMM*{VDq7^eGi{E<<6ZN`=PMKM2VA>{Qye}4 zgZf{1AySLP|4b*hT-MS9vBi4 zJkT=aqeF0wA6A&=XUraOLVVkc3;R?=OfvW94;R`LVOoL26-obqfEipl8#3M*8!o)& z0C^)=vEDr*LYER-3Q9b^??I8Sz*-1HNHZAt@#K-E(r}ufZJC|Bm64u733-%wj4~NjXQt6-PL<-U|^^i7%J4^ z3Tf2!b8%*^XYsyzjgl@>Bq)K`aTD>nL|sWZUE;B+rLKDQcb8Y}=cFPGa-WbCpEUNH z{8K{RIU6sr)B@81o5K_gpyURU@R*tCaJc>EVYigco=Dk!+1lFRef3HuBg<5wbb#sN ziXBo>UfT%v;|CoxcBzm~I!H)ssz!*C`*%3+KjEX|;bV=pkiC%7THCZ#ujH(~m81+N zBB;uEtCzxVCYD-!gpGLz2t@cyT4x5$nw&<>AVVjX;P!wdi1k399XA1C<%_r&&%x0h z;%%&;=In_=k;e}Sh>o4T@9u-BDv^}02n2%QY)rr_N#S;{#+Sae$iiF`U(vr$MveOv zC;M#8sN^j;uqFK^X3B#am`kJk6%`ebhG`Z^N>*0i*Yyk7P!s^{Vu4t>OKf`m4*9am zru?L}N_HU}%gj=c-mA{??sMv&yQWY zgzUBVj<+``zsS?d0}G@+S+YF#-NNyFE*wQPINB973zQ3e-93JNr61Z{o?h8-RQ@NDIymQ zp&4AJ{)Da9X@%&0$5_iF7^}c;Y_7BYO<<^+O<}ey5PJl6lNEolTlExVmKGihaU7aOpGkZ zY@#Ck#}!G_J;dxh^golb<>h5yB(-W>+8TQjd`XZG`e z*>_=PXFbHhJ&=aQRY2Ud`wYo~=z~S(Z5n|_FvssK2I&B_g%ERT-p`UONQo)9K;U7R zUjs0}h}Cak5~{052ly$jUaLv2o|w<#964BAEltu~O~+)4R8vt&@aV**<^C7@631V%YoDr} z&a_PL-Ys#zzXFrC zc03E51X3-hUMo5_KgPLqoA_I_>naZpSvHlq+LA8S0c8`MW$IqtnEslfN>` zceMOP{0m#172#Rm-z|0LaVZd?#tHsdjqHB!4ucm6F)6XlZX9}DUtU>RT3PWY<5*9( zs04H?y)w&vjAMKA<2N5HM5V>h`^N99AQ9pE{(*Y=!&i`3@8e63D~yn5C;Hm8jyC48 zS_E=RXLWoPYg|roBv?E<=YWyz$c77j9`K6A*8sTg^6n-A{Em)f;}J3xqh8|D$oCm7Jj7KSHzc`2aXpGidHlssf1aZ`b z8J!Xk&BD{B3<*RMwsm=NSl?za=R)RDhFf&Ed+V5(;%rr!sV(#7j zo6dcTn7sB!-WFCD)wV)`>215zp1qf!tSOp14r-eAXyq*qY00x+4hWpx;9D|r%GsBr%09Rvr{TggqImxfzKV&9Na=@c>iO|YB z9Fx4^)d)f!ax^u7rUB!=OU4@!9iII5$xqeNxc$jnLPB*`uGm{UJFj)$iDqUL6`7AO zeV4+rX#AJ#&i4r=-R_>a(gVyJT3Tlx-%+WS7J<;MPhfnYxCGAn-3rH@+-GxunSA-| zCoLx0*49>9oDYE9f07}M0^Ai*I?V|}SFe5S-BO`=TeUJx$fz^DShe>J|5J{P58vs<#@psZcVIbl(HCESc-8%m8JlLtVVGjt7 zZ}+q=X&EYj1SH6VjiPTcuvx#6OyDhA9*g6!=?JJ}J$|b-p%!30J0SM3mdWT@;}GQ4C>-Ci}ePbI`XDL=>(Q% z)X0s{DnQc=Y*ZGiLlU6o{cBEe#>k~|^x<5g%9m&kHnBmvhMA?&G2=r5ueE^UC zzoShK)5qSEkrg{k?-S63ZQjPJSFgqqh2ZCoJ0)n9*|Z$9fB$kHvoZR%k}ptsfaK~= z$nP}SF8W^MI54s1B?NrnMe7%45sY!{7Bd~cjlC%-0iUydg4o_}!8=zNLFliEyc7gE z${NBxrBt>(bg!`MK`h?zV)yRjEUOGqSgpGzIDE{qw;=}@B@9c0?cMlqD%wtYg%-A5 zm1=adApFRv=VmJnGAJxwM~Y)G>(pNina^wz6wKPR9LaS<&p_5k=fPa^yjCYM?<4)K z5)E9xu>VHvGu;OA)Ftb4E7vwOJ*?htH;6bDH!Q z#cRgxiIe8v+}SXlQjp<0t;n&aJdaO9=|5|GsIt_gPb}hpT5Dtw7iT@lERB_n>fIq*}TZ<4D3q8$Hl2lCdW?Kz&`*Vmsg^37*qwJ)28-|s*vg{0)>8a=t^bWSqg*LrphF+XK=omcK>Yf|rW zHv110k>_hj$a9BWpvLYVySkRR?pA#(l8_(b`ok%gs-~4*YmglW^SIU(jNFdsh=S<@ z=%QL$;O5buSK+PFHM~krbMA0z)xQ6aq{bCtolL(*+?`Fs|CQ31cF&%jg3_J>kju<4 zh`h4A&GGgD*kbBf=|vLyCOS=u-F-s}L%?7|9tVruy`%ZFTajFZGCa*lKi$Z%kUZ;$ zz5(T*oJ>ZXV^p`j4jvulj5QY|Mg9lag~{PAkaGdD?u#%z#+`kHUUVyD22A)$>&=^P z!6H9%)6=tn8cFLiP%?65@LTTQ48|mhce4Cbzb8PUVe+jY;Y|_v8Lf(;P5MGtd`Jd4K@1 zfd#i*GNPlO%N||W_?UMGT5v0ARE+gOYmkC5hKr<4Wd&;236US-|COzd>g!RsJo~Ep zjFWq`;4B{Roii5>;rfAn0^XbYkWrXxp7ib(b*e$Onu^T}5Iss7-k$@Z`ih&Y z##XMh95Si0KuQL=*5*3Ry9xOlq995Dk^d`!lMdKM0M0-vmsl!r>oS$}WI<{}cXRA_ zRFn+JnfJ^}E!*^MC@bzm`#>V5mUI`$VrHT^&nxuB4MjzWrvw&56`eG|&)>Xxyg<_g zdT)T-Ap-9ezaP_j(;H6nxKDY$()e@DTi!gPYDDsEFfw*{z46XPB-XRssn3Ip*XLr| zAMX$hGrvHoK*16KQ#L*f2xGwSB^Ra({OZXG0f;<%f)V8nQ=V|4Wtf*5{<2qxh;c7? zQ#@*sJK?{wG^#P?ht>QH$7tu>35~2hZ zpg*=!_dYO#feJEUkAhIzV@f08qq-BJnz7?7!OW?x9V3Z^x3f4S0o^@gTT<#XGu`%A zuR#;1)_@9EdRk(2`t#{dsmr|bxJEp)R`t$EWY!?lSrvxf?xyuv@tN`DmYaq~{LKWs ze^v!ez%J#3N|6pl)zpCG3ku=D#Enhs0Rr!)U$P_ggIMw{KO9ilpi4=z=Ie|4`0INa z(bH^Y8ht*!A_17N^`MY&K4?UQ!l0ZmTx73p{VAilKQZ~3fVXU>erQ}q)F zErhQVA%<;#gP@Q8%fvratkxqmg6X}bj^6R<-pAG4t-3UdvjD{2cw-h&%EE!)DhH_o zgDo}4vB1lG(<#R8-s@4mRY#TW!vc!N6dBdx#uQQc9zT^}F{r46$d!?7kfA>fW=1P# zzv=CxC3yPcKi^B;Z5q=1xlPRy9@Nqy*+NB0@7PvCn(ZQack1>|41?eWHv?s0yZ?!1 z?J`}ST1P|6{_r>X64ZlV^Y?1Lyna3R!)18~G_?O#gZtkW>;CyF^Ro*DJU!EMSg`8P^xzxVUiZGL9Gbgv7(KRU0!mwZ! zjH;O#C}k3+KHGvb&#sD9!j+okua|H1a;$3Px^imL7&`7RMfHdCk(_yc!R2+8T06Vs z(=9AkOiY*8b#q&BT&c->O3AP-OGOl@jMHmoT1)JE`LWQ7`eVgB!!ZYPRA?>9#Z2w6 z`O^2XvF}b>Kg0P$TqUO@`EGE#ne}V6K>e0lyJV)-l4F=C|2*8(>hTpRkaPRH_qIoM zB9%CEie`)DXWzeu$nJiXA?xeeVM{)ZigLDWPp1m4zN|Ce_61u*LmSOr#+zyrfLrdf zCTe9OqW2FwqtaHgr*(T1I9Iomx7H`vOD?b&+LYGkp`wXQdAs>YC3qRn+?NFRMLX)G z23tabYLC3r)Hub82{)iTVJRfl#Uw=X9=QNF2M;YygkdhSsmYz zhP}t~yqn8~x>B7k3+7iOJ87}mLls&0X2&6pHSHw6vg@#uJ2+fT3?d}JQ@G=_(*5WN zsdY0;WWe=s!{3M6iqib@=ZrtmNO`s;_-Vc4WtwHpAjN4Ukq+Q`7wH?vU5cZ6>r}Ti zpm58M2q$0#nUPgbirZhJPRB!Z`ni+|$sfD16#Zhj%Vq1=H)`?&r~vG!_GM*JToR9C zB3#VHbg0$1G0c-( z?oANr_-*vUA62>Lkw5n4D}C5erx9+?Z*5^pBD|RtO&pD&FoYoGrVX2cfYcJIz)S`8 z{=I_<_GjNGi=xpw-4E-(F@9W#r%9^hdM!!D@n=S*@C0tCY94%c+$~s54DB>h6xD~= zEh?{kd;O}Wo|oywuF{0cIkm8qYyn@~F9Z2Ce&v_hyC8Dj2O}kHKeNIGGapkbWDk+7 zu7ZsBj+}$8FRmH4?{3FdM-Ts5u*K}}K*ki)lUBxTGb>j21SJ6@N}DD9Erb30l)ZRM z_hg83BNlc$5{-tqwCLD88i|(QXp=iX7_JHbSp1dIul$IYtI~RJWE;%l;J^=!v4@cR zi7K_waTi|uuJY}@!;1Y%wR=5%zc#U$7CM~b(r<(m#Zk6By!nq;?1aalx$f8xvUgKZ z>WK^KwM81LoOnCOX*y6SgoKwA&NAdexi-W50tZY%w%6Vj1mCo%h@9V<|{!p^WV3^}; z5x&rt%L!h0G@ZGA9>RS!K3lPSn7^9HtZxZLc~)32^=i5sI&8R>)?6R%tt&E_9e3oH z%XxeANM%gEt={!ORVDiNF#gr89kxWmE3lCci~7Y=ctADG$c9ToSZV8l+%4L-O63Lxzg5%j9@G$QV6f<@_Yf>XqsQ*Rpj`9RM%Uj*&M8o zqwiwh0n;^uV&*eBkg?I6tiDRQnLX=O>vENJ@nvON0N&+$7fIrEq2cx9&qsEhS!ZGJ zAXp3Ax>KuT@`Nt`zgtj}_nHLcgda&~Txa!T@;c-G$-+$z1*^;d{#G2re`S!hKfcWQ zNj=*ur8`387S56G3Kyql0Kw7f*#@~N#j7R)`(F}(#hq>wQvW#0@fz~ycI2eURSAf@ zkt6bX=TYwVy?v+;P{IGnUQ3?c`?&?YH%8fL3Anva#*p^*Bx{?xqHM zxf@oXZCSt?2Om{^YZE^=cUtC*{FD2ugC1rEdNBL3z_gXiulJINeV0P{%!V~hnwU%X zMJ1onnj5H@Vj@RVNKQ(E)GFod+O08?A}uT_0ab3*3r)+jEn;afHb)8<`;6L|o-lyS zZpmJXiy?b*^z~jJ9vNvF8S$GK?CWFYMC8S8O)U_88mf*lQ(NXGs|Aa2~oUtL-0jOAHO zCx<0+xDTR?q(?_fTHKG7Clpx8P>PTgP$c-kHvI)jrw_SwG>^yh06o61aovZ;Oh?pd z#R+G%z2B~039uD`P`o&zt7j*Ba>oLFJf;0A378L%}* zE9_U1V;)pVMca4q@fX)7u8tMuFAo$p*PXb&2_%^OslxjHyQl9b!S630W%R^CC^dEm zBHj44q@__4--STS#Z*}nDD>6-fEg$gjT+3(ip^>Z5-?|=V{#F%zr-xvlp>y5xS%m$ za&q6(+QgIJ)7Hl)ZgQwJ92wM{}(l!Q$X0@qTrpXr<7da!Chiv9&@y~u4hw;@kG&GU|e@ulFA?{Qp z0U_ba%1WGoV};B2Lq-S%MIHEq>o>PKzn&|o)UK7cRH{{4vqk}usQpA#DHI|>I}Wzj zh|4|qmYm@|RP<<9Gdx@Ygu?6U5>ER1`piapL%{K#vi&6)CY+6fM5(}O*_vc#<^4sJdydcfNePuh^6U}3HLkVJcAm(JaQ0+~UBuPx{xaR;r zTh}u;S65Wr{mHe}-rmkOC?O@~=UYXJH&WjVg76Okb8=t)3-#yXrSQ(jep8V@sII4Jo|5G%*S)2jLA zQd0bj*{}5=H1vdjz#w;v(|Vid&_Tz_+9w{FGyd6kyj*W=4C_*+k_ z;b5{uL>@boSkOox8aMdrSn)i$v_xkw{$#{}19j_vS=4`~L2SV4DLefE{AYB3N@@c( z^-r0J{~$X7Ah9v`)uqAq^mx$z_Yu8+XjuPrmJb9`jXDqeTbndz;h?82@q~rT8}IM{ zcRM?Y(Ce9RpFlw;VBuVTEGcBgjT=^{%SK*;J&*bRe75gR{MT1OhTOIZmGB*-(#7_8 z6av9!(W<|j{4x1tb$4N8a<4s8Z*WjCA|k>}k+HPcqVMsmoHN7vE|~e5`Dyy(Bog!& z9dfHqsGpXIf1WAIeQ83vIsC;2GMb;9`6OJOpw9EjwQ3F^9^}k9m~ECm=DG1^vck~9 zVn<0R^9@aC@z(l*_OJcML}qRAG>Q1rCwm(sea*4!m$QpDDg?3xhLZF0tasy_Yfd#t zNhWS$xo}c$a;PzdwnTuXvkEWH{+I<026xZXXZ`p=NojTD2=X|kG%7syH^NrLzokQ; z+pZK5qGOX{AST;d8k12xA7^G}MshY2_+r=Vny~^c`AM%qM>pFlcHlR6?quTJjBMir z#}TFZqm!6S1}YRpMO)xb7co$9ZblBMV0Ic|V)JEt0{6rHI4qToEG=tnhY~hW(!92Z zWnT)n780TSL=MMCs2VV1H1r^{`l}J7l58z>9xOMb(j^mjjz&UDN-S*11r!|z{F^Hu z;Fy@3yPb|d#q!yjZkAi<)N~X1}WFxZDfUqIH~?dIz)Btl+1_ zIH{>dWo0iMQ4fdH0wi-2_^{UCtTqknh8-~&cWyu)-^aXh^;N(mNCGLyt(n_8aB*=# zVUdP}hckwYfunoBay4VYt}M0Gm{8Y%9_#4Iyz8OArna?nC_u&+!)Mk+Ow9dfi;(~m z+0ab8w6tW0xo#w2^Mb@f0N!6|0_o=|8_HJ0=}YV4c3)5zHOc(=(S5AMVQ0_KZ0wD+ zbkGFHLrlMnlp;3T#rI;Vo5%zt^bjhj*>8uR0u;tfqIs$3{j5wf=a~l@HY2 z%s74x3JMClftD+{F)F4cBc0QJ_lP$9 zM_Edg@0~k$`VD|_x8L~&s-H0(g65^Hm5!FE-qbBEqD$-I=^%9V7?+Al2n3L}7NwM! z0Te;J5fQN#5y^v4mb-BsXLUc=!e=-{4rS-M-U6L?DMJ)R2B>#!XX=LCe4r|EASjQ>|oR5H9%L+B(nZYn*4G{#eSeup0H_$NA)qx*On(Cfgvd&0|FOcPiJtV{gQzVVlYY0}ZLo(HJeZ zjpcH+oI_>^2iXKAz8Fb|d%_*oXp$_(93+nP7myiC0;f**ah#9(mszh|IYrFo`YvsT zueZ0m-sOVCa&WAzA#2X2n}>(Xe*Q$L*e*mzM-SvE%E;t9OqBAuTn~?o6oyPHo`D&! ze*f;$pd9b%U~G$*sjp2=z_OLgU3%xFQ)K->*Pl2QR$9r!ufEXH<9c99LDyV-KL}`m zTvzSFpt?_p)n81G6mn;UYi4H?59_Qq6x z-zrj`F8fCahSwWJvrKX`A@OaG$<5iZz{?)>Q?NV0*4F{t*2(-txzaE^I(j)(T*>k^ zWNkbjX@P!;Pu9tO2&YOP3Ba^fmRdncDAC8bJYyW8wC-%W4SA)c7e`gV79{zHId{Tj zyZqq)eD0d)=pF+D@8yR5Z*9C+)8sY(p+}c)z7__y_vK&zb&mVL!{wqMer9AS7rSI@ zz5Etm{lz1_R#Lzcz#z$9){8NEKGc=}@rLj9iq`49pZ@;si(OkING{t&9v&Xa*yqtF zOH;=1-rmh(mo0OY>q7rCP~sc_E;(B1y=6RIOUuEiJk8w}{}BMwpfyXZpfzwt2G1&N zgvDwN0lKoB?j&3eyy&f7;?)!8MFe6FfoQN?U0#lkjYYrdV4|)RL@w}@jt)dXynb!W z!p%ZJ>7ce8&FT`jk>2Su_+-5IYkPwLGWds=8VrmQPr`0y%VJ`_L)^N0HR(gt_Z@a3 zU0kDIkLY}jyHf(5KcT}yetPjGDV~G#WA?od??jUBE(|if|ZlE zC--FP8$*6m?VR!LOd~qDR)4x57mXhwwnjk@rA4$L+r>`zhecYa_HP5`P9tlQDdZV0=I(kE&JJp4$)8{g-%Q6GS^g#@wfuF`Kj-( zuu~{P?O%A5{C;~v6jf?5R+8;^>T|IadATru+yv;eN%~IaajMPEkNZ44mGPAh(orv{ z=|R6C)oKchzUD-PTBTvC?DsOe1Do;jJHEH76jB?Bu}cC|s;@z-(wmNTtXk zHa5~qPcJmoy%ylJ$83}io#V_wB=-dy8mk@Gjt`Jy9Am8(k*Pn++?E9*6eD1fy%Uv= z9tWw<7pL&iZEKXvZASWBF2Hg3=vy?-^_~1#qt3V!YgEGfNliiDhEF%N$a!ik)DAbp zsPR9Gt7{CLemwqk5A*d-_2H#W;IW@vGVSv zH%2y-MIPoo1UmD~KX?H5IQyuXyJwBET8b1)h1LXAL*v5k!;Bmc%StEX9uSwVVmM@6 z{1jgE9qVfV%X7zfwv1%gq^G4dNipFI`V;c~sFshZq1Q8%Fqf1>u79hYYZNUh>EguA zt~opj(DX<@4m|JD<*?&U%Hv@ESOi3zeCvGk)XK}C zadv+fa;2wli$k;@@pN=_+(m!&>OJ^F6tJ*9oqQWFWTBx$99}d3&PvC?fcjJSv;6+n z4mpsuIBK8SNk9yhqbRKj`p_+m=zCDVD?A3&gnsx+?7F zcJ3R-6Hj6mPEL4ea6-`&E(W(4#jNGtRB;JAd1Ynadz2RS75krFPvi8_)6>t}IBVB_ zb+(1$2yP?$Tz_kyHKgcc@16Sj-Hx$|yt_Ko4mHwTAAT3lJ@Q&uQUEl~>v2pc=eKA} zy9-*xSq!Wc&(A*y#KOXv{w~8xdFFY+m^?*|`?iZK@8+7F%KPifA2Nz(4XoG=JXb*% zvPBTjTA1s{a-@~mX`ThbrqT=Q(E%CRB$eFpFE%U+Y3SD`t5=b;sI_N#&h-~dLG+me-cO@(|K;FZbeQ3b zK)?88#Y)JVPsA~FM(ND~gXxLFgYTs)?cRajV_PAOK#5IqF4QPD?T{9j$a6nESh#!P zy=fXDx3!B$UuQc!BM}EKnTJO}fP7XEY^v&?KijR%sX`^ASaJ?}?+g0gZloS_OhJcn z94__b#&AAAAAi(+JW#~NcdXQ^%Rd@9y_{v^K>W08stS#n&WcW1aunSDuVZ#svMriG!WSD11@!4c=VOgS&_w}3Uq zv2n@IBX&PjDvdUEJZ)3LA|u-Vtq)B1LZ`dc%%^uuOia=n7m(#8J><&xOAU>QeLo6X z>4!O*;UOm@r#0?J5>YQw%Tge>^8eWMe7BaKdM@!urcsYaW&`j+r?Ne1aSi%U`^SYR zW=gqL0%5PmN@!1aEOc~aq;}u;y*_hUD-X=KT_8HWkH0rLV!K#kHCDw#RXr7pceG~| z9nE)0PN}yC_c&Z`iW@0RXk9vASKru^(JZG{^_silxMD*>5VNe^BP!uhd9`(QI%6ev z^|Ja(7k0`k$*i`U!c}1S#AV$KbR+BCm8JM3K@%#LiVf*AF50-g2>Nx2KMv{14Xm(R zE>O&31Z#P_EjKAS4V!3gb+DctSn>iMgCL28g^t8XCbi(GvTxwqNgHgRf1U0aVDsK8 zE9;w>EFm^}60~%k+g@t_iP-#%8Qz-dx@Q?6EJybM*sb&L zjzQ5%Pgiqxie9aWnx2GrGlz?vxRIY6ZhY{^;^#jw=;HSO%8MXqo3uovlrKYyc&v6J2J{q!fNZjb3|NyDs@0g z(Qp!40qnQi{%E~$AoO4$zg|n9vp(be$!Pa$v6n!_itum)DTjrV_=SZ9?~mWU)qeZ- zGT@Ev{(6rWhN$h#LaG&fv#d0)pa56U7-hQZ+S=Ae&d9}!LAt~<+zr~4xK!6< z^*T>~fYp#bKiCky*uiOmSIgc*PYx$5)3dU(v(H3&2eO9l^i^@8!>m_(lEv|b&CFJD zs5GCJ@1uhFPg70>t2N;bYIbsTUiV(VPS+SPrGO|X4CK$2$8+1eOcj+m>QeoJ=U72E zTLc7-VdN*XAN{2q96Fx8J%1M#uKTrj_YV%Y5z7zRSve?9-9gdd&~&L2`_o?;gnemm zK3Pmu2xSd`T8C?i2qfr6;_!$Sq*eCiorLQ&7IeKtrHu=IUmDF)>MCB9jaQI*9?jrv z@>!k9W1sD)Gakv~m}3Q4ZOMYgUG@u9T6@dbeFK} z@AqY)fQ)+Ei={s2)vY-Ds`IRHa)WRwo&fgpR!^xkisIu50A-6Xa(F|fxiTzGE?ufl zuURtc{&NPF+A%ODCZh9qT5mS_AJ}7nC~io^kf^$>4i->WwLphP*;e>$}`UMzxP3%e`3b51BO6gpH8#^A{JHJ;V+7Bd2G$6-H zjhfnb*S8h8Dgy|(xPcY!)&hh)Dhx5bJf<;FujeT(I71mP6&a~DwTz_ww7$E6jS*1h zfwA3hA|pp-gg9BJ5;#M{ADU#Y;RS$0P$&a%IH?$tA2trIbrbX!vkPJK^yt`kU0#I# zX#L?Zh)h_!w1flCdkClCa!N{^P+gIE=I|_~R3JVe$BSKmH9S>`K2jf8xoZ;3f2t23 zWv36vY)oy7oL#5^y;|9MIA|E{Er5mU5*W*sr_0f-R3vW&Bz(NejhHw#4si|8V89K4 zZH79}eY;1=v{r4q*kuJT8Bw&XWH4x)7eC7{DsX!d{X;A3XZBfFpZocYpmm!9SWr9E z9GJ*u%Rx;&(romtu#oPV&ijua>q{(_FZ^6ij_KJeC_vbr--ULec}F`_LktWIDacg8 zThS6U;bP>zX+6fX_?@LCB@rTzleI|gmwOEW%myoaDN+l-8FIGYL)kuND-{Ck)7eaWna1#R?n@^tBED5$(xsvu=B0r>FGDIm_T8~5O6--cHC#*c^(Q)M#~ zQB;}&Iayht0c08UB;D6H4~CNRSk2B?D@}#r?OECeT^#F{xFPCQ`q@{wLb%h3;KyIkMuE` z8k$4vI01N{*y}o}*E$qG?7TzKFqq9lng05Lo9#?9dG6diKFRNPFj_Wq59&`Z+~$@V zznodNGT#53Q|+>&UdgmG*CH3mq!Abx%@_&1Qb$iuep%UL23=J{#obbPf_h@bGmF`@ z?$UHX56-U4o*NU-!654HyHT;Rs1^}v)i9EW{E}G%cFVogG~9Fy43JyFB{~{zzmnE{ zm&3!uL7o$f_V224)Gdm`-DN)gJP%#j0w342hK3uG-gImWPX@=6F~O-dWT|Q0nFCP};JYBeCYtaV9}a z#jtMa5AW+XV1bdEPaUj#AI(gHNC`k7?A_Gy%s|vW=^TQ<)dw9P zUsG3ih3#+tmP^OH`yEtA<}$I#XdOY`aLg?Vh?R@TCE;aTnwH@2;@V%yI5X*6fE6O4 zQ2r}IKD>>z=YSY38}o+sHSoK$wBBIHmANR0?HU~&?IKxqX2NDda48zRizLZC3alD8 zsfDo@88j-4&))x3@KBqbo)loT&%QUa)wP?eQN4Nk_MydC!v0XHm)C2q5p*g1lhyJ? z?xxNI)XIEW*h%OZc)O8(q6;?@Z$Z44^>oEU0j`sVt~6#xW?NV z4}0{9;32`+*jRinK5ka}%Bm#6RGXE_YPH#(FflI(?uxF1L-k3H^MWzV)YF4`p?m0P?%{liB?!qPf}&*tvUxu1acfIZe&e zb(7uQcyM1^>stR_|6o5R1q0~Qx)*ppezb^}0vZ1oJcOa8<=W!n(ZM3u4&T9z8ywH0 ztE*4&f-a(T5;$NfLu>f?Tq|C8_;)bxF96STbXxAQFYX}FZ< zOdK6S-=e##VPXbO4R&V~^}2@DfO_&ggTB9)1irS=%_CYG>Mjp~8b8O8Jg8*4RQ!kL zz*v^2`pVj#0C?zVY+pMa&qZ}^XD@~ld6T@HCjZH&nv{e$?QcqO^#ZPF;|^cH zc&s+=B^*GbsT-Ee`y&rKrLkO|Xk@{-Uwi&!w9uEAk!Kc+rkZQ*Mqe-fvFrXH4S?qT zUr+tV!yG*m-+@-e{1c^T!5*pwFWvzw+vLT_^b~_+caxSJNy<`t(t>+zmEm(F?XIS6 zMCintNA4ZpU-XJM={H;DH$l7cyCW_x}ewJ_BZyNdA*IGz!9-$8}-_m=%+Sf(3 zTAQv>@ghP=o@6+Y%gw1fbSMLm9N?1o_S}v)(wl9!0^RM0fmDF;@mv}5!~pR5-^XVN z`CO0D0PF#vf2RXjy7iAfA^6m9PcHb>J16x0SO14TAgL^+AwR3n4-bw>ktX#ehKNfK zO%3*DFxPM3N>BnVpm*!Ir74KEzKy{2hG%$d%h#pPuDc2)(bSgNTuL|mK9 z9>*6Nm*-#$#K_Y_`Z7op4wDB#n)B}5wnJ+ec_;i#hiVt|1K&dg!5nPo0WRre=fN-S)*HZ9D}Z3?iXqNBY&y=mx-KAI2y zDCm3c2;v1`?qh~(<;a4t2a(Jt&AmVJohK44$A`D}JP=~=UHMB%rAVs%?~uEssOT=> zz=)q>63SCkQ+XC@758QY-}z-_je2}d6zpNwUiu0`?ohsc_S?5_o497)6Ql_hP3=Wb zGGQCt_u2v>1Zv!tB_&~v3OkjofryAn?!$Sd9+^H6RkdH8j;XM;ABit$Djcmch*d@E zZc|#EuO74;>mM<1e_;Tjooe2@lAsH3*A+6b{q70gxqat%N5_{qviRt54NaNvvRwA4 z$?Of|e-Hkx^un_hvnC1`$*rWL`z3@f%q?Cc?$S*<`Yb!{_|QT%cWFtlton}t4W!ZmxpKtqfEX6}$_zU^1jMD@`v!pk z-JjuK`#;0?Z#o6`8>jI#_X`68I%fZ$-Qxh6R$xEP!TiGd`1{r0OSW#^sx@`;L-mOs zZ{AvSD;Dw96+S%`dYHY-t*7GYTJ!rFdU`AOZ+JOB_}#J)F2$~{f4_V0uUfS#`FOFk zrMtd%@eR{GIIDY7U{LHk|PxqH^0$y!Z_qXc0Uf)?OFOU}u zT!HpM8t?BPuHxgKZ&vYQ!`19-$6efgdpU%imgGh(579cTyzDo>q?qw*+4+t4IzMu^ z2-N=mB4>2N-cYmh-|vsUdq7QhtI}5@Hf48OJQaa6E_;Bfv ${_t("export_chat|html_title")} - -
-
-
-
-
-
-
-
- ${roomAvatar} -
-
-
-
- ${safeRoomName} -
-
-
${safeTopic}
-
-
- ${previousMessagesLink} -
-
-
-
-
-
    +
    +
    +
    +
    +
    +
    +
    +
    + ${roomAvatar} +
    +
    +
    +
    - ${ - currentPage == 0 - ? `
    - ${roomAvatar} -

    ${safeRoomName}

    -

    ${safeCreatedText}

    ${safeExportedText}

    -
    -

    ${safeTopicText}

    -
    ` - : "" - } - ${content} -
+ ${safeRoomName} +
+
+
${safeTopic}
+
+
+ ${previousMessagesLink} +
+
+
+
+
+
    + ${ + currentPage == 0 + ? `
    + ${roomAvatar} +

    ${safeRoomName}

    +

    ${safeCreatedText}

    ${safeExportedText}

    +
    +

    ${safeTopicText}

    +
    ` + : "" + } + ${content} +
+
+
+
+
+
+
+
-
-
-
-
-
+ ${nextMessagesLink} +
-
- ${nextMessagesLink} -
-
`; diff --git a/src/utils/exportUtils/exportCSS.ts b/src/utils/exportUtils/exportCSS.ts index bd7ddac01b..15716ad544 100644 --- a/src/utils/exportUtils/exportCSS.ts +++ b/src/utils/exportUtils/exportCSS.ts @@ -14,74 +14,80 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type { Rule, StyleSheet } from "css-tree"; + import customCSS from "!!raw-loader!./exportCustomCSS.css"; const cssSelectorTextClassesRegex = /\.[\w-]+/g; function mutateCssText(css: string): string { // replace used fonts so that we don't have to bundle Inter & Inconsalata + const sansFont = `-apple-system, BlinkMacSystemFont, avenir next, + avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`; return css - .replace( - /font-family: ?(Inter|'Inter'|"Inter")/g, - `font-family: -apple-system, BlinkMacSystemFont, avenir next, - avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`, - ) + .replace(/font-family: ?(Inter|'Inter'|"Inter")/g, `font-family: ${sansFont}`) + .replace(/--cpd-font-family-sans: ?(Inter|'Inter'|"Inter")/g, `--cpd-font-family-sans: ${sansFont}`) .replace( /font-family: ?Inconsolata/g, "font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace", ); } -function isLightTheme(sheet: CSSStyleSheet): boolean { - return (sheet.ownerNode)?.dataset.mxTheme?.toLowerCase() === "light"; -} +function includeRule(rule: Rule, usedClasses: Set): boolean { + if (rule.prelude.type === "Raw") { + // cull empty rules + if (rule.block.children.isEmpty) return false; -async function getRulesFromCssFile(path: string): Promise { - const doc = document.implementation.createHTMLDocument(""); - const styleElement = document.createElement("style"); - - const res = await fetch(path); - styleElement.textContent = await res.text(); - // the style will only be parsed once it is added to a document - doc.body.appendChild(styleElement); - - return styleElement.sheet!; + return rule.prelude.value.split(",").some((subselector) => { + const classes = subselector.trim().match(cssSelectorTextClassesRegex); + if (classes && !classes.every((c) => usedClasses.has(c.substring(1)))) { + return false; + } + return true; + }); + } + return true; } // naively culls unused css rules based on which classes are present in the html, // doesn't cull rules which won't apply due to the full selector not matching but gets rid of a LOT of cruft anyway. +// We cannot use document.styleSheets as it does not handle variables in shorthand properties sanely, +// see https://github.com/element-hq/element-web/issues/26761 const getExportCSS = async (usedClasses: Set): Promise => { - // only include bundle.css and the data-mx-theme=light styling - const stylesheets = Array.from(document.styleSheets).filter((s) => { - return s.href?.endsWith("bundle.css") || isLightTheme(s); + const csstree = await import("css-tree"); + + // only include bundle.css and light theme styling + const hrefs = ["bundle.css", "theme-light.css"].map((name) => { + return document.querySelector(`link[rel="stylesheet"][href$="${name}"]`)?.href; }); - // If the light theme isn't loaded we will have to fetch & parse it manually - if (!stylesheets.some(isLightTheme)) { - const href = document.querySelector('link[rel="stylesheet"][href$="theme-light.css"]')?.href; - if (href) stylesheets.push(await getRulesFromCssFile(href)); - } - let css = ""; - for (const stylesheet of stylesheets) { - for (const rule of stylesheet.cssRules) { - if (rule instanceof CSSFontFaceRule) continue; // we don't want to bundle any fonts - const selectorText = (rule as CSSStyleRule).selectorText; + for (const href of hrefs) { + if (!href) continue; + const res = await fetch(href); + const text = await res.text(); - // only skip the rule if all branches (,) of the selector are redundant - if ( - selectorText?.split(",").every((selector) => { - const classes = selector.match(cssSelectorTextClassesRegex); - if (classes && !classes.every((c) => usedClasses.has(c.substring(1)))) { - return true; // signal as a redundant selector - } - }) - ) { - continue; // skip this rule as it is redundant + const ast = csstree.parse(text, { + context: "stylesheet", + parseAtrulePrelude: false, + parseRulePrelude: false, + parseValue: false, + parseCustomProperty: false, + }) as StyleSheet; + + for (const rule of ast.children) { + if (rule.type === "Atrule") { + if (rule.name === "font-face") { + continue; + } } - css += mutateCssText(rule.cssText) + "\n"; + if (rule.type === "Rule" && !includeRule(rule, usedClasses)) { + continue; + } + + css += mutateCssText(csstree.generate(rule)); } } diff --git a/src/utils/exportUtils/exportCustomCSS.css b/src/utils/exportUtils/exportCustomCSS.css index 4807e316a8..bd0de64265 100644 --- a/src/utils/exportUtils/exportCustomCSS.css +++ b/src/utils/exportUtils/exportCustomCSS.css @@ -18,6 +18,11 @@ limitations under the License. This file is raw-imported (imported as plain text) for the export bundle, which is the reason for the .css format and the colours being hard-coded hard-coded. */ +html, +body { + font-size: var(--cpd-font-size-root) !important; +} + #snackbar { display: flex; visibility: hidden; diff --git a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap index aca4e162c8..3958005c5b 100644 --- a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap +++ b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap @@ -12,77 +12,73 @@ exports[`HTMLExport should export 1`] = ` Exported Data - -
-
-
-
-
-
-
-
- ! -
-
-
-
- !myroom:example.org -
-
-
-
-
- -
-
-
-
-
-
    +
    +
    +
    +
    +
    +
    +
    +
    + ! +
    +
    +
    +
    -
    - ! -

    !myroom:example.org

    -

    created this room.

    This is the start of export of !myroom:example.org. Exported by @userId:matrix.org at 11/17/2022.

    -
    -

    + !myroom:example.org +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
      +
      + ! +

      !myroom:example.org

      +

      created this room.

      This is the start of export of !myroom:example.org. Exported by @userId:matrix.org at 11/17/2022.

      +
      +

      +
      +
    1. @user49:example.com
      Message #49
    2. @user48:example.com
      Message #48
    3. @user47:example.com
      Message #47
    4. @user46:example.com
      Message #46
    5. @user45:example.com
      Message #45
    6. @user44:example.com
      Message #44
    7. @user43:example.com
      Message #43
    8. @user42:example.com
      Message #42
    9. @user41:example.com
      Message #41
    10. @user40:example.com
      Message #40
    11. @user39:example.com
      Message #39
    12. @user38:example.com
      Message #38
    13. @user37:example.com
      Message #37
    14. @user36:example.com
      Message #36
    15. @user35:example.com
      Message #35
    16. @user34:example.com
      Message #34
    17. @user33:example.com
      Message #33
    18. @user32:example.com
      Message #32
    19. @user31:example.com
      Message #31
    20. @user30:example.com
      Message #30
    21. @user29:example.com
      Message #29
    22. @user28:example.com
      Message #28
    23. @user27:example.com
      Message #27
    24. @user26:example.com
      Message #26
    25. @user25:example.com
      Message #25
    26. @user24:example.com
      Message #24
    27. @user23:example.com
      Message #23
    28. @user22:example.com
      Message #22
    29. @user21:example.com
      Message #21
    30. @user20:example.com
      Message #20
    31. @user19:example.com
      Message #19
    32. @user18:example.com
      Message #18
    33. @user17:example.com
      Message #17
    34. @user16:example.com
      Message #16
    35. @user15:example.com
      Message #15
    36. @user14:example.com
      Message #14
    37. @user13:example.com
      Message #13
    38. @user12:example.com
      Message #12
    39. @user11:example.com
      Message #11
    40. @user10:example.com
      Message #10
    41. @user9:example.com
      Message #9
    42. @user8:example.com
      Message #8
    43. @user7:example.com
      Message #7
    44. @user6:example.com
      Message #6
    45. @user5:example.com
      Message #5
    46. @user4:example.com
      Message #4
    47. @user3:example.com
      Message #3
    48. @user2:example.com
      Message #2
    49. @user1:example.com
      Message #1
    50. @user0:example.com
      Message #0
    51. +
    -
  1. @user49:example.com
    Message #49
  2. @user48:example.com
    Message #48
  3. @user47:example.com
    Message #47
  4. @user46:example.com
    Message #46
  5. @user45:example.com
    Message #45
  6. @user44:example.com
    Message #44
  7. @user43:example.com
    Message #43
  8. @user42:example.com
    Message #42
  9. @user41:example.com
    Message #41
  10. @user40:example.com
    Message #40
  11. @user39:example.com
    Message #39
  12. @user38:example.com
    Message #38
  13. @user37:example.com
    Message #37
  14. @user36:example.com
    Message #36
  15. @user35:example.com
    Message #35
  16. @user34:example.com
    Message #34
  17. @user33:example.com
    Message #33
  18. @user32:example.com
    Message #32
  19. @user31:example.com
    Message #31
  20. @user30:example.com
    Message #30
  21. @user29:example.com
    Message #29
  22. @user28:example.com
    Message #28
  23. @user27:example.com
    Message #27
  24. @user26:example.com
    Message #26
  25. @user25:example.com
    Message #25
  26. @user24:example.com
    Message #24
  27. @user23:example.com
    Message #23
  28. @user22:example.com
    Message #22
  29. @user21:example.com
    Message #21
  30. @user20:example.com
    Message #20
  31. @user19:example.com
    Message #19
  32. @user18:example.com
    Message #18
  33. @user17:example.com
    Message #17
  34. @user16:example.com
    Message #16
  35. @user15:example.com
    Message #15
  36. @user14:example.com
    Message #14
  37. @user13:example.com
    Message #13
  38. @user12:example.com
    Message #12
  39. @user11:example.com
    Message #11
  40. @user10:example.com
    Message #10
  41. @user9:example.com
    Message #9
  42. @user8:example.com
    Message #8
  43. @user7:example.com
    Message #7
  44. @user6:example.com
    Message #6
  45. @user5:example.com
    Message #5
  46. @user4:example.com
    Message #4
  47. @user3:example.com
    Message #3
  48. @user2:example.com
    Message #2
  49. @user1:example.com
    Message #1
  50. @user0:example.com
    Message #0
  51. -
+
+
+
+
+
+
-
-
-
-
-
+ +
-
- -
-
" diff --git a/yarn.lock b/yarn.lock index 11678cb0be..b6585e26ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2479,6 +2479,11 @@ resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.4.tgz#e3e331b7e0d5496873d417839f3b2bbcf555bb73" integrity sha512-aqBg5oAGo/qh/+wxUfuMadDu2WO0MEWOblyzwaM1Ske2xilUxBfgPqapAFVAfrVTDMVwa0UMarzGot8m64IAzA== +"@types/css-tree@^2.3.8": + version "2.3.8" + resolved "https://registry.yarnpkg.com/@types/css-tree/-/css-tree-2.3.8.tgz#0eabc115e45051b2f7abe51ee1531074b234ed19" + integrity sha512-zABG3nI2UENsx7AQv63tI5/ptoAG/7kQR1H0OvG+WTWYHOR5pfAT3cGgC8SdyCrgX/TTxJBZNmx82IjCXs1juQ== + "@types/diff-match-patch@^1.0.32": version "1.0.36" resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz#dcef10a69d357fe9d43ac4ff2eca6b85dbf466af"