From b29d5c702e2afebc960e82dc6736094d50ab89fd Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Tue, 8 Jul 2025 01:06:09 -0500 Subject: [PATCH 01/48] correcting/theming icons --- resources/img/average-dark-16.png | Bin 0 -> 427 bytes resources/img/average-dark-32.png | Bin 0 -> 791 bytes resources/img/average-dark-64.png | Bin 0 -> 1550 bytes .../{average-16.png => average-light-16.png} | Bin .../{average-32.png => average-light-32.png} | Bin .../{average-64.png => average-light-64.png} | Bin resources/img/check-dark-16.png | Bin 0 -> 307 bytes resources/img/check-dark-32.png | Bin 0 -> 449 bytes resources/img/check-dark-64.png | Bin 0 -> 940 bytes .../img/{check-16.png => check-light-16.png} | Bin .../img/{check-32.png => check-light-32.png} | Bin .../img/{check-64.png => check-light-64.png} | Bin resources/img/circle-dark-16.png | Bin 0 -> 389 bytes resources/img/circle-dark-32.png | Bin 0 -> 722 bytes resources/img/circle-dark-64.png | Bin 0 -> 1553 bytes .../{circle-16.png => circle-light-16.png} | Bin .../{circle-32.png => circle-light-32.png} | Bin .../{circle-64.png => circle-light-64.png} | Bin resources/img/circledots-dark-16.png | Bin 0 -> 396 bytes resources/img/circledots-dark-32.png | Bin 0 -> 773 bytes resources/img/circledots-dark-64.png | Bin 0 -> 1656 bytes ...cledots-16.png => circledots-light-16.png} | Bin ...cledots-32.png => circledots-light-32.png} | Bin ...cledots-64.png => circledots-light-64.png} | Bin resources/img/clipboarddata-dark-16.png | Bin 0 -> 320 bytes resources/img/clipboarddata-dark-32.png | Bin 0 -> 537 bytes resources/img/clipboarddata-dark-64.png | Bin 0 -> 969 bytes ...data-16.png => clipboarddata-light-16.png} | Bin ...data-32.png => clipboarddata-light-32.png} | Bin ...data-64.png => clipboarddata-light-64.png} | Bin resources/img/download-dark-16.png | Bin 0 -> 340 bytes resources/img/download-dark-32.png | Bin 0 -> 556 bytes resources/img/download-dark-64.png | Bin 0 -> 1030 bytes ...{download-16.png => download-light-16.png} | Bin ...{download-32.png => download-light-32.png} | Bin ...{download-64.png => download-light-64.png} | Bin resources/img/eye-dark-16.png | Bin 0 -> 374 bytes resources/img/eye-dark-32.png | Bin 0 -> 729 bytes resources/img/eye-dark-64.png | Bin 0 -> 1448 bytes .../img/{eye-16.png => eye-light-16.png} | Bin .../img/{eye-32.png => eye-light-32.png} | Bin .../img/{eye-64.png => eye-light-64.png} | Bin resources/img/flag-dark-16.png | Bin 0 -> 293 bytes resources/img/flag-dark-32.png | Bin 0 -> 469 bytes resources/img/flag-dark-64.png | Bin 0 -> 766 bytes .../img/{flag-16.png => flag-light-16.png} | Bin .../img/{flag-32.png => flag-light-32.png} | Bin .../img/{flag-64.png => flag-light-64.png} | Bin resources/img/gear-dark-16.png | Bin 0 -> 484 bytes resources/img/gear-dark-32.png | Bin 0 -> 1034 bytes resources/img/gear-dark-64.png | Bin 0 -> 2185 bytes .../img/{gear-16.png => gear-light-16.png} | Bin .../img/{gear-32.png => gear-light-32.png} | Bin .../img/{gear-64.png => gear-light-64.png} | Bin resources/img/reply-dark-16.png | Bin 0 -> 280 bytes resources/img/reply-dark-32.png | Bin 0 -> 414 bytes resources/img/reply-dark-64.png | Bin 0 -> 749 bytes .../img/{reply-16.png => reply-light-16.png} | Bin .../img/{reply-32.png => reply-light-32.png} | Bin .../img/{reply-64.png => reply-light-64.png} | Bin resources/img/settings-dark-16.png | Bin 0 -> 418 bytes resources/img/settings-dark-32.png | Bin 0 -> 792 bytes resources/img/settings-dark-64.png | Bin 0 -> 1496 bytes ...{settings-16.png => settings-light-16.png} | Bin ...{settings-32.png => settings-light-32.png} | Bin ...{settings-64.png => settings-light-64.png} | Bin resources/img/trash-dark-16.png | Bin 0 -> 378 bytes resources/img/trash-dark-32.png | Bin 0 -> 607 bytes resources/img/trash-dark-64.png | Bin 0 -> 1089 bytes .../img/{trash-16.png => trash-light-16.png} | Bin .../img/{trash-32.png => trash-light-32.png} | Bin .../img/{trash-64.png => trash-light-64.png} | Bin resources/img/upload-dark-16.png | Bin 0 -> 349 bytes resources/img/upload-dark-32.png | Bin 0 -> 547 bytes resources/img/upload-dark-64.png | Bin 0 -> 1039 bytes .../{upload-16.png => upload-light-16.png} | Bin .../{upload-32.png => upload-light-32.png} | Bin .../{upload-64.png => upload-light-64.png} | Bin resources/img/x-dark-16.png | Bin 0 -> 222 bytes resources/img/x-dark-32.png | Bin 0 -> 314 bytes resources/img/x-dark-64.png | Bin 0 -> 632 bytes resources/img/{x-16.png => x-light-16.png} | Bin resources/img/{x-32.png => x-light-32.png} | Bin resources/img/{x-64.png => x-light-64.png} | Bin resources/svg2img.ps1 | 49 +++++++++++++++--- 85 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 resources/img/average-dark-16.png create mode 100644 resources/img/average-dark-32.png create mode 100644 resources/img/average-dark-64.png rename resources/img/{average-16.png => average-light-16.png} (100%) rename resources/img/{average-32.png => average-light-32.png} (100%) rename resources/img/{average-64.png => average-light-64.png} (100%) create mode 100644 resources/img/check-dark-16.png create mode 100644 resources/img/check-dark-32.png create mode 100644 resources/img/check-dark-64.png rename resources/img/{check-16.png => check-light-16.png} (100%) rename resources/img/{check-32.png => check-light-32.png} (100%) rename resources/img/{check-64.png => check-light-64.png} (100%) create mode 100644 resources/img/circle-dark-16.png create mode 100644 resources/img/circle-dark-32.png create mode 100644 resources/img/circle-dark-64.png rename resources/img/{circle-16.png => circle-light-16.png} (100%) rename resources/img/{circle-32.png => circle-light-32.png} (100%) rename resources/img/{circle-64.png => circle-light-64.png} (100%) create mode 100644 resources/img/circledots-dark-16.png create mode 100644 resources/img/circledots-dark-32.png create mode 100644 resources/img/circledots-dark-64.png rename resources/img/{circledots-16.png => circledots-light-16.png} (100%) rename resources/img/{circledots-32.png => circledots-light-32.png} (100%) rename resources/img/{circledots-64.png => circledots-light-64.png} (100%) create mode 100644 resources/img/clipboarddata-dark-16.png create mode 100644 resources/img/clipboarddata-dark-32.png create mode 100644 resources/img/clipboarddata-dark-64.png rename resources/img/{clipboarddata-16.png => clipboarddata-light-16.png} (100%) rename resources/img/{clipboarddata-32.png => clipboarddata-light-32.png} (100%) rename resources/img/{clipboarddata-64.png => clipboarddata-light-64.png} (100%) create mode 100644 resources/img/download-dark-16.png create mode 100644 resources/img/download-dark-32.png create mode 100644 resources/img/download-dark-64.png rename resources/img/{download-16.png => download-light-16.png} (100%) rename resources/img/{download-32.png => download-light-32.png} (100%) rename resources/img/{download-64.png => download-light-64.png} (100%) create mode 100644 resources/img/eye-dark-16.png create mode 100644 resources/img/eye-dark-32.png create mode 100644 resources/img/eye-dark-64.png rename resources/img/{eye-16.png => eye-light-16.png} (100%) rename resources/img/{eye-32.png => eye-light-32.png} (100%) rename resources/img/{eye-64.png => eye-light-64.png} (100%) create mode 100644 resources/img/flag-dark-16.png create mode 100644 resources/img/flag-dark-32.png create mode 100644 resources/img/flag-dark-64.png rename resources/img/{flag-16.png => flag-light-16.png} (100%) rename resources/img/{flag-32.png => flag-light-32.png} (100%) rename resources/img/{flag-64.png => flag-light-64.png} (100%) create mode 100644 resources/img/gear-dark-16.png create mode 100644 resources/img/gear-dark-32.png create mode 100644 resources/img/gear-dark-64.png rename resources/img/{gear-16.png => gear-light-16.png} (100%) rename resources/img/{gear-32.png => gear-light-32.png} (100%) rename resources/img/{gear-64.png => gear-light-64.png} (100%) create mode 100644 resources/img/reply-dark-16.png create mode 100644 resources/img/reply-dark-32.png create mode 100644 resources/img/reply-dark-64.png rename resources/img/{reply-16.png => reply-light-16.png} (100%) rename resources/img/{reply-32.png => reply-light-32.png} (100%) rename resources/img/{reply-64.png => reply-light-64.png} (100%) create mode 100644 resources/img/settings-dark-16.png create mode 100644 resources/img/settings-dark-32.png create mode 100644 resources/img/settings-dark-64.png rename resources/img/{settings-16.png => settings-light-16.png} (100%) rename resources/img/{settings-32.png => settings-light-32.png} (100%) rename resources/img/{settings-64.png => settings-light-64.png} (100%) create mode 100644 resources/img/trash-dark-16.png create mode 100644 resources/img/trash-dark-32.png create mode 100644 resources/img/trash-dark-64.png rename resources/img/{trash-16.png => trash-light-16.png} (100%) rename resources/img/{trash-32.png => trash-light-32.png} (100%) rename resources/img/{trash-64.png => trash-light-64.png} (100%) create mode 100644 resources/img/upload-dark-16.png create mode 100644 resources/img/upload-dark-32.png create mode 100644 resources/img/upload-dark-64.png rename resources/img/{upload-16.png => upload-light-16.png} (100%) rename resources/img/{upload-32.png => upload-light-32.png} (100%) rename resources/img/{upload-64.png => upload-light-64.png} (100%) create mode 100644 resources/img/x-dark-16.png create mode 100644 resources/img/x-dark-32.png create mode 100644 resources/img/x-dark-64.png rename resources/img/{x-16.png => x-light-16.png} (100%) rename resources/img/{x-32.png => x-light-32.png} (100%) rename resources/img/{x-64.png => x-light-64.png} (100%) diff --git a/resources/img/average-dark-16.png b/resources/img/average-dark-16.png new file mode 100644 index 0000000000000000000000000000000000000000..42ccab572665a8a9343d7fcf3ecfcf5fa74cfc37 GIT binary patch literal 427 zcmV;c0aX5pP)TfFP(3V1qtEX+c2` zh_Z5;N@#w9=-LAx-CL6ezXoR=UH7=pqtg`^EZFR`*82bV-fQi>P8*=@lpWiE*(YgC zQdN?6{zsrrN!>R7EU8hzXIRp6^OdAK0*Y!C7zG{yYiyQPU=kPsDgpmtpl-Hxpbt0)E=JU%=5gep^lw_DFgn>5!Y7m-JN9qHVqGC=N(EE$N`M zos)D|(tX?ZhNKgcrq?YpR(>S0>en`xw`2XlRA5)?b_`ASX+e4%_#fa(0pO&LCkUJi z*@Yb2fjI|$yac#UxL9V1@+p_V4d+y5<;iTn=i*(>BTxmtnem$zfX8S4*5KnR zlypQ9?H6HYlpY0W* zW?}+Z34ksG4~go$GvJ>|U@Qkbq#eM1(~hEmNsV)$D(SMMSCVE6-t8@syg#EYm%n}o Vb>=Nb*;D`k002ovPDHLkV1gpzWwHPO literal 0 HcmV?d00001 diff --git a/resources/img/average-dark-64.png b/resources/img/average-dark-64.png new file mode 100644 index 0000000000000000000000000000000000000000..1174698d7838a772528a68ff81c05d8589836178 GIT binary patch literal 1550 zcmV+p2J!icP)Yh4v zUWX*6Gwx`$0IkM!GQfPGBilJ04A2dH3tR$5fJf60J8ttmpc@zj)}{K+0!x!0%{IVH zU|B_gE}#=PEiVJ(z$ng3*Gm}7QXW9SJm6{~P$MxHcm~)EYy{Q=8pqU0e_lYEUQGe*^&locxAH{|&?)I?VBmd{R+gZ+Wrzc1 zDqq9^v*h>u6}hQiw&K~=FKMC-<%<}=x8~1DTGs^SsVFxbFu*__et8Cd*)!m2<=f!u zRo=JZrLnDS13Vx*syo+)%1bVKTol7M_Bi|=)!+FE;CWyyl1DuVedkY-j_n;50PjX| zjX1O0KEp&NtF}el;Qa_{TkW9 zQt%X6YY*r%`aTCvN3E~o=)#@beX;!(s$=_KfiDdD*McJjtTRv5JO7O)ct7`%HZu#d zXNPe~bA9iG1bNXA;BXr&pR^;!yD#|YLrY+j(YGr&o$5pJBkirQWZdkf4p{QIVac90 z_I%Qo8$5pU(fgRpRP~NJ@;^{MX=52alagjww7w;@vX7rsYAV>(^xYYL}#BzjwZ%Fcuo+`2?2X%$%jq0G&qP zWuvbMx^R~rKF@EVxaSl6ZS>t~5jOjBv>D3hg3kb!x98V6Rx_2iynV?c^xX|w#P=2! zr@SC17M85mvhdD;QKN5FL17xb;aOO1^o^L5fk0?76HJV@NstrM&Su8Cnl})-jyqna zu@p}*;X$*feS#Ih;!NL3;2(=P^c6T{^gSCXM5~ksSC5dzcRu3026#0>fL1CGwv2;b z>cBdhtNaO(R;~G@SqYH~lIHl{3C(~BV0Wgk19;m*--_Kni+89lh1*Sa+WaPJSTr^% z+G@`uX|1Gd#`qgT@5L}4G?=E-&4ns68qh2`_tknYhVhdg<@(hgo=4If#`rfRJywPR zko2W7z=Zs_w9Qn0z3l7qT`}*+4zft`C2!4CzDLq;h799!g% zi^$6M`dWpnP>;x6=jGyCw(<$4Bz+=R*;TLNYb1RtJIG1!brI!b3{dMkA5$oKMAGYW zH#V_?t&sb+A64x_$(?ffi$VqtmCDAR>bB!bWfw%OxU;xRi_^gG+*&GoKQ5E=5Y^{- z3m1VMRG(M!!DE@c;cZeZUCUc#VUMK6D(LleD>CgvpSs88%4Zsd{fY>0^RzORPqz({ zK9GyZ>cxc$ui&Y!Xv^*adx7VSl~Od5GY5E@n}3;k5O+}-!W1x!OJfZI2ML|$>qOcB zbAT&UClty|W6i)_QZ51R=H_L~W#BJd&EY7X1ZE?UodIJN;Sw|>hu2bML*Z9&Mi4H& zbz3VB_WuSuaJR!{-3GTcZuftV*2Hx9?{EN&PEET=zyn-V~9oX(aVNj%!wi_4{j>F z6v>^WcdMeaWZFtwmn_9tzJ$5j5fvd;8i&TCPH~xI@ z&z^_>xu48mdKI;Vst0Q{wW AIsgCw literal 0 HcmV?d00001 diff --git a/resources/img/check-dark-32.png b/resources/img/check-dark-32.png new file mode 100644 index 0000000000000000000000000000000000000000..7c2e09ca1b68a120634d68638bb13ac1802909d8 GIT binary patch literal 449 zcmV;y0Y3hTP)_@0(O=nTloamzJQhDI|x>mRw9Bn zg4l>ZEV#!a7cywFnaPh?Fc+p7?#wxv;R{IzP?I&&D7_VE1^!qdlGJTV;6T!?q|+t@ z29l04c|e_`4}22W9kBSbr%reecro}hu$I*(q_!!uk)L%7x z7Pug({k%14lU*PxC~)GykA2ppO`h8Z!$A0<13$CVC?wz>;hraaKz!c-SHPUFc-rIw zaATW|f!&-<`s8FiR6xKoNsLCoRz{y!){6)TSOuPJ^EZBH**oh+1q7@Eug(+pY(6OK zWdsCllHBnlutH$g%L)kC2EGjV?!djWUWI^&Bk7iR)~gZ_@yiL|l7OsF8XXA!F^MES rNE(&;OtC5=P-*?Eui{o%1_Y_uKhq z8!BJ2`cfN6tr|dT1F2O5NNpgsY5=JXq*e_ewSm;C0i-sNS~Y;w1|nz(_y$;%XYy17 z@Dp$!I17vbgY6bfB>;PYgP(n#1N_i#(NqDj2{=|{VPgt3M23Jnz^s<>e}GlMa<}>zC z8Jp%%A8?Lvt-l6*U)oM;yJJxO%TGF#V@q?{N9Y@pQ#JV z-mmCyz#(8)X}94gWj)uA0_!O7hfZ7Npfsqy4P23b)y*YdjUn|-V4t_@*JWS%k4N5# z8$Tr6s(0$z5{&e#zXjrtsXJpO-w6Od7#Rzg{L|)LQq;tJbtDj1^~adWcM1S?Uxv|F z>OcVlGt}D|F;B)$z6b!!E>Ta_g&o9c7_Km?*VXBh6+lBhonicmIXKe2-z@;tx$5H#2H^xzfyn6fN=b?dC6*hYyhZ>)fX8M_L;cW_W}UwGWCx@f@A8h8OfhW znY>zhQ* zK~y-6m6JhE0#Oh|-%NC&2avcE5IvBBCfr5wGzxkW1-H7u$kOOW-5Ha4F6hyp$pF$x zC6jbl{hHtPGnEC^09x_=47}7%sVOq`M4dRVuYl@U?ZwP5CnayI*Xp}^rtT-bj(VVw*>xJuEAnNjJaY@Ei)%gCw7r`k;gCSiLOxx_%dD+PAdqVmtH& z2ljw(;Azo5Yib&tW)3V2!ivg(A_r!`CeU7k+yOR#SUdv1AMMzrz zxWIO-@u(W4VT5FvNLzhWFRMW!;yDZ_1N9*39jX^`@>N}wQzzXBN#WmPD%4x`*vey? j+o-RdWHVFXt#0TCbV(a3C9X<#00000NkvXXu0mjfD-jZvxGBSI&XfTCPaDiJW>nGzNyj9e zw6>b0T}kVb_A*5S(qIUf13m(EI$IrB0PfnDp0Rs%7zAztKRXca7?P|8%mU@S0*(Uj zVnG*yd%#)Xkn3Bqw);e@)7|1{# zPy!wS4JV>)C6fO0uE%TIIZRiYILjcTCwkI z1q!%oe%Uw=1W3{g^HYj0ihkR)q=^_;TQaKIRk_;dWK;&$E%qjGI4KthxZx7UlRgS~ z=Hg!Wza=THTE~7BBz{u!^e9L~R5oq6LH}Bk1xrtB<=B{c;7BK)G`n|(=IMyrt|7@bi8fsBhl$4!Q5p4t?oYFQ z;5~2~chtI}1essQBZ?lU$JP2c-BI*7{hcLP9szx|ztbLgA^yt8y8r+H07*qoM6N<$ Ef<)y(#Q*>R literal 0 HcmV?d00001 diff --git a/resources/img/circle-dark-64.png b/resources/img/circle-dark-64.png new file mode 100644 index 0000000000000000000000000000000000000000..8de9fee1a3ee61cdeaaec3290d9c35df05c7238c GIT binary patch literal 1553 zcmV+s2JZQZP)fJb=fJr~>dxz@l$UnjM4l1!s{D7u=CA{>!DpcfNl(V; zxGnwie-^C|D*#sWeME;o$vQ zr2*)67|AN0|M^^xQRNOe|(Dir+b^zKP7aR?ZPW(9H&=vVOT>vt`nJ^i}jA6`u@5K`Dzz)Dd zYy30*3Cw!H6ea`h#Uk&(8-Nt1R_Dcq2uQ_^i`KZ+JMaeJI!brMdC@nelc zci20ykHhnTKPV2&c}(5D-cE#m$KeIQUj>D|11?~Ca=d86)amODk5cSBz_)mu2ltE6 zZinuvkVwS|7v~b+%1w&qfKK42u-HY6yMU8anG`(_d}8Tl;ZBv$RBWWww~w)$2Yd@7 z_c;m+IZ3N#K;fg>!bq2EevqL-hmqql<)@^b^pk5OopkWt?{`g*u}(=-4rAM9fTMom zYT-#Mpr>#;gIgU2B~yw{+F4r$e7L?Ablr4DTP2-w7@d@9N=Vx2g7nJWATcDVrIr97 z(`GW^Fg`v*g3v4JvV-3h1@r|}5&&cxj$D|Ov@SvC2jt%*^>jHlDp_oc&q9*)o}_DI zbi7&a_MHMYm20cgg?9ujdP3$uIBMrt%KcYY!1hY)RYPENz~VVc?@Q{4!11M$b}6`3 zc2lKxt0l8u?gwK5SxFyCdR(UAx!QG_Bt0SNBLz20Us0L&ye+k~%P+^Y%P*d0-WaCB zdI*nEY)`LrO8PWli-wK;lI{+Jd)N=EE}8q))nHjG(lXDL6=8CY z>a)2vOhZy1=F^&Tx3!HdCXKZl)B5&%DC|6lDga*0#k8gLU~2Onz|DDmDnCb}mIfbIlD52T<8cTv2C9zZ}(qTp5+8n;F_jyq#A&jmHwnG9g8 zq>@fo{a?S*zcZC3(E>Vgeg{-_K)w7s9uCsEgq#q_J{QXRY qdZQj%`AiEL_0a^n$-BSRHGKox;$`MEds_kk0000*J*fESss|6o7;ioZU_zou zR1kt7`T`h@CaejVU0=Y>{5(uITieXS?CcOKsr00#tG=(RyZTQ7&|XSM2A@Nc&PX~e z>6o*XC2dGrkyLFd8jwc2fHB}BP@}ojfLY+K^Vu=>P8~Xco51fTfa^|^qzsG#rK|)F z0`G#LbHF{|1hCKa?R2*L#96ZP7U*u3z;WQ43GxXT*lr|&4gyQYe--FWOQ1Ude+d*@ z0QXt|o&b>{u_Y5pbMDg~0V5fJLmf3FW(d|q*!h+j`O_T0y*@W_8wM4YnqTdU8PncO zi@>srUB;BC?KEaswzA;T>X3KB!kb+r@Y=N{NvAD=qppo4{R*7=BuzzLPt#13kj9WEpyzBa8Ir*^MoUQ%7jy!HkpmWj{vwBW-EJTEMymjfV)NmvQacfU*(c)Fq&ZG!ABEcK!_S2 zj08{?50tyQoAH0DWkz3ehXXf|eOk>ZP(>*;iec6qFQV(5I zU8kz1`t&(HhpQQn#QW#?bFbC)crU320&A@m|?jPV3a11yO z>;t|8{!Y@kTtaiet-xYnAuxmbGpz{x0BkDJAF){Hhom^EThca3dBu;sq^**=;eM9JV+52#f<>29^QM5%0+ZzhIv23ngAFFcG+&=<`w@cneq#oR4@< z1sVy!EZ}|M&MI=JfSte|U_a0gTnUgJ155(CfO)`tpskAjeqaIceI%=G)R{WZNg51E z(SoEck{*-P+(3$sD(To#(iwySNzc|ZMy)sGB)t-1NKw*yNs}9JI*6u7+E5xJ2&<9| zKu*%zA%h>3bYB9F+jPIALm_soODX`bh79_Sq%kQt@6l*UuPHbiR>mEGWdQ>XNP03w z$8B1q;JIjNTmhIJFvxjHJsCRh(L<6hIP4pcG_%nFjFZ$KpnNzu?+=3OPdXu~wUGd< zaVRY++q4g&MGjjf>5X~=&?#xip>Xvua5jkEbl5v2>7IH5@VP_T0i`?J*Gest4m<4r ztkwW@Ig}KX&U?O5dO+^iqto`FefoXU>ax#j#6HT$pd#_HfLAi%79)fx1?JcInIMAt8KG*qku)Wpeq0i9lUpAFe+`h zY+hh9>_=b%aE9{0oWi6|K2GaWz-Zt%;3glh0NjWvzyg&S@SuaYlac2C=7@6&o4^y$BDDIsg8J(@3fgT!%3V;czoGHoUU z4&@hyNf72sy6CX)vO@X-Y6$=`4M#2vN?M$u^Suggl6oeJj#>s=6)=z_y(j6~6diAo zyM4#Qim1A39e7>HpoeAN!AUzmNA6dhhqblntH*$qA%hnsZIslWfaB97ZBlrv?220T z)@#gCxhF;s1xX)BdR(UAx!!R`OL{`4iWAl>eOVEod7Bz(m!FAgm!DKc?hNLHc`v5m zasn6(kZl3(!c=m4fQPBv(%R_5ockY)WVMAxKbU9*UILyc`iBk$Orhco{~f%H#W;E! z(LcNx0@jwsk8*(HMg!^Tr%p+qhNNiR*dbH!Y}od=C#u;p&#SA!vQZ4l%qz3vv^lBI z=Grg~NeeNb)$T|R-BkPZ+GsnNdq7Q_ox_Kf10000N&PEET=&YxUV~9oX+sn3IERG^95Axf2 zS(JIF@qXcI%E-`}!O`?x@V&BxgwCS$fDdd<6C2%_xi+P4u)WaJdr~Uu!=$SEtG;_Z zg?P_wFn=`f!>`3gp0cr%7OT`w+8+2t_4-xY$zG1_(lU>k(-+5Hzi~jvb8V~dori@Auac$aZOKo4Eq2fQbCJS6#vRx8X|B`@Oagk0 N!PC{xWt~$(695jSekK3_ literal 0 HcmV?d00001 diff --git a/resources/img/clipboarddata-dark-32.png b/resources/img/clipboarddata-dark-32.png new file mode 100644 index 0000000000000000000000000000000000000000..ce74129e34a35d1fac5b61653290636d0505b41c GIT binary patch literal 537 zcmV+!0_OdRP)wU#~Sa2C(;67TT4mK_mw?Q%q{#3)u9_+?wI z>{*V8OLhD@7aYMyTuJjC9K#k~v{>LVPGoyv88`8#s|D_6H+T>cHzHyf-7$=a>k+Y# zt=$=SR$UBSP5lO5rSUyH(H@BFc!p1@J;fl7$Wx09{CNo`VWS7eUh&=dmq z6ukny0+W137jZnBuVFKdPb!|^62I_uFBQ12_~CMF;d~l@Q@nmT=CRl&_`tYe5g+V! zLHXTYFr^C)j0O4_=wqOdfqjdCIt9v1D9>V=1aA8GFH|w8$Q(hP3zkxU8jl(dZo`5i zAB4~22wl_i4K+7tz#uz(MUj^8E>I>cOs2t?n4&PrEpbbcQV*JlW3a6-`+MUGl7z0s b>AvAFdEu-LlkUDV00000NkvXXu0mjf+g;}l literal 0 HcmV?d00001 diff --git a/resources/img/clipboarddata-dark-64.png b/resources/img/clipboarddata-dark-64.png new file mode 100644 index 0000000000000000000000000000000000000000..37e72455de768ab06879285ddf7d59647fefca43 GIT binary patch literal 969 zcmV;)12+7LP)#h(?3P#{U3oVob0yBBGVX_!rn{ z;tvv$h@B-`P#7g@#00^@L=m*$0H4LI$+^35JG*b495f{0Mh zG`M!aHH|sz7lG!C8neKXf-kY;6@V_F2iOm+ha7z1tqgBi7=h=}&*mgL7xZGQ`* zn*_Unhnj-R*m$fZrkMl_zy(FU<>(1)sgQL9v;yaW6Tmj2IpqiNGK&GAL^P2u0-t~< zz#ZUyYW0+Jrzj%VMWkG1&WU{eYkVT~ko0hfVZ8*$ql&D+j)+uL8|PhyPT}bI*W%z&YR` zu*GUfMwtd)0e6A#G504iE5Kix0$*p6h}#4puKHRogMI^UTWDt?&WFU6V%@(F@Bwh` zfNOjJTszcB z@O6Nu7`_hhbwJJG>i{jG0AB~->wwzA*8!Sh_&UJX0X2uO1GIz!d>z0lt^+dd#mNzy zy-N{n0x(N-?q%Hpra?>SdI95WB5VTi8<+^SwE>4LG_>gec8A&~Fuo?jb`=9tS?#h-ILK=r0du`0vH$07cR*r4^rn9Jaw~(J%ku&{Pp~FzTf3 z7jtG1ID_SZi8Phq5^x$AzzjLpECAO?(Rt80G}Vo%;IN&PEET=%J^JV~9oX-OC&GS{y}=e!L%b zQGhj&i}kIUS7*yHZ;d0u7c#fru4L5eblTL)z97)Mg0Yk9Y|slccjMnTJ-+=uRQB(r z%H*BT=T$#XTQ>EGijV0L{=y*j8F>Z~M-QpUntbx-z4oEw>%`5XcNPbyZ|KOadBnK= zXpW?L#Ubgk4I!zj6PHY0GXII~$-+rxle%jzdmBxDvLeH7QjW?>o?V{nPS@?Y<#}#W z%(N#LZ-h>|ta8R8btoKQjs;jte6~h#)CMS_BnAE{s61wl1PY|3SO9x|3S83;GKJcLjl9 zp}TTbiwX=}&9>5|jf9|nTD&`Ux%0+(jZ=d-2OfO6?{Lm}_s93WS5oOF%3XCoVt_KR z0IUE*ZL6s5Ug!OTsdjaaG6Q{0>c7IV-UGb=y#T!cCo8}>@DX?dlv;2f0iS`1Ldpeu z_QR;}KUYiYg=lO#@Mfd2$3?V@1{hS|Cm}|Y0I~U+dZvhW$;JITsV)YO&)s+I~s8=HWoAvSjXgkDTHG2h4a1FRufS0Fv;B{;|;mHW_9jJGu zOaH)YEULfNi?kDdBI{f~$$mPvI%GZwJOGA)D)9p*N9BfEYiKc@!!PxA?3wp2xCqPv u*U9>SC$LR6yHCm9EeFU8@L$pOQ}7!}hll!Nuau(z0000#i~a29eb)c|?{~lZ zUGLhfK@vYQBfJH86Akbd;EksN(}B~#6=0>8hz6%ga5V4_i3tFGz!Knoy@hyjfW5~1I8G!(REiroCKo*(Ex7&-b4eu1$Yw;@D|`rG{9ScH_-rZ0p3Idyajj@ z4e%D=O*Ft;fH%26`Kc4tFc}MWgGC=h}CL>)p>vqz^f{2 z1u5Hroxpn|UabkC&i4U3fUcU<8I)~62e1ryYrMA!xB!gIsj-)E=eGlw2cfTmS`BCd zn!gN` z#Aq{L(pzJ;m!u9O_AHRwBWa^if48JgBln@C!;&Tz>06P`ugd7iKvo)U=SljnK-&<# zlC~Ii=Sb?$Y1=7jyV15P16sd`D8jBljjWN7Md*`9(&{Se>y@-9R4eNrBIkh3M*Icv?||3ufD^#Ioatsha2uGAlh+Gu1uo{~ z{R-^k`=#*=V5X6K2WT;?1e_Ts>7(Ihn+CTG%^L}MpL~&Hn<~Gy{gS3y`Jp0<4RJRI zDJ*YJ=#jLcq;s4$>i=%#hSmYr71!OIAz74mUT21<5EQ z)>E^bO4@Z=wN# zc&8C-9!eo4V_k54uo6QCJU3#kz_?l!)saSEn-PCzC5JAD8%Ru;l8WCYS-$=a49CaK zegO^wt5fkV;Af28rCDZh@KXo4^XN4RnAya0h^jx~X>5LvV;ZXC)K0WuB)Kd)RwxRM$~!V zwV=+aFY0pP{6KwFYoo#Uiyyt@X0wg}-EY0i#EoDb2a}-o)ktniJxT4F`pLw_m{>=Z-h$$ U=P?no{Qv*}07*qoM6N<$f-X3ke*gdg literal 0 HcmV?d00001 diff --git a/resources/img/eye-dark-32.png b/resources/img/eye-dark-32.png new file mode 100644 index 0000000000000000000000000000000000000000..6aceb841e51236f5f8c081cc34fdc68b140eaafd GIT binary patch literal 729 zcmV;~0w(>5P) zK~z|U?Uu_+RZ$d&zpF_iW>HdB(1TuH!$$rEH7OcokPw8C&>(0SGzuDo5EQ+rQS_jo zku-~HVo}3pYDG{~gDeV*G)vR^8r*H}d%WkGQrrf2!Gg2b{;josd+l}BIT@8wN<~St zmkE>!{0{`?%+jwKXaH)%`U~(D82wiS>VfURR-gq~4OAvDKY*9OBj65j3mBL-vRX_x zs#nz!XD;8=%W6}x9GR3!SO;_h2NG3!0(1ktz$;)V*yn|+Gy|J~Y~(WzTmX)cyQ*m` z(5OC*PV!kjsjiJMmFmu5?^JiIRnh)>^^`go#d#d;dHIsbH>n>Zrz`4`1ZI(Xw?N5z z)NBIVpk9kG18Q>`fmQ0e031>GrrcXs6!5kBJX-gv)dhSyob021b!C!3g?c{##?@UZ z$+Jd_eJ(SDZV?@v4D9Pa{eaJ9tAj*5&*R=!YxSb^#%L+BL0UX|8}+DBrG-^ z$D-$&=s6Rur*?WiuHaixq27zGpe^MD%cC-Osq@tu^>}o$))e2?$oE0=3NY6g{Q)py zu5k(lIvpkZ83k4CcCs&G-LH-Y%o}yZ#9k8EZ0=Q=kn8Hw1g2U&6OzYrQ>{;6m#H@* z%m;JRHFXY543WObX)q*gOyIKNWF1Z&w@vCvnBA?q~!mEWddaaB`5F;Ga})TD__ev00000 LNkvXXu0mjfg_S?R literal 0 HcmV?d00001 diff --git a/resources/img/eye-dark-64.png b/resources/img/eye-dark-64.png new file mode 100644 index 0000000000000000000000000000000000000000..539fe6fc52d3e9f1e0e9723d2d39abb569a19e37 GIT binary patch literal 1448 zcmV;Z1y}lsP)l&KtQrPsOYE9yTMhd#AKJ;w^iQOOrD4tN+?O$cOJu@Cqe*aQ4c2zp0L&nB0+ zrUI7$R{%3|a329y0qcSO9Q|@T;A&tCL0}Rj1h1`x0Jf7b=scJTv;nseVoGa|C0{(V*U`ewm`FLmfX-2TV{uPB5}hU98rn8B$x+T`8hfPpE55 zcV?ZsMBSgjwJpsFX>aT81oD^ENf`!?dY#&nVerH8mg!DFovf});CVBNC&{~;VhD51 zF~j`+E7#D6@LUc%_o~NYc($nZQT^h`*2WNZs`GM)!te&7(5=3%ZZLj6_Zy~8R$HxV zR}9zctPWVH4nzPMep<@C>Lor#WD<`bR|P94X#Jx%~%TIx|5%(WrSQ#Ko zMQ9{dqo{p(T1_D8d0&P0>(UwwA6!2nnJCB8!2AjYbVl53PtkK*#Ql90*uDvPD&qdB z2~*j;nP}M0m!n6$s)BjAB!X+NA(f75zapZ2X9ad!)Wb1ci9rym9{tu~{Van)5(ed37f)Qt(Em*)^QPW>cdkZ~AL+jH1itR9cy+HB0;BZ;uVkdVZo zLv6|^u37yo$Kd}}mu2LhVsg6vh7}}3gV%UNN*RX(hQugKIy_!|*7&zHh;L2bEMZ7) z?lXGZJBSY)K{`BKct?upuhhF73TjE$`RXI;`|57Ps_?nGUR_|iGaJ>r)vr@Dy=<6% ziF&fmFAPgsUy9K#L(t1o;>vqnsID{=VUlp%5Dzl(<}Hf00h@uA3~hhp)S~F&JcfW~ zhKi!?gwv%o>?9OLcV#tFNm;xRcmQ~&Br+=w5X$0vfo{S%U@u|2+Elul31#uQz;X0an+R{eVpQ{R zv>sRp+)hYnW>t&0ggu0q@-DEQDjlC!(*fm}LkM6kz?G%nlM;Ij;V9vFd^aJWZ3BKC z1&yQXfYWRN7ZHji(+KB*O{HhOgh3x6oc2bg(W4{k3_9R{i822_LNx~zEud)B98k1? zqET}|(E^G_%>hLVC>k{f6fK}=)ErQ>fTA(x9q=DrB$(i8eJVZx0000N&PEETXp^UlV~9oX-ART*jDZ3!_q94Z z&WLh|bSiVmIH(orJ`uWJz$n=1;jm$*(1Lj>;kmWl{wr2n{jfESHMX@6)Lg)9QrXYE z;hel@LIF$Gfv2v;%GvMiM7Ev?k!6WGx3Mbhx2x^71v;jac2i>7Bn`F%+;ZJMiwhsF$SlBkw;851J;M j9IC&-w(WrKwFi7lzKXBu_^Eyb=z0cES3j3^P6=7|`3_R$?I%Vqu}1n__0k7BhEWW_Bd74_wY2&N*}b_q`TEaG#ov_?lQpy38GiGsvHC)CL$^@9gM=T;>Q?mK`CY7aKJWKlr zI36UaOn~R9y~UeBlgb3xmTb0$Wb`vt1AN6!+F!xDssUi%zqNmfS5*T9Y~eKhKE#t) zLhLH;YF^Gg9?@&bk!;`y%Q(*pj6FteEDTvVPhapN%Q23tI7@eVjzN~-y&=wZhC29J z1mFVS3jjn_)e=kqCIAzF3Bdme@H@5F;{=F3$4xxN+a8!o0V>g7n3kaFVh@I400000 LNkvXXu0mjfZE?Sa literal 0 HcmV?d00001 diff --git a/resources/img/flag-dark-64.png b/resources/img/flag-dark-64.png new file mode 100644 index 0000000000000000000000000000000000000000..25f44d48e800b5d5ca4d9f06106c9dee2f3e19bc GIT binary patch literal 766 zcmVtz@!w!+HX%eb3JM*af=F~?3j~FT$dYWM-HzBwBuGR;B@)F}#CJ%1 zbt;{Lf&|%6Xe2g1i7ruln+A9M3aThB}^+_DWo6v=OK|lwWvA*KI&c4ErkT zw{fQF;<*dhh7%R_S2!#n^vdKcV2wYA-cEs`S0+~h{kUfiLa$7Y0%i-n5e-AHOpXGE zREOS8oQ}O&jskX6)IN$7dS!AH@VcV5QgQa9lcRuB7^|qC$ClWejy{)+K z0=72YAa?;2p}5z~Q0&7s4MVXP1Tcr8*ee2ePjRvC zf}Mb?!;ckTROH44Hse{Hrd~P%e`7I*>o(0N0R&vHoKFJ2;#}RPSRpv9J0bWdCKTE9 zp>AU=0i(KeX)HSP=?DbT3P?RbYFYuQ2S`mTAoT#LX$7PnAT_Ok)B~iZ6_9#>)U*Op z50IKxKu2Bz!dNuxSF6g<)b=Mhw7I4 zNTmUh+Kq4iI)FKKT^*?7v31n3=#_ewj&7F#mHH*JWwy1t*OiEeq^bzk7vY@`mh6>0VCi^!R<2eHLM>4yFf3#A<)e>1~uxs z`YSV|?x}B!s_)d@f{W8^h)z(agWP%)K-JL4(m6HzL$y~_?5TbAeWb5GE&MODt=SGR z8PsE7B2^B6=fM5QGvKi3;wZ6GHR5(wbDmXQx5mwMqxwm$O6Sdx`8Pz>hg9nmDX^;E zYr90$g`juU6)(^DB%C~s?^c=9*5*zE2kM&oCL~{|ADZ!%|NqVvFb52PpRwHqZUEn! a!T$h+L1M_S`2}JC0000T_x%l39q>pRUz`ZE7nRI}~U;)!*!dC3PamOh@$5wKy;u8QLDv zcc>qPC|prLR`*2wp-AuHsy*t8a7(mqMe9?*7|>5v$crVtbX{qn4!{r)SubxRL zoUO}Y4eqt`da52*dsTYOI^hIxGz8^E;B`RY!YZlT22 zmjitgIGTI9KX~>>7pHn!{WWo_hWb<&`nm1_b){No!}Gcjjn&{8;0<6Xye|X<*bKZ0 z?5ofp1+EY42Cz~C8`K|zXG<3EbdH-M@6W2cBHsP#`6vn;+mniQcDP3cUR>Z(S)z z1L3(0{9WR$<-?(ucGLT_XtnLlcnFW`J2fe|H^JW(@gGa@X9BNbmhM6dhN2KP)jJag z2BHw3Q+KO90UuDmjZ(BRp})thz*T3}YhwqV4~%8Aaa+{`^6{)$HOKS6rA}4o$L)D4 zcHmGT{!*cw<5Z9c!ISjl4yiGRcz6u2y!;!#`Tk#p-BjEBX zMK~1Xo+WGJ1lubS=v6;UoNBI%Q&r)l?b?Xpq}kOh+=5}VQQ1l;O!)6NpGw$K{Y1Sf zm&>PN3^#|64FazLw*%*Z+g7{e30Mz&58McR4ICt!tWDrVL9*r)`y$UbC;0t&C`-6K z(i^YZqudG0a4A~H--HhWACgVN3uHebBN0EgTmFG5LQMqjiB2+YyReq*gr@CJ@WX-D ziu0YqZi?x6w8_P2J#W^)Ub5Z7uUh;`d9TiXHx6u7PnAylgT;0|R&q|WzoIR5_EWbY z1%1Fc*|*&+@D}h_NoEjulI#YZ2HvG}qIN*H4*X~E4?jh+gjD&w;s5{u07*qoM6N<$ Ef?-zYivR!s literal 0 HcmV?d00001 diff --git a/resources/img/gear-dark-64.png b/resources/img/gear-dark-64.png new file mode 100644 index 0000000000000000000000000000000000000000..743040e80229ca11a1d5024ab50333083da3fd99 GIT binary patch literal 2185 zcmV;42zK|0P)2l|+76Y%P!%iKLE0HB@ezDbz{D5$06!4rA*5I}2mws6l^EoM z-;60iVlYvOijsg$)1Y863f3~TJR%iPvC>j}6=*xmv>3*D0KI=dtaIBl`>u1&KKGoN zYV@D%WX_(w)?RC!eb!og?R`5W@xP4DBQ3y@#&jBB)->|24RssfHNaPa-vIvvUIF$3 zgTQ6L>_|QLP^gnA5ThJmcwJdq}vL33`ts5m)vzJ zzfw}8K=3ApGmJ1#($+{Gl63uliUH1)G+9D$N!lgp#6bCPW`) zNZL^H?Ml*4N&N|rPDvL_8gs^7P(_ET2Ka&_(6^j%-IDGrq5QLw`W!x2J7aIIqC?d% z_&TR=v(q;ToCj=(;Q1u*VeIn>vC-)}K7wZ^sv2O_>6?=?_I%6`o3QUYjXwcj#{9`s z89cl^Vxm~gY*fwGUUvGj4A8{SlGX$C0ds-fm_sYVJg4ua2)ayEHNcrp-+@Tle__kL z491GyX1M zq|zJ7hcHKaSwH?4cgJE<(%Yx;?Kj&=lEzEO^{AvX zviM~!v#%*Ww{PO|8Bo62R+W@(LK&Zl$NcjpnU71lbcPg9w<}a`57LnIsUidPN_ruZ z;jpCd%Fozls=Y(fU5a0=&q`X5<`Hg&t_9WxZelFyzX#~TiXNlDt~Q)lh-S{<i z0@q+Mx(MCC>V#&X;@l13??GP|DV;(C><^y%Fa_Gha^NB0Si~a_-N26>+MQFLf^X(D`5|Su4p(Pl+f5nY z_%=yrOX^CE?UM8nNsndF+)#4vqa@v%!Rxc>u^#b{G6HZw77%;vPy7&2zBs?=S$j>!QjeB0oFLQ+X|FV8{u(>{!5W_pPM23kff8t@mWmf z$}dwt5-G%>Lwj~4Es{R!(BB_PyV0TDC26TQKCdi3A9x0MecEMUp0%h9Jp?RDJr5^* z4i?Zg(UtH>m(z#vYmjMlV-=!w>1+&(MqxMv@@s-)*qL4$9l+uOA?5(b1${e-SDs|- z(?Q=Nipy6(-d0u+mJ+0pfMD}Sn_mrKaCN75&=_{Ch59Kho7B5XT6iCvAkh}IB! z3vhW^4^|RC2hMf+2eGi(gN3?1z!vOgnRm6uP~lBn;#hlFhc8G@M>|ff{^_9XlWmJ< zl_cl(#KLLxVpmtE3#VZrFO7A;9qlr#n&o*#ip=UufFA7Ft&g}fL-6k$Z;hmNQPlto zoWAVu%@Uuhv8v4|X28F2_~P_A1@n3J$XMgKsA>T3;$l`-H-Mcz-VD4e_}^?y`HjGO z{9-Kl<+#`t=c1|syaK7`q5FVe18Abq+uH#2jX&eiz0^Se22g?ceV{!lc zv|hpSFblX8SP3jkc)p4iU^W5Y#;)4eg1QaxUx)wyc=|{SaHR2Xivq{)s%KQd00000 LNkvXXu0mjf#X>I= literal 0 HcmV?d00001 diff --git a/resources/img/gear-16.png b/resources/img/gear-light-16.png similarity index 100% rename from resources/img/gear-16.png rename to resources/img/gear-light-16.png diff --git a/resources/img/gear-32.png b/resources/img/gear-light-32.png similarity index 100% rename from resources/img/gear-32.png rename to resources/img/gear-light-32.png diff --git a/resources/img/gear-64.png b/resources/img/gear-light-64.png similarity index 100% rename from resources/img/gear-64.png rename to resources/img/gear-light-64.png diff --git a/resources/img/reply-dark-16.png b/resources/img/reply-dark-16.png new file mode 100644 index 0000000000000000000000000000000000000000..d500c5478de5c5edfe50ece7cf30e1b01e7b35b7 GIT binary patch literal 280 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#X#yh2s3WX6WN&PEETXsM@*V~9oX*~xqP7y|{|&TrDn z^6K>Sf0*d%Vm$MG`XtYgqzhfQ{;@jl zU$dUW?Eqth*`@`H_^xi=6=t2Pze1>#;Z39U2Ie?!Qz7jMl6_!X` zXLluUt7VY>E3JkFYoEQ+SsQV>!84&peFF2|EoM7RcepZ&cplO3X$Nd1G=hkT za5N)!Wes6=m)Mg$nJ1Yy@A=QnnP;Y!L?_kLQTHxD7vL`dEMc*w1W7)s?YwECWZjSI zE9Qqu2HFX*RcC&jWTaK*%>deJoN{v>zBzUyTn+`vxJPh-4-EWjf!i+&9HN)~V61i@KF0PtKKYo7tY zUWLX@_3Qr!TA|nReoNyZYL})khh5xs0-!Do(-i8*^?MvGh7Q1-JqL&G063R>4WHN>wMJw{+4*HW7lC2pR;fAfz78n zvzDDPKM-znU}0CJkA{{)$RDi_TdL;#(o_O6r3%=7m`rprHD1@C_wbRw@XO`=4NJ~O zh1dO>)Yo5FyEuAv_kEuO)Aa599kjcqKbgx>+IV{2@uOA+>=l3a&-Z5i$yjqHYW<6& z>l$7k;5~1_I9KT5t7YsqOz)W2AKRG_a=>H*)0>iGIVsPR9*KWo*uhnEQhG-yE6>j} z36s9rm?V@aZDBl9eay%B2j`I!bqRtNqEZ206f49dW`*qHG-Q@xNozdZXt?@-;e)64 z>0JBRuB*)5P*?hNi}EBf=9~9DZ#|5gT)sI%)yH!df6D#{7l$)K6H3lc{=It1OeLnI zX&YjLfSjB_Mni9l_b2~s=wVRNWl{N}de&?~MEq^_m&+%YuUyk$!>K)q&tySDeD&j$ zSUKDH65j&89dBRWpS!nxcE|3J?d=EeFlubql9}{j+mXGm67F99UT(bnjZ8h?55AaF zeffe9=P^vVFCl4U@^bSXk$DX2bNB7#Dchp;|8#cI!{mu&?Q0T|=48?0XnubJOgIp8f_$Fx(0@fzbAPbOTkU(e8d XnroKlSG#Axq{`sw>gTe~DWM4f+}TR` literal 0 HcmV?d00001 diff --git a/resources/img/reply-16.png b/resources/img/reply-light-16.png similarity index 100% rename from resources/img/reply-16.png rename to resources/img/reply-light-16.png diff --git a/resources/img/reply-32.png b/resources/img/reply-light-32.png similarity index 100% rename from resources/img/reply-32.png rename to resources/img/reply-light-32.png diff --git a/resources/img/reply-64.png b/resources/img/reply-light-64.png similarity index 100% rename from resources/img/reply-64.png rename to resources/img/reply-light-64.png diff --git a/resources/img/settings-dark-16.png b/resources/img/settings-dark-16.png new file mode 100644 index 0000000000000000000000000000000000000000..26be45149678e46961bbc750e3142f4cf8fe3641 GIT binary patch literal 418 zcmV;T0bTxyP))@z0T~UCD%?P;ii15*j7{1BaSiT10Rb2mb-V-NCUX99)g&{(**u zra%{mC>lf%QBbsd?Q!T0uZMfRk`Fxap7Z1VzQ6Z5zf&b~A2n*v6IjL|-r;*cK-JN~ z5O(nkExg1OPWk~Ru~$xohd9R}25{M9Zws#*_w1KgEC$MY z5|7cm>lHpGxhVe7ErXRL`$>K#nePIql1vv@owX{-6uw~*Pq2=+WfpGJ1wL0vX0eT@ zCCDQj<4+$z8?UM)BPH1b3>Wf@-V=C3|NezBEcp9*w)C0*Zh-&R72j?VnDavd00000 M07*qoM6N<$f-B#!w*UYD literal 0 HcmV?d00001 diff --git a/resources/img/settings-dark-32.png b/resources/img/settings-dark-32.png new file mode 100644 index 0000000000000000000000000000000000000000..c9a7650e26663a4eb46f8eb9ef675897e35deaeb GIT binary patch literal 792 zcmV+z1LypSP)Ar)B_8fiDr2j{_HhZ=L~#r~*2H7T_=N z7{~%UftkP$T-b&p8jwSg1X(4iH-G_ylD5Rff-nzwmOT5$!a8@K`t7P+$lSYz@V zAQjaE|2yg>-3##89Z7Xz_u@3*kx%I#Non8wK{+940p^s-nGh@ku9|!aIE7=@d7$0a zw*qfX9>-C7(gOzqTo(ny%I_0NNh1O0(vo&anrz2IxS}&vWN@_gkhM{WB+v-Cc6=XL z1k8@8xD~hs)B{I>%eLN*V-mQ+)?@N8@C1l02@{fD`&|4<(r4fNm7S?53mPSL1;jfg zsUhrMVIkUXuJT?$9l%N8Lz$sS0(*hAHtz=RQtTDwv*x&5pD)Cyq-`;JD$oPVfp@?x z;3IGys0EH#VtoZx;&C@dcr4g$%141sWT);g>;=nK929WU#SZd!M@*B-d zK)9-QI40#DMu6L%0e$vu0_w~{a9u57@^A8s%v3Od*Cj0(nk8i|>AEEClXOT@zpV{O zs;xv4`X}cC7$8X*NqZ{Ag2IJ1xhf_(N~RRk^Scxc3sGb7PRA7UP?UO-RUFg)cl-~U W+_Hl#0>>r*0000`Vu)0cHaAz&_w>U^|fQSyn{K zNv$HXSwu#~O}~gdAR@J8wv$*Yr}ASV1Gm{GB28%z)|ILxS{(2!u)?#>Z@@g@V3Y=v zB8meR1MdP=5!%}fER0YuF?=0R3;YP2W#x|oZv!6ze*+f)4+6~&8#e%-c=t3ZJl~m{ z9PjrL5ovRjohBk%9pyKr{oaSo0gb>Lz_mb4i9UQBcs}4-Gw>^Lf{`}_tns@|v9TL? z1^E5H9LpnX9e1o4jYYh?A%e{$`CddONxpw}B-rBV(IdcMkurUrHj}0Wm^=2vcED$z z#_EAnijwD}M^mg-)m@-9V>LqcO4v7%($@9dJDGGceoA9|m4WEz^}hq=qdGyh zkn%#w`6ALC5_B98kw-JnZck zkp&X43LFRgLwu)2aquk~2ZYfBT6wU!7sG^%Cj+Rj7u&2!?^kxHv4#=S~ z{;*vmMdko?A=m4G^F3{Lp~5WSjss=@=Xe^8i)fBv^+kPNzGr^`UZF6}mBUmDz_)^2-UUVnd7e}WmiOT^|XW^fK%Sir7h!uMkmp=bkeHhMTZ7q|X)KiNAC4#!#+esL@_4+ktE z^&-;aDE~-G%Vycj;AyC0V$utoOK?!qhW|GOX>kZWOSlWzKbC2}07Yb7;8np85mD|Y5p+hfNC_tYO*S{ zqo*`B)u>!;fBD%*#y*G5lS5zWm6%eSH7mMQ;X@*_L`15Lawmz%TaL0h5ot+#W{p(Wq?jPmfC>^Fz4-0AB0XOP!?7HvpIn^xwu>?mtK{O2KcSNu(#>E$OfvVguA& z35+D|7EV&E)h8@Bu=+r`1%P+pV4#Kl;H0vDAIg=wSbMS=3-vOeDLTIM6xi*#!E<0G Yzn)uWm_L<@F#rGn07*qoM6N<$f^KV>lK=n! literal 0 HcmV?d00001 diff --git a/resources/img/trash-dark-32.png b/resources/img/trash-dark-32.png new file mode 100644 index 0000000000000000000000000000000000000000..b76e1e2bfff430398ae246360f9adaed4b02d9fd GIT binary patch literal 607 zcmV-l0-*hgP)Ri-2!bC#Y}9XHqiCUx53toB z2sVQF08JD#7CX_+a(B7CI}yP@91~_{XMdTSJ+?_i_)WDWSThYw09}s%88`$EysC*v z+$Sd@o6=Ytnj5RG55SVO$(@K?InSLHFa89;z82pTk=B5C-pbjJTQ}$Nwg%N61MdOx zH{i(0ZKx*sJ_UJT8yJlMXaydC`;eSIpjQEJLUJyESp}{G7zVE5lBd)NP=fknSOVrT z9T*3ufEQpbew(}DuAdC z_~;)X6M!_|0())ZAg$ z)}7qi+KL1GrKoFp8S`y-0ao>~bq zXMuqPn-2nW`uij-$-b1jn!w!Sa+rCz^#th0oPrNvlJ6^fy&Bx-7Dc2i$yFASg{VEE tI#AOvFar#EcHkP=#l8UtC=S4n>Ju4Ta-FDJ+zvwe$lN1VM=iq9Asu?d$|?gsTJ# zC7mE*BcjGa0)hktMF@UGVgkYB5@Xh4HgRw6-R{op&fdk_KU^_ycK-9`f3t7j?9LQC z&!d@9thWR8MoY&1Z3hYw*Lc8O2EG991t% zfk(iRggh^Rlfd`H9tm(Izz*OQa3CeVNtBSYDe*OdV>e(1@5HCw+0u}o=<{pn*~oO@=P$Kt{=Do>@wu+7i?p|Z$q9|!8QOq zHRSmL+(HH5D%5%kbfccVoraw?D&|q~lPU1{~0`}u?$NUwFLO!&X>AD98)a; z#(_~_!I?YRVF5Lkaj7yY6Kn&v1j=85_CR?pN3SPdQkfqt0bPOeFJQS=G*Lvwp&&990OG&r za{*}vHkANU-%bUP5~IyA+I0ixseHjv3xoc(-7&7sdbwT!Lg zw~1}mL0vN4d zvX}ub(GWABsRXbIf-bQkf}l&}w}4>F1u!AV1u*O|7eKHz)nX{00WqD(3b_D+Ef>Iq zAQ!-}!+b3e(}}E*`3zX1de>%@0Qx`T`8*Q0{-HAJ-_-MfRYJZ4N-!0`quQw`0miiQ z6Xdrx3w~W#TOE4eWumv?RlsJO+#;24f};c|p|-f40Nw*PL#{o8dM|hgwavB^!JX)< z5ZG;$oC5X*%G1CeQ<9W0P30b@DWJ!Mz~-}-u5u%-1&n7v8Ui%ZQ=l#aw37Y73;aIy zelrn3is1E_1&MP2uoGZLuyqMG*O0y)O1_1{MgVCK??nrqq!N&PEET=#{67V~9oX-OJWq&VeGW5B1eD zSvOvAik5HX3Nv$Zd%&UdhV|&VOP4Mwazwr4So4M@H}}djKLsJCTbiep%q8?X*)IP) z{O~}X&bOQ0!QM$iQcrDri*pwL^FFWsZbQ3Ye1g394PEZKMSa0CkCy0fXsEu^WBHR$ zEHUd$+r%%d_YA{Nnie#An`rN>+|#HsX^rYjm6vLkewRForsl7kY%*D9s_9*o*`8O} za^@VhnpfDSyXOJ(>WTx5VIMlmO4--%xVpJSQdi_@=F6QX!PmC1-;aFCDLpm9P-U6b r!TCSfnGR30@K~7s*duFl-E;jbhvOw9pNY2tea+zM>gTe~DWM4fCnbvk literal 0 HcmV?d00001 diff --git a/resources/img/upload-dark-32.png b/resources/img/upload-dark-32.png new file mode 100644 index 0000000000000000000000000000000000000000..06556e1c58809836ad97d3ac04fa39da41e79091 GIT binary patch literal 547 zcmV+;0^I$HP)>naHao$zv=N zMM&Z^xcg+;&gEVmuEF_g)jE5xv(|Uk`S#xDBqWhb2S5vO3(Ny|5p|##7z8STOQ0dF1C^3q9n&GNGf`Nd z=|p`nZbeuFB%FU9(UV)88NW7ga=r>;0+zsw@$)mdw*pvpmdqgD=6tVZR9YzM$*FWk zQsN6mS4i41s@{{7{ALwQxY$=4>j%x5q}P%zB&|v+`l5J%U5`Y8qy?Ab%ET?%nm&P~ zq|K1zO>Vwi(ssyvfCEWYF1AkZ`$~WOeXx^V&YG)VUF$S<fVM;NY?Z z@JNhANuXINrH`AjFak&#b+N5c5(u+&1hTS!wG+k!VgfONm_W98N#kdQuwS!GaRjUY l&%o(VlZ1T_{3&v-fj0)V=!}*f2qFLg002ovPDHLkV1gZZ>B#^9 literal 0 HcmV?d00001 diff --git a/resources/img/upload-dark-64.png b/resources/img/upload-dark-64.png new file mode 100644 index 0000000000000000000000000000000000000000..3314ff14bf129f2adf3bafd59af3665aaed34c1a GIT binary patch literal 1039 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9GUaUjfi{$NBOP*AeO zHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IX1_tIio-U3d6?5Ls^34bd7dc)( z*(}LxYnh@L)7H)@Y^9+YVO`6*O{R$Hc4@DDy)7g#z`LbPwB=f>>g+Pb6(UhDoVaqQ z3TVV$>CIRev^tXW?*bDcCByrf|6})X&a5ncH{0A;-%|KfU4i-YyVdu0{;$3__qQRd z=bF^0J zWuID>%KgM(0ZS|vB@tn_? zD%cNS)z+G<+uq;hu5sV;nB|85mOA-UC;nR< zPW!4kgRx$En{8=(@VU5C*8Cghy%1S$ez^R|&(mupZv<@doMmDrlCHAVWJBOh-ONKD zcAw~3VlH)uM|SEKfzQ9^FDYAHcd7A&=!1^L|I9niUn;BM`603+Fg2vAAzxFzvRm>} zEuVD*f5UUHoZB1x*Vi$`neJ85xT*QF@KDB7Bh{Bj+1%{>w)#|rT{=8v-!s+H_L@-d z<;|;nmJ7URt@(1|-<(y3x-YjjoHx(@@VV}QyB6bwFS{b>psuJ{HTo3ZJX|)yj0ZG|i;e zyI4PAW9L%qCd~x1Z#Cv0Y(H$d_^tSX`2(jjbs`m=FU}aAlPPG(5Yk<0`mL`p%XYQT zOvbHKmTdX&6SUBxs_>rPHhGRMytRD1Q`byCw7cQ;4P|?dJP(#=Q*28hx&&4fSYm#oFO7(dC1@ztNYw5MYi+W4!dmr;rBqRVB)d-34YAwjm<9RLCn0rw3V|N zN&PEETDBIJ;F~p+x>SRZ*0}4DW*XuSd zSjzu-`B$U8EnzJ;oIhM>n4VZ($gAcs@ut%|QO3}qnJ+eH-edd~+`uNoJbfeElX?A0 zjMfE=&)JKa-I}xKv3?2vrp4KjpXk|oLAmXEgzk~m(|kW~IP%{9GOxvj`5qN9?-GF4 OGI+ZBxvXgIBVn<>t)?h;IVIgWxQc6%`S1VYOl)cn6O;K$*kT;Jb`m>&>3l*_jx`lI)yUrL7FyLL>edn?|ecj!W{ z#H|je=)xC?B6TwD@g70CIhUlB!h5frnwhYthwZ?8<~@uLV~pkPJ@qw#USjZc^>bP0 Hl+XkKZt-_H literal 0 HcmV?d00001 diff --git a/resources/img/x-dark-64.png b/resources/img/x-dark-64.png new file mode 100644 index 0000000000000000000000000000000000000000..00e8c35047a96995ddf82752e0cb44a7ceb390d7 GIT binary patch literal 632 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9GUaUjfi{$NBOP*AeO zHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IX1_mZpPZ!6KiaBp*#ReTR5NWk{ z5xb!I`+;Nl0zQ77$fLX`n%_7dWBpNFYam;%vBh(8wes1SFTX5Qw0xfJeEGJYON-&_ zGq)PHaxr8xU(;e(!yB=hVS`!1TBZXqK`Z`0cSUv-{Wkp|b#~(Xv;(Fe_-uCUo$qp_ ze0JA~sC`=nCt59-zcBjkvAiv1_xAkal&sMFT@}<8u-ou|*2~-BC+Zz;Z;AY1D*CG7 zUrp%ljbggT|12+I_&c-1X4{49JB0Y=Ru;}n+gIBrwbDxAjBv~PC*SAv8$AnfjuM#E zBG2W(?X>BGA;Xfk2_`i<3@a5>_~nHiayYaeCNp=iI_lPNF*qx(X!B=P5NWFVF@>Q< zaYloitB0EJiC%bdql97U>3_?&bT5)W5bZv@vu&c=EC#Iyd^;>Ir7_-NE*5ov z!T3UW-$bpJi}%?~#hhZhWx7AjI^}gCy)~UpvHim`fg;X)HpS&1)C7vS*YhbJ|FBA+ zh<81k;`0w$0!94uWe(MI%olO$n0Rr&T)brKd*;Nax8qMdE{Jw?-aKh@adK{SU#V#H zH_-w%zgNmDFH{{kS);Z*R^+VT;{QQg&|(HDy8Pp&PcG_s$NmnOY8X6S{an^LB{Ts5 D1Dy(t literal 0 HcmV?d00001 diff --git a/resources/img/x-16.png b/resources/img/x-light-16.png similarity index 100% rename from resources/img/x-16.png rename to resources/img/x-light-16.png diff --git a/resources/img/x-32.png b/resources/img/x-light-32.png similarity index 100% rename from resources/img/x-32.png rename to resources/img/x-light-32.png diff --git a/resources/img/x-64.png b/resources/img/x-light-64.png similarity index 100% rename from resources/img/x-64.png rename to resources/img/x-light-64.png diff --git a/resources/svg2img.ps1 b/resources/svg2img.ps1 index c71f0ff..d1a0760 100644 --- a/resources/svg2img.ps1 +++ b/resources/svg2img.ps1 @@ -1,6 +1,11 @@ $svgDir = "./svg" $outDir = "./img" $sizes = @(16, 32, 64) +$themes = @{ + "light" = "#000000" + "dark" = "#ffffff" +} +$tempSvg = "temp.svg" # Ensure output directory exists if (!(Test-Path -Path $outDir)) { @@ -13,16 +18,48 @@ if (-not (Get-Command "inkscape" -ErrorAction SilentlyContinue)) { exit 1 } -# Process SVGs +# Helper: inject color into tag +function Inject-Color { + param ($original, $color) + $content = Get-Content $original -Raw + + if ($content -match ']*>') { + # Inject color style + $patched = $content -replace ']*?)>', "" + Set-Content -Path $tempSvg -Value $patched + } + else { + throw "Couldn't find tag to patch." + } +} + +# Process each SVG file Get-ChildItem -Path $svgDir -Filter *.svg | ForEach-Object { $svgPath = $_.FullName $baseName = $_.BaseName - foreach ($size in $sizes) { - $outFile = Join-Path $outDir "$baseName-$size.png" - Write-Host "Converting $($_.Name) to $outFile ($size x $size)..." - & inkscape "$svgPath" --export-type=png --export-filename="$outFile" --export-width=$size --export-height=$size + foreach ($theme in $themes.Keys) { + $color = $themes[$theme] + + # Create themed temp SVG + Inject-Color $svgPath $color + + foreach ($size in $sizes) { + $outFile = Join-Path $outDir "$baseName-$theme-$size.png" + Write-Host "Exporting $outFile (color $color)..." + & inkscape $tempSvg ` + --export-type=png ` + --export-filename="$outFile" ` + --export-width=$size ` + --export-height=$size ` + --actions="export-do" + } + } + + # Cleanup + if (Test-Path $tempSvg) { + Remove-Item $tempSvg -Force } } -Write-Host "Conversion complete." +Write-Host "Done generating light/dark themed PNGs." From 7e64a428ac438a94d9b05c4bb8642520fc13028c Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Tue, 8 Jul 2025 01:30:37 -0500 Subject: [PATCH 02/48] Add theme support with dynamic icons --- README.md | 1 + background.js | 113 ++++++++++++++++++++++++------------------- details.js | 6 +++ options/options.html | 39 ++++++++++----- options/options.js | 38 ++++++++++++++- 5 files changed, 132 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index bdff14a..4447c9b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ message meets a specified criterion. - **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page. - **Markdown conversion** – optionally convert HTML bodies to Markdown before sending them to the AI service. - **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service. +- **Light/Dark themes** – automatically match Thunderbird's appearance with optional manual override. - **Automatic rules** – create rules that tag or move new messages based on AI classification. - **Rule ordering** – drag rules to prioritize them and optionally stop processing after a match. - **Context menu** – apply AI rules from the message list or the message display action button. diff --git a/background.js b/background.js index 2916c06..fcccc6b 100644 --- a/background.js +++ b/background.js @@ -27,24 +27,41 @@ let stripUrlParams = false; let altTextImages = false; let collapseWhitespace = false; let TurndownService = null; +let userTheme = 'auto'; +let currentTheme = 'light'; + +function iconPaths(name) { + return { + 16: `resources/img/${name}-${currentTheme}-16.png`, + 32: `resources/img/${name}-${currentTheme}-32.png`, + 64: `resources/img/${name}-${currentTheme}-64.png` + }; +} + +async function detectSystemTheme() { + try { + const t = await browser.theme.getCurrent(); + const scheme = t?.properties?.color_scheme; + if (scheme === 'dark' || scheme === 'light') { + return scheme; + } + const color = t?.colors?.frame || t?.colors?.toolbar; + if (color && /^#/.test(color)) { + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return lum < 0.5 ? 'dark' : 'light'; + } + } catch {} + return 'light'; +} const ICONS = { - logo: "resources/img/logo.png", - circledots: { - 16: "resources/img/circledots-16.png", - 32: "resources/img/circledots-32.png", - 64: "resources/img/circledots-64.png" - }, - circle: { - 16: "resources/img/circle-16.png", - 32: "resources/img/circle-32.png", - 64: "resources/img/circle-64.png" - }, - average: { - 16: "resources/img/average-16.png", - 32: "resources/img/average-32.png", - 64: "resources/img/average-64.png" - } + logo: () => 'resources/img/logo.png', + circledots: () => iconPaths('circledots'), + circle: () => iconPaths('circle'), + average: () => iconPaths('average') }; function setIcon(path) { @@ -57,19 +74,29 @@ function setIcon(path) { } function updateActionIcon() { - let path = ICONS.logo; + let path = ICONS.logo(); if (processing || queuedCount > 0) { - path = ICONS.circledots; + path = ICONS.circledots(); } setIcon(path); } -function showTransientIcon(path, delay = 1500) { +function showTransientIcon(factory, delay = 1500) { clearTimeout(iconTimer); + const path = typeof factory === 'function' ? factory() : factory; setIcon(path); iconTimer = setTimeout(updateActionIcon, delay); } +function refreshMenuIcons() { + browser.menus.update('apply-ai-rules-list', { icons: iconPaths('eye') }); + browser.menus.update('apply-ai-rules-display', { icons: iconPaths('eye') }); + browser.menus.update('clear-ai-cache-list', { icons: iconPaths('trash') }); + browser.menus.update('clear-ai-cache-display', { icons: iconPaths('trash') }); + browser.menus.update('view-ai-reason-list', { icons: iconPaths('clipboarddata') }); + browser.menus.update('view-ai-reason-display', { icons: iconPaths('clipboarddata') }); +} + function byteSize(str) { return new TextEncoder().encode(str || "").length; @@ -286,9 +313,11 @@ async function clearCacheForMessages(idsInput) { } try { - const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "aiRules"]); + const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "aiRules", "theme"]); logger.setDebug(store.debugLogging); await AiClassifier.setConfig(store); + userTheme = store.theme || 'auto'; + currentTheme = userTheme === 'auto' ? await detectSystemTheme() : userTheme; await AiClassifier.init(); htmlToMarkdown = store.htmlToMarkdown === true; stripUrlParams = store.stripUrlParams === true; @@ -341,12 +370,19 @@ async function clearCacheForMessages(idsInput) { collapseWhitespace = changes.collapseWhitespace.newValue === true; logger.aiLog("collapseWhitespace updated from storage change", { debug: true }, collapseWhitespace); } + if (changes.theme) { + userTheme = changes.theme.newValue || 'auto'; + currentTheme = userTheme === 'auto' ? await detectSystemTheme() : userTheme; + updateActionIcon(); + refreshMenuIcons(); + } }); } catch (err) { logger.aiLog("failed to load config", { level: 'error' }, err); } logger.aiLog("background.js loaded – ready to classify", { debug: true }); + updateActionIcon(); if (browser.messageDisplayAction) { browser.messageDisplayAction.setTitle({ title: "Details" }); if (browser.messageDisplayAction.setLabel) { @@ -359,62 +395,39 @@ async function clearCacheForMessages(idsInput) { id: "apply-ai-rules-list", title: "Apply AI Rules", contexts: ["message_list"], - icons: { - 16: "resources/img/eye-16.png", - 32: "resources/img/eye-32.png", - 64: "resources/img/eye-64.png" - } + icons: iconPaths('eye') }); browser.menus.create({ id: "apply-ai-rules-display", title: "Apply AI Rules", contexts: ["message_display_action"], - icons: { - 16: "resources/img/eye-16.png", - 32: "resources/img/eye-32.png", - 64: "resources/img/eye-64.png" - } + icons: iconPaths('eye') }); browser.menus.create({ id: "clear-ai-cache-list", title: "Clear AI Cache", contexts: ["message_list"], - icons: { - 16: "resources/img/trash-16.png", - 32: "resources/img/trash-32.png", - 64: "resources/img/trash-64.png" - } + icons: iconPaths('trash') }); browser.menus.create({ id: "clear-ai-cache-display", title: "Clear AI Cache", contexts: ["message_display_action"], - icons: { - 16: "resources/img/trash-16.png", - 32: "resources/img/trash-32.png", - 64: "resources/img/trash-64.png" - } + icons: iconPaths('trash') }); browser.menus.create({ id: "view-ai-reason-list", title: "View Reasoning", contexts: ["message_list"], - icons: { - 16: "resources/img/clipboarddata-16.png", - 32: "resources/img/clipboarddata-32.png", - 64: "resources/img/clipboarddata-64.png" - } + icons: iconPaths('clipboarddata') }); browser.menus.create({ id: "view-ai-reason-display", title: "View Reasoning", contexts: ["message_display_action"], - icons: { - 16: "resources/img/clipboarddata-16.png", - 32: "resources/img/clipboarddata-32.png", - 64: "resources/img/clipboarddata-64.png" - } + icons: iconPaths('clipboarddata') }); + refreshMenuIcons(); browser.menus.onClicked.addListener(async (info, tab) => { if (info.menuItemId === "apply-ai-rules-list" || info.menuItemId === "apply-ai-rules-display") { diff --git a/details.js b/details.js index f84ebf4..cad1979 100644 --- a/details.js +++ b/details.js @@ -1,4 +1,10 @@ const aiLog = (await import(browser.runtime.getURL("logger.js"))).aiLog; +const storage = (globalThis.messenger ?? browser).storage; +const { theme } = await storage.local.get('theme'); +const mode = (theme || 'auto') === 'auto' + ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') + : theme; +document.documentElement.dataset.theme = mode; const qMid = parseInt(new URLSearchParams(location.search).get("mid"), 10); if (!isNaN(qMid)) { diff --git a/options/options.html b/options/options.html index 0fc5cae..58cfe37 100644 --- a/options/options.html +++ b/options/options.html @@ -37,22 +37,22 @@
- AI Filter Logo + AI Filter Logo
@@ -60,7 +60,7 @@

- + Settings

@@ -94,13 +94,26 @@
+
+ +
+
+ +
+
+
+
@@ -202,7 +215,7 @@ @@ -273,6 +279,14 @@

+ + diff --git a/options/options.js b/options/options.js index ee2914a..ebd7497 100644 --- a/options/options.js +++ b/options/options.js @@ -19,7 +19,9 @@ document.addEventListener('DOMContentLoaded', async () => { 'tokenReduction', 'aiRules', 'aiCache', - 'theme' + 'theme', + 'showDebugTab', + 'lastPayload' ]); const tabButtons = document.querySelectorAll('#main-tabs li'); const tabs = document.querySelectorAll('.tab-content'); @@ -64,6 +66,10 @@ document.addEventListener('DOMContentLoaded', async () => { } await applyTheme(themeSelect.value); + const payloadDisplay = document.getElementById('payload-display'); + if (defaults.lastPayload) { + payloadDisplay.textContent = JSON.stringify(defaults.lastPayload, null, 2); + } themeSelect.addEventListener('change', async () => { markDirty(); await applyTheme(themeSelect.value); @@ -119,6 +125,16 @@ document.addEventListener('DOMContentLoaded', async () => { const tokenReductionToggle = document.getElementById('token-reduction'); tokenReductionToggle.checked = defaults.tokenReduction === true; + const debugTabToggle = document.getElementById('show-debug-tab'); + const debugTabBtn = document.getElementById('debug-tab-button'); + function updateDebugTab() { + const visible = debugTabToggle.checked; + debugTabBtn.classList.toggle('is-hidden', !visible); + } + debugTabToggle.checked = defaults.showDebugTab === true; + debugTabToggle.addEventListener('change', () => { updateDebugTab(); markDirty(); }); + updateDebugTab(); + const aiParams = Object.assign({}, DEFAULT_AI_PARAMS, defaults.aiParams || {}); for (const [key, val] of Object.entries(aiParams)) { @@ -797,8 +813,9 @@ document.addEventListener('DOMContentLoaded', async () => { const altTextImages = altTextToggle.checked; const collapseWhitespace = collapseWhitespaceToggle.checked; const tokenReduction = tokenReductionToggle.checked; + const showDebugTab = debugTabToggle.checked; const theme = themeSelect.value; - await storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging, htmlToMarkdown, stripUrlParams, altTextImages, collapseWhitespace, tokenReduction, aiRules: rules, theme }); + await storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging, htmlToMarkdown, stripUrlParams, altTextImages, collapseWhitespace, tokenReduction, aiRules: rules, theme, showDebugTab }); await applyTheme(theme); try { await AiClassifier.setConfig({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging }); From d2de1818ca11956dfa73bb21cddc0feeb4e8c349 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sat, 19 Jul 2025 19:24:47 -0500 Subject: [PATCH 43/48] Adding diff library --- README.md | 2 + ai-filter.sln | 1 + resources/js/diff_match_patch_uncompressed.js | 2236 +++++++++++++++++ 3 files changed, 2239 insertions(+) create mode 100644 resources/js/diff_match_patch_uncompressed.js diff --git a/README.md b/README.md index 43a515b..64fbf26 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,8 @@ uses the following third party libraries: - MIT License - [turndown v7.2.0](https://github.com/mixmark-io/turndown/tree/v7.2.0) - MIT License +- [diff](https://github.com/google/diff-match-patch/blob/62f2e689f498f9c92dbc588c58750addec9b1654/javascript/diff_match_patch_uncompressed.js) + - Apache-2.0 license ## License diff --git a/ai-filter.sln b/ai-filter.sln index 57705eb..ea71f1b 100644 --- a/ai-filter.sln +++ b/ai-filter.sln @@ -108,6 +108,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "img", "img", "{F266602F-175 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "js", "js", "{21D2A42C-3F85-465C-9141-C106AFD92B68}" ProjectSection(SolutionItems) = preProject + resources\js\diff_match_patch_uncompressed.js = resources\js\diff_match_patch_uncompressed.js resources\js\turndown.js = resources\js\turndown.js EndProjectSection EndProject diff --git a/resources/js/diff_match_patch_uncompressed.js b/resources/js/diff_match_patch_uncompressed.js new file mode 100644 index 0000000..88a702c --- /dev/null +++ b/resources/js/diff_match_patch_uncompressed.js @@ -0,0 +1,2236 @@ +/** + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * 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. + */ + +/** + * @fileoverview Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * @author fraser@google.com (Neil Fraser) + */ + +/** + * Class containing the diff, match and patch methods. + * @constructor + */ +var diff_match_patch = function() { + + // Defaults. + // Redefine these in your program to override the defaults. + + // Number of seconds to map a diff before giving up (0 for infinity). + this.Diff_Timeout = 1.0; + // Cost of an empty edit operation in terms of edit characters. + this.Diff_EditCost = 4; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose). + this.Match_Threshold = 0.5; + // How far to search for a match (0 = exact location, 1000+ = broad match). + // A match this many characters away from the expected location will add + // 1.0 to the score (0.0 is a perfect match). + this.Match_Distance = 1000; + // When deleting a large block of text (over ~64 characters), how close do + // the contents have to be to match the expected contents. (0.0 = perfection, + // 1.0 = very loose). Note that Match_Threshold controls how closely the + // end points of a delete need to match. + this.Patch_DeleteThreshold = 0.5; + // Chunk size for context length. + this.Patch_Margin = 4; + + // The number of bits in an int. + this.Match_MaxBits = 32; +}; + + +// DIFF FUNCTIONS + + +/** + * The data structure representing a diff is an array of tuples: + * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] + * which means: delete 'Hello', add 'Goodbye' and keep ' world.' + */ +var DIFF_DELETE = -1; +var DIFF_INSERT = 1; +var DIFF_EQUAL = 0; + +/** + * Class representing one diff tuple. + * Attempts to look like a two-element array (which is what this used to be). + * @param {number} op Operation, one of: DIFF_DELETE, DIFF_INSERT, DIFF_EQUAL. + * @param {string} text Text to be deleted, inserted, or retained. + * @constructor + */ +diff_match_patch.Diff = function(op, text) { + this[0] = op; + this[1] = text; +}; + +diff_match_patch.Diff.prototype.length = 2; + +/** + * Emulate the output of a two-element array. + * @return {string} Diff operation as a string. + */ +diff_match_patch.Diff.prototype.toString = function() { + return this[0] + ',' + this[1]; +}; + + +/** + * Find the differences between two texts. Simplifies the problem by stripping + * any common prefix or suffix off the texts before diffing. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean=} opt_checklines Optional speedup flag. If present and false, + * then don't run a line-level diff first to identify the changed areas. + * Defaults to true, which does a faster, slightly less optimal diff. + * @param {number=} opt_deadline Optional time when the diff should be complete + * by. Used internally for recursive calls. Users should set DiffTimeout + * instead. + * @return {!Array.} Array of diff tuples. + */ +diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines, + opt_deadline) { + // Set a deadline by which time the diff must be complete. + if (typeof opt_deadline == 'undefined') { + if (this.Diff_Timeout <= 0) { + opt_deadline = Number.MAX_VALUE; + } else { + opt_deadline = (new Date).getTime() + this.Diff_Timeout * 1000; + } + } + var deadline = opt_deadline; + + // Check for null inputs. + if (text1 == null || text2 == null) { + throw new Error('Null input. (diff_main)'); + } + + // Check for equality (speedup). + if (text1 == text2) { + if (text1) { + return [new diff_match_patch.Diff(DIFF_EQUAL, text1)]; + } + return []; + } + + if (typeof opt_checklines == 'undefined') { + opt_checklines = true; + } + var checklines = opt_checklines; + + // Trim off common prefix (speedup). + var commonlength = this.diff_commonPrefix(text1, text2); + var commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = this.diff_commonSuffix(text1, text2); + var commonsuffix = text1.substring(text1.length - commonlength); + text1 = text1.substring(0, text1.length - commonlength); + text2 = text2.substring(0, text2.length - commonlength); + + // Compute the diff on the middle block. + var diffs = this.diff_compute_(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix) { + diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, commonprefix)); + } + if (commonsuffix) { + diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, commonsuffix)); + } + this.diff_cleanupMerge(diffs); + return diffs; +}; + + +/** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean} checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster, slightly less optimal diff. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_compute_ = function(text1, text2, checklines, + deadline) { + var diffs; + + if (!text1) { + // Just add some text (speedup). + return [new diff_match_patch.Diff(DIFF_INSERT, text2)]; + } + + if (!text2) { + // Just delete some text (speedup). + return [new diff_match_patch.Diff(DIFF_DELETE, text1)]; + } + + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + var i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + diffs = [new diff_match_patch.Diff(DIFF_INSERT, longtext.substring(0, i)), + new diff_match_patch.Diff(DIFF_EQUAL, shorttext), + new diff_match_patch.Diff(DIFF_INSERT, + longtext.substring(i + shorttext.length))]; + // Swap insertions for deletions if diff is reversed. + if (text1.length > text2.length) { + diffs[0][0] = diffs[2][0] = DIFF_DELETE; + } + return diffs; + } + + if (shorttext.length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + return [new diff_match_patch.Diff(DIFF_DELETE, text1), + new diff_match_patch.Diff(DIFF_INSERT, text2)]; + } + + // Check to see if the problem can be split in two. + var hm = this.diff_halfMatch_(text1, text2); + if (hm) { + // A half-match was found, sort out the return data. + var text1_a = hm[0]; + var text1_b = hm[1]; + var text2_a = hm[2]; + var text2_b = hm[3]; + var mid_common = hm[4]; + // Send both pairs off for separate processing. + var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline); + var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline); + // Merge the results. + return diffs_a.concat([new diff_match_patch.Diff(DIFF_EQUAL, mid_common)], + diffs_b); + } + + if (checklines && text1.length > 100 && text2.length > 100) { + return this.diff_lineMode_(text1, text2, deadline); + } + + return this.diff_bisect_(text1, text2, deadline); +}; + + +/** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_lineMode_ = function(text1, text2, deadline) { + // Scan the text on a line-by-line basis first. + var a = this.diff_linesToChars_(text1, text2); + text1 = a.chars1; + text2 = a.chars2; + var linearray = a.lineArray; + + var diffs = this.diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + this.diff_charsToLines_(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + this.diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, '')); + var pointer = 0; + var count_delete = 0; + var count_insert = 0; + var text_delete = ''; + var text_insert = ''; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + count_insert++; + text_insert += diffs[pointer][1]; + break; + case DIFF_DELETE: + count_delete++; + text_delete += diffs[pointer][1]; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + diffs.splice(pointer - count_delete - count_insert, + count_delete + count_insert); + pointer = pointer - count_delete - count_insert; + var subDiff = + this.diff_main(text_delete, text_insert, false, deadline); + for (var j = subDiff.length - 1; j >= 0; j--) { + diffs.splice(pointer, 0, subDiff[j]); + } + pointer = pointer + subDiff.length; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + pointer++; + } + diffs.pop(); // Remove the dummy entry at the end. + + return diffs; +}; + + +/** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_bisect_ = function(text1, text2, deadline) { + // Cache the text lengths to prevent multiple calls. + var text1_length = text1.length; + var text2_length = text2.length; + var max_d = Math.ceil((text1_length + text2_length) / 2); + var v_offset = max_d; + var v_length = 2 * max_d; + var v1 = new Array(v_length); + var v2 = new Array(v_length); + // Setting all elements to -1 is faster in Chrome & Firefox than mixing + // integers and undefined. + for (var x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + var delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + var front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + var k1start = 0; + var k1end = 0; + var k2start = 0; + var k2end = 0; + for (var d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if ((new Date()).getTime() > deadline) { + break; + } + + // Walk the front path one step. + for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + var k1_offset = v_offset + k1; + var x1; + if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + var y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length && + text1.charAt(x1) == text2.charAt(y1)) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + var k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + var x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + var k2_offset = v_offset + k2; + var x2; + if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + var y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length && + text1.charAt(text1_length - x2 - 1) == + text2.charAt(text2_length - y2 - 1)) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + var k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + var x1 = v1[k1_offset]; + var y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [new diff_match_patch.Diff(DIFF_DELETE, text1), + new diff_match_patch.Diff(DIFF_INSERT, text2)]; +}; + + +/** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_bisectSplit_ = function(text1, text2, x, y, + deadline) { + var text1a = text1.substring(0, x); + var text2a = text2.substring(0, y); + var text1b = text1.substring(x); + var text2b = text2.substring(y); + + // Compute both diffs serially. + var diffs = this.diff_main(text1a, text2a, false, deadline); + var diffsb = this.diff_main(text1b, text2b, false, deadline); + + return diffs.concat(diffsb); +}; + + +/** + * Split two texts into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {{chars1: string, chars2: string, lineArray: !Array.}} + * An object containing the encoded text1, the encoded text2 and + * the array of unique strings. + * The zeroth element of the array of unique strings is intentionally blank. + * @private + */ +diff_match_patch.prototype.diff_linesToChars_ = function(text1, text2) { + var lineArray = []; // e.g. lineArray[4] == 'Hello\n' + var lineHash = {}; // e.g. lineHash['Hello\n'] == 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray[0] = ''; + + /** + * Split a text into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * Modifies linearray and linehash through being a closure. + * @param {string} text String to encode. + * @return {string} Encoded string. + * @private + */ + function diff_linesToCharsMunge_(text) { + var chars = ''; + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + var lineStart = 0; + var lineEnd = -1; + // Keeping our own length variable is faster than looking it up. + var lineArrayLength = lineArray.length; + while (lineEnd < text.length - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length - 1; + } + var line = text.substring(lineStart, lineEnd + 1); + + if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : + (lineHash[line] !== undefined)) { + chars += String.fromCharCode(lineHash[line]); + } else { + if (lineArrayLength == maxLines) { + // Bail out at 65535 because + // String.fromCharCode(65536) == String.fromCharCode(0) + line = text.substring(lineStart); + lineEnd = text.length; + } + chars += String.fromCharCode(lineArrayLength); + lineHash[line] = lineArrayLength; + lineArray[lineArrayLength++] = line; + } + lineStart = lineEnd + 1; + } + return chars; + } + // Allocate 2/3rds of the space for text1, the rest for text2. + var maxLines = 40000; + var chars1 = diff_linesToCharsMunge_(text1); + maxLines = 65535; + var chars2 = diff_linesToCharsMunge_(text2); + return {chars1: chars1, chars2: chars2, lineArray: lineArray}; +}; + + +/** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param {!Array.} diffs Array of diff tuples. + * @param {!Array.} lineArray Array of unique strings. + * @private + */ +diff_match_patch.prototype.diff_charsToLines_ = function(diffs, lineArray) { + for (var i = 0; i < diffs.length; i++) { + var chars = diffs[i][1]; + var text = []; + for (var j = 0; j < chars.length; j++) { + text[j] = lineArray[chars.charCodeAt(j)]; + } + diffs[i][1] = text.join(''); + } +}; + + +/** + * Determine the common prefix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ +diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) { + return 0; + } + // Binary search. + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerstart = 0; + while (pointermin < pointermid) { + if (text1.substring(pointerstart, pointermid) == + text2.substring(pointerstart, pointermid)) { + pointermin = pointermid; + pointerstart = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; +}; + + +/** + * Determine the common suffix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ +diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || + text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) { + return 0; + } + // Binary search. + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerend = 0; + while (pointermin < pointermid) { + if (text1.substring(text1.length - pointermid, text1.length - pointerend) == + text2.substring(text2.length - pointermid, text2.length - pointerend)) { + pointermin = pointermid; + pointerend = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; +}; + + +/** + * Determine if the suffix of one string is the prefix of another. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of the first + * string and the start of the second string. + * @private + */ +diff_match_patch.prototype.diff_commonOverlap_ = function(text1, text2) { + // Cache the text lengths to prevent multiple calls. + var text1_length = text1.length; + var text2_length = text2.length; + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.substring(0, text1_length); + } + var text_length = Math.min(text1_length, text2_length); + // Quick check for the worst case. + if (text1 == text2) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: https://neil.fraser.name/news/2010/11/04/ + var best = 0; + var length = 1; + while (true) { + var pattern = text1.substring(text_length - length); + var found = text2.indexOf(pattern); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.substring(text_length - length) == + text2.substring(0, length)) { + best = length; + length++; + } + } +}; + + +/** + * Do the two texts share a substring which is at least half the length of the + * longer text? + * This speedup can produce non-minimal diffs. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + * @private + */ +diff_match_patch.prototype.diff_halfMatch_ = function(text1, text2) { + if (this.Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { + return null; // Pointless. + } + var dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Does a substring of shorttext exist within longtext such that the substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length substring within longtext. + * @return {Array.} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diff_halfMatchI_(longtext, shorttext, i) { + // Start with a 1/4 length substring at position i as a seed. + var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); + var j = -1; + var best_common = ''; + var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + var prefixLength = dmp.diff_commonPrefix(longtext.substring(i), + shorttext.substring(j)); + var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (best_common.length < suffixLength + prefixLength) { + best_common = shorttext.substring(j - suffixLength, j) + + shorttext.substring(j, j + prefixLength); + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length * 2 >= longtext.length) { + return [best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common]; + } else { + return null; + } + } + + // First check if the second quarter is the seed for a half-match. + var hm1 = diff_halfMatchI_(longtext, shorttext, + Math.ceil(longtext.length / 4)); + // Check again based on the third quarter. + var hm2 = diff_halfMatchI_(longtext, shorttext, + Math.ceil(longtext.length / 2)); + var hm; + if (!hm1 && !hm2) { + return null; + } else if (!hm2) { + hm = hm1; + } else if (!hm1) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length > hm2[4].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + var text1_a, text1_b, text2_a, text2_b; + if (text1.length > text2.length) { + text1_a = hm[0]; + text1_b = hm[1]; + text2_a = hm[2]; + text2_b = hm[3]; + } else { + text2_a = hm[0]; + text2_b = hm[1]; + text1_a = hm[2]; + text1_b = hm[3]; + } + var mid_common = hm[4]; + return [text1_a, text1_b, text2_a, text2_b, mid_common]; +}; + + +/** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) { + var changes = false; + var equalities = []; // Stack of indices where equalities are found. + var equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + var lastEquality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + var pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + var length_insertions1 = 0; + var length_deletions1 = 0; + // Number of characters that changed after the equality. + var length_insertions2 = 0; + var length_deletions2 = 0; + while (pointer < diffs.length) { + if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. + equalities[equalitiesLength++] = pointer; + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastEquality = diffs[pointer][1]; + } else { // An insertion or deletion. + if (diffs[pointer][0] == DIFF_INSERT) { + length_insertions2 += diffs[pointer][1].length; + } else { + length_deletions2 += diffs[pointer][1].length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastEquality && (lastEquality.length <= + Math.max(length_insertions1, length_deletions1)) && + (lastEquality.length <= Math.max(length_insertions2, + length_deletions2))) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, + new diff_match_patch.Diff(DIFF_DELETE, lastEquality)); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + // Throw away the equality we just deleted. + equalitiesLength--; + // Throw away the previous equality (it needs to be reevaluated). + equalitiesLength--; + pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastEquality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if (changes) { + this.diff_cleanupMerge(diffs); + } + this.diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.length) { + if (diffs[pointer - 1][0] == DIFF_DELETE && + diffs[pointer][0] == DIFF_INSERT) { + var deletion = diffs[pointer - 1][1]; + var insertion = diffs[pointer][1]; + var overlap_length1 = this.diff_commonOverlap_(deletion, insertion); + var overlap_length2 = this.diff_commonOverlap_(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.length / 2 || + overlap_length1 >= insertion.length / 2) { + // Overlap found. Insert an equality and trim the surrounding edits. + diffs.splice(pointer, 0, new diff_match_patch.Diff(DIFF_EQUAL, + insertion.substring(0, overlap_length1))); + diffs[pointer - 1][1] = + deletion.substring(0, deletion.length - overlap_length1); + diffs[pointer + 1][1] = insertion.substring(overlap_length1); + pointer++; + } + } else { + if (overlap_length2 >= deletion.length / 2 || + overlap_length2 >= insertion.length / 2) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.splice(pointer, 0, new diff_match_patch.Diff(DIFF_EQUAL, + deletion.substring(0, overlap_length2))); + diffs[pointer - 1][0] = DIFF_INSERT; + diffs[pointer - 1][1] = + insertion.substring(0, insertion.length - overlap_length2); + diffs[pointer + 1][0] = DIFF_DELETE; + diffs[pointer + 1][1] = + deletion.substring(overlap_length2); + pointer++; + } + } + pointer++; + } + pointer++; + } +}; + + +/** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) { + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * Closure, but does not reference any external variables. + * @param {string} one First string. + * @param {string} two Second string. + * @return {number} The score. + * @private + */ + function diff_cleanupSemanticScore_(one, two) { + if (!one || !two) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + var char1 = one.charAt(one.length - 1); + var char2 = two.charAt(0); + var nonAlphaNumeric1 = char1.match(diff_match_patch.nonAlphaNumericRegex_); + var nonAlphaNumeric2 = char2.match(diff_match_patch.nonAlphaNumericRegex_); + var whitespace1 = nonAlphaNumeric1 && + char1.match(diff_match_patch.whitespaceRegex_); + var whitespace2 = nonAlphaNumeric2 && + char2.match(diff_match_patch.whitespaceRegex_); + var lineBreak1 = whitespace1 && + char1.match(diff_match_patch.linebreakRegex_); + var lineBreak2 = whitespace2 && + char2.match(diff_match_patch.linebreakRegex_); + var blankLine1 = lineBreak1 && + one.match(diff_match_patch.blanklineEndRegex_); + var blankLine2 = lineBreak2 && + two.match(diff_match_patch.blanklineStartRegex_); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + var pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] == DIFF_EQUAL && + diffs[pointer + 1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + var equality1 = diffs[pointer - 1][1]; + var edit = diffs[pointer][1]; + var equality2 = diffs[pointer + 1][1]; + + // First, shift the edit as far left as possible. + var commonOffset = this.diff_commonSuffix(equality1, edit); + if (commonOffset) { + var commonString = edit.substring(edit.length - commonOffset); + equality1 = equality1.substring(0, equality1.length - commonOffset); + edit = commonString + edit.substring(0, edit.length - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, looking for the best fit. + var bestEquality1 = equality1; + var bestEdit = edit; + var bestEquality2 = equality2; + var bestScore = diff_cleanupSemanticScore_(equality1, edit) + + diff_cleanupSemanticScore_(edit, equality2); + while (edit.charAt(0) === equality2.charAt(0)) { + equality1 += edit.charAt(0); + edit = edit.substring(1) + equality2.charAt(0); + equality2 = equality2.substring(1); + var score = diff_cleanupSemanticScore_(equality1, edit) + + diff_cleanupSemanticScore_(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (diffs[pointer - 1][1] != bestEquality1) { + // We have an improvement, save it back to the diff. + if (bestEquality1) { + diffs[pointer - 1][1] = bestEquality1; + } else { + diffs.splice(pointer - 1, 1); + pointer--; + } + diffs[pointer][1] = bestEdit; + if (bestEquality2) { + diffs[pointer + 1][1] = bestEquality2; + } else { + diffs.splice(pointer + 1, 1); + pointer--; + } + } + } + pointer++; + } +}; + +// Define some regex patterns for matching boundaries. +diff_match_patch.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/; +diff_match_patch.whitespaceRegex_ = /\s/; +diff_match_patch.linebreakRegex_ = /[\r\n]/; +diff_match_patch.blanklineEndRegex_ = /\n\r?\n$/; +diff_match_patch.blanklineStartRegex_ = /^\r?\n\r?\n/; + +/** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) { + var changes = false; + var equalities = []; // Stack of indices where equalities are found. + var equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + var lastEquality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + var pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + var pre_ins = false; + // Is there a deletion operation before the last equality. + var pre_del = false; + // Is there an insertion operation after the last equality. + var post_ins = false; + // Is there a deletion operation after the last equality. + var post_del = false; + while (pointer < diffs.length) { + if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. + if (diffs[pointer][1].length < this.Diff_EditCost && + (post_ins || post_del)) { + // Candidate found. + equalities[equalitiesLength++] = pointer; + pre_ins = post_ins; + pre_del = post_del; + lastEquality = diffs[pointer][1]; + } else { + // Not a candidate, and can never become one. + equalitiesLength = 0; + lastEquality = null; + } + post_ins = post_del = false; + } else { // An insertion or deletion. + if (diffs[pointer][0] == DIFF_DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastEquality && ((pre_ins && pre_del && post_ins && post_del) || + ((lastEquality.length < this.Diff_EditCost / 2) && + (pre_ins + pre_del + post_ins + post_del) == 3))) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, + new diff_match_patch.Diff(DIFF_DELETE, lastEquality)); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + equalitiesLength--; // Throw away the equality we just deleted; + lastEquality = null; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalitiesLength = 0; + } else { + equalitiesLength--; // Throw away the previous equality. + pointer = equalitiesLength > 0 ? + equalities[equalitiesLength - 1] : -1; + post_ins = post_del = false; + } + changes = true; + } + } + pointer++; + } + + if (changes) { + this.diff_cleanupMerge(diffs); + } +}; + + +/** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupMerge = function(diffs) { + // Add a dummy entry at the end. + diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, '')); + var pointer = 0; + var count_delete = 0; + var count_insert = 0; + var text_delete = ''; + var text_insert = ''; + var commonlength; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + count_insert++; + text_insert += diffs[pointer][1]; + pointer++; + break; + case DIFF_DELETE: + count_delete++; + text_delete += diffs[pointer][1]; + pointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete + count_insert > 1) { + if (count_delete !== 0 && count_insert !== 0) { + // Factor out any common prefixies. + commonlength = this.diff_commonPrefix(text_insert, text_delete); + if (commonlength !== 0) { + if ((pointer - count_delete - count_insert) > 0 && + diffs[pointer - count_delete - count_insert - 1][0] == + DIFF_EQUAL) { + diffs[pointer - count_delete - count_insert - 1][1] += + text_insert.substring(0, commonlength); + } else { + diffs.splice(0, 0, new diff_match_patch.Diff(DIFF_EQUAL, + text_insert.substring(0, commonlength))); + pointer++; + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = this.diff_commonSuffix(text_insert, text_delete); + if (commonlength !== 0) { + diffs[pointer][1] = text_insert.substring(text_insert.length - + commonlength) + diffs[pointer][1]; + text_insert = text_insert.substring(0, text_insert.length - + commonlength); + text_delete = text_delete.substring(0, text_delete.length - + commonlength); + } + } + // Delete the offending records and add the merged ones. + pointer -= count_delete + count_insert; + diffs.splice(pointer, count_delete + count_insert); + if (text_delete.length) { + diffs.splice(pointer, 0, + new diff_match_patch.Diff(DIFF_DELETE, text_delete)); + pointer++; + } + if (text_insert.length) { + diffs.splice(pointer, 0, + new diff_match_patch.Diff(DIFF_INSERT, text_insert)); + pointer++; + } + pointer++; + } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1][1] += diffs[pointer][1]; + diffs.splice(pointer, 1); + } else { + pointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + } + if (diffs[diffs.length - 1][1] === '') { + diffs.pop(); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + var changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] == DIFF_EQUAL && + diffs[pointer + 1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + if (diffs[pointer][1].substring(diffs[pointer][1].length - + diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { + // Shift the edit over the previous equality. + diffs[pointer][1] = diffs[pointer - 1][1] + + diffs[pointer][1].substring(0, diffs[pointer][1].length - + diffs[pointer - 1][1].length); + diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; + diffs.splice(pointer - 1, 1); + changes = true; + } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == + diffs[pointer + 1][1]) { + // Shift the edit over the next equality. + diffs[pointer - 1][1] += diffs[pointer + 1][1]; + diffs[pointer][1] = + diffs[pointer][1].substring(diffs[pointer + 1][1].length) + + diffs[pointer + 1][1]; + diffs.splice(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + this.diff_cleanupMerge(diffs); + } +}; + + +/** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. 'The cat' vs 'The big cat', 1->1, 5->8 + * @param {!Array.} diffs Array of diff tuples. + * @param {number} loc Location within text1. + * @return {number} Location within text2. + */ +diff_match_patch.prototype.diff_xIndex = function(diffs, loc) { + var chars1 = 0; + var chars2 = 0; + var last_chars1 = 0; + var last_chars2 = 0; + var x; + for (x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion. + chars1 += diffs[x][1].length; + } + if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion. + chars2 += diffs[x][1].length; + } + if (chars1 > loc) { // Overshot the location. + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + // Was the location was deleted? + if (diffs.length != x && diffs[x][0] === DIFF_DELETE) { + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); +}; + + +/** + * Convert a diff array into a pretty HTML report. + * @param {!Array.} diffs Array of diff tuples. + * @return {string} HTML representation. + */ +diff_match_patch.prototype.diff_prettyHtml = function(diffs) { + var html = []; + var pattern_amp = /&/g; + var pattern_lt = //g; + var pattern_para = /\n/g; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; // Operation (insert, delete, equal) + var data = diffs[x][1]; // Text of change. + var text = data.replace(pattern_amp, '&').replace(pattern_lt, '<') + .replace(pattern_gt, '>').replace(pattern_para, '¶
'); + switch (op) { + case DIFF_INSERT: + html[x] = '' + text + ''; + break; + case DIFF_DELETE: + html[x] = '' + text + ''; + break; + case DIFF_EQUAL: + html[x] = '' + text + ''; + break; + } + } + return html.join(''); +}; + + +/** + * Compute and return the source text (all equalities and deletions). + * @param {!Array.} diffs Array of diff tuples. + * @return {string} Source text. + */ +diff_match_patch.prototype.diff_text1 = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_INSERT) { + text[x] = diffs[x][1]; + } + } + return text.join(''); +}; + + +/** + * Compute and return the destination text (all equalities and insertions). + * @param {!Array.} diffs Array of diff tuples. + * @return {string} Destination text. + */ +diff_match_patch.prototype.diff_text2 = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_DELETE) { + text[x] = diffs[x][1]; + } + } + return text.join(''); +}; + + +/** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param {!Array.} diffs Array of diff tuples. + * @return {number} Number of changes. + */ +diff_match_patch.prototype.diff_levenshtein = function(diffs) { + var levenshtein = 0; + var insertions = 0; + var deletions = 0; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; + var data = diffs[x][1]; + switch (op) { + case DIFF_INSERT: + insertions += data.length; + break; + case DIFF_DELETE: + deletions += data.length; + break; + case DIFF_EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.max(insertions, deletions); + return levenshtein; +}; + + +/** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param {!Array.} diffs Array of diff tuples. + * @return {string} Delta text. + */ +diff_match_patch.prototype.diff_toDelta = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + switch (diffs[x][0]) { + case DIFF_INSERT: + text[x] = '+' + encodeURI(diffs[x][1]); + break; + case DIFF_DELETE: + text[x] = '-' + diffs[x][1].length; + break; + case DIFF_EQUAL: + text[x] = '=' + diffs[x][1].length; + break; + } + } + return text.join('\t').replace(/%20/g, ' '); +}; + + +/** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param {string} text1 Source string for the diff. + * @param {string} delta Delta text. + * @return {!Array.} Array of diff tuples. + * @throws {!Error} If invalid input. + */ +diff_match_patch.prototype.diff_fromDelta = function(text1, delta) { + var diffs = []; + var diffsLength = 0; // Keeping our own length var is faster in JS. + var pointer = 0; // Cursor in text1 + var tokens = delta.split(/\t/g); + for (var x = 0; x < tokens.length; x++) { + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + var param = tokens[x].substring(1); + switch (tokens[x].charAt(0)) { + case '+': + try { + diffs[diffsLength++] = + new diff_match_patch.Diff(DIFF_INSERT, decodeURI(param)); + } catch (ex) { + // Malformed URI sequence. + throw new Error('Illegal escape in diff_fromDelta: ' + param); + } + break; + case '-': + // Fall through. + case '=': + var n = parseInt(param, 10); + if (isNaN(n) || n < 0) { + throw new Error('Invalid number in diff_fromDelta: ' + param); + } + var text = text1.substring(pointer, pointer += n); + if (tokens[x].charAt(0) == '=') { + diffs[diffsLength++] = new diff_match_patch.Diff(DIFF_EQUAL, text); + } else { + diffs[diffsLength++] = new diff_match_patch.Diff(DIFF_DELETE, text); + } + break; + default: + // Blank tokens are ok (from a trailing \t). + // Anything else is an error. + if (tokens[x]) { + throw new Error('Invalid diff operation in diff_fromDelta: ' + + tokens[x]); + } + } + } + if (pointer != text1.length) { + throw new Error('Delta length (' + pointer + + ') does not equal source text length (' + text1.length + ').'); + } + return diffs; +}; + + +// MATCH FUNCTIONS + + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + */ +diff_match_patch.prototype.match_main = function(text, pattern, loc) { + // Check for null inputs. + if (text == null || pattern == null || loc == null) { + throw new Error('Null input. (match_main)'); + } + + loc = Math.max(0, Math.min(loc, text.length)); + if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (!text.length) { + // Nothing to match. + return -1; + } else if (text.substring(loc, loc + pattern.length) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return this.match_bitap_(text, pattern, loc); + } +}; + + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + * @private + */ +diff_match_patch.prototype.match_bitap_ = function(text, pattern, loc) { + if (pattern.length > this.Match_MaxBits) { + throw new Error('Pattern too long for this browser.'); + } + + // Initialise the alphabet. + var s = this.match_alphabet_(pattern); + + var dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Compute and return the score for a match with e errors and x location. + * Accesses loc and pattern through being a closure. + * @param {number} e Number of errors in match. + * @param {number} x Location of match. + * @return {number} Overall score for match (0.0 = good, 1.0 = bad). + * @private + */ + function match_bitapScore_(e, x) { + var accuracy = e / pattern.length; + var proximity = Math.abs(loc - x); + if (!dmp.Match_Distance) { + // Dodge divide by zero error. + return proximity ? 1.0 : accuracy; + } + return accuracy + (proximity / dmp.Match_Distance); + } + + // Highest score beyond which we give up. + var score_threshold = this.Match_Threshold; + // Is there a nearby exact match? (speedup) + var best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length); + if (best_loc != -1) { + score_threshold = + Math.min(match_bitapScore_(0, best_loc), score_threshold); + } + } + + // Initialise the bit arrays. + var matchmask = 1 << (pattern.length - 1); + best_loc = -1; + + var bin_min, bin_mid; + var bin_max = pattern.length + text.length; + var last_rd; + for (var d = 0; d < pattern.length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at this + // error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min); + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + var start = Math.max(1, loc - bin_mid + 1); + var finish = Math.min(loc + bin_mid, text.length) + pattern.length; + + var rd = Array(finish + 2); + rd[finish + 1] = (1 << d) - 1; + for (var j = finish; j >= start; j--) { + // The alphabet (s) is a sparse hash, so the following line generates + // warnings. + var charMatch = s[text.charAt(j - 1)]; + if (d === 0) { // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | + (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | + last_rd[j + 1]; + } + if (rd[j] & matchmask) { + var score = match_bitapScore_(d, j - 1); + // This match will almost certainly be better than any existing match. + // But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + // No hope for a (better) match at greater error levels. + if (match_bitapScore_(d + 1, loc) > score_threshold) { + break; + } + last_rd = rd; + } + return best_loc; +}; + + +/** + * Initialise the alphabet for the Bitap algorithm. + * @param {string} pattern The text to encode. + * @return {!Object} Hash of character locations. + * @private + */ +diff_match_patch.prototype.match_alphabet_ = function(pattern) { + var s = {}; + for (var i = 0; i < pattern.length; i++) { + s[pattern.charAt(i)] = 0; + } + for (var i = 0; i < pattern.length; i++) { + s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1); + } + return s; +}; + + +// PATCH FUNCTIONS + + +/** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param {!diff_match_patch.patch_obj} patch The patch to grow. + * @param {string} text Source text. + * @private + */ +diff_match_patch.prototype.patch_addContext_ = function(patch, text) { + if (text.length == 0) { + return; + } + if (patch.start2 === null) { + throw Error('patch not initialized'); + } + var pattern = text.substring(patch.start2, patch.start2 + patch.length1); + var padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) && + pattern.length < this.Match_MaxBits - this.Patch_Margin - + this.Patch_Margin) { + padding += this.Patch_Margin; + pattern = text.substring(patch.start2 - padding, + patch.start2 + patch.length1 + padding); + } + // Add one chunk for good luck. + padding += this.Patch_Margin; + + // Add the prefix. + var prefix = text.substring(patch.start2 - padding, patch.start2); + if (prefix) { + patch.diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, prefix)); + } + // Add the suffix. + var suffix = text.substring(patch.start2 + patch.length1, + patch.start2 + patch.length1 + padding); + if (suffix) { + patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.length; + patch.start2 -= prefix.length; + // Extend the lengths. + patch.length1 += prefix.length + suffix.length; + patch.length2 += prefix.length + suffix.length; +}; + + +/** + * Compute a list of patches to turn text1 into text2. + * Use diffs if provided, otherwise compute it ourselves. + * There are four ways to call this function, depending on what data is + * available to the caller: + * Method 1: + * a = text1, b = text2 + * Method 2: + * a = diffs + * Method 3 (optimal): + * a = text1, b = diffs + * Method 4 (deprecated, use method 3): + * a = text1, b = text2, c = diffs + * + * @param {string|!Array.} a text1 (methods 1,3,4) or + * Array of diff tuples for text1 to text2 (method 2). + * @param {string|!Array.=} opt_b text2 (methods 1,4) or + * Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). + * @param {string|!Array.=} opt_c Array of diff tuples + * for text1 to text2 (method 4) or undefined (methods 1,2,3). + * @return {!Array.} Array of Patch objects. + */ +diff_match_patch.prototype.patch_make = function(a, opt_b, opt_c) { + var text1, diffs; + if (typeof a == 'string' && typeof opt_b == 'string' && + typeof opt_c == 'undefined') { + // Method 1: text1, text2 + // Compute diffs from text1 and text2. + text1 = /** @type {string} */(a); + diffs = this.diff_main(text1, /** @type {string} */(opt_b), true); + if (diffs.length > 2) { + this.diff_cleanupSemantic(diffs); + this.diff_cleanupEfficiency(diffs); + } + } else if (a && typeof a == 'object' && typeof opt_b == 'undefined' && + typeof opt_c == 'undefined') { + // Method 2: diffs + // Compute text1 from diffs. + diffs = /** @type {!Array.} */(a); + text1 = this.diff_text1(diffs); + } else if (typeof a == 'string' && opt_b && typeof opt_b == 'object' && + typeof opt_c == 'undefined') { + // Method 3: text1, diffs + text1 = /** @type {string} */(a); + diffs = /** @type {!Array.} */(opt_b); + } else if (typeof a == 'string' && typeof opt_b == 'string' && + opt_c && typeof opt_c == 'object') { + // Method 4: text1, text2, diffs + // text2 is not used. + text1 = /** @type {string} */(a); + diffs = /** @type {!Array.} */(opt_c); + } else { + throw new Error('Unknown call format to patch_make.'); + } + + if (diffs.length === 0) { + return []; // Get rid of the null case. + } + var patches = []; + var patch = new diff_match_patch.patch_obj(); + var patchDiffLength = 0; // Keeping our own length var is faster in JS. + var char_count1 = 0; // Number of characters into the text1 string. + var char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + var prepatch_text = text1; + var postpatch_text = text1; + for (var x = 0; x < diffs.length; x++) { + var diff_type = diffs[x][0]; + var diff_text = diffs[x][1]; + + if (!patchDiffLength && diff_type !== DIFF_EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (diff_type) { + case DIFF_INSERT: + patch.diffs[patchDiffLength++] = diffs[x]; + patch.length2 += diff_text.length; + postpatch_text = postpatch_text.substring(0, char_count2) + diff_text + + postpatch_text.substring(char_count2); + break; + case DIFF_DELETE: + patch.length1 += diff_text.length; + patch.diffs[patchDiffLength++] = diffs[x]; + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + + diff_text.length); + break; + case DIFF_EQUAL: + if (diff_text.length <= 2 * this.Patch_Margin && + patchDiffLength && diffs.length != x + 1) { + // Small equality inside a patch. + patch.diffs[patchDiffLength++] = diffs[x]; + patch.length1 += diff_text.length; + patch.length2 += diff_text.length; + } else if (diff_text.length >= 2 * this.Patch_Margin) { + // Time for a new patch. + if (patchDiffLength) { + this.patch_addContext_(patch, prepatch_text); + patches.push(patch); + patch = new diff_match_patch.patch_obj(); + patchDiffLength = 0; + // Unlike Unidiff, our patch lists have a rolling context. + // https://github.com/google/diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (diff_type !== DIFF_INSERT) { + char_count1 += diff_text.length; + } + if (diff_type !== DIFF_DELETE) { + char_count2 += diff_text.length; + } + } + // Pick up the leftover patch if not empty. + if (patchDiffLength) { + this.patch_addContext_(patch, prepatch_text); + patches.push(patch); + } + + return patches; +}; + + +/** + * Given an array of patches, return another array that is identical. + * @param {!Array.} patches Array of Patch objects. + * @return {!Array.} Array of Patch objects. + */ +diff_match_patch.prototype.patch_deepCopy = function(patches) { + // Making deep copies is hard in JavaScript. + var patchesCopy = []; + for (var x = 0; x < patches.length; x++) { + var patch = patches[x]; + var patchCopy = new diff_match_patch.patch_obj(); + patchCopy.diffs = []; + for (var y = 0; y < patch.diffs.length; y++) { + patchCopy.diffs[y] = + new diff_match_patch.Diff(patch.diffs[y][0], patch.diffs[y][1]); + } + patchCopy.start1 = patch.start1; + patchCopy.start2 = patch.start2; + patchCopy.length1 = patch.length1; + patchCopy.length2 = patch.length2; + patchesCopy[x] = patchCopy; + } + return patchesCopy; +}; + + +/** + * Merge a set of patches onto the text. Return a patched text, as well + * as a list of true/false values indicating which patches were applied. + * @param {!Array.} patches Array of Patch objects. + * @param {string} text Old text. + * @return {!Array.>} Two element Array, containing the + * new text and an array of boolean values. + */ +diff_match_patch.prototype.patch_apply = function(patches, text) { + if (patches.length == 0) { + return [text, []]; + } + + // Deep copy the patches so that no changes are made to originals. + patches = this.patch_deepCopy(patches); + + var nullPadding = this.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + + this.patch_splitMax(patches); + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + var delta = 0; + var results = []; + for (var x = 0; x < patches.length; x++) { + var expected_loc = patches[x].start2 + delta; + var text1 = this.diff_text1(patches[x].diffs); + var start_loc; + var end_loc = -1; + if (text1.length > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = this.match_main(text, text1.substring(0, this.Match_MaxBits), + expected_loc); + if (start_loc != -1) { + end_loc = this.match_main(text, + text1.substring(text1.length - this.Match_MaxBits), + expected_loc + text1.length - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = this.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= patches[x].length2 - patches[x].length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + var text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, start_loc + text1.length); + } else { + text2 = text.substring(start_loc, end_loc + this.Match_MaxBits); + } + if (text1 == text2) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + + this.diff_text2(patches[x].diffs) + + text.substring(start_loc + text1.length); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + var diffs = this.diff_main(text1, text2, false); + if (text1.length > this.Match_MaxBits && + this.diff_levenshtein(diffs) / text1.length > + this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + this.diff_cleanupSemanticLossless(diffs); + var index1 = 0; + var index2; + for (var y = 0; y < patches[x].diffs.length; y++) { + var mod = patches[x].diffs[y]; + if (mod[0] !== DIFF_EQUAL) { + index2 = this.diff_xIndex(diffs, index1); + } + if (mod[0] === DIFF_INSERT) { // Insertion + text = text.substring(0, start_loc + index2) + mod[1] + + text.substring(start_loc + index2); + } else if (mod[0] === DIFF_DELETE) { // Deletion + text = text.substring(0, start_loc + index2) + + text.substring(start_loc + this.diff_xIndex(diffs, + index1 + mod[1].length)); + } + if (mod[0] !== DIFF_DELETE) { + index1 += mod[1].length; + } + } + } + } + } + } + // Strip the padding off. + text = text.substring(nullPadding.length, text.length - nullPadding.length); + return [text, results]; +}; + + +/** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param {!Array.} patches Array of Patch objects. + * @return {string} The padding string added to each side. + */ +diff_match_patch.prototype.patch_addPadding = function(patches) { + var paddingLength = this.Patch_Margin; + var nullPadding = ''; + for (var x = 1; x <= paddingLength; x++) { + nullPadding += String.fromCharCode(x); + } + + // Bump all the patches forward. + for (var x = 0; x < patches.length; x++) { + patches[x].start1 += paddingLength; + patches[x].start2 += paddingLength; + } + + // Add some padding on start of first diff. + var patch = patches[0]; + var diffs = patch.diffs; + if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs[0][1].length) { + // Grow first equality. + var extraLength = paddingLength - diffs[0][1].length; + diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1]; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches[patches.length - 1]; + diffs = patch.diffs; + if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs[diffs.length - 1][1].length) { + // Grow last equality. + var extraLength = paddingLength - diffs[diffs.length - 1][1].length; + diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; +}; + + +/** + * Look through the patches and break up any which are longer than the maximum + * limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param {!Array.} patches Array of Patch objects. + */ +diff_match_patch.prototype.patch_splitMax = function(patches) { + var patch_size = this.Match_MaxBits; + for (var x = 0; x < patches.length; x++) { + if (patches[x].length1 <= patch_size) { + continue; + } + var bigpatch = patches[x]; + // Remove the big old patch. + patches.splice(x--, 1); + var start1 = bigpatch.start1; + var start2 = bigpatch.start2; + var precontext = ''; + while (bigpatch.diffs.length !== 0) { + // Create one of several smaller patches. + var patch = new diff_match_patch.patch_obj(); + var empty = true; + patch.start1 = start1 - precontext.length; + patch.start2 = start2 - precontext.length; + if (precontext !== '') { + patch.length1 = patch.length2 = precontext.length; + patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, precontext)); + } + while (bigpatch.diffs.length !== 0 && + patch.length1 < patch_size - this.Patch_Margin) { + var diff_type = bigpatch.diffs[0][0]; + var diff_text = bigpatch.diffs[0][1]; + if (diff_type === DIFF_INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length; + start2 += diff_text.length; + patch.diffs.push(bigpatch.diffs.shift()); + empty = false; + } else if (diff_type === DIFF_DELETE && patch.diffs.length == 1 && + patch.diffs[0][0] == DIFF_EQUAL && + diff_text.length > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length; + start1 += diff_text.length; + empty = false; + patch.diffs.push(new diff_match_patch.Diff(diff_type, diff_text)); + bigpatch.diffs.shift(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, + patch_size - patch.length1 - this.Patch_Margin); + patch.length1 += diff_text.length; + start1 += diff_text.length; + if (diff_type === DIFF_EQUAL) { + patch.length2 += diff_text.length; + start2 += diff_text.length; + } else { + empty = false; + } + patch.diffs.push(new diff_match_patch.Diff(diff_type, diff_text)); + if (diff_text == bigpatch.diffs[0][1]) { + bigpatch.diffs.shift(); + } else { + bigpatch.diffs[0][1] = + bigpatch.diffs[0][1].substring(diff_text.length); + } + } + } + // Compute the head context for the next patch. + precontext = this.diff_text2(patch.diffs); + precontext = + precontext.substring(precontext.length - this.Patch_Margin); + // Append the end context for this patch. + var postcontext = this.diff_text1(bigpatch.diffs) + .substring(0, this.Patch_Margin); + if (postcontext !== '') { + patch.length1 += postcontext.length; + patch.length2 += postcontext.length; + if (patch.diffs.length !== 0 && + patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) { + patch.diffs[patch.diffs.length - 1][1] += postcontext; + } else { + patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, postcontext)); + } + } + if (!empty) { + patches.splice(++x, 0, patch); + } + } + } +}; + + +/** + * Take a list of patches and return a textual representation. + * @param {!Array.} patches Array of Patch objects. + * @return {string} Text representation of patches. + */ +diff_match_patch.prototype.patch_toText = function(patches) { + var text = []; + for (var x = 0; x < patches.length; x++) { + text[x] = patches[x]; + } + return text.join(''); +}; + + +/** + * Parse a textual representation of patches and return a list of Patch objects. + * @param {string} textline Text representation of patches. + * @return {!Array.} Array of Patch objects. + * @throws {!Error} If invalid input. + */ +diff_match_patch.prototype.patch_fromText = function(textline) { + var patches = []; + if (!textline) { + return patches; + } + var text = textline.split('\n'); + var textPointer = 0; + var patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; + while (textPointer < text.length) { + var m = text[textPointer].match(patchHeader); + if (!m) { + throw new Error('Invalid patch string: ' + text[textPointer]); + } + var patch = new diff_match_patch.patch_obj(); + patches.push(patch); + patch.start1 = parseInt(m[1], 10); + if (m[2] === '') { + patch.start1--; + patch.length1 = 1; + } else if (m[2] == '0') { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = parseInt(m[2], 10); + } + + patch.start2 = parseInt(m[3], 10); + if (m[4] === '') { + patch.start2--; + patch.length2 = 1; + } else if (m[4] == '0') { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = parseInt(m[4], 10); + } + textPointer++; + + while (textPointer < text.length) { + var sign = text[textPointer].charAt(0); + try { + var line = decodeURI(text[textPointer].substring(1)); + } catch (ex) { + // Malformed URI sequence. + throw new Error('Illegal escape in patch_fromText: ' + line); + } + if (sign == '-') { + // Deletion. + patch.diffs.push(new diff_match_patch.Diff(DIFF_DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.push(new diff_match_patch.Diff(DIFF_INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else if (sign === '') { + // Blank line? Whatever. + } else { + // WTF? + throw new Error('Invalid patch mode "' + sign + '" in: ' + line); + } + textPointer++; + } + } + return patches; +}; + + +/** + * Class representing one patch operation. + * @constructor + */ +diff_match_patch.patch_obj = function() { + /** @type {!Array.} */ + this.diffs = []; + /** @type {?number} */ + this.start1 = null; + /** @type {?number} */ + this.start2 = null; + /** @type {number} */ + this.length1 = 0; + /** @type {number} */ + this.length2 = 0; +}; + + +/** + * Emulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indices are printed as 1-based, not 0-based. + * @return {string} The GNU diff string. + */ +diff_match_patch.patch_obj.prototype.toString = function() { + var coords1, coords2; + if (this.length1 === 0) { + coords1 = this.start1 + ',0'; + } else if (this.length1 == 1) { + coords1 = this.start1 + 1; + } else { + coords1 = (this.start1 + 1) + ',' + this.length1; + } + if (this.length2 === 0) { + coords2 = this.start2 + ',0'; + } else if (this.length2 == 1) { + coords2 = this.start2 + 1; + } else { + coords2 = (this.start2 + 1) + ',' + this.length2; + } + var text = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n']; + var op; + // Escape the body of the patch with %xx notation. + for (var x = 0; x < this.diffs.length; x++) { + switch (this.diffs[x][0]) { + case DIFF_INSERT: + op = '+'; + break; + case DIFF_DELETE: + op = '-'; + break; + case DIFF_EQUAL: + op = ' '; + break; + } + text[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n'; + } + return text.join('').replace(/%20/g, ' '); +}; + +// CLOSURE:begin_strip +// Lines below here will not be included in the Closure-compatible library. + +// Export these global variables so that they survive Google's JS compiler. +// In a browser, 'this' will be 'window'. +// Users of node.js should 'require' the uncompressed version since Google's +// JS compiler may break the following exports for non-browser environments. +/** @suppress {globalThis} */ +this['diff_match_patch'] = diff_match_patch; +/** @suppress {globalThis} */ +this['DIFF_DELETE'] = DIFF_DELETE; +/** @suppress {globalThis} */ +this['DIFF_INSERT'] = DIFF_INSERT; +/** @suppress {globalThis} */ +this['DIFF_EQUAL'] = DIFF_EQUAL; From 55ccf083c836043c7c242448762571494d41a75d Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sat, 19 Jul 2025 19:32:36 -0500 Subject: [PATCH 44/48] Add debug diff view --- background.js | 11 ++++++++++- options/options.html | 6 ++++++ options/options.js | 12 +++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/background.js b/background.js index b5667e6..58a5de5 100644 --- a/background.js +++ b/background.js @@ -33,6 +33,7 @@ let userTheme = 'auto'; let currentTheme = 'light'; let detectSystemTheme; let errorPending = false; +let showDebugTab = false; const ERROR_NOTIFICATION_ID = 'sortana-error'; function normalizeRules(rules) { @@ -262,12 +263,16 @@ async function processMessage(id) { try { const full = await messenger.messages.getFull(id); let text = buildEmailText(full); + const originalText = text; if (tokenReduction && maxTokens > 0) { const limit = Math.floor(maxTokens * 0.9); if (text.length > limit) { text = text.slice(0, limit); } } + if (showDebugTab) { + await storage.local.set({ lastFullText: originalText, lastPromptText: text }); + } let hdr; let currentTags = []; let alreadyRead = false; @@ -425,7 +430,7 @@ async function clearCacheForMessages(idsInput) { } try { - const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "tokenReduction", "aiRules", "theme", "errorPending"]); + const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "tokenReduction", "aiRules", "theme", "errorPending", "showDebugTab"]); logger.setDebug(store.debugLogging); await AiClassifier.setConfig(store); userTheme = store.theme || 'auto'; @@ -440,6 +445,7 @@ async function clearCacheForMessages(idsInput) { maxTokens = parseInt(store.aiParams.max_tokens) || maxTokens; } errorPending = store.errorPending === true; + showDebugTab = store.showDebugTab === true; const savedStats = await storage.local.get('classifyStats'); if (savedStats.classifyStats && typeof savedStats.classifyStats === 'object') { Object.assign(timingStats, savedStats.classifyStats); @@ -494,6 +500,9 @@ async function clearCacheForMessages(idsInput) { tokenReduction = changes.tokenReduction.newValue === true; logger.aiLog("tokenReduction updated from storage change", { debug: true }, tokenReduction); } + if (changes.showDebugTab) { + showDebugTab = changes.showDebugTab.newValue === true; + } if (changes.errorPending) { errorPending = changes.errorPending.newValue === true; updateActionIcon(); diff --git a/options/options.html b/options/options.html index d14ce99..3618a20 100644 --- a/options/options.html +++ b/options/options.html @@ -31,6 +31,10 @@ .tag { --bulma-tag-h: 318; } + #diff-display { + white-space: pre-wrap; + font-family: monospace; + } @@ -286,9 +290,11 @@ Debug

+                
+ diff --git a/options/options.js b/options/options.js index ebd7497..3a09d37 100644 --- a/options/options.js +++ b/options/options.js @@ -21,7 +21,9 @@ document.addEventListener('DOMContentLoaded', async () => { 'aiCache', 'theme', 'showDebugTab', - 'lastPayload' + 'lastPayload', + 'lastFullText', + 'lastPromptText' ]); const tabButtons = document.querySelectorAll('#main-tabs li'); const tabs = document.querySelectorAll('.tab-content'); @@ -67,9 +69,17 @@ document.addEventListener('DOMContentLoaded', async () => { await applyTheme(themeSelect.value); const payloadDisplay = document.getElementById('payload-display'); + const diffDisplay = document.getElementById('diff-display'); if (defaults.lastPayload) { payloadDisplay.textContent = JSON.stringify(defaults.lastPayload, null, 2); } + if (defaults.lastFullText && defaults.lastPromptText && diff_match_patch) { + const dmp = new diff_match_patch(); + dmp.Diff_EditCost = 4; + const diffs = dmp.diff_main(defaults.lastFullText, defaults.lastPromptText); + dmp.diff_cleanupEfficiency(diffs); + diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + } themeSelect.addEventListener('change', async () => { markDirty(); await applyTheme(themeSelect.value); From 841a697c69a9204bcaf7c22c30be8fd5259837f5 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sat, 19 Jul 2025 19:46:44 -0500 Subject: [PATCH 45/48] Update debug tab with live refresh --- README.md | 2 +- options/options.js | 37 +++++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 64fbf26..82649d0 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ message meets a specified criterion. - **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page. - **Markdown conversion** – optionally convert HTML bodies to Markdown before sending them to the AI service. - **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service. -- **Debug tab** – view the last request payload sent to the AI service. +- **Debug tab** – view the last request payload and message diff with live updates. - **Light/Dark themes** – automatically match Thunderbird's appearance with optional manual override. - **Automatic rules** – create rules that tag, move, copy, forward, reply, delete, archive, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages and can ignore messages outside a chosen age range. - **Rule ordering** – drag rules to prioritize them and optionally stop processing after a match. diff --git a/options/options.js b/options/options.js index 3a09d37..3402dcf 100644 --- a/options/options.js +++ b/options/options.js @@ -70,13 +70,18 @@ document.addEventListener('DOMContentLoaded', async () => { await applyTheme(themeSelect.value); const payloadDisplay = document.getElementById('payload-display'); const diffDisplay = document.getElementById('diff-display'); - if (defaults.lastPayload) { - payloadDisplay.textContent = JSON.stringify(defaults.lastPayload, null, 2); + + let lastFullText = defaults.lastFullText || ''; + let lastPromptText = defaults.lastPromptText || ''; + let lastPayload = defaults.lastPayload ? JSON.stringify(defaults.lastPayload, null, 2) : ''; + + if (lastPayload) { + payloadDisplay.textContent = lastPayload; } - if (defaults.lastFullText && defaults.lastPromptText && diff_match_patch) { + if (lastFullText && lastPromptText && diff_match_patch) { const dmp = new diff_match_patch(); dmp.Diff_EditCost = 4; - const diffs = dmp.diff_main(defaults.lastFullText, defaults.lastPromptText); + const diffs = dmp.diff_main(lastFullText, lastPromptText); dmp.diff_cleanupEfficiency(diffs); diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); } @@ -729,6 +734,30 @@ document.addEventListener('DOMContentLoaded', async () => { } catch { cacheCountEl.textContent = '?'; } + + try { + if (debugTabToggle.checked) { + const latest = await storage.local.get(['lastPayload', 'lastFullText', 'lastPromptText']); + const payloadStr = latest.lastPayload ? JSON.stringify(latest.lastPayload, null, 2) : ''; + if (payloadStr !== lastPayload) { + lastPayload = payloadStr; + payloadDisplay.textContent = payloadStr; + } + if (latest.lastFullText !== lastFullText || latest.lastPromptText !== lastPromptText) { + lastFullText = latest.lastFullText || ''; + lastPromptText = latest.lastPromptText || ''; + if (lastFullText && lastPromptText && diff_match_patch) { + const dmp = new diff_match_patch(); + dmp.Diff_EditCost = 4; + const diffs = dmp.diff_main(lastFullText, lastPromptText); + dmp.diff_cleanupEfficiency(diffs); + diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + } else { + diffDisplay.innerHTML = ''; + } + } + } + } catch {} } refreshMaintenance(); From bcac4ad01709c85002e2f8123406bacffcf1d4cd Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sat, 19 Jul 2025 21:58:37 -0500 Subject: [PATCH 46/48] Improve debug tab layout --- options/options.html | 15 +++++++++------ options/options.js | 22 ++++++++++++++++++++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/options/options.html b/options/options.html index 3618a20..ddc5ee0 100644 --- a/options/options.html +++ b/options/options.html @@ -154,6 +154,11 @@ Aggressive token reduction +
+ +
@@ -220,11 +225,6 @@
-
- -
@@ -290,7 +290,10 @@ Debug

-                
+ diff --git a/options/options.js b/options/options.js index 3402dcf..d881d6a 100644 --- a/options/options.js +++ b/options/options.js @@ -70,6 +70,7 @@ document.addEventListener('DOMContentLoaded', async () => { await applyTheme(themeSelect.value); const payloadDisplay = document.getElementById('payload-display'); const diffDisplay = document.getElementById('diff-display'); + const diffContainer = document.getElementById('diff-container'); let lastFullText = defaults.lastFullText || ''; let lastPromptText = defaults.lastPromptText || ''; @@ -83,7 +84,16 @@ document.addEventListener('DOMContentLoaded', async () => { dmp.Diff_EditCost = 4; const diffs = dmp.diff_main(lastFullText, lastPromptText); dmp.diff_cleanupEfficiency(diffs); - diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + const hasDiff = diffs.some(d => d[0] !== 0); + if (hasDiff) { + diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + diffContainer.classList.remove('is-hidden'); + } else { + diffDisplay.innerHTML = ''; + diffContainer.classList.add('is-hidden'); + } + } else { + diffContainer.classList.add('is-hidden'); } themeSelect.addEventListener('change', async () => { markDirty(); @@ -751,9 +761,17 @@ document.addEventListener('DOMContentLoaded', async () => { dmp.Diff_EditCost = 4; const diffs = dmp.diff_main(lastFullText, lastPromptText); dmp.diff_cleanupEfficiency(diffs); - diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + const hasDiff = diffs.some(d => d[0] !== 0); + if (hasDiff) { + diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + diffContainer.classList.remove('is-hidden'); + } else { + diffDisplay.innerHTML = ''; + diffContainer.classList.add('is-hidden'); + } } else { diffDisplay.innerHTML = ''; + diffContainer.classList.add('is-hidden'); } } } From 9cad2674e3841fbc40d237f7233fbd6784f61760 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sat, 19 Jul 2025 22:44:04 -0500 Subject: [PATCH 47/48] Capture raw message text for debug --- README.md | 2 +- background.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 82649d0..57a0285 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ message meets a specified criterion. - **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page. - **Markdown conversion** – optionally convert HTML bodies to Markdown before sending them to the AI service. - **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service. -- **Debug tab** – view the last request payload and message diff with live updates. +- **Debug tab** – view the last request payload and a diff between the unaltered message text and the final prompt. - **Light/Dark themes** – automatically match Thunderbird's appearance with optional manual override. - **Automatic rules** – create rules that tag, move, copy, forward, reply, delete, archive, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages and can ignore messages outside a chosen age range. - **Rule ordering** – drag rules to prioritize them and optionally stop processing after a match. diff --git a/background.js b/background.js index 58a5de5..fc585ff 100644 --- a/background.js +++ b/background.js @@ -210,17 +210,38 @@ function collectText(part, bodyParts, attachments) { } } -function buildEmailText(full) { +function collectRawText(part, bodyParts, attachments) { + if (part.parts && part.parts.length) { + for (const p of part.parts) collectRawText(p, bodyParts, attachments); + return; + } + const ct = (part.contentType || "text/plain").toLowerCase(); + const cd = (part.headers?.["content-disposition"]?.[0] || "").toLowerCase(); + const body = String(part.body || ""); + if (cd.includes("attachment") || !ct.startsWith("text/")) { + const nameMatch = /filename\s*=\s*"?([^";]+)/i.exec(cd) || /name\s*=\s*"?([^";]+)/i.exec(part.headers?.["content-type"]?.[0] || ""); + const name = nameMatch ? nameMatch[1] : ""; + attachments.push(`${name} (${ct}, ${part.size || byteSize(body)} bytes)`); + } else if (ct.startsWith("text/html")) { + const doc = new DOMParser().parseFromString(body, 'text/html'); + bodyParts.push(doc.body.textContent || ""); + } else { + bodyParts.push(body); + } +} + +function buildEmailText(full, applyTransforms = true) { const bodyParts = []; const attachments = []; - collectText(full, bodyParts, attachments); + const collect = applyTransforms ? collectText : collectRawText; + collect(full, bodyParts, attachments); const headers = Object.entries(full.headers || {}) .map(([k, v]) => `${k}: ${v.join(' ')}`) .join('\n'); const attachInfo = `Attachments: ${attachments.length}` + (attachments.length ? "\n" + attachments.map(a => ` - ${a}`).join('\n') : ""); let combined = `${headers}\n${attachInfo}\n\n${bodyParts.join('\n')}`.trim(); - if (tokenReduction) { + if (applyTransforms && tokenReduction) { const seen = new Set(); combined = combined.split('\n').filter(l => { if (seen.has(l)) return false; @@ -228,7 +249,7 @@ function buildEmailText(full) { return true; }).join('\n'); } - return sanitizeString(combined); + return applyTransforms ? sanitizeString(combined) : combined; } function updateTimingStats(elapsed) { @@ -262,8 +283,8 @@ async function processMessage(id) { updateActionIcon(); try { const full = await messenger.messages.getFull(id); + const originalText = buildEmailText(full, false); let text = buildEmailText(full); - const originalText = text; if (tokenReduction && maxTokens > 0) { const limit = Math.floor(maxTokens * 0.9); if (text.length > limit) { From c622c07c66a6936c4c8d3bb039b1f0bba4692384 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Mon, 18 Aug 2025 19:39:35 -0500 Subject: [PATCH 48/48] Supporting v140+ After testing in Betterbird v140, updating manifest. --- manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index a3c9f7c..e7cb9d8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,13 +1,13 @@ { "manifest_version": 2, "name": "Sortana", - "version": "2.1.2", + "version": "2.2.0", "default_locale": "en-US", "applications": { "gecko": { "id": "ai-filter@jordanwages", "strict_min_version": "128.0", - "strict_max_version": "139.*" + "strict_max_version": "140.*" } }, "icons": {