From 046c376448dd9fbb74f49882bb8d3dde384548f6 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:46:58 +0200 Subject: [PATCH 1/4] feat: serve platform logos as 80 px webp Shown at 40 px, the 96 px PNG logos were 136 KiB of the home page. WebP at twice the display size is 27 KiB for the twelve raster marks. SVG stays as each project publishes it. --- docs_assets/logos/amberelec.png | Bin 5202 -> 0 bytes docs_assets/logos/amberelec.webp | Bin 0 -> 1608 bytes docs_assets/logos/bizhawk.png | Bin 9207 -> 0 bytes docs_assets/logos/bizhawk.webp | Bin 0 -> 2128 bytes docs_assets/logos/emudeck.png | Bin 11793 -> 0 bytes docs_assets/logos/emudeck.webp | Bin 0 -> 2210 bytes docs_assets/logos/emuelec.png | Bin 12839 -> 0 bytes docs_assets/logos/emuelec.webp | Bin 0 -> 1578 bytes docs_assets/logos/knulli.png | Bin 9856 -> 0 bytes docs_assets/logos/knulli.webp | Bin 0 -> 1962 bytes docs_assets/logos/minui.png | Bin 6158 -> 0 bytes docs_assets/logos/minui.webp | Bin 0 -> 2254 bytes docs_assets/logos/misterfpga.png | Bin 12939 -> 0 bytes docs_assets/logos/misterfpga.webp | Bin 0 -> 1966 bytes docs_assets/logos/nextui.png | Bin 17587 -> 0 bytes docs_assets/logos/nextui.webp | Bin 0 -> 2286 bytes docs_assets/logos/retrobat.png | Bin 11980 -> 0 bytes docs_assets/logos/retrobat.webp | Bin 0 -> 3660 bytes docs_assets/logos/retropie.png | Bin 6916 -> 0 bytes docs_assets/logos/retropie.webp | Bin 0 -> 2492 bytes docs_assets/logos/rocknix.png | Bin 7276 -> 0 bytes docs_assets/logos/rocknix.webp | Bin 0 -> 2946 bytes docs_assets/logos/romm.png | Bin 5979 -> 0 bytes docs_assets/logos/romm.webp | Bin 0 -> 1824 bytes docs_assets/logos/sources.json | 24 ++++++++++++------------ scripts/fetch_site_images.py | 19 ++++++++++++------- 26 files changed, 24 insertions(+), 19 deletions(-) delete mode 100644 docs_assets/logos/amberelec.png create mode 100644 docs_assets/logos/amberelec.webp delete mode 100644 docs_assets/logos/bizhawk.png create mode 100644 docs_assets/logos/bizhawk.webp delete mode 100644 docs_assets/logos/emudeck.png create mode 100644 docs_assets/logos/emudeck.webp delete mode 100644 docs_assets/logos/emuelec.png create mode 100644 docs_assets/logos/emuelec.webp delete mode 100644 docs_assets/logos/knulli.png create mode 100644 docs_assets/logos/knulli.webp delete mode 100644 docs_assets/logos/minui.png create mode 100644 docs_assets/logos/minui.webp delete mode 100644 docs_assets/logos/misterfpga.png create mode 100644 docs_assets/logos/misterfpga.webp delete mode 100644 docs_assets/logos/nextui.png create mode 100644 docs_assets/logos/nextui.webp delete mode 100644 docs_assets/logos/retrobat.png create mode 100644 docs_assets/logos/retrobat.webp delete mode 100644 docs_assets/logos/retropie.png create mode 100644 docs_assets/logos/retropie.webp delete mode 100644 docs_assets/logos/rocknix.png create mode 100644 docs_assets/logos/rocknix.webp delete mode 100644 docs_assets/logos/romm.png create mode 100644 docs_assets/logos/romm.webp diff --git a/docs_assets/logos/amberelec.png b/docs_assets/logos/amberelec.png deleted file mode 100644 index ddc713f4374ec7272208d0d636da8e27c5747c70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5202 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 KN2N;17V$r4;O$NT diff --git a/docs_assets/logos/amberelec.webp b/docs_assets/logos/amberelec.webp new file mode 100644 index 0000000000000000000000000000000000000000..f517613973b424d65b405cfed3f7dd6b7077f171 GIT binary patch literal 1608 zcmV-O2DkZANk&FM1^@t8MM6+kP&gno1^@uiApo5LDo_AW06sAih(jVFAr@;jR3HNc zvjA%N;9mFi4nVzG@UBQ71-$3_fc!W9)6{RI2kNh=582TO~i1e|7x8<_7KGU=QTK(0|nWFMnhAZ|v{#U+&-Uzbw9je~$k$ z{~i6$_dEOd|Nj=hvEIp_vWPQm328jki`Znl!5JONXfsm~n5!n#XIEKFbZW-e)qdw( zhoXOkEX8bX(D`cRir3Z8Yz>b_zF8}y*b+Zdb!gq{<`v_8}kY5)NK{|#jq zMh&8}ENR%|K@x%~3Z>_?D`|hiA_{>1lMk_jUVCO0TjY)EKAgl@-Mp};M%vFzel zLfjT@d2jZS>e7;z#*u-X1)`~-iMjICcZHZ!W!?XVor92*386J+fH;2jMQzHGSMy)? zteyFsj?JE*RleBE7js2?yEwO1>@ZzEUFp~<_WfP_QyQYWEHAw`yk~yx4e6lwY&+3L zTd?~Hc%Tw%)Y}V2##eDRn$;-5-`IQWyh8F4iGpW${M~V!J~zpZ^;2b}A)}Rw=<6)= zSr_{{m)k(iQ_tnak2|yu={??eyiUoiQLv$Mp>L4pnf3=-P0K;XS5HxE7*>0;}MGygaEmb9tog%)yq@ER-@3Za%I zfwW5F6XO$kuxnI4=hWW7xT003Ygn-l0wlQRs&zPX1H9QT!PSq7p!*7 zCp{Zfz2F36tZ16)>Qg&tK0`aDC+%QEkbC#nP$roTI@ zoO4d-+(fD%^^z2&v`7yzVz^*E<_7w+`o&mz3{%)&>6Trxtkl49u+O-ct6^L^~)R17G1x zjAoT)d%~ufib6+xl@GS=PY`dnJjYnXwvJN108!}SxLlOg_M_ZXC% zklJ@oe4+}}ujfQhv_4x{AO6NjMfO;qfqkN~w%%8GsWkqd6i<_ox33xIzlr})Qdk)Q zcBnyDw{Y(qX_okZ5stlImZ8vM6Zh2M)&HEcNBD?nk8eEy?3Sl8_YikpdY*;v(nc_P z6J#`ZbjSekTF-}|k74$62~9RMr3>BQnnlL@gPxuagi2q+}^6BL?- zI~GXCDoE!#>40IWT|wcdbA_K6%`)e>tJX8pO6qrH%9($Y&hm0rb2zbv?tHxFsw&qz zlWyF~mZiWBI3RhD31A0btTc!KsyBYsfaYUW{{zcQ=_fXUwC38ZNY#d04jHZ@Fq!}W G0000Q(K2cP literal 0 HcmV?d00001 diff --git a/docs_assets/logos/bizhawk.png b/docs_assets/logos/bizhawk.png deleted file mode 100644 index 511597b29170bc90ce886c59f169cd7248053b9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9207 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 diff --git a/docs_assets/logos/emudeck.webp b/docs_assets/logos/emudeck.webp new file mode 100644 index 0000000000000000000000000000000000000000..011379cf33ef13864ff6a0597f7fb6a4654d06ef GIT binary patch literal 2210 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 FV1gIwpKJgC diff --git a/docs_assets/logos/emuelec.webp b/docs_assets/logos/emuelec.webp new file mode 100644 index 0000000000000000000000000000000000000000..1201668d64a882ce20eee384e9bb53239abacad7 GIT binary patch literal 1578 zcmV+_2G#jeNk&E@1^@t8MM6+kP&gnK1^@uC9RQsHDo_AW06sAoi9@0xp%F|4ydVPv zv4C91_QUJ!xqYFY2y^Hj&Kt}awG`j>x3yXV{fm8P7I8cHvi$@7evKC@B>+D;|9bnG z`cnRX{`Zytoqz2Ak@yDwWBot=A7TGWf)}Zs2aLTa*o$)`w@I5|D80`vg_OLjyfke7 zt~22q%w_#3r6P|i`ow54NM1LLTMET zePRXvYonT|d5Z+TNmEC&CN7h_cS?NBo`rcV=!RX9mx`5ao8cVa)l0092}o{pWk{rRLLGx;7Ga+`zfJtu$q3ZLLq`!Td>^y~raWqQM; z#9Q8o2g4YA(mjtQ${4c|@jTARQC%rMu;2Io=7qXn;-FJY;30G=n&wSwktrw&JW%EP z2V*q;=i-dVC;tfRb&ds_?vlC06^vCTuSYQ>AzEbX=j znJ-R0gM%k%-D-pGSn>5_XhUnghF8}9e&}AB>zac6C;qhj5LJ!^4jsqjC*~(-f*B_i z&5=K}K%s>|DQ5!7;(9oioRR!8V)p|LdChA}>B*NF&nr*f$a_3Qp^MatHA}dybI=x* zkv3xMCr9l_eQ$@DLv{oflT%9=ygq!V`C$1dP?q@ytyrjdQX3YndO>IwO5mSOjjM?( zNW*QOZBLF^KLyLn)@G-k&$B$!&+5nJ#;Pb|-qqGT^cOSvGb0@gGLI$k3gnS!d~n7T z>%T}oMX`0n(koC8y_}vOF5JETv^<`K1HrPbC&$wl4d752R2uuM2DoaFRW;Y3Xgn_u zU9k3Bc;S`8?=9}4XsngsilP1n&rSs-+J1r4={IiLlUeipBB1>fYe}QYT*q!hculNg- z%>iONb8@zs%nZRVlo&pr4NGfK>OOnc7z6YZ#> z7r=i{Uut7a1XW<8+{ar@=PLz8H61M!|1dPy*T|nMf74nF*!kBw@Jh*isjTj*1|JI$ z1Ygc{n7Lfg=v80opMJypG|a*Rv%l(6;Om-qggzW627A#T^gN?q=$cOj|Dw3(0Ja$O z>FR(jmkUsp`|1tZ`@pH$VX7dXL6^KHh0k_Ks|PxdO4gr}<|xwDz1&TcE>)$ zD372|aXUERb!)1Iz9kwcIiD;{_~jDV8t9L0>B7vvB@EViMGOuizrqOkDMR)0S`AK` zffy}4c#8zk!H{RfJIWaA03LUF(6;z;=csR;#ng&!23%Q`N zZzUSdj1o|&jUmsQLwDEeye{;xQT-v@kDI8F-B`Y*$73n_w~u`2REw!r&xWX?Td5M? ceXKd>j^eeGCJi=a&V13gxrQ5$uZB&~0RPA%8~^|S literal 0 HcmV?d00001 diff --git a/docs_assets/logos/knulli.png b/docs_assets/logos/knulli.png deleted file mode 100644 index fc8885f1cc93c4e1eef4f3dd11b96ef00a9bc027..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9856 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

qaN=xTjXZ*sulyR6kXqVevLDxnT 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 literal 0 HcmV?d00001 diff --git a/docs_assets/logos/minui.png b/docs_assets/logos/minui.png deleted file mode 100644 index cc59063a47ea4a9dd79a94cb309174bb781f9ad7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6158 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 diff --git a/docs_assets/logos/minui.webp b/docs_assets/logos/minui.webp new file mode 100644 index 0000000000000000000000000000000000000000..94ea79816cd161f525c470e1c304285e0bccd769 GIT binary patch literal 2254 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 BQ-S~h diff --git a/docs_assets/logos/misterfpga.webp b/docs_assets/logos/misterfpga.webp new file mode 100644 index 0000000000000000000000000000000000000000..45c44e4a7490d158e6108d5080694b59b019a21e GIT binary patch literal 1966 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 diff --git a/docs_assets/logos/nextui.webp b/docs_assets/logos/nextui.webp new file mode 100644 index 0000000000000000000000000000000000000000..8c5968a002bf3cb94ceecf3c40dc591620071fcf GIT binary patch literal 2286 zcmVUo@ps6XoehWG{kWBq6TM`RyI zv|(%`zohE%*_e2G{W24(9;Yz9?mI3+ZcCylXRav0#=1(WHP?)HgXi|MS!N5$8)x|c z1IVtFg3#ynIj12ZZa;0N_L4X8s8&SZ_8?JYEAY%BEWmcsJhkI$)t!JOZ*A*L4q0Tv z3W8H-{H2N0wc^#Bm3rf5-FkonFc`2J^j{D_8=Z84eAd?tDnB+_-hD36tUv$&{`?m& zfPt}7c|-=vnekYF<)8iXyQP7QuK)iC0A_@5Tr0DM0wtHINfWE4iF}oiVK&^zNGjtQoC>iHI-(Ph% z8{8vY?ltG!q-^{y^?jfiGKU{v}o2$xo@Es{6@c57&;KOJ$`AecKNr^0QC0X<0=$oTn8;s1xP?A%bN?;U-FIg z`A)k3yO1l9(_t^}hx-n}6EG~an*TMI;%+MTI}ru4s9XfPb@B7!&Ryoz6^Lw!FW($$ z#r4W^94osRuQi<|>9mXdTLNDA(O6<>3w+P+&S-wK9-UdjnJPzT|bbM)x~+fn8^B+bK_r~=wPU|&)D zl+*5t3DSGzky1VF0>AUvhdq(L=9xCa4_+J}<>(uMN3!v%=hrKMmp=YK|3dD?*e{K@ z_OBpW@&z&8zWr6bf74gT86|r)WXcU%R{4(!JEw&&P~n+4;^I9#Ih~ zix@Ev+;l6O+B|>gK!%CDk>jYV@?gE){+5`^N^ooZU2OP2_2_fIW*A|MF|=dUgzn(I z+t~W=T$&q^n*iB>{YN5luZLO5M~&2Bgp4-S)VQ$|tWOJ=bNEF0l_c1dZ-8aMHf<6< zS0)kuG&rOyB)S{M_kqj}N`;GsdTaTr1h{=Fis-U})wlA5eKZC-vi~&DYM{9V;$8hl zYnw=~ep6`ECk|(Q(y&dFjw9eTzr!wobu!7n`oCf~i|WPC0mVqLtvSG{eD6l^;&;VE z3%=yHYG=)Md`J4PMF~PfQ0FqosyfGC-sjq8>dD{3yCKQ{O_k))tEZYYD4n7uD-Rqg z-Mh+59)78jifZEim00e9JHD-S<-Q)EJR#Ndr#+n9N?{dqQoQ$qdZl*TvboRrA+&=gd~X6N_#d@V8tI;vZ33a| z;>PfSzyltOyKK{?Fl$Lud7h8RK1|L*2>0&ve5lqM(pAQL7k&gZ^WH-mt%&Qr@=9Jq zs2bF)#e>dLHbVi>`5o*R0JfeIIW+a|#tAI=v>jQXT{zZYRv+}|Ks8|=$bO;gtc>Bg zJ`Lg*p`D?iI$!vK$EQjPnO9Hc?tDpZx8E))khLgaGofvCtZW19184DsB~;`yyb#Z| z0xM-mV-b_|HtnGZO`N2Kksch26WnU`{FFXT^$Fj@6ZSdC(!XkvA zR&E1dG{;k&Fv!AMzyLfZ3L3eBCii3ryeZ33eA7v*6B*KYV#O9}d60p1${0?>g>*1I7RLr>cwj zusuR6y}xf#F{EcITY|?F7|}RX?h+nWlhW{E!*+0w+qu z46CbV6E{1HvJsES!afrh@3SxPZ$8R;7T8YvOg`IkJCGe2cV_dfg`RsEcVNjmWbD9HKjVh~8>YU5t I|Mjr|0KZh8wEzGB literal 0 HcmV?d00001 diff --git a/docs_assets/logos/retrobat.png b/docs_assets/logos/retrobat.png deleted file mode 100644 index 2b4395ac6ad1e8b598a1fde302121405103bf90f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11980 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 diff --git a/docs_assets/logos/retrobat.webp b/docs_assets/logos/retrobat.webp new file mode 100644 index 0000000000000000000000000000000000000000..b086f4f641a8bb754b996c8e73eb94dba81caaea GIT binary patch literal 3660 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 literal 0 HcmV?d00001 diff --git a/docs_assets/logos/retropie.png b/docs_assets/logos/retropie.png deleted file mode 100644 index e388562524f2802ff6a21750a961ae3ef76b2e6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6916 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 KMNUMnLSTaGz;QwV diff --git a/docs_assets/logos/retropie.webp b/docs_assets/logos/retropie.webp new file mode 100644 index 0000000000000000000000000000000000000000..70328f9b954c9ccee44675b6f91ce09aecf56ff4 GIT binary patch literal 2492 zcmV;t2}AZ$Nk&Gr2><|BMM6+kP&il$0000G0000_002(_06|PpNZSGc00E#a+qNOw zTA9b^+O}=mwr$(@?LOJQPquB_wrwk8#*7&=`bD0qm2>3>M8pK7i1i`|T>0odr))e$ z>c?>he?YrjzE~eD{5_bOyD4zuh~hQ?aVBWOePat-9(4w8c&aLDQdMWjhJ%W_4P|I$ zrh=xy$lzBNw6#kHN5__PCzb(Y-eT&X@Ei@R7c)-s2;23}4#kZ4CD+=;qTsSV3 zKgpGDE$%<8m3nao+LTjLKTZzbc2G}@l{R79o~VCnYgoM}(pzoHVtsKN_CI6xzBmg7 z+?l#Bj&8#z>WdwamOn=J#ac8iUsrvxG}7{NZ_JDV#*L{L$4)+OX&i7Ko^0$Sqe~jO z)V8}G@r0nxgvI~5XTR+?p0|ou&Aau9pVQ$J+pIKAtQj8#5D_(ZAHdz+L_`73nzD*A z_o;7yiaH&dh{Kl`Oc!Dyz>RUqzrcN3N65GK_;W&4^K^J5%LZ-lY)4R>(7vQmQdao^&o+ zhH+DsX*-uP#CJ%U7eNbQ)m+tErX`3qqjIGcDA8fFlz*`Y+@qOaFiYxkQo^~9OWj_M z#ZoR#9WRK+GUn>4{t}Pmd{}jrj(3S9en{$kUNn+1b9ePySRCVt6mUx9AowvV;Ve$^W}&b=~UMk%DyS+>;y3;i;~VmBMAGYuw#F7>rQ#A zj*}Jz>q_~4x)gbi-w^M@louySv8Q+r#B-U#rK3~ydZqWk#C!;M6^A?LNF|P3?>YXj zYUb{4D#rgWU$9DrRL=(X;#?&giG#x_`#blIA-%jCaF^A09H^q zAhHGk08k?UodGIP08juvF%*eIA|W9aDh8Aw0|d4JXRH@JSMpcb#-RMq@Gmlfo9grO zA8VfZy-j(5evyBS>b>Rz_Cxi9-UIum>o>a(Ko94C>AK841N^T)>VL}kHT|jmx%Qv` z>3OgBPK%Eo`xTr+?GNF8(EngL)Ays2t=Dh?^$m-)6J(|I+Su$ z+jorlYlRQw$`ut3T0Xcw$PwgK;fvzQhf?9=y%)?GxNB@{vh+1us&@B#y!2WAfIuJej@44z0WGm4Pl?5I7jVP= z?*qVsZYzFY7Nwcsbjp5)+sS3pjOSQKPB^^(08x?P)-A0ZfHN2S+`N&g>8DV5_}WeT-`OK^j&?I5I+1XC{>tmU-M4air~yxih=lwjG1Zgo9n|1ps?{#gk~E3r;oQr*>HeMKzsws+U-Z~|f9mowP^`8AH^DiVenKZbSv@Ptxi z1M4Cby@VxvEh$Um`4afICNmnT_z$%wYr3kF4I1kf!aFwnD@2O_BNG0}clqb=MFb3_ z4lwd^;`i?nZ=A?mtdu zO~x=z0p@F|2-`;K{^B-XDxxLp{`G{E%jVEKd#+4+2TpnL3l(G_`31j>_WM0HcXvYZXXk^zR#qyq2H{zUgJIbVt6q2#)GrR*)-*O9ey zL%u9Ff34th#;|G6Ad$U$0)s%L|G=Xf^q&W_&2lU+`;R$TA-H;vX~KyJr*q{c zZtwSLKl&qMa6C+Wn-IPJ&hCLy6OB;HSn1NEnuf{3VL2R&*#Yrtl^%TcT0#*v)?>T; zE`Is9P)lAU1AwQ8WQBlb1rkk-+Wb zUV8LN3Ye~ZU+t|2!Aa3W$i1vov*xMnI!FiA`nzKx$vtKNNWl{Cd;bpQZOeV* G1ONbB*Xtty literal 0 HcmV?d00001 diff --git a/docs_assets/logos/rocknix.png b/docs_assets/logos/rocknix.png deleted file mode 100644 index 8158615dd6587ca243591cabbf12a5862b9ce2a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7276 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 diff --git a/docs_assets/logos/rocknix.webp b/docs_assets/logos/rocknix.webp new file mode 100644 index 0000000000000000000000000000000000000000..b75370eeaeb06810efa520e8028d3f71e4b81157 GIT binary patch literal 2946 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 literal 0 HcmV?d00001 diff --git a/docs_assets/logos/romm.png b/docs_assets/logos/romm.png deleted file mode 100644 index 71d38413a8bd48bb63cf874f38b7d8c4ab8ee148..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5979 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 JPDHLkV1gcGrAYt) diff --git a/docs_assets/logos/romm.webp b/docs_assets/logos/romm.webp new file mode 100644 index 0000000000000000000000000000000000000000..0a2fafaf9312a9c8bb8484fcd4b536149ac4ba8b GIT binary patch literal 1824 zcmV+*2jBQoNk&E(2LJ$9MM6+kP&il$0000G0000_002(_06|PpNKFC&00E$-NRlN< zy8lu4kvN9qIF4c1M`u)KM&IJ7_>S&X@(S}EiiJW~9D0Q-&tiPJYBN>l z*iO(IlmSmeTw%y^s@>OD|Q-yrm_2aSsb zjZz=u@9F*;;%fwbjKQy&`WoVM@@tH_q}TC^UYA!Szn%DvkG1TC_nqKVF?M)og`QD*dmem*u&uq!n2)iJc_8@)D|8>;VFmAE_#I~OrmDAC z!CMr5jTO41`UC)0P&go@1ONcA7yz9CDo_AW06sAkh(e+vAr&hJ3?Kspu>fS@2roY= ze+~HQ&VR}M_U0+KdC_gD^8xx_`yKAl=mYWtg%{8Rr5DO0HAnh?YyZ|cT;5gfa`>MH z4+dpkWUur*Y3TySud1)||Fd3t{)zSp{c7ti=*FkoHPX`M+jke(bwertM;DAWtV{gd zp+aOd)u$2caLu%OaQ}O{8_i#&#_KNX!j|`JaIL_KTE%18_ zk+r(99bkl~1(hehM?Q`z0-6r@t!#>A-1p@yB}h+54LOdHchKW@dKG=nBCy$V+AN#q zrB#Oj6Fn;r$?92$=KN8-1%6XJJ`}(I>(~AM+P8YkwG&#HD%w92|8&v%9rl+!1s>-~ z2m2DQC+5mMgJK>EuDr9OL82b<~h-$devg;>O<2JXI~$0JndQ=&x>(!`I&) zB@r?6FN2Ih0o)XKWiX$Bk103_ZB}MNn2(}qX$4~uOE{-^(28ENYK7nQwGv~z-x%#k z7h42vkWKl2sO~AN$xgT|1{q?V0#exV)L|sg^W*~aA3jBln$En$e!1+ekciILznl(3Ho0j?gjN5t zAjp`6Og~9_|HfEWtls9=kGMpSc5I)x!Q^8fR?+XH*!D*(-nQznr!IcHt+^alHde8d?Fll`o-am-c& zKb4A&ZVvJQTOAwy>tD6};Tir;?1Ptv=xtV4K}YpZu!lo$qk$4#|CVR&F3M>ZpBZXV zo0WODO+mKH-{KpktJdgN9JG$iJe6W3quHDEf;K~=7|sqhyVcA6OSAK literal 0 HcmV?d00001 diff --git a/docs_assets/logos/sources.json b/docs_assets/logos/sources.json index 590c551d..c5a3f527 100644 --- a/docs_assets/logos/sources.json +++ b/docs_assets/logos/sources.json @@ -2,7 +2,7 @@ "note": "Each platform's own logo, owned by its project and shown only to name that platform. Copied once by scripts/fetch_site_images.py.", "images": [ { - "file": "amberelec.png", + "file": "amberelec.webp", "platform": "amberelec", "source": "https://avatars.githubusercontent.com/u/102820909", "owner": "AmberELEC", @@ -16,28 +16,28 @@ "licence": "repository licence MIT" }, { - "file": "bizhawk.png", + "file": "bizhawk.webp", "platform": "bizhawk", "source": "https://avatars.githubusercontent.com/u/11743303", "owner": "TASVideos", "licence": "GitHub organisation avatar of TASEmulators" }, { - "file": "emudeck.png", + "file": "emudeck.webp", "platform": "emudeck", "source": "https://raw.githubusercontent.com/dragoonDorise/EmuDeck/main/icons/EmuDeck.png", "owner": "EmuDeck", "licence": "repository licence GPL-3.0" }, { - "file": "emuelec.png", + "file": "emuelec.webp", "platform": "emuelec", "source": "https://avatars.githubusercontent.com/u/57574835", "owner": "EmuELEC", "licence": "GitHub organisation avatar of EmuELEC" }, { - "file": "knulli.png", + "file": "knulli.webp", "platform": "knulli", "source": "https://avatars.githubusercontent.com/u/166268572", "owner": "KNULLI", @@ -51,21 +51,21 @@ "licence": "file from libretro/retroarch-assets, CC-BY-4.0" }, { - "file": "minui.png", + "file": "minui.webp", "platform": "minui", "source": "https://avatars.githubusercontent.com/u/225589", "owner": "Shaun Inman", "licence": "GitHub avatar of shauninman, the author of MinUI" }, { - "file": "misterfpga.png", + "file": "misterfpga.webp", "platform": "misterfpga", "source": "https://avatars.githubusercontent.com/u/29345272", "owner": "MiSTer FPGA", "licence": "GitHub organisation avatar of MiSTer-devel" }, { - "file": "nextui.png", + "file": "nextui.webp", "platform": "nextui", "source": "https://avatars.githubusercontent.com/u/202386615", "owner": "LoveRetro", @@ -86,7 +86,7 @@ "licence": "repository licence GPL-3.0" }, { - "file": "retrobat.png", + "file": "retrobat.webp", "platform": "retrobat", "source": "https://raw.githubusercontent.com/RetroBat-Official/retrobat/main/system/resources/retrobat_logo_notext.png", "owner": "RetroBat", @@ -100,21 +100,21 @@ "licence": "repository licence GPL-3.0" }, { - "file": "retropie.png", + "file": "retropie.webp", "platform": "retropie", "source": "https://avatars.githubusercontent.com/u/11378204", "owner": "RetroPie", "licence": "GitHub organisation avatar of RetroPie" }, { - "file": "rocknix.png", + "file": "rocknix.webp", "platform": "rocknix", "source": "https://avatars.githubusercontent.com/u/163176886", "owner": "ROCKNIX", "licence": "GitHub organisation avatar of ROCKNIX" }, { - "file": "romm.png", + "file": "romm.webp", "platform": "romm", "source": "https://avatars.githubusercontent.com/u/164058018", "owner": "RomM", diff --git a/scripts/fetch_site_images.py b/scripts/fetch_site_images.py index 49ae239b..783f911c 100644 --- a/scripts/fetch_site_images.py +++ b/scripts/fetch_site_images.py @@ -8,8 +8,9 @@ into ``docs_assets/logos/`` and ``docs_assets/system-icons/``, which are committed, and records where each came from in a ``sources.json`` beside them. The site build only reads those copies (``sitediagrams.localize_images``). -Raster images are normalised to a square PNG (96 px for logos, 48 px for -system icons) so every tile has the same weight, and SVG is kept as published. +Raster logos are normalised to an 80 px square WebP, twice the 40 px they +are shown at, and system icons to a 48 px PNG, so every tile has the same +weight. SVG is kept as published. Normalising needs Pillow. This is a maintenance tool run by hand when a logo changes, not a build step: the build stays on the standard library and PyYAML. @@ -36,7 +37,7 @@ sys.path.insert(0, str(ROOT / "scripts")) LOGO_DIR = ROOT / "docs_assets" / "logos" ICON_DIR = ROOT / "docs_assets" / "system-icons" -LOGO_SIZE = 96 +LOGO_SIZE = 80 ICON_SIZE = 48 AVATAR_HOST = "avatars.githubusercontent.com" USER_AGENT = "retrobios-site-images" @@ -47,7 +48,7 @@ def fetch(url: str) -> bytes: return response.read() -def square_png(data: bytes, size: int) -> bytes: +def square_image(data: bytes, size: int, kind: str = "PNG") -> bytes: try: from PIL import Image except ImportError as exc: @@ -57,7 +58,11 @@ def square_png(data: bytes, size: int) -> bytes: tile = Image.new("RGBA", (size, size), (0, 0, 0, 0)) tile.paste(image, ((size - image.width) // 2, (size - image.height) // 2), image) out = io.BytesIO() - tile.save(out, "PNG", optimize=True) + if kind == "WEBP": + # Quality 90: half the bytes of lossless, edges still clean at 40 px. + tile.save(out, "WEBP", quality=90, method=6) + else: + tile.save(out, "PNG", optimize=True) return out.getvalue() @@ -98,7 +103,7 @@ def fetch_logos() -> None: if is_svg(data): file_name, payload = f"{name}.svg", safe_svg(data, url) else: - file_name, payload = f"{name}.png", square_png(data, LOGO_SIZE) + file_name, payload = f"{name}.webp", square_image(data, LOGO_SIZE, "WEBP") for stale in LOGO_DIR.glob(f"{name}.*"): if stale.name != file_name: stale.unlink() @@ -141,7 +146,7 @@ def fetch_icons() -> None: continue raise file_name = f"{name}.png" - (ICON_DIR / file_name).write_bytes(square_png(data, ICON_SIZE)) + (ICON_DIR / file_name).write_bytes(square_image(data, ICON_SIZE)) entries.append({"file": file_name, "source": url}) print(f" {file_name}") write_manifest( From e19c3f86c41f18dfd1af5adca3fc6a9d8b1316b6 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:47:05 +0200 Subject: [PATCH 2/4] fix: title logo plate and footer licence The title mark sits on the tiles' white 40 px square with its width written, so a black logo stays visible in the dark theme. A setup with no logo shows its initials. The level label is dropped on a tab's own page. The footer licence line now matches LICENSE. --- website/src/components/Footer.astro | 2 +- website/src/components/PageTitle.astro | 48 ++++++++++++++++++++------ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/website/src/components/Footer.astro b/website/src/components/Footer.astro index a038c1c6..e96db4d8 100644 --- a/website/src/components/Footer.astro +++ b/website/src/components/Footer.astro @@ -29,7 +29,7 @@ const date = releaseDate(tag); Packs from release {tag}{date && <>, }.

diff --git a/website/src/components/PageTitle.astro b/website/src/components/PageTitle.astro index c0dd8d4d..168949b3 100644 --- a/website/src/components/PageTitle.astro +++ b/website/src/components/PageTitle.astro @@ -1,9 +1,16 @@ --- // Breadcrumbs, the level label and the title. The trail says which tab a -// deep page belongs to; the label says who the page is written for, and is -// left out where the first crumb already names that tab. A page with a -// `logo` (a path under the site root) shows it beside its title. +// deep page belongs to, and the label says who the page is written for. The +// label is left out where the header tab already says it: on a tab's own +// page, and where the first crumb names that tab. A page with a `logo` (a +// path under the site root) shows it beside its title, on a white square so +// a dark mark stays visible in the dark theme. A setup page with no logo file +// gets its initials in the same square, as its tile does (sitemarkup.py +// platform_tiles). +import setups from '../../public/data/picker/index.json'; + const PAGE_TITLE_ID = '_top'; +const MARK = 40; const LABELS = { start: 'Start here', @@ -22,11 +29,19 @@ const TABS = { contribute: 'Contribute', } as const; -const { title, level, logo } = Astro.locals.starlightRoute.entry.data; +// Up to two capitals of a name: MyMinUI is MM (sitemarkup.initials). +const initials = (name: string): string => { + const capitals = [...name].filter((c) => c !== c.toLowerCase()); + return capitals.slice(0, 2).join('') || name.slice(0, 2).toUpperCase(); +}; + +const { id, data } = Astro.locals.starlightRoute.entry; +const { title, level, logo } = data; const crumbs = Astro.locals.rbBreadcrumbs ?? []; -const badge = level && crumbs[0]?.label !== TABS[level] && crumbs[0]?.label !== LABELS[level]; +const badge = level && crumbs.length > 0 && crumbs[0].label !== TABS[level] && crumbs[0].label !== LABELS[level]; const base = import.meta.env.BASE_URL.replace(/\/?$/, '/'); const logoSrc = logo && (/^(https?:)?\//.test(logo) ? logo : base + logo); +const setup = !logoSrc && setups.find((entry) => id === `platforms/${entry.platform}`); --- { @@ -42,9 +57,17 @@ const logoSrc = logo && (/^(https?:)?\//.test(logo) ? logo : base + logo); } {badge && {LABELS[level]}} { - logoSrc ? ( + logoSrc || setup ? ( ) : ( @@ -67,11 +90,16 @@ const logoSrc = logo && (/^(https?:)?\//.test(logo) ? logo : base + logo); margin-top: 0.25rem; } .rb-title-logo h1 { margin-top: 0; } - .rb-title-logo img { + /* The tiles' square: white in both themes, a hairline, the mark inside. */ + .rb-title-mark { flex: none; - width: auto; + box-sizing: border-box; + width: 40px; height: 40px; - max-width: 8rem; + padding: 3px; + border: 1px solid var(--rb-border); + border-radius: 6px; + background: #fff; object-fit: contain; } From b239be4984b579437e57598fbdf11d4b19262480 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:47:05 +0200 Subject: [PATCH 3/4] feat: hash verdict, built filters, unbroken flags Tables of four columns stack on a phone, and a long table's filter box is written at build time, which ends the layout shift on gaps-files-to-source. Find a file names its counts, its emulators and accepts md5=, crc32= and sha256=, hashes a chosen file on the device and points an unknown hash to contributing. A hash typed in the search folds the pages that only mention it. Words holding a hyphen in command blocks no longer break across lines. --- website/astro.config.mjs | 18 +- website/src/components/FilePicker.astro | 24 +- website/src/components/Header.astro | 17 + website/src/components/MarkdownContent.astro | 21 + website/src/plugins/ec-unbroken-tokens.mjs | 96 ++++ website/src/plugins/rehype-stack-tables.mjs | 111 ++++- website/src/scripts/site.ts | 476 ++++++++++++++++--- 7 files changed, 663 insertions(+), 100 deletions(-) create mode 100644 website/src/plugins/ec-unbroken-tokens.mjs diff --git a/website/astro.config.mjs b/website/astro.config.mjs index cb2996f8..71fa2b65 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -13,6 +13,7 @@ import starlightLinksValidator from 'starlight-links-validator'; import sidebar from './src/generated/sidebar.json' with { type: 'json' }; import lastmod from './src/generated/lastmod.json' with { type: 'json' }; import stackTables from './src/plugins/rehype-stack-tables.mjs'; +import unbrokenTokens from './src/plugins/ec-unbroken-tokens.mjs'; const SITE = 'https://abdess.github.io'; const BASE = '/retrobios/'; @@ -84,11 +85,14 @@ export default defineConfig({ routeMiddleware: ['./src/routeData.ts'], tableOfContents: { minHeadingLevel: 2, maxHeadingLevel: 3 }, // Commands wrap instead of scrolling, so the end of a one-liner stays - // visible. A command block is titled (Terminal, PowerShell) and drawn - // without the three window dots. Starlight writes the dots into each - // theme after the global style overrides, so they are set here. + // visible, and never inside a word that holds a hyphen (`--platform`, + // `nintendo-64dd`: src/plugins/ec-unbroken-tokens.mjs). A command block + // is titled (Terminal, PowerShell) and drawn without the three window + // dots. Starlight writes the dots into each theme after the global + // style overrides, so they are set here. expressiveCode: { defaultProps: { wrap: true }, + plugins: [unbrokenTokens()], customizeTheme(theme) { theme.styleOverrides.frames = { ...theme.styleOverrides.frames, terminalTitlebarDotsOpacity: '0' }; return theme; @@ -102,14 +106,18 @@ export default defineConfig({ // ten times its text, so "dolphin" finds Dolphin, and the file names of // a system page (its `files` field, written by sitestarlight.py) count // half, so "scph5501" finds the Sony page without a file name winning - // every other query. + // every other query. Two fields name the page's subject in other words + // and weigh half a title: `aliases`, the ids setups file a system under + // (psx, dreamcast, gba), and `devices`, the handhelds and boxes a + // setup's own site names (R36S, Miyoo Mini). All three are attributes + // on an empty element, so no text of theirs reaches an excerpt. pagefind: { ranking: { pageLength: 0.1, termFrequency: 0.5, termSaturation: 1.4, termSimilarity: 9, - metaWeights: { title: 10, files: 0.5 }, + metaWeights: { title: 10, files: 0.5, aliases: 5, devices: 5 }, }, }, plugins: [ diff --git a/website/src/components/FilePicker.astro b/website/src/components/FilePicker.astro index 6444d7a4..1c09e871 100644 --- a/website/src/components/FilePicker.astro +++ b/website/src/components/FilePicker.astro @@ -66,6 +66,25 @@ const packs = Object.fromEntries( const INSTALL = 'curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s --'; const MAX_ROWS = 500; + // A word that holds a hyphen (--platform, nintendo-64dd) never breaks + // inside: the same rule as the command blocks (ec-unbroken-tokens.mjs). + // Only spans are added, the text copied is the command itself. + const UNBROKEN = /(? { + const nodes: (Node | string)[] = []; + let last = 0; + for (const match of command.matchAll(UNBROKEN)) { + nodes.push(command.slice(last, match.index)); + const word = document.createElement('span'); + word.className = 'rb-nb'; + word.textContent = match[0]; + nodes.push(word); + last = match.index! + match[0].length; + } + nodes.push(command.slice(last)); + target.replaceChildren(...nodes); + }; const size = (bytes: number) => bytes >= 1024 ** 3 ? `${(bytes / 1024 ** 3).toFixed(1)} GB` @@ -107,7 +126,7 @@ const packs = Object.fromEntries( ? `${count(files.length)} files, ${size(total)}, for ${count(picked.length)} of ${count(data.systems.length + (data.other ? 1 : 0))} choices. Folder: ${data.folder || 'the setup BIOS folder'}.` : 'Choose at least one system.'; output.hidden = !picked.length; - cmd.textContent = `${INSTALL} --platform ${data.platform}${all ? '' : ` --system ${picked.join(',')}`}`; + showCommand(cmd, `${INSTALL} --platform ${data.platform}${all ? '' : ` --system ${picked.join(',')}`}`); tbody.replaceChildren(...files.slice(0, MAX_ROWS).map(([dest, bytes]) => { const row = document.createElement('tr'); const where = document.createElement('td'); @@ -221,7 +240,8 @@ const packs = Object.fromEntries( .rb-picker .rb-pick-systems-head { display: flex; gap: 0.5rem; align-items: center; } .rb-picker .rb-pick-systems-head > * { margin: 0; } .rb-picker .rb-pick-filter { width: 100%; margin: 0.5rem 0; } - .rb-picker .rb-pick-cmd code { white-space: pre-wrap; overflow-wrap: anywhere; } + .rb-picker .rb-pick-cmd code { white-space: pre-wrap; overflow-wrap: break-word; } + .rb-picker .rb-pick-cmd .rb-nb { white-space: nowrap; } .rb-picker .rb-pick-result { margin-top: 1rem; font-weight: 600; } .rb-picker fieldset { flex-direction: column; diff --git a/website/src/components/Header.astro b/website/src/components/Header.astro index 271f7904..b8d5299b 100644 --- a/website/src/components/Header.astro +++ b/website/src/components/Header.astro @@ -38,7 +38,24 @@ const topics = Astro.locals.starlightSidebarTopics?.topics ?? []; border-radius: 0.5rem; overflow-wrap: anywhere; } + .rb-search-hash p { margin: 0; } .rb-search-hash a { font-weight: 600; } + /* Under a hash, the pages that only mention its characters stay folded. */ + .pagefind-ui__drawer[data-rb-hash]:not([data-rb-pages-shown]) .pagefind-ui__results-area { display: none; } + .rb-search-hash .rb-search-pages { + min-height: 24px; + margin-top: 0.5rem; + padding: 0; + border: 0; + background: none; + color: var(--sl-color-gray-2); + font: inherit; + font-size: var(--sl-text-sm); + text-decoration: underline; + cursor: pointer; + } + .rb-search-hash .rb-search-pages::before { content: '+ '; } + .rb-search-hash .rb-search-pages[aria-expanded='true']::before { content: '- '; } /* Pagefind's own display rules would show what site.ts hides. */ #starlight__search [hidden] { display: none !important; } diff --git a/website/src/components/MarkdownContent.astro b/website/src/components/MarkdownContent.astro index 402cedf4..bb507b3b 100644 --- a/website/src/components/MarkdownContent.astro +++ b/website/src/components/MarkdownContent.astro @@ -30,4 +30,25 @@ const { searchWeight } = Astro.locals.starlightRoute.entry.data; max-width: 40rem; box-sizing: border-box; } + /* The file chooser site.ts adds under the form, and the four hashes it + shows once the file is read. */ + .sl-markdown-content .rb-file-pick { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.35rem; margin-top: 1rem; } + .sl-markdown-content .rb-file-pick > * { margin: 0; } + .sl-markdown-content .rb-file-pick label { font-weight: 600; } + .sl-markdown-content .rb-file-pick input { width: 100%; min-width: 0; font: inherit; } + .sl-markdown-content .rb-file-pick input::file-selector-button { + min-height: 44px; + margin-inline-end: 0.75rem; + padding: 0 1rem; + border: 1px solid var(--rb-border-strong); + border-radius: 4px; + background: var(--sl-color-bg); + color: var(--sl-color-text); + font: inherit; + font-weight: 600; + cursor: pointer; + } + .sl-markdown-content .rb-file-pick-note { color: var(--sl-color-gray-3); font-size: var(--sl-text-sm); } + .sl-markdown-content .rb-file-pick-hashes code { overflow-wrap: anywhere; } + .sl-markdown-content .rb-file-pick-hashes[hidden] { display: none; } diff --git a/website/src/plugins/ec-unbroken-tokens.mjs b/website/src/plugins/ec-unbroken-tokens.mjs new file mode 100644 index 00000000..ab59a79d --- /dev/null +++ b/website/src/plugins/ec-unbroken-tokens.mjs @@ -0,0 +1,96 @@ +// Command blocks wrap (astro.config.mjs, wrap: true) so the end of a +// one-liner stays visible. A browser may also break a line after a hyphen, +// which split `--platform` into `--` and `platform`, and `nintendo-64dd` +// into `nintendo-` and `64dd`: a reader who types the command reads two +// things. Each word that holds a hyphen is put in a span that does not +// break inside. Only spans are added: the text, the copy button's +// data-code and a selection copied by hand are unchanged. +// +// A word longer than LONGEST is left breakable: an address that long would +// push the block wider than a phone. + +export const LONGEST = 48; +export const UNBROKEN = new RegExp(`(? (node.type === 'text' ? node.value : (node.children ?? []).map(textOf).join('')); + +// A token span holds one text node: it can be cut in two, both halves +// keeping its colours. +const cuttable = (node) => + node.type === 'text' || (node.type === 'element' && node.children?.length === 1 && node.children[0].type === 'text'); + +const slice = (node, from, to) => + node.type === 'text' + ? { type: 'text', value: node.value.slice(from, to) } + : { ...node, properties: { ...node.properties }, children: [{ type: 'text', value: node.children[0].value.slice(from, to) }] }; + +// The children of *container*, with every hyphenated word wrapped in one +// unbreakable span. A word that crosses a node this cannot cut is left as is. +export const unbreakWords = (container) => { + const children = container.children ?? []; + const text = children.map(textOf).join(''); + const ranges = [...text.matchAll(UNBROKEN)].map((match) => [match.index, match.index + match[0].length]); + if (!ranges.length) return children; + const cuts = [...new Set(ranges.flat())].sort((a, b) => a - b); + const pieces = []; + let position = 0; + for (const child of children) { + const end = position + textOf(child).length; + const inner = cuts.filter((cut) => cut > position && cut < end); + if (inner.length && cuttable(child)) { + let start = position; + for (const cut of [...inner, end]) { + pieces.push({ node: slice(child, start - position, cut - position), from: start, to: cut }); + start = cut; + } + } else { + pieces.push({ node: child, from: position, to: end }); + } + position = end; + } + const ends = new Map(ranges); + const out = []; + for (let index = 0; index < pieces.length; ) { + const end = ends.get(pieces[index].from); + if (end !== undefined && pieces[index].to > pieces[index].from) { + let last = index; + while (last < pieces.length && pieces[last].to < end) last += 1; + if (last < pieces.length && pieces[last].to === end) { + out.push({ + type: 'element', + tagName: 'span', + properties: { className: [CLASS] }, + children: pieces.slice(index, last + 1).map((piece) => piece.node), + }); + index = last + 1; + continue; + } + } + out.push(pieces[index].node); + index += 1; + } + return out; +}; + +// The element of a rendered line that holds its tokens (div.code), else +// the line itself. +const tokensOf = (line) => { + for (const child of line.children ?? []) { + if (child.type === 'element' && String(child.properties?.className ?? '').split(/[\s,]+/).includes('code')) return child; + } + return line; +}; + +export default function unbrokenTokens() { + return { + name: 'rb-unbroken-tokens', + baseStyles: `.${CLASS} { white-space: nowrap; }`, + hooks: { + postprocessRenderedLine({ renderData }) { + const container = tokensOf(renderData.lineAst); + container.children = unbreakWords(container); + }, + }, + }; +} diff --git a/website/src/plugins/rehype-stack-tables.mjs b/website/src/plugins/rehype-stack-tables.mjs index 29b9235a..061d6bfa 100644 --- a/website/src/plugins/rehype-stack-tables.mjs +++ b/website/src/plugins/rehype-stack-tables.mjs @@ -1,15 +1,22 @@ -// Tables a phone reads one row at a time, marked when the page is built so -// the stacked layout is there on first paint (the CSS is retrobios.css, -// table[data-rb-stack]). The rule reads the table, never the page that -// wrote it: +// Tables, settled when the page is built so the layout is right on first +// paint (the CSS is retrobios.css, table[data-rb-stack] and .rb-table-filter). +// The rule reads the table, never the page that wrote it. +// +// Which tables a phone reads one row at a time: +// - two columns: a list of label and value, data-rb-stack="kv"; +// - four columns or more: too wide for a phone column, one block per row, +// each cell under its column name; // - a row that ends in an action (a release download, a button, the link to // a setup's install command) or a header that names a ZIP column: one -// block per row, each cell under its column name; -// - two columns: a list of label and value, data-rb-stack="kv"; -// - on a level 0 or 1 page (`level: start` or `setup`), a table short enough -// to read as records (under 20 rows, the size from which site.ts adds a -// filter): one block per row. A longer one is a list scanned by column and -// keeps its scroll. +// block per row, so the action is never behind a horizontal scroll; +// - on a level 0 or 1 page (`level: start` or `setup`), a three-column table +// short enough to read as records (under RECORD_ROWS rows): one block per +// row. A longer one is a list scanned by column and keeps its scroll. +// +// A table of RECORD_ROWS rows or more gets its filter box here, above it. +// Written by site.ts once the page had painted, the box pushed the table +// down (CLS 0.066 on gaps-files-to-source). site.ts only wires it. +// // Satteri runs hast plugins (the rehype stage of its pipeline): this one is // registered in astro.config.mjs through satteri({ hastPlugins }). @@ -17,7 +24,11 @@ const ACTION_HREF = /\/releases\/(download\/|latest\/?$)|#install$/; const ACTION_CLASS = /\bclass=["'][^"']*\brb-button\b/; const DOWNLOAD_HEADER = /^(zip|download)$/i; const READER_LEVELS = new Set(['start', 'setup']); +const HEADING = /^h[1-6]$/; export const RECORD_ROWS = 20; +export const WIDE_COLUMNS = 4; +// The column names a filter's placeholder offers, at most. +const PLACEHOLDER_COLUMNS = 4; const elements = (node, tag, found = []) => { for (const child of node.children ?? []) { @@ -29,23 +40,87 @@ const elements = (node, tag, found = []) => { return found; }; +const classes = (node) => String(node.properties?.className ?? '').split(/[\s,]+/); + const holdsAction = (node) => (node.children ?? []).some( (child) => (child.type === 'element' && ((child.tagName === 'a' && ACTION_HREF.test(String(child.properties?.href ?? ''))) || - String(child.properties?.className ?? '').split(/[\s,]+/).includes('rb-button') || + classes(child).includes('rb-button') || holdsAction(child))) || (child.type === 'raw' && ACTION_CLASS.test(child.value ?? '')), ); export const stackKind = ({ labels, rows, action, level }) => { if (labels.length === 2) return 'kv'; + if (labels.length >= WIDE_COLUMNS) return 'true'; if (action || DOWNLOAD_HEADER.test(labels.at(-1) ?? '')) return 'true'; if (READER_LEVELS.has(level) && labels.length > 2 && rows < RECORD_ROWS) return 'true'; return null; }; +// "File, region, required, hash...": what the rows can be filtered by, in +// the table's own words. A word that is not capitalised prose (MD5, ZIP) +// keeps its case. +export const filterPlaceholder = (labels) => { + const words = labels.filter(Boolean).slice(0, PLACEHOLDER_COLUMNS); + if (!words.length) return 'Type to filter...'; + const lowered = words.map((word, index) => + index > 0 && /^[A-Z][a-z]/.test(word) ? word[0].toLowerCase() + word.slice(1) : word, + ); + return `${lowered.join(', ')}...`; +}; + +// The heading a table sits under: the nearest one before it, looking up +// through the blocks that hold it, else the page title. site.ts names the +// table's scroll region the same way. +const headingBefore = (ctx, table, title) => { + for (let block = table; block; block = ctx.parent(block)) { + const parent = ctx.parent(block); + if (!parent) break; + const siblings = parent.children ?? []; + for (let index = (ctx.indexOf(block) ?? 0) - 1; index >= 0; index -= 1) { + const node = siblings[index]; + if (node.type !== 'element') continue; + const heading = HEADING.test(node.tagName) + ? node + : classes(node).includes('sl-heading-wrapper') + ? (node.children ?? []).find((child) => child.type === 'element' && HEADING.test(child.tagName)) + : undefined; + const text = heading && ctx.textContent(heading).trim(); + if (text) return text; + } + } + return title || 'this table'; +}; + +const filterBox = ({ id, heading, labels, rows }) => ({ + type: 'element', + tagName: 'div', + properties: { className: ['rb-table-filter'] }, + children: [ + { + type: 'element', + tagName: 'label', + properties: { htmlFor: id }, + children: [{ type: 'text', value: `Filter ${heading}` }], + }, + { + type: 'element', + tagName: 'input', + properties: { id, type: 'search', autoComplete: 'off', placeholder: filterPlaceholder(labels) }, + children: [], + }, + { + type: 'element', + tagName: 'span', + properties: { className: ['rb-table-filter-count'], ariaLive: 'polite' }, + children: [{ type: 'text', value: `${rows.toLocaleString('en')} rows` }], + }, + ], +}); + export default function stackTables() { return { name: 'rb-stack-tables', @@ -57,11 +132,19 @@ export default function stackTables() { if (!head || !body) return; const labels = elements(head, 'th').map((cell) => ctx.textContent(cell).trim()); const rows = elements(body, 'tr'); - const level = ctx.data.astro?.frontmatter?.level; - const kind = stackKind({ labels, rows: rows.length, action: holdsAction(body), level }); + const frontmatter = ctx.data.astro?.frontmatter ?? {}; + if (rows.length >= RECORD_ROWS) { + ctx.data.rbFilters = (ctx.data.rbFilters ?? 0) + 1; + const heading = headingBefore(ctx, table, frontmatter.title); + ctx.insertBefore(table, filterBox({ id: `rb-table-filter-${ctx.data.rbFilters}`, heading, labels, rows: rows.length })); + } + const kind = stackKind({ labels, rows: rows.length, action: holdsAction(body), level: frontmatter.level }); if (!kind) return; ctx.setProperty(table, 'dataRbStack', kind); - if (kind === 'kv') return; + // A long table's labels are written by site.ts when it nears the + // screen: on mame they were 122 KB of the page. The label is drawn + // in the room the cell already keeps for it, so nothing moves. + if (kind === 'kv' || rows.length >= RECORD_ROWS) return; for (const row of rows) { elements(row, 'td').forEach((cell, column) => { if (labels[column] && cell.properties?.dataLabel === undefined) ctx.setProperty(cell, 'dataLabel', labels[column]); diff --git a/website/src/scripts/site.ts b/website/src/scripts/site.ts index c503a0ee..07066177 100644 --- a/website/src/scripts/site.ts +++ b/website/src/scripts/site.ts @@ -1,17 +1,22 @@ // Progressive enhancement of the generated pages: table captions, filters -// and keyboard scrolling, the hash lookup on files/, moved cards, a hash -// typed in the search, glossary terms on a tap, and the 404 page's hints. +// and keyboard scrolling, the hash lookup on files/ (a pasted hash or a file +// hashed on this device), moved cards, a hash typed in the search, glossary +// terms on a tap, the OS tabs kept in view, and the 404 page's hints. // Which tables stack on a phone is decided when the page is built // (src/plugins/rehype-stack-tables.mjs), so the layout is right on first paint. const HASH_LENGTHS: Record = { 8: 'CRC32', 32: 'MD5', 40: 'SHA1', 64: 'SHA256' }; -const LAYER_LABELS: Record = { - collection: 'In the collection', - 'data-directory': 'In a data directory the packs ship', - platform: 'Declared by a platform', - emulator: 'Read by an emulator', +// One line of api/v1/lookup/. `system_name` is read when a record carries it. +type Item = { + algorithm: string; + layer: string; + name?: string; + value: string; + url: string; + owner?: string; + sha1?: string; + system_name?: string; }; -type Item = { algorithm: string; layer: string; name?: string; value: string; url: string; owner?: string }; const siteRoot = () => new URL(import.meta.env.BASE_URL.replace(/\/?$/, '/'), location.origin); const normalizeHash = (raw: string) => raw.trim().toLowerCase().replace(/^0x/, '').replace(/\s+/g, ''); @@ -75,26 +80,17 @@ const setScrollable = (table: HTMLTableElement, overflows: boolean) => { } }; -const filterTable = (table: HTMLTableElement, index: number, heading: string) => { +// The filter box above a long table is written when the page is built +// (src/plugins/rehype-stack-tables.mjs), so the table never moves down once +// drawn. Here it only filters. +const filterTable = (table: HTMLTableElement) => { + const controls = table.previousElementSibling; + if (!controls?.matches('.rb-table-filter')) return; + const input = controls.querySelector('input'); + const count = controls.querySelector('.rb-table-filter-count'); + if (!input || !count || input.dataset.rbWired) return; + input.dataset.rbWired = 'true'; const rows = [...table.querySelectorAll('tbody tr')]; - if (rows.length < 20) return; - const controls = document.createElement('div'); - controls.className = 'rb-table-filter'; - const id = `rb-table-filter-${index}`; - const label = document.createElement('label'); - label.htmlFor = id; - label.textContent = `Filter ${heading}`; - const input = document.createElement('input'); - input.id = id; - input.type = 'search'; - input.autocomplete = 'off'; - input.placeholder = 'Name, platform, system, hash...'; - const count = document.createElement('span'); - count.className = 'rb-table-filter-count'; - count.setAttribute('aria-live', 'polite'); - count.textContent = `${rows.length} rows`; - controls.append(label, input, count); - table.before(controls); // Each row's text is read on the first keystroke, not on every page load. let searchable: { row: HTMLTableRowElement; text: string }[] | null = null; input.addEventListener('input', () => { @@ -105,14 +101,31 @@ const filterTable = (table: HTMLTableElement, index: number, heading: string) => entry.row.hidden = !!query && !entry.text.includes(query); if (!entry.row.hidden) visible += 1; } - count.textContent = `${visible} of ${rows.length} rows`; + count.textContent = query + ? `${visible.toLocaleString('en')} of ${rows.length.toLocaleString('en')} rows` + : `${rows.length.toLocaleString('en')} rows`; }); }; +// A stacked table of many rows leaves its column names out of the HTML +// (rehype-stack-tables.mjs): each cell gets its own here, as the table nears +// the screen. The name sits in the left room every stacked cell keeps, so +// writing it moves nothing. +const labelCells = (table: HTMLTableElement) => { + if (table.dataset.rbStack !== 'true') return; + const labels = [...table.querySelectorAll('thead th')].map((cell) => cell.textContent?.trim() ?? ''); + for (const row of table.tBodies[0]?.rows ?? []) { + [...row.cells].forEach((cell, column) => { + if (labels[column] && !cell.dataset.label) cell.dataset.label = labels[column]; + }); + } +}; + const enhanceTables = () => { const tables = [...document.querySelectorAll('.sl-markdown-content table')]; const enhance = (table: HTMLTableElement) => { for (const cell of table.querySelectorAll('thead th')) if (!cell.hasAttribute('scope')) cell.setAttribute('scope', 'col'); + labelCells(table); const heading = tableName(table); if (!table.querySelector('caption') && !table.closest('.rb-meta-card')) { const caption = document.createElement('caption'); @@ -120,7 +133,7 @@ const enhanceTables = () => { caption.textContent = heading; table.prepend(caption); } - filterTable(table, tables.indexOf(table), heading); + filterTable(table); }; // A table is set up and measured when it nears the screen. Doing all of // them at load relaid out every row of a 3,800-row page after its first @@ -159,16 +172,100 @@ const enhanceTables = () => { }); }; +// Hashing a file on this device: SHA1 and SHA256 by Web Crypto, MD5 and +// CRC32 here, since Web Crypto has neither. The bytes never leave the page. + +const CRC_TABLE = (() => { + const table = new Uint32Array(256); + for (let n = 0; n < 256; n += 1) { + let c = n; + for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + table[n] = c >>> 0; + } + return table; +})(); + +const crc32 = (bytes: Uint8Array): string => { + let crc = 0xffffffff; + for (let i = 0; i < bytes.length; i += 1) crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8); + return ((crc ^ 0xffffffff) >>> 0).toString(16).padStart(8, '0'); +}; + +const MD5_SHIFTS = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21]; +const MD5_TABLE = Array.from({ length: 64 }, (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32) >>> 0); + +const hexBytes = (bytes: Uint8Array) => [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join(''); + +// RFC 1321, block by block over the file's own buffer: no padded copy of a +// 500 MB file. +const md5 = (bytes: Uint8Array): string => { + const state = new Int32Array([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]); + const words = new Int32Array(16); + const block = (view: DataView, offset: number) => { + for (let i = 0; i < 16; i += 1) words[i] = view.getInt32(offset + i * 4, true); + let [a, b, c, d] = state; + for (let i = 0; i < 64; i += 1) { + const round = i >> 4; + const f = round === 0 ? (b & c) | (~b & d) : round === 1 ? (d & b) | (~d & c) : round === 2 ? b ^ c ^ d : c ^ (b | ~d); + const g = round === 0 ? i : round === 1 ? (5 * i + 1) % 16 : round === 2 ? (3 * i + 5) % 16 : (7 * i) % 16; + const shift = MD5_SHIFTS[round * 4 + (i % 4)]; + const sum = (a + f + MD5_TABLE[i] + words[g]) | 0; + a = d; + d = c; + c = b; + b = (b + ((sum << shift) | (sum >>> (32 - shift)))) | 0; + } + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + }; + const whole = bytes.length - (bytes.length % 64); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + for (let offset = 0; offset < whole; offset += 64) block(view, offset); + const tail = new Uint8Array(bytes.length - whole < 56 ? 64 : 128); + tail.set(bytes.subarray(whole)); + tail[bytes.length - whole] = 0x80; + const tailView = new DataView(tail.buffer); + const bits = bytes.length * 8; + tailView.setUint32(tail.length - 8, bits >>> 0, true); + tailView.setUint32(tail.length - 4, Math.floor(bits / 2 ** 32), true); + for (let offset = 0; offset < tail.length; offset += 64) block(tailView, offset); + return hexBytes(new Uint8Array(state.buffer)); +}; + +type FileHashes = { sha1: string; md5: string; crc32: string; sha256: string }; + +const hashFile = async (file: Blob): Promise => { + const buffer = await file.arrayBuffer(); + const bytes = new Uint8Array(buffer); + const [sha1, sha256] = await Promise.all( + ['SHA-1', 'SHA-256'].map(async (name) => hexBytes(new Uint8Array(await crypto.subtle.digest(name, buffer)))), + ); + return { sha1, md5: md5(bytes), crc32: crc32(bytes), sha256 }; +}; + // files/?hash= reads one shard of the lookup, named by the first two hex -// characters of the value. +// characters of the value. ?md5=, ?crc32= and ?sha256= are read the same way. + +const HASH_PARAMS = ['hash', 'md5', 'crc32', 'sha256']; const lookupShard = (value: string): Promise => fetch(new URL(`api/v1/lookup/${value.slice(0, 2)}.json`, siteRoot())) .then((response) => (response.ok ? response.json() : { items: [] })) .then((shard) => shard.items || []); +const lookupValues = async (values: string[]): Promise => { + const prefixes = [...new Set(values.map((value) => value.slice(0, 2)))]; + const shards = await Promise.all(prefixes.map(lookupShard)); + const wanted = new Set(values); + return shards.flat().filter((item) => wanted.has(item.value)); +}; + type Held = { name: string; path: string; sha1: string; size: number }; type Declared = { name?: string; destination?: string; required?: boolean; [hash: string]: unknown }; +type ProfileFile = { name?: string; region?: string | string[]; category?: string; [field: string]: unknown }; +type Reader = { name: string; files: ProfileFile[] }; const json = (path: string) => fetch(new URL(path, siteRoot())).then((response) => (response.ok ? response.json() : Promise.reject(response.status))); @@ -204,15 +301,33 @@ const strong = (text: string) => { return element; }; +const count = (n: number, one: string, many: string) => `${n.toLocaleString('en')} ${n === 1 ? one : many}`; + +const listed = (words: string[]) => + words.length < 3 ? words.join(' and ') : `${words.slice(0, -1).join(', ')} and ${words.at(-1)}`; + +// A region: value in words, as the system pages write it. +const REGION_WORDS: Record = { uk: 'UK', 'asia-ntsc': 'Asia (NTSC)', 'asia-pal': 'Asia (PAL)' }; +const regionWord = (region: string) => + REGION_WORDS[region] ?? region.replace(/-/g, ' ').replace(/\b\w/g, (letter) => letter.toUpperCase()); +const CATEGORY_WORDS: Record = { bios: 'BIOS', game_data: 'game data', bios_zip: 'BIOS ZIP' }; + // The card a collection match points at, read from its manufacturer's file -// of api/v1/systems/: the path in the collection and the size. -const heldFile = async (item: Item & { sha1?: string }): Promise => { +// of api/v1/systems/: the path in the collection and the size. A console's +// own page (systems/sony-playstation/) is part of its manufacturer's file +// (systems/sony.json), found by dropping words from the end of the slug. +const heldFile = async (item: Item): Promise => { const slug = /^systems\/([^/]+)\//.exec(item.url)?.[1]; if (!slug || !item.sha1) return undefined; - const data = await json(`api/v1/systems/${slug}.json`); - for (const files of Object.values(data.items?.[0]?.consoles ?? {})) { - const found = files.find((file) => file.sha1 === item.sha1); - if (found) return found; + const words = slug.split('-'); + for (let size = words.length; size > 0; size -= 1) { + const data = await json(`api/v1/systems/${words.slice(0, size).join('-')}.json`).catch(() => undefined); + if (!data) continue; + for (const files of Object.values(data.items?.[0]?.consoles ?? {})) { + const found = files.find((file) => file.sha1 === item.sha1); + if (found) return found; + } + return undefined; } return undefined; }; @@ -220,15 +335,40 @@ const heldFile = async (item: Item & { sha1?: string }): Promise => { +const declaredEntry = async (item: Item): Promise => { const system = decodeURIComponent(new URL(item.url, siteRoot()).hash.slice(1)); const data = await json(`api/v1/platforms/${item.owner}.json`); const files: Declared[] = data.items?.[0]?.contract?.systems?.[system]?.files ?? []; const holds = (entry: Declared) => - String(entry[item.algorithm] ?? '').toLowerCase().split(',').map((hash) => hash.trim()).includes(value); + String(entry[item.algorithm] ?? '').toLowerCase().split(',').map((hash) => hash.trim()).includes(item.value); return files.find((entry) => holds(entry) && entry.name === item.name) ?? files.find(holds); }; +// An emulator by the name its page carries, with the entries of its profile. +const readerProfile = async (owner: string): Promise => { + const data = await json(`api/v1/emulators/${owner}.json`).catch(() => undefined); + const profile = data?.items?.[0]?.profile; + if (!profile) return undefined; + return { name: String(profile.emulator || profile.display_name || owner), files: profile.files ?? [] }; +}; + +// The entry of a profile that reads the value: by name and hash, else by hash. +const readerEntry = (reader: Reader | undefined, item: Item): ProfileFile | undefined => { + const holds = (entry: ProfileFile) => + String(entry[item.algorithm] ?? '').toLowerCase().split(',').map((hash) => hash.trim()).includes(item.value); + return reader?.files.find((entry) => holds(entry) && entry.name === item.name) ?? reader?.files.find(holds); +}; + +// One value every reader that says something agrees on, else nothing. +const agreed = (values: (string | undefined)[]): string | undefined => { + const said = new Set(values.filter((value): value is string => !!value)); + return said.size === 1 ? [...said][0] : undefined; +}; + +// A name a setup writes in place of a file name: EmuDeck lists a hash under +// "system:md5", and any file that has it passes. +const anyName = (name: string) => !name || name.includes(':'); + const setupTable = (rows: { item: Item; entry?: Declared; setup: string }[]) => { const table = document.createElement('table'); const head = table.createTHead().insertRow(); @@ -242,12 +382,10 @@ const setupTable = (rows: { item: Item; entry?: Declared; setup: string }[]) => for (const { item, entry, setup } of rows) { const row = body.insertRow(); const name = entry?.name ?? item.name ?? ''; - // EmuDeck lists a hash with no file name: any file that has it passes. - const named = name && !name.includes(':'); const destination = entry?.destination; const cells: (Node | string)[] = [ link(item.url, setup), - named ? code(name) : 'Any name', + anyName(name) ? 'Any name' : code(name), destination ? code(destination) : '-', item.algorithm.toUpperCase(), entry?.required === undefined ? '-' : entry.required ? 'Yes' : 'No', @@ -262,30 +400,83 @@ const setupTable = (rows: { item: Item; entry?: Declared; setup: string }[]) => return table; }; -// The answer first: which file this is and where the collection holds it, -// then the setups that list it (where it goes, whether it is required), -// then the emulators that read it. -const renderMatches = async (status: HTMLElement, result: HTMLElement, value: string, items: Item[]) => { +// One declaration per setup, name and section: a value found by two of its +// hashes (a file hashed here) is still one line of the setup's list. +const distinct = (items: Item[], key: (item: Item) => string) => { + const seen = new Map(); + for (const item of items) if (!seen.has(key(item))) seen.set(key(item), item); + return [...seen.values()]; +}; + +// The answer first: which file this is, for which system and region, and +// how many setups list it under which names. Then where the collection +// holds it, the setups that list it (where it goes, whether it is +// required), and the emulators that read it, by name. +const renderMatches = async (status: HTMLElement, result: HTMLElement, items: Item[]) => { const byLayer = (layer: string) => items.filter((item) => item.layer === layer); - const held = byLayer('collection'); - const platforms = byLayer('platform'); - const [files, entries, names] = await Promise.all([ + const held = distinct(byLayer('collection'), (item) => item.sha1 ?? item.url); + const platforms = distinct(byLayer('platform'), (item) => `${item.owner}|${item.name}|${item.url}`); + const emulators = distinct(byLayer('emulator'), (item) => `${item.owner}|${item.name}|${item.url}`); + const owners = [...new Set(emulators.map((item) => item.owner ?? ''))]; + const [files, entries, names, readers] = await Promise.all([ Promise.all(held.map((item) => heldFile(item).catch(() => undefined))), - Promise.all(platforms.map((item) => declaredEntry(item, value).catch(() => undefined))), + Promise.all(platforms.map((item) => declaredEntry(item).catch(() => undefined))), json('data/picker/index.json').catch(() => []) as Promise<{ platform: string; name: string }[]>, + Promise.all(owners.map((owner) => readerProfile(owner))), ]); const setupName = new Map(names.map((entry) => [entry.platform, entry.name])); - status.textContent = `${items.length.toLocaleString('en')} ${items.length === 1 ? 'match' : 'matches'} for ${HASH_LENGTHS[value.length]} ${value}.`; + const reader = new Map(owners.map((owner, index) => [owner, readers[index]])); + const readEntries = emulators.map((item) => readerEntry(reader.get(item.owner ?? ''), item)); + const setups = new Set(platforms.map((item) => item.owner)); + + const counted = [ + held.length ? count(held.length, 'file in the collection', 'files in the collection') : 'not in the collection', + ...(platforms.length ? [`${count(platforms.length, 'declaration', 'declarations')} by ${count(setups.size, 'setup', 'setups')}`] : []), + ...(owners.length ? [count(owners.length, 'emulator that reads it', 'emulators that read it')] : []), + ]; + status.textContent = `Found: ${counted.join(', ')}.`; + const blocks: Node[] = []; + const category = agreed(readEntries.map((entry) => entry && CATEGORY_WORDS[String(entry.category ?? 'bios')])); + const regions = agreed(readEntries.map((entry) => { + const raw = entry?.region; + const list = Array.isArray(raw) ? raw : raw ? [String(raw)] : []; + return list.length ? listed(list.map(regionWord)) : undefined; + })); held.forEach((item, index) => { const file = files[index]; - const name = file?.name ?? item.name ?? value; + const name = file?.name ?? item.name ?? item.value; + const system = item.system_name; + const what: (Node | string)[] = system + ? [', ', link(item.url, system), category ? ` ${category}` : ' file'] + : category ? [`, a ${category} file`] : []; + const others = new Map>(); + for (const declared of platforms) { + const other = declared.name ?? ''; + if (anyName(other) || other === name) continue; + if (!others.has(other)) others.set(other, new Set()); + others.get(other)!.add(declared.owner ?? ''); + } + const renamed = new Set([...others.values()].flatMap((set) => [...set])); + const otherNames = [...others.keys()].sort(); + const named: (Node | string)[] = []; + otherNames.forEach((other, position) => { + if (position) named.push(position === otherNames.length - 1 ? ' and ' : ', '); + named.push(code(other)); + }); + const listing: (Node | string)[] = setups.size + ? [ + ` ${count(setups.size, 'setup lists', 'setups list')} it`, + ...(renamed.size + ? [`, ${renamed.size.toLocaleString('en')} of them under the ${otherNames.length === 1 ? 'name' : 'names'} `, ...named] + : []), + '.', + ] + : []; blocks.push( - file - ? paragraph('This is ', code(name), ', held in the collection at ', code(file.path), '.') - : paragraph('This is ', code(name), ', held in the collection.'), + paragraph('This is ', code(name), ...what, ...(regions ? [` for ${regions}`] : []), '.', ...listing), paragraph( - ...(file ? ['SHA1 ', code(file.sha1), `, ${formatSize(file.size)}. `] : []), + ...(file ? ['Held in the collection at ', code(file.path), '. SHA1 ', code(file.sha1), `, ${formatSize(file.size)}. `] : []), link(item.url, 'Open its card'), '.', ), @@ -300,12 +491,11 @@ const renderMatches = async (status: HTMLElement, result: HTMLElement, value: st row.append(link(item.url, code(item.name || item.value))); list.append(row); } - blocks.push(paragraph(strong(LAYER_LABELS['data-directory'])), list); + blocks.push(paragraph(strong('In a data directory the packs ship')), list); } if (platforms.length) { - const owners = new Set(platforms.map((item) => item.owner)); blocks.push( - paragraph(strong(`Setups that list it (${owners.size})`)), + paragraph(strong('Setups that list it: '), `${count(platforms.length, 'declaration', 'declarations')} by ${count(setups.size, 'setup', 'setups')}`), setupTable(platforms.map((item, index) => ({ item, entry: entries[index], @@ -313,20 +503,96 @@ const renderMatches = async (status: HTMLElement, result: HTMLElement, value: st }))), ); } - const readers = byLayer('emulator'); - if (readers.length) { + if (owners.length) { const list = document.createElement('ul'); - for (const item of readers) { + emulators.forEach((item) => { const row = document.createElement('li'); - row.append(link(item.url, item.owner ?? item.name ?? '')); - if (item.name && item.name !== held[0]?.name) row.append(' as ', code(item.name)); + row.append(link(item.url, reader.get(item.owner ?? '')?.name ?? item.owner ?? item.name ?? '')); + if (item.name && item.name !== (files[0]?.name ?? held[0]?.name)) row.append(' as ', code(item.name)); list.append(row); - } - blocks.push(paragraph(strong(`${LAYER_LABELS.emulator} (${readers.length})`)), list); + }); + blocks.push(paragraph(strong(`Emulators that read it (${owners.length.toLocaleString('en')})`)), list); } result.replaceChildren(...blocks); }; +const WANTED = 'provenance/#wanted-catalogued-dumps-this-project-does-not-have'; + +// Nothing the site knows has the value: where a dump of one's own can go. +const renderNoMatch = (status: HTMLElement, result: HTMLElement) => { + status.textContent = 'No file, setup or emulator the site knows has this hash.'; + result.replaceChildren( + paragraph('If you dumped it yourself, ', link('contributing/', 'add it'), '.'), + paragraph('The ', link(WANTED, 'wanted list'), ' names the catalogued dumps the collection still lacks.'), + ); +}; + +const lookUp = (status: HTMLElement, result: HTMLElement, values: string[]) => { + status.textContent = 'Looking it up...'; + result.replaceChildren(); + return lookupValues(values) + .then((matches) => (matches.length ? renderMatches(status, result, matches) : renderNoMatch(status, result))) + .catch(() => { + status.textContent = 'The file index could not be read.'; + }); +}; + +// A file chosen on files/ is hashed here, its four hashes shown, and all +// four looked up: a setup may list only the MD5. +const MAX_HASHED = 512 * 1024 ** 2; + +const fileHasher = (status: HTMLElement, result: HTMLElement) => { + const form = document.querySelector('form.rb-file-lookup'); + if (!form) return; + const box = document.createElement('div'); + box.className = 'rb-file-pick'; + const label = document.createElement('label'); + label.htmlFor = 'rb-file'; + label.textContent = 'Or choose the file'; + const input = document.createElement('input'); + input.type = 'file'; + input.id = 'rb-file'; + const note = paragraph('It is hashed on this device. Nothing is uploaded.'); + note.className = 'rb-file-pick-note'; + const hashes = document.createElement('p'); + hashes.className = 'rb-file-pick-hashes'; + hashes.hidden = true; + box.append(label, input, note, hashes); + form.after(box); + input.addEventListener('change', async () => { + const file = input.files?.[0]; + if (!file) return; + hashes.hidden = true; + if (file.size > MAX_HASHED) { + status.textContent = `This page hashes files up to ${formatSize(MAX_HASHED)}. ${file.name} is ${formatSize(file.size)}.`; + result.replaceChildren(); + return; + } + status.textContent = `Hashing ${file.name}...`; + result.replaceChildren(); + let found: FileHashes; + try { + found = await hashFile(file); + } catch { + status.textContent = `${file.name} could not be read.`; + return; + } + hashes.replaceChildren( + strong(file.name), ` ${formatSize(file.size)}`, + ...(['sha1', 'md5', 'crc32', 'sha256'] as const).flatMap((algorithm) => [ + document.createElement('br'), `${algorithm.toUpperCase()} `, code(found[algorithm]), + ]), + ); + hashes.hidden = false; + const field = document.getElementById('rb-hash') as HTMLInputElement | null; + if (field) field.value = found.sha1; + const url = new URL(location.href); + url.search = `?hash=${found.sha1}`; + history.replaceState(null, '', url); + await lookUp(status, result, [found.sha1, found.md5, found.crc32, found.sha256]); + }); +}; + // files/?sha1= is a file's content address and opens its card // wherever it is now; files/?hash= also takes an MD5, CRC32 or SHA256 and // lists every page that names the value. @@ -336,9 +602,11 @@ const resolveFileAddress = () => { const result = document.createElement('div'); result.id = 'rb-file-lookup-result'; status.after(result); + fileHasher(status, result); const params = new URLSearchParams(location.search); const permalink = params.has('sha1'); - const value = normalizeHash(params.get('sha1') || params.get('hash') || ''); + const raw = params.get('sha1') || HASH_PARAMS.map((key) => params.get(key)).find(Boolean) || ''; + const value = normalizeHash(raw); if (!value) return; const input = document.getElementById('rb-hash') as HTMLInputElement | null; if (input) input.value = value; @@ -347,18 +615,22 @@ const resolveFileAddress = () => { status.textContent = 'A hash is 8 (CRC32), 32 (MD5), 40 (SHA1) or 64 (SHA256) hexadecimal characters.'; return; } + if (!prefix && !permalink) { + lookUp(status, result, [value]); + return; + } status.textContent = 'Looking it up...'; lookupShard(value) .then((items) => { const matches = items.filter((item) => prefix ? item.algorithm === 'sha1' && item.value.startsWith(value) : item.value === value); const cards = matches.filter((item) => item.layer === 'collection' || item.layer === 'data-directory'); - if (permalink && new Set(cards.map((item) => item.url)).size === 1) { + if (new Set(cards.map((item) => item.url)).size === 1) { location.replace(new URL(cards[0].url, siteRoot())); } else if (matches.length) { - return renderMatches(status, result, prefix ? matches[0].value : value, prefix ? cards : matches); + return renderMatches(status, result, prefix ? cards : matches); } else { - status.textContent = 'No file the site knows has this hash.'; + renderNoMatch(status, result); } }) .catch(() => { @@ -383,32 +655,52 @@ const followMovedCard = () => { }; // The site search holds no hashes. A hash typed in it gets "Look up this -// hash" as its first result, and Enter opens the lookup. -const hashLookupRow = (drawer: Element, value: string) => { +// hash" as its first result, and Enter opens the lookup. The pages Pagefind +// finds for the same characters (a file name that holds them, a word that +// starts like them) are no answer: they stay folded under a button that +// says what they are. +const hashLookupRow = (drawer: HTMLElement, value: string) => { let row = drawer.querySelector(':scope > .rb-search-hash'); if (!isHash(value)) { row?.remove(); + delete drawer.dataset.rbHash; + delete drawer.dataset.rbPagesShown; return; } + drawer.dataset.rbHash = ''; + const target = new URL(`files/?hash=${value}`, siteRoot()).href; + if (row?.querySelector('a')?.href === target) return; if (!row) { - row = document.createElement('p'); + row = document.createElement('div'); row.className = 'rb-search-hash'; drawer.prepend(row); } - const target = new URL(`files/?hash=${value}`, siteRoot()).href; - if (row.querySelector('a')?.href === target) return; + delete drawer.dataset.rbPagesShown; const anchor = document.createElement('a'); anchor.href = target; anchor.append('Look up this hash'); - row.replaceChildren(anchor, ` ${HASH_LENGTHS[value.length]} `, code(value)); + const toggle = document.createElement('button'); + toggle.type = 'button'; + toggle.className = 'rb-search-pages'; + toggle.setAttribute('aria-expanded', 'false'); + toggle.textContent = 'Pages that mention these characters'; + toggle.addEventListener('click', () => { + const open = toggle.getAttribute('aria-expanded') !== 'true'; + toggle.setAttribute('aria-expanded', String(open)); + if (open) drawer.dataset.rbPagesShown = ''; + else delete drawer.dataset.rbPagesShown; + }); + row.replaceChildren(paragraph(anchor, ` ${HASH_LENGTHS[value.length]} `, code(value)), toggle); }; // Pagefind lists a page's title twice: once for the page and once for the // anchor of its h1 (#_top). The second title is hidden, its excerpt kept. -// The `files` field of a system page weighs its file names in the ranking -// and is never shown: a result is not a list of 120 names. Pagefind reuses +// The `files`, `aliases` and `devices` fields weigh in the ranking and are +// never shown: a result is not a list of 120 names. Pagefind reuses // its nodes from one query to the next, so both are worked out again on // every change. +const UNSHOWN_FIELDS = new Set(['files', 'aliases', 'devices']); + const tidyResults = (root: Element) => { for (const title of root.querySelectorAll('.pagefind-ui__result-nested > .pagefind-ui__result-title')) { title.hidden = !!title.querySelector('a')?.getAttribute('href')?.endsWith('#_top'); @@ -416,7 +708,7 @@ const tidyResults = (root: Element) => { for (const tags of root.querySelectorAll('.pagefind-ui__result-tags')) { let shown = 0; for (const tag of tags.querySelectorAll('.pagefind-ui__result-tag')) { - tag.hidden = tag.dataset.pagefindUiMeta === 'files'; + tag.hidden = UNSHOWN_FIELDS.has(tag.dataset.pagefindUiMeta ?? ''); if (!tag.hidden) shown += 1; } tags.hidden = shown === 0; @@ -429,7 +721,7 @@ const hashSearch = () => { const refresh = () => { tidyResults(root); const input = root.querySelector('input'); - const drawer = root.querySelector('.pagefind-ui__drawer'); + const drawer = root.querySelector('.pagefind-ui__drawer'); if (input && drawer) hashLookupRow(drawer, normalizeHash(input.value)); }; new MutationObserver(refresh).observe(root, { @@ -457,6 +749,31 @@ const hashSearch = () => { ); }; +// The OS tabs scroll sideways on a phone instead of wrapping to a second +// row (retrobios.css): the chosen one is kept in view, on load and on a +// change, by scrolling the row and never the page. +const keepTabInView = (tab: Element) => { + const list = tab.closest('[role="tablist"]'); + if (!list) return; + const row = list.getBoundingClientRect(); + const box = tab.getBoundingClientRect(); + if (box.left < row.left) list.scrollLeft -= row.left - box.left; + else if (box.right > row.right) list.scrollLeft += box.right - row.right; +}; + +const osTabs = () => { + const selected = () => document.querySelectorAll('starlight-tabs [role="tab"][aria-selected="true"]').forEach(keepTabInView); + if (!document.querySelector('starlight-tabs')) return; + const later = () => requestAnimationFrame(selected); + for (const type of ['click', 'keyup']) { + document.addEventListener(type, (event) => { + if ((event.target as Element | null)?.closest?.('starlight-tabs [role="tab"]')) later(); + }); + } + if (document.readyState === 'complete') later(); + else addEventListener('load', later, { once: true }); +}; + // A glossary term shows its definition on hover; a tap or Enter shows it // on a phone or to a keyboard, and Escape or a tap elsewhere closes it. const glossaryTerms = () => { @@ -505,4 +822,5 @@ followMovedCard(); enhanceTables(); hashSearch(); glossaryTerms(); +osTabs(); notFound(); From e3f7ebd4bc1e54c3325037f70281b83e0f1675ef Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:47:09 +0200 Subject: [PATCH 4/4] chore: tests for the round 3 site components --- tests/test_site_r2_components.py | 18 +- tests/test_site_r2_gate.py | 2 +- tests/test_site_r3_components.py | 390 +++++++++++++++++++++++++++++++ wiki/testing-guide.md | 2 +- 4 files changed, 402 insertions(+), 10 deletions(-) create mode 100644 tests/test_site_r3_components.py diff --git a/tests/test_site_r2_components.py b/tests/test_site_r2_components.py index 6aad12ed..449e2142 100644 --- a/tests/test_site_r2_components.py +++ b/tests/test_site_r2_components.py @@ -84,8 +84,10 @@ class TablesStackWhenBuilt(unittest.TestCase): "action": {"labels": ["A", "B", "C"], "rows": 40, "action": True}, "pair": {"labels": ["Where", "BIOS folder"], "rows": 3, "action": False}, "short-start": {"labels": ["System", "Runs", "BIOS folder"], "rows": 8, "action": False, "level": "start"}, - "long-setup": {"labels": ["System", "Files", "Status", "Emulators"], "rows": 97, "action": False, "level": "setup"}, + "long-setup": {"labels": ["System", "Files", "Status"], "rows": 97, "action": False, "level": "setup"}, "reference": {"labels": ["A", "B", "C"], "rows": 5, "action": False, "level": "reference"}, + # Four columns do not fit a phone column, on any page (round 3). + "wide": {"labels": ["File", "Region", "Required", "Hash"], "rows": 105, "action": False, "level": "reference"}, } script = ( f"import {{ stackKind }} from {json.dumps(PLUGIN.as_uri())};" @@ -98,7 +100,7 @@ class TablesStackWhenBuilt(unittest.TestCase): ) self.assertEqual(json.loads(result.stdout), { "zip-header": "true", "action": "true", "pair": "kv", - "short-start": "true", "long-setup": None, "reference": None, + "short-start": "true", "long-setup": None, "reference": None, "wide": "true", }) @@ -142,7 +144,7 @@ class SearchAnswersAHashAndShowsNoFileDump(unittest.TestCase): self.assertIn("Look up this hash", source) self.assertIn("files/?hash=${value}", source) self.assertIn("endsWith('#_top')", source) - self.assertIn("tag.dataset.pagefindUiMeta === 'files'", source) + self.assertIn("UNSHOWN_FIELDS.has(tag.dataset.pagefindUiMeta ?? '')", source) # Pagefind's display rules beat the hidden attribute without this. self.assertIn("#starlight__search [hidden] { display: none !important; }", _read(COMPONENTS / "Header.astro")) @@ -167,8 +169,8 @@ def _title_block(html: str) -> str: class LevelBadgeOnlyWhereItAddsSomething(unittest.TestCase): def test_badge_follows_the_crumb(self): - # A tab root has no crumb and keeps its badge. - self.assertIn('class="rb-level rb-level-setup', _title_block(_built("platforms"))) + # A tab root: the header tab already says it (round 3, 2.1). + self.assertNotIn('class="rb-level ', _title_block(_built("platforms"))) # Under the "Start here" or "Your setup" crumb the badge repeated it. for page in ("which-pack", "platforms/batocera"): with self.subTest(page=page): @@ -181,7 +183,7 @@ class TitleLogo(unittest.TestCase): def test_front_matter_field(self): self.assertIn("logo: z.string().optional()", _read(WEBSITE / "src" / "content.config.ts")) title = _read(COMPONENTS / "PageTitle.astro") - self.assertIn('', title) + self.assertIn('', title) class FooterSaysWhoAndWhen(unittest.TestCase): @@ -200,10 +202,10 @@ class FooterSaysWhoAndWhen(unittest.TestCase): class FindAFileGivesAVerdict(unittest.TestCase): def test_lookup_renderer(self): source = _read(SITE_TS) - self.assertIn("', held in the collection at '", source) + self.assertIn("'Held in the collection at '", source) self.assertIn("['Setup', 'Name as declared', 'Destination', 'Hash matched', 'Required']", source) self.assertIn("api/v1/platforms/${item.owner}.json", source) - self.assertIn("api/v1/systems/${slug}.json", source) + self.assertIn("api/v1/systems/${words.slice(0, size).join('-')}.json", source) style = _read(COMPONENTS / "MarkdownContent.astro") self.assertIn("max-width: 40rem;", style) self.assertIn("width: 100%;", style) diff --git a/tests/test_site_r2_gate.py b/tests/test_site_r2_gate.py index 3a08cfa8..a83dffa2 100644 --- a/tests/test_site_r2_gate.py +++ b/tests/test_site_r2_gate.py @@ -192,7 +192,7 @@ class TheIntegrationItems(unittest.TestCase): with self.subTest(platform=key): block = re.search(r'
.*?
', html, re.S) self.assertIsNotNone(block) - self.assertRegex(block[0], r' str: + return path.read_text(encoding="utf-8") + + +def _built(page: str) -> str: + path = DIST / page / "index.html" + if not path.is_file(): + raise unittest.SkipTest(f"no built site at {path}") + return _read(path) + + +def _title_block(html: str) -> str: + """What PageTitle draws: from the main column to the end of the h1.""" + end = html.index("", html.index('id="_top"')) + return html[html.rfind(" list[str]: + return re.findall(r"", html, re.S) + + +def _headers(table: str) -> list[str]: + head = re.search(r"(.*?)", table, re.S) + cells = re.findall(r"]*>(.*?)", head.group(1), re.S) if head else [] + return [re.sub(r"<[^>]+>", "", cell).strip() for cell in cells] + + +def _node_module(script: str) -> str: + node = shutil.which("node") + if not node: + raise unittest.SkipTest("node is missing") + result = subprocess.run( + [node, "--input-type=module-typescript", "-e", script], + capture_output=True, text=True, check=True, cwd=WEBSITE, + ) + return result.stdout + + +def _webp_size(data: bytes) -> tuple[int, int]: + """Width and height from a WebP header (VP8, VP8L or VP8X).""" + if data[:4] != b"RIFF" or data[8:12] != b"WEBP": + raise ValueError("not a WebP file") + chunk = data[12:16] + if chunk == b"VP8 ": + width, height = struct.unpack("> 14) & 0x3FFF) + 1 + if chunk == b"VP8X": + return int.from_bytes(data[24:27], "little") + 1, int.from_bytes(data[27:30], "little") + 1 + raise ValueError(f"unknown WebP chunk {chunk!r}") + + +class TitleLogoOnAPlate(unittest.TestCase): + """The RetroArch and Batocera marks are black: beside the title they + vanished on the dark theme, and the image had no width (Lighthouse + unsized-images, V3, T-F3).""" + + def test_the_component_draws_the_tile_square(self): + title = _read(COMPONENTS / "PageTitle.astro") + self.assertIn('width={MARK} height={MARK}', title) + self.assertIn("const MARK = 40;", title) + style = title[title.index(".rb-title-mark {"):] + style = style[:style.index("}")] + for rule in ("width: 40px;", "height: 40px;", "padding: 3px;", "border: 1px solid var(--rb-border);", + "border-radius: 6px;", "background: #fff;", "object-fit: contain;"): + with self.subTest(rule=rule): + self.assertIn(rule, style) + + def test_every_setup_page_has_a_sized_mark(self): + index = json.loads(_read(WEBSITE / "public" / "data" / "picker" / "index.json")) + for entry in index: + with self.subTest(setup=entry["platform"]): + block = _title_block(_built(f"platforms/{entry['platform']}")) + self.assertIn('class="rb-title-mark', block) + for image in re.findall(r"]*>", block): + self.assertIn('width="40"', image) + self.assertIn('height="40"', image) + + def test_a_setup_with_no_logo_gets_its_initials(self): + block = _title_block(_built("platforms/myminui")) + self.assertNotIn("]*>.*?>MM") + + +class NoLevelBadgeOnATabRoot(unittest.TestCase): + def test_tab_roots(self): + for page in TAB_ROOTS: + with self.subTest(page=page or "home"): + self.assertNotIn('class="rb-level ', _title_block(_built(page))) + + +class FooterLicenceMatchesTheLicenceFile(unittest.TestCase): + """The footer said MIT covers the tooling. LICENSE names the metadata too.""" + + def test_footer(self): + self.assertIn("metadata", _read(REPO_ROOT / "LICENSE").split("MIT License")[0]) + html = _built("data") + footer = re.sub(r"<[^>]+>", "", html[html.index('class="rb-footer'):]) + self.assertIn("MIT for the tooling and metadata.", footer) + + +class WideTablesStackAndFiltersAreBuilt(unittest.TestCase): + """Level 2 and 3 tables overflowed a phone (emulator summary, gaps, + verification modes, emulators index, data: V5, J8). The filter box, + added after paint, pushed the table down: CLS 0.066 on + gaps-files-to-source (T-F10).""" + + def test_every_wide_table_stacks(self): + for page in ("emulators/duckstation", "gaps", "gaps-files-to-source", "emulators", "data", + "wiki/verification-modes"): + html = (DIST / page / "index.html") + if not html.is_file(): + continue + for table in _tables(_read(html)): + if len(_headers(table)) < 4: + continue + with self.subTest(page=page, header=_headers(table)): + self.assertIn('data-rb-stack="true"', table.split(">", 1)[0]) + + def test_long_tables_get_their_labels_at_runtime(self): + """Labels on every cell of a long stacked table were 122 KB of + emulators/mame and took it over the 600,000-byte page budget.""" + self.assertIn("if (kind === 'kv' || rows.length >= RECORD_ROWS) return;", _read(PLUGIN)) + source = _read(SITE_TS) + self.assertIn("const labelCells = (table: HTMLTableElement) => {", source) + self.assertIn("labelCells(table);", source) + html = _built("emulators/mame") + self.assertLess(len(html.encode()), 600_000) + + def test_the_filter_box_is_in_the_html(self): + html = _built("gaps-files-to-source") + boxes = re.findall(r'
(.*?)
\s*Filter [^<]+') + self.assertRegex(box, r'[\d,]+ rows') + source = _read(SITE_TS) + self.assertNotIn("controls.className = 'rb-table-filter'", source) + self.assertIn("controls?.matches('.rb-table-filter')", source) + + def test_every_long_table_has_its_box(self): + html = _built("systems") + for table in _tables(html): + rows = len(re.findall(r"", 1)[-1])) + if rows < 20: + continue + start = html.index(table) + self.assertIn('class="rb-table-filter"', html[max(0, start - 600):start]) + + def test_the_placeholder_names_the_columns(self): + """Every filter offered "Name, platform, system, hash...", over the + console list too, which has no hash (V small).""" + self.assertNotIn("Name, platform, system, hash", _read(SITE_TS)) + cases = [["File", "Region", "Required", "Hash"], ["Console", "Files"], ["System", "MD5", "Size"]] + out = _node_module( + f"import {{ filterPlaceholder }} from {json.dumps(PLUGIN.as_uri())};" + f"console.log(JSON.stringify({json.dumps(cases)}.map(filterPlaceholder)));" + ) + self.assertEqual(json.loads(out), ["File, region, required, hash...", "Console, files...", "System, MD5, size..."]) + html = _built("systems") + self.assertRegex(html, r'placeholder="Console[^"]*\.\.\."') + self.assertNotRegex(html, r'placeholder="[^"]*hash[^"]*"') + + +class HashLookupGivesOneVerdict(unittest.TestCase): + """The lookup read "24 matches", "Setups that list it (12)" over 17 rows, + and emulators by id. ?md5= showed the empty form (N8, J7).""" + + def test_parameters(self): + source = _read(SITE_TS) + self.assertIn("const HASH_PARAMS = ['hash', 'md5', 'crc32', 'sha256'];", source) + self.assertIn("HASH_PARAMS.map((key) => params.get(key))", source) + + def test_counts_say_what_they_count(self): + source = _read(SITE_TS) + self.assertNotIn("'match' : 'matches'", source) + self.assertIn("count(platforms.length, 'declaration', 'declarations')} by ${count(setups.size, 'setup', 'setups')}", source) + self.assertIn("count(setups.size, 'setup lists', 'setups list')", source) + self.assertIn("'file in the collection', 'files in the collection'", source) + + def test_emulators_by_name(self): + source = _read(SITE_TS) + self.assertIn("json(`api/v1/emulators/${owner}.json`)", source) + self.assertIn("profile.emulator || profile.display_name || owner", source) + profile = json.loads(_read(DIST / "api" / "v1" / "emulators" / "duckstation.json")) \ + if (DIST / "api" / "v1" / "emulators" / "duckstation.json").is_file() else None + if profile is None: + self.skipTest("no built site") + self.assertEqual(profile["items"][0]["profile"]["emulator"], "DuckStation") + + def test_a_console_page_card_reads_its_manufacturer_file(self): + """Every collection match on a console page (systems/sony-playstation/) + asked for api/v1/systems/sony-playstation.json, which does not exist: + the verdict lost the file's path.""" + source = _read(SITE_TS) + self.assertIn("api/v1/systems/${words.slice(0, size).join('-')}.json", source) + api = DIST / "api" / "v1" / "systems" + if not api.is_dir(): + self.skipTest("no built site") + self.assertFalse((api / "sony-playstation.json").exists()) + self.assertTrue((api / "sony.json").is_file()) + + def test_no_match_leads_somewhere(self): + source = _read(SITE_TS) + self.assertIn("link('contributing/', 'add it')", source) + anchor = re.search(r"const WANTED = 'provenance/#([^']+)';", source) + self.assertIsNotNone(anchor) + self.assertIn(f'id="{anchor[1]}"', _built("provenance")) + self.assertNotIn("No file the site knows has this hash.", source) + + +class HashAFileHere(unittest.TestCase): + """Find a file asked for a hash only: hashing meant another tool (J7).""" + + def test_the_chooser(self): + source = _read(SITE_TS) + self.assertIn("input.type = 'file';", source) + self.assertIn("It is hashed on this device. Nothing is uploaded.", source) + self.assertIn("crypto.subtle.digest(name, buffer)", source) + self.assertIn("['SHA-1', 'SHA-256']", source) + self.assertIn("lookUp(status, result, [found.sha1, found.md5, found.crc32, found.sha256])", source) + + def test_md5_and_crc32_match_python(self): + source = _read(SITE_TS) + block = source[source.index("// Hashing a file on this device"):source.index("type FileHashes")] + sizes = [0, 1, 55, 56, 63, 64, 65, 119, 120, 128, 1000, 70_001] + out = _node_module(block + ( + f"console.log(JSON.stringify({json.dumps(sizes)}.map((n) => {{" + "const bytes = new Uint8Array(n);" + "for (let i = 0; i < n; i += 1) bytes[i] = (i * 31 + 7) & 0xff;" + "return [md5(bytes), crc32(bytes)]; })));" + )) + for size, (md5, crc) in zip(sizes, json.loads(out)): + data = bytes((i * 31 + 7) & 0xFF for i in range(size)) + with self.subTest(size=size): + self.assertEqual(md5, hashlib.md5(data).hexdigest()) + self.assertEqual(crc, f"{zlib.crc32(data):08x}") + + +class SearchFoldsPagesUnderAHash(unittest.TestCase): + """Under "Look up this hash" came 14 unrelated pages: Magnavox for + E490.WAV, Philips for cdi490a.zip (N8).""" + + def test_fold(self): + source = _read(SITE_TS) + self.assertIn("toggle.textContent = 'Pages that mention these characters';", source) + self.assertIn("toggle.setAttribute('aria-expanded', 'false');", source) + header = _read(COMPONENTS / "Header.astro") + self.assertIn( + ".pagefind-ui__drawer[data-rb-hash]:not([data-rb-pages-shown]) .pagefind-ui__results-area { display: none; }", + header, + ) + + def test_search_fields_are_weighted_and_never_shown(self): + config = _read(CONFIG) + self.assertIn("metaWeights: { title: 10, files: 0.5, aliases: 5, devices: 5 },", config) + self.assertIn("const UNSHOWN_FIELDS = new Set(['files', 'aliases', 'devices']);", _read(SITE_TS)) + + +class OsTabsKeepTheChoiceInView(unittest.TestCase): + def test_the_row_scrolls_not_the_page(self): + source = _read(SITE_TS) + self.assertIn("list.scrollLeft", source) + self.assertNotIn("scrollIntoView", source) + self.assertIn("osTabs();", source) + + +class CommandsNeverBreakInsideAWord(unittest.TestCase): + """`--` / `platform retroarch` at 1440 px, `--` / `system` and + `nintendo-` / `64dd` on a phone: a browser breaks a line after a hyphen, + whatever overflow-wrap says.""" + + def test_the_words_are_wrapped_and_the_text_kept(self): + # Shiki cuts `--platform` into two tokens: the span has to hold both. + line = {"type": "element", "tagName": "div", "properties": {"className": ["code"]}, "children": [ + {"type": "element", "tagName": "span", "properties": {"style": "a"}, "children": [ + {"type": "text", "value": "sh -s "}]}, + {"type": "element", "tagName": "span", "properties": {"style": "b"}, "children": [ + {"type": "text", "value": "--"}]}, + {"type": "element", "tagName": "span", "properties": {"style": "c"}, "children": [ + {"type": "text", "value": "platform retroarch --system nintendo-64dd"}]}, + ]} + out = json.loads(_node_module( + f"import {{ unbreakWords }} from {json.dumps(UNBROKEN.as_uri())};" + f"console.log(JSON.stringify(unbreakWords({json.dumps(line)})));" + )) + + def text(node: dict) -> str: + return node.get("value", "") + "".join(text(child) for child in node.get("children", [])) + + self.assertEqual("".join(text(n) for n in out), "sh -s --platform retroarch --system nintendo-64dd") + words = [text(n) for n in out if n.get("properties", {}).get("className") == ["rb-nb"]] + self.assertEqual(words, ["-s", "--platform", "--system", "nintendo-64dd"]) + # The colours of each token survive the cut. + platform = next(n for n in out if text(n) == "--platform") + self.assertEqual([c["properties"]["style"] for c in platform["children"]], ["b", "c"]) + + def test_registered(self): + config = _read(CONFIG) + self.assertIn("import unbrokenTokens from './src/plugins/ec-unbroken-tokens.mjs';", config) + self.assertIn("plugins: [unbrokenTokens()],", config) + self.assertIn("baseStyles: `.${CLASS} { white-space: nowrap; }`", _read(UNBROKEN)) + picker = _read(COMPONENTS / "FilePicker.astro") + self.assertIn("showCommand(cmd, `${INSTALL} --platform", picker) + self.assertIn(".rb-picker .rb-pick-cmd .rb-nb { white-space: nowrap; }", picker) + self.assertIn("navigator.clipboard?.writeText(cmd.textContent ?? '')", picker) + + def test_copy_gives_the_rendered_text(self): + """The copy button's text is the block's text: the spans add nothing. + Expressive Code leaves a leading comment line out of the copy.""" + import html as html_lib + + page = _built("platforms/retroarch") + figures = re.findall(r"", page, re.S) + self.assertTrue(figures) + self.assertIn('', page) + for figure in figures: + copied = re.search(r'data-code="([^"]*)"', figure) + if not copied: + continue + lines = re.findall(r'
(.*?)
', figure, re.S) + shown = "\n".join(html_lib.unescape(re.sub(r"<[^>]+>", "", line)) for line in lines) + code = html_lib.unescape(copied[1]).replace("\x7f", "\n") + with self.subTest(code=code[:60]): + self.assertTrue(shown.endswith(code), (shown, code)) + + +class LogosAtTheirDisplaySize(unittest.TestCase): + """136 KiB of the home page was 96 px PNG logos shown at 40 px (T-F7).""" + + def test_raster_logos_are_small_webp(self): + manifest = json.loads(_read(LOGOS / "sources.json"))["images"] + self.assertTrue(manifest) + for entry in manifest: + path = LOGOS / entry["file"] + with self.subTest(file=entry["file"]): + self.assertTrue(path.is_file()) + self.assertIn(path.suffix, (".webp", ".svg")) + if path.suffix == ".webp": + self.assertEqual(_webp_size(path.read_bytes()), (LOGO_PIXELS, LOGO_PIXELS)) + self.assertEqual(sorted(p.name for p in LOGOS.glob("*.png")), []) + script = _read(REPO_ROOT / "scripts" / "fetch_site_images.py") + self.assertIn(f"LOGO_SIZE = {LOGO_PIXELS}", script) + self.assertIn('square_image(data, LOGO_SIZE, "WEBP")', script) + + def test_pages_point_at_the_copies(self): + html = _built("platforms/emudeck") + self.assertIn('src="/retrobios/assets/logos/emudeck.webp"', html) + + +if __name__ == "__main__": + unittest.main() diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 2e67ef63..e7d37756 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -54,7 +54,7 @@ Every module in `tests/`, by the area it guards. | Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_offline`, `test_install_selection` | | Handheld port | stubbed PortMaster dialogs, fake installer | `test_port` | | Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` | -| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` | +| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` | ## Test architecture