From 30609c3bb3b4c185559a4423763e0869705525f5 Mon Sep 17 00:00:00 2001 From: 0xa0 Date: Tue, 21 Jul 2026 20:45:47 +0200 Subject: [PATCH] all the website mainly done --- blog.php | 11 + config.php | 2 +- css/style.css | 108 ++- img/0xa0.png | Bin 0 -> 1168 bytes img/42-perpignan.png | Bin 0 -> 11213 bytes inc/Parsedown.php | 1712 +++++++++++++++++++++++++++++++++++++++ inc/buttons.php | 79 ++ inc/footer.php | 4 + inc/header.php | 4 +- inc/posts.php | 87 ++ index.php | 22 +- js/idiomorph-ext.min.js | 1 + post.php | 31 + posts/42-piscine.md | 9 + status.php | 82 +- 15 files changed, 2127 insertions(+), 25 deletions(-) create mode 100644 blog.php create mode 100644 img/0xa0.png create mode 100644 img/42-perpignan.png create mode 100644 inc/Parsedown.php create mode 100644 inc/buttons.php create mode 100644 inc/posts.php create mode 100644 js/idiomorph-ext.min.js create mode 100644 post.php create mode 100644 posts/42-piscine.md diff --git a/blog.php b/blog.php new file mode 100644 index 0000000..214959b --- /dev/null +++ b/blog.php @@ -0,0 +1,11 @@ + +

blog

+

things I've written, newest first.

+ + + + diff --git a/config.php b/config.php index a19d906..48a29ad 100644 --- a/config.php +++ b/config.php @@ -1,5 +1,5 @@ +K|J&j!7_H+CHGftuNzjHi>`WbiZb2XJ%hA-@bXf5!*_o(tUe-J4HM{ zcXxNxc6N4le|>%JcSbxPU0q#~;Fly4{K6OoC%xSK^xM_Sjo;ScdG+RiNU$cjBOYFh zZ!1+wB*#?SDHOe4_gaie-o0QezvIgYCDut)eaKkIG)ZMJW~WpB`CG?WgRP%7-4-1; zb2Whq^f5eUnkR(IwiKL9Ce<;ON>MtU_Iev*Zf=gcySr(5dD-uiG#h(mWknr>;aKR-|S-i1QJ?`N8gy|=eV z>+9<@K0f}6f~CY(2oMj!M@L7L%jIZycUSdcfwpba*4CCct3LUBo+c(HXnJ~@78e(3 za&poiYc=-kW$;g1ePFz!lPb5a9NFe`i5v^V{$eRXr>Cd>qFKu3=BAPckWJw5@Q?-u z25JQ1dTe`>l%UuwT!OKsr6two&*NB#jn4*aYinu;aM|HZ$L=^kKd0^OZR+dmQ)3cy ziSPsi7O4x=1p~~H{rWWw3%x;%0EtN_&Qug=v)fVOL$zXxo5-< zup2HfFV!yLvVfa`hpLP}Gc%(ySuYQe8!O#Y^>dp#Z%S~o{E*$nKMsxcC;T?Rd3Ixi zvJ37XbPB`nuR93~<}v`}yDC3OusFF@hlYmeqBAWiUxM6^A;@MM_x~?btRsjyoraL?WT);27_ZEAg>G z_hux#oATD+%w+-va|SVx1X!w+$MAWWcwqWr@V_H#F2h+ID)_m1Yl8KfEBU*3772a| z_2hy`@Dq@7^&|NGtziBo7nO)`WX%nd3nIZSU@8kHxgZkUB&N)jNV&i$IKoULHCHDR z+#tKkxCjH30x8 zfJ7)71Xyn6kYi+21GnDxdBaqRrG^oYKdF$x6iAYjHOU?j)7{nLUcDcI;#OO}zr-W| z;>&~ouq0#%S~6lXwz%4NwsD~gO%mhvvhcY!i57`kiKTGKZKNd-e_gJC?yR5=hfr^pZ)kfy zw~9q25C`a3igjOB^Wdxsx|7(dhsaLvY-OCk4E2ZnNH)yA-ou9IJlTDJ*yM+h5#z3u zJJTNsVd*F$B#Ge_tQD;;_K3?0a1W+4eH`*OcKwzlBhD?C&D7nOz`b-Gu;L2zLR^-c z*U$O-0jPt5$J`fQa}Hn5hk808vIRt+=Q$7sd@j?jrB_Ug{pAF-V+N-p+~P`UsgGlj zV%|+t&Ncmcq>K4S+(v4o5ob?OZ^*)j8V5~tQ+P7JQoW9Hi>t;~(W8y;y6tCL<3AGJ zxMkHD!_tKPZStOGvGyoZWUV`0y*jF$Tg_66d%5gY&p`WbdSDNV^w%@>5aJ(AAvKiu zz(-iYT>x}iG)iCaBB8!%Y9`JA#~>kpxr2!i-S8UAX5-Wu(XN5cvJfLkEO+e?TmZVM zPRHjuI9_~A#*itVhSA67ZoCQ3U$LmjaftXwM+nr}-Zo;MMMAXt-|9PpBAPSlcJs~J zHl=rr`_0|`y7`-1TN_UT; zNJACXoDQR=nVv;wtASAU8lpYo?CO9~ep)bBo z(}wY%WI=Q(4c=Fxvf2j7NJC3U>bU;x>7Qtg@3pTg&DqGcuRW;dQcSZ0Z=_*Yd+_hh zZP<~Kx>e!;g?1FQ8lt%1+uz#x7px#^bXp85!O$pk|1v-~$Rs8{A-q=8ZvA?;cFD|5`cgKp_pjJ5=sHgS_fJhF@{4KOpel{_xSosKTh{O?S~X?0#goo4 z>+_IX;)Fyr*0g)BqIzK~zLLc>TY^-b{?h&Fp?jY_9pl$P!pQs}Jolg00DGy6und_JYg< z*|pq386>Egob{`MA@u$@$S0xye7eprZTLTlmK7+2ic$j&#RzB%s^e+(f6Q3!pa?3H zNXu#~9;tJxnL7C#g)NuVv~Gt=2ebVl25ltP*euiUa`?&kVUbhq9Yv6O`lAzi4((U| zfUy~@$RDVN$Tllj7P+#jG z-6n+!tvNb@R_9Q*_N25%(iO!^5?b&-O4W#x%BB<}`-hB%pVe%4nhRfDbKV+g7yi5P zZN(~3J?&n#jiw5qp&5g;{~ymM<`+q*@w7W%)@UH%YS6OYE34eKB;91-F2j;>cMeQwf?J^gV zbpjysx?8lr#)qO=ef0U|tpBOskh)o8v|F2NO99m}}X~F71Ja#Lf)Dk5*OgbmjqlU|v6695b3D29-&& zdkKKMD#VF7V{d`b`kCrD3>u3)+V=$ogjH2i`UCT6OscfVlwk|c<&r794N#ey5GO{C zE+c6J>BL0zX6W&$pCLt0iApDr&@JB;UUTyBG8yoRbf-U0tgmKpzyvQ5bnE-!-J2qY z6hSrp;~=YogIiLU6uSGJqn5>>J3nFu!}eCfy3+^5K|p-*B?eM(*9HIZ8STUnGla~G z7tUobPKTIlO;tqJQc{a=+FL?ldGyePvNa4Z-YMgra#y&Ry?2}u4r!@0)sj;04caER z;3e;m{qbULae6o;d#3uRQ%nZO=aD*AuA@CvfQjA1MeTCuI%^l*zp2R8PgnVmf#H&zLS>10W2bgOw z1z%fBF~f@zd2EL;4ZJMH4OFH#sgKHut+P4Du`mP+uE9ie|J?X?R@=ECcC{M)TlUi3 z!dD<++BQOR<72ZaH@7>ZjPRADRD-*WSbd7mRU zF{pmdQOVdq<-`zcgiPfNeWTmPzTvg$Y&KHwD@kSRy$z*;HUB~D0>UEE?UHSA?ig?1 z7`o{+i#k?JA*=+VBi%4bR@kv>8V&1ikU`%dFy|@@B7Tlv30a5@180+`XURe}; zU*rgCLxPcUPhDg5kNW$~x6H>SRle}s#7*fl((kF;;0NuiD9$-=@9!eC2BSl|?boh! z?R+g31KHx3Nxx{D(c%~}Zg&_Wq9{GS0}gR4vJzm# zvHK2MKw^jzLZ+p3-MkZiL) za@AcQ;H)H-0Pn6~r#wbNJrS0rPZAKFRmz=Jlb+4vJ1l^|RhMb<>90$@rO_>r0A&&W zySrM@9VtMb<>Y4anESw2ZoVeUw`K9U&d2elH1s~cEw_ZLm@n>30%KerlZmaCh|{;N zI^@!jAM>nQDTRZCUf@wVn$V_!Xsz+BRD}eD%87SxGE*L_@+`3icBP{(Ld?Axq$8G- zpgUi{g^i>?@YQ*OJ!y!@X6{@fER`r*&kUk7%j{r30XSU?*-iRIi_G}#jh31(dPSq^sxGUMk z9Q9{^hERU1LUg`W=}d!!h{=yYkU0yQJWFZce(eQO?zTWe_~om~(M-`47A0~|VwF3a zMtyUxSacAha4qx^m_S;9 zsBo?+pU3t+5+uB|>5g|Rz+Y@MvfV2rq9!NL{XLC|@oN|0wPbw^{oRPV;auB5V-=11 z3S5QMFrn!m%lA;v6g-a~BKfi3IucRXs<2CzV($B}7}674{5`CBdm0dazskK79lOWL zc{GL6iQSdT1F+XH)QY=j(4E%S*IkSC^;`m6zwIqQFNp$bYmHIM+X%!Ye$5eJPf2Lt zf@7L>Ez@^H|ERwGW5j&&!2B~(S;bvZ9&3L1Q3C0Ye+Xcvm7wqankgiI$x}tshfD9i8ZxGqlUpKg~0gDR+b!zmqM>}Wz(mdsP&BDP@ zIDlI~L>50zeRv1!**~MSTMFIz)Oua{6?1==(!-^G4wcfHb zTg&tnr`QHXzQCB=Jf`AokbWSj2Rb+=R7RQ!?jr0hX14bBmkD4#Gog69FBQcY0FR`H#fm7QAVVx(f|Dxu4Qy__g)=N*1uJp7*4%VwWEul|$HV zpMt`MlBDj0df%oc#_=bzpsS)O24o%*3`30>S)4FW(|h!BAIBlt*$F{v`Dy<6DAV?m zcnTOj)yBRSxiOv@*x5CmjdJw?e~ey znHcxJ(d7txo38PG{1k*Vnh)e&s)P<76a?BP<5DZ`M1OaeqT|bDfNuAw5?(g8N4h9^ zX*d@ci9^r!h+j=nQq}r&O>L7b`W`(z&RvZ~x%*Qbcr$#^{vOI(gbdiu=Ezwg+gg@H ztN$nbOuUV;BW^tRtl_%`M+~yuAtKVG$HM_d(KQ86FNr&8$cS}FHsHRu@3;3u5IDXY z%q0L3p4+b?)-l`iPBcKT_)Q!rWf7x~zsuYZ_3^$9Yz2}_B(^FcLT-t=c>4d9>Ve|I z<8xmXdVGg}L(j;*hX&Fr&!L$&Gg-L$2zWj!uV#6RyIK506~&^&(L**hL8;$sAT?8) zmMR06x55Li5Z~uJ)6m<|@6y$9v*bHW8v8lRFY2l;Snkr^-J?C?3T#*&xDjfZy=x0345-Q`o*E#`1w?&# zw4cccEDQ-FBzg8Hx&ob;PMwpe0aXFSQ>^~Vi^wAc-$283> z5Q%k)U?H2`KOo|CR}qC(aX=-q-d!R)aztd`th$@gle6}MKjT$wGT?)EyI{M?@z?oD zgCXJP6R(7jRsW~w*q7Agl}JkS<_8?Qr%eooV^?94XsWQQ7e?^Gz%sX0dhX%IY-Fn~ zuaH<-KXVva__Ni5Z{8~nDJKo_m4--5t3IL8)iS;idy%BtPUM*)A_NwD-nxrSx%I$p~ z^`hd@OU@P9tNeFX+-Wp5Lzr?XlVEungZTqtTJ-CF>0OTd(u{jF6vtG;1hR8`t`y4G zVHsH!WvfpNL937NzGw6&;Vjo38pdNQXpHDnMajIL-9HiC!H)kpHo=_&aS$`x` z5W25(1>$4+t|ekwRqv4^o9VmeN$F2Q_hcx-ziZkrPCudx7bLy)E&amPN0f`k!MogJ zIxj2Ih6>H2--<}N9JBX4@oq!u#0T{-sBbH43#>@mEJV27dX23DvnRp9fQ8oAw}sb6 zgUjyjj;LqXUPjJaQtDozv{PakO~!M{+L_OI+5s_5T51^w>jhG0%z%Z+DfqRLs=$Dohq_I2RlTg-)z9f2Kt8UMc%vo`sB_ z3m+G440CvYf8F|X{o^;b>Jwx5BHK!r#6l%ORZx-qz?^65+8y%>l_FlBX z?J=pEmv)9!lyA^kw3;7z^GG~QycKqzK=T_4Vqe6$jYA!FcuU`ok24zVI5l|>m8XY1 zQLYl7sZn>!y!qyR$`Q-6opnK}g zo^n3191IaHmR@m%!PeNTO#HCr`2el}kclr;GPDr;QAL!frp0;GTlc_;&!l8uaN4eLU5{?5O%@U!@mIiP ztmqWPO_#P6n`{t)rhmJ4r$xV#1`7>qE16Aaa(~XWSOw|FgwmLJ#bPTTsAAv~-&8HE zU+h-rcoq1aW)}8NF7LZaB&WmgKae9+$Y{C#c$sHxLQ?cX!TfQP08SxrLKoocNr3s% zSX3UY5~oMPg1nfu$gpyU-ic#R)nqB_@*fiHfvNGi+R`9+_@MZ^TwAH8Sq8o%m1A0s zioUl(e7sL#tcVvO|U7T+9X&Pqwaucw3%0~*Gy>j^>n5hz5AqD_KdIbKCkfSSSJR6|@77>m_UkHD(YH3;eYEl%2*~)0b2awhG%w811b4Dg z(|AYzW$(VxT`7q~(y9s@UV#*g)n&%izJOw;1KR0=^&@w8>lus0F&%UiSjtk5 z?&9T;@LJukDI;4|UNz6)4Xs^UkC%I;VQP;0 z1cvTMdu2C2%rLd+L|1$i#%KCLom=SM^G8eyB`5N{Gd(r#tZSdVQVt4)j6y5`uLSM2 zQgPf*3+C`$261BjojTp&1)+P(eo`GQFN2Lk?y+OL$6syWg8h6mZ?kroN7xMERSIlZ zqSNy~mUGtijM>-jAvDV_2(J}WgDrBB(5OERw-%dvddVh<78Og{5&iYT_{X!QImsWJ z*N_$ZBbZ3_%B5>$Y?FzYq8s{|MJt&4`IBb3kyg(fhaQnW-Ev}!QT+7izM|fhV|y8H z0h?d`F@u2?@U=FUpRy~DL;HPGHLk4GJrMXh+P8ZRkYG#29AF3RGT^`I0dLA0*orO= zNLAL!Sn7g=4E;q~0Y_f()X+w8d;Uf#G8SQ7Ocn|aGGDPG){$aiDRelVff?P^V@tf; z=nhtlZTe$+`iv=V^+}w$q0c7E-qf2n@k;OpSTAP(JTSaAVOleuh=1J2rqkWJitY(g zFI(;XkB#isd*5MS*K(>@#}6eghnq*p9+&BklqXJnEGI`VyNI5cv(_awMal*LO>s07 zx1s624;1;~XuSe~nf@+*rlJz$K=ul}Qvv_0u+61rdh9}Rh`CR*tFpH6z6NmIyFSv2KvlbGAww&V}(}K^$0k1)&Udgm5GdC=rb+?YIbZUKQ zkb3T+$Mf}#6n)T%9O-|uHpF}(4usoF{TF>3(BU}I3M!*h_xk6!7NW3KrIM}`)IkR> zKZ;1+&>*AHOKYx|PEyl*dj)OMWA44Jos*E`P3ln+%?q{yk}dJXglaoS=YUxBzWrKs zm!O=SWKTs}?q$01?Y{No*F!kMsT67N@uV4SQ#R$}(4#tqNbU^x) zLE@Ob(zOjQm0$;9Kd4$+-cdZ^Jw_jb$$Cl&OMz6zt53ai<$w6qg96ZG?z`}2hzZGx z3BI@Rti7f^!p4fOJzT3@{++ETxS;Ic&Ik|!vIKjeDt@h z08{gK;9y6Q$*0TiXMvg7`c@muSxQlodW3+an6*T=_W z{P@H~#<4=qZNlZ#6JA>dC1k%|0YM)__?YU|_nMCrL}Tc2l|%|J`MhS`8 zlg}z73uK&)16xx#REEl(&^_3;0JY^;gRF)oi$J>ON4&L;uX{OV=f1P#6+EU$USm=m z@1rLd#;|nxBtn|L$$<7+^=y#DHu>ynlcxr2!|nwBF{1?1BvwhUey|Ct5j$VliXH=0 zC?oA??=U&_7R+xKodiD3rS)cI^d3|eHcNVcalODQ2ERN#azo#hq$M(#Yr{Md^QmmU zZSVEz&G2YLO}#7o1_=xHZjs;rE7i|yA6f~R+uW(fW?d6rqoz7$Z=RaD?4xM>OoCT8 z<)L1RcxD>6U5WGYxcKSiaCDk@kBCKcci6P?w6Y$h(MpAH6jW=$hh4*Bu9r{1x@5A9 z*5B)BXfM|I?GHcALvGl+;Y-)YBZSPk6vt0`TR0n=k4shy-*Md#aZf(L#;c@V8`tLy zvgy5mSg6>!F%b)CR)8V@#k+3jhBKW{nQHNgdwR^WP3uZ;>XB9poyJZ zR@_TIUEe||I()O9NN(TDrC5o$I;3=fkZ2D3EK|ZrCcO7H8l^F8rPo zcY!mps=Y*Xl>yaw3d8S*fcAjul|&4+)!dJ3=}bKBEKm+Bn5PgtR+oTz`Zie_(xFZ0 zjo4Jdjrv|C3u~_{q>bSxKD4|w4~L1)HEaLQ@Big%Tb^v7rkvLkxVto_?>YV1i^nha zFLb8zu!VA1>GQWf!~pfsMb1*Qp9w2bBJF^J*z8R`*FzhgB|or9e2Wi~G;wP3poGXpPac zLz3_iIas~o@M3|~5b(nTP2bG1GX8)Tf}J7_8X~AfN-6gWFe@FA-`e)ZrwB+q@l$gu z(3!u;`^_Sl(^&DUvqjcxmIN~7TuByK!e)Y_b~*J#49c0L=6jc)+GqEL`M9x|e}Id^ zc$J!L%ey2XGQu1p6g}X;RUd3RIKeaf3e_wHe_5EmoY^r4=$vTPdYw)~5|piWwSdNl zv})=V%QO!OCV)0y;IZviF4z@MpkFrVdv$QCgt$B{^P1R=X7SYk1UZ+skH@lns-@5- z;*6EKvkQmjt0=nWQY=z}t-DqsnsWVqkZ-(AJahOJC#iLd^=OYd#NC-SpC10 z+4T2pb^-WNppNAw<6l=q zYcvJJOzKRNhPm&n>Hm6?Jy5gj&>jAxVN*s1pQfK2X-crNqA(=l)aa29HLp1Sfdm-P zb<4EfpI=}nM~ zDge6>S32TUA1}L(lD)9)F2E`5=EqEeZ9)*aA&8FXaes&XW*LC#yPm(An-X!BD;Zf5 zZ&~sKc^faf{j3|dQF}Vj%DTf1tgGtq{_`kpiLE*{o$B+%3K7+pU{*{}2}+mfKH!iW z*;SIlV!>x2OQvAF8A^t(k?FIQ9guvhA;)@OQPJvU^#vrnUx08yQfvPdsOw38QmB5? zi)dFS`DF=MW`mu^WNdF&<>#<0A>vQfKek9@1Nks?*lne({LgJN3|qN}{6vWdh3d!G zMk@Y0x5%i%N0^jX7TvXW!-5jOwh6sbg>ix7_tLCi6~mb@8&WBVP+9q!t_J{ybY6qC zlKabW*v!?@=03e#o34oYpO)^#9_1kh3VN0aHLWgC#Ub|RPcISD_!XK8Y)b1R{uc{T%TWDfOwpR7`&!VJL&iCq$0II zhElbB(sM6Rk4R97Wc_XnJXEsav-|X$3b0!+21j^*V*3SHN>XTJ?%QIHpZq>{VkH&c zyum4yhBWRFkE{^MP)%b3OIL*G%s97U<*7YmV1i-A$n|KG!A&!X%_*6c3iP3M+bH958Zrs?RU z=PQkWB3`2b6As$S(l0kO%@UMI&a%9AghWrrkXmeuxZg3}H0^oZyKn4bCUOs~xTwOf zb#tx>jt2Y56$q5s8Q;O0%O+NM{)Fh9n3*n@Y2n(&`HDkA;s2uWKvKoEpVp$a#r_f2 zrO&{2x+Ss@!bksUAD19C4ZKWkbqqVE0;Ym-HeaMaq0>IC)yHh*k}R?gJSo5AqqvI< z3bjmE?TNB;X|}x(QAu*U>)lGX=rA6Ub<_2(TjKh%R@#bm=LXOZ^DTnDa4OSl5EL0c z9G5A{DV&}yn{#E;wC&4Sg{53D(#jYutf}?qeUxO-C}mnQ6WxrSyF_(0dFVTdg3%8W z^t23ytt?{|C0VNV%2cD0j8nUfr*vr9e!z1HoSvf-=!pX%p~4u+o8Vgtf0DiVq~d&} z8oadJ<<^MG(yw=@0C@OF&6UVP@%Q6{21Y&^uS70Yg#{S1O&dHD@ozLM^K05(X`W2X zn0@_p*?ZIkC&ii<6ymL*_x;pMuC;RK7JvztRxMjKE`G~8>tpe3`pddZUV&6;pl@#| zq_yfMXKB282BkdVa?<)4{98pupKl>eV;XxTGzppXi3u iCchK-LPqRwI!ChAp7PQ3hu~|v08&{CQK@u4;{O1jErxpl literal 0 HcmV?d00001 diff --git a/inc/Parsedown.php b/inc/Parsedown.php new file mode 100644 index 0000000..1b9d6d5 --- /dev/null +++ b/inc/Parsedown.php @@ -0,0 +1,1712 @@ +DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + $markup = $this->lines($lines); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = true; + } + + continue; + } + + if (strpos($line, "\t") !== false) + { + $parts = explode("\t", $line); + + $line = $parts[0]; + + unset($parts[0]); + + foreach ($parts as $part) + { + $shortage = 4 - mb_strlen($line, 'utf-8') % 4; + + $line .= str_repeat(' ', $shortage); + $line .= $part; + } + } + + $indent = 0; + + while (isset($line[$indent]) and $line[$indent] === ' ') + { + $indent ++; + } + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + $Blocks []= $CurrentBlock; + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) + { + $CurrentBlock['element']['text'] .= "\n".$text; + } + else + { + $Blocks []= $CurrentBlock; + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + + # ~ + + $Blocks []= $CurrentBlock; + + unset($Blocks[0]); + + # ~ + + $markup = ''; + + foreach ($Blocks as $Block) + { + if (isset($Block['hidden'])) + { + continue; + } + + $markup .= "\n"; + $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); + } + + $markup .= "\n"; + + # ~ + + return $markup; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block'.$Type.'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block'.$Type.'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['element']['text']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['text']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') + { + $Block = array( + 'markup' => $Line['body'], + ); + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['markup'] .= "\n" . $Line['body']; + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) + { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if (isset($matches[1])) + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r")); + + $class = 'language-'.$language; + + $Element['attributes'] = array( + 'class' => $class, + ); + } + + $Block = array( + 'char' => $Line['text'][0], + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => $Element, + ), + ); + + return $Block; + } + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) + { + $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['text']['text'] .= "\n".$Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + if (isset($Line['text'][1])) + { + $level = 1; + + while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') + { + $level ++; + } + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '# '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'text' => $text, + 'handler' => 'line', + ), + ); + + return $Block; + } + } + + # + # List + + protected function blockList($Line) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); + + if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'element' => array( + 'name' => $name, + 'handler' => 'elements', + ), + ); + + if($name === 'ol') + { + $listStart = stristr($matches[0], '.', true); + + if($listStart !== '1') + { + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $matches[2], + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['li']['text'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $text, + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + return $Block; + } + + if ($Line['indent'] > 0) + { + $Block['li']['text'] []= ''; + + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + unset($Block['interrupted']); + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['text'] as &$li) + { + if (end($li['text']) !== '') + { + $li['text'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => 'lines', + 'text' => (array) $matches[1], + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text'] []= ''; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['text'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) + { + $Block = array( + 'element' => array( + 'name' => 'hr' + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (chop($Line['text'], $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'markup' => $Line['text'], + ); + + $length = strlen($matches[0]); + + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + $Block['closed'] = true; + + $Block['void'] = true; + } + } + else + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + return; + } + + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) + { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open + { + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close + { + if ($Block['depth'] > 0) + { + $Block['depth'] --; + } + else + { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) + { + $Block['markup'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['markup'] .= "\n".$Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) + { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => null, + ); + + if (isset($matches[3])) + { + $Data['title'] = $matches[3]; + } + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') + { + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + continue; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['text']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'text' => $headerCell, + 'handler' => 'line', + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => 'text-align: '.$alignment.';', + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'handler' => 'elements', + ), + ); + + $Block['element']['text'] []= array( + 'name' => 'thead', + 'handler' => 'elements', + ); + + $Block['element']['text'] []= array( + 'name' => 'tbody', + 'handler' => 'elements', + 'text' => array(), + ); + + $Block['element']['text'][0]['text'] []= array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $HeaderElements, + ); + + return $Block; + } + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); + + foreach ($matches[0] as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => 'line', + 'text' => $cell, + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: '.$Block['alignments'][$index].';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $Elements, + ); + + $Block['element']['text'][1]['text'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + $Block = array( + 'element' => array( + 'name' => 'p', + 'text' => $Line['text'], + 'handler' => 'line', + ), + ); + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '"' => array('SpecialCharacter'), + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), + '>' => array('SpecialCharacter'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!"*_&[:<>`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables=array()) + { + $markup = ''; + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strpos($text, $marker); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables)) + { + continue; + } + + $Inline = $this->{'inline'.$inlineType}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + foreach ($nonNestables as $non_nestable) + { + $Inline['element']['nonNestables'][] = $non_nestable; + } + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $markup .= $this->unmarkedText($unmarkedText); + + # compile the inline + $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $markup .= $this->unmarkedText($unmarkedText); + + $text = substr($text, $markerPosition + 1); + } + + $markup .= $this->unmarkedText($text); + + return $markup; + } + + # + # ~ + # + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = 'mailto:' . $url; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => 'line', + 'text' => $matches[1], + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'markup' => $Excerpt['text'][1], + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'nonNestables' => array('Url', 'Link'), + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['text'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['text']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) + { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + + if (isset($SpecialCharacter[$Excerpt['text'][0]])) + { + return array( + 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', + 'extent' => 1, + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) + { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + if ($this->breaksEnabled) + { + $text = preg_replace('/[ ]*\n/', "
\n", $text); + } + else + { + $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text); + $text = str_replace(" \n", "\n", $text); + } + + return $text; + } + + # + # Handlers + # + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + $markup = '<'.$Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= ' '.$name.'="'.self::escape($value).'"'; + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + if (isset($text)) + { + $markup .= '>'; + + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']); + } + elseif (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + + $markup .= ''; + } + else + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + foreach ($Elements as $Element) + { + $markup .= "\n" . $this->element($Element); + } + + $markup .= "\n"; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $markup = $this->lines($lines); + + $trimmedMarkup = trim($markup); + + if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

') + { + $markup = $trimmedMarkup; + $markup = substr($markup, 3); + + $position = strpos($markup, "

"); + + $markup = substr_replace($markup, '', $position, 4); + } + + return $markup; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/inc/buttons.php b/inc/buttons.php new file mode 100644 index 0000000..c9d2e00 --- /dev/null +++ b/inc/buttons.php @@ -0,0 +1,79 @@ + 'https://...', 'src' => 'https://.../banner.png', 'alt' => 'name']. + * The image is fetched + cached server-side, so nothing hotlinks the remote host on every visit. + */ +const BUTTONS_88x31 = [ + ['href' => 'https://patate.dev', 'src' => 'https://patate.dev/images/patate_dev_banner.jpeg', 'alt' => 'patate.dev'], + ['href' => 'https://blog.counter-strike.net/', 'src' => 'https://matdoes.dev/buttons/i/8678a5dfa58ce412f7d8f978b14e2f64.gif', 'alt' => 'https://blog.counter-strike.net/'], + ['href' => 'caddyserver.com', 'src' => 'https://caddyserver.com/resources/images/nostalgia/caddy.png', 'alt' => 'caddyserver.com'], + ['href' => '', 'src' => 'https://matdoes.dev/buttons/i/9025eae1851e8f32ff70f43f2c3393f0.png', 'alt' => 'ownpaws'], + ['href' => 'https://ssi.fyi/', 'src'=>'https://ssi.fyi/88x31.gif', 'alt'=>'https://ssi.fyi'], + ['href' => 'https://vmfunc.re/', 'src' => 'https://vmfunc.re/button.png', 'alt' => 'vmfunc.re'], + ['href' => 'https://0xa0.dev/', 'src' => '/img/0xa0.png', 'alt' => '0xA0.dev'] +]; + +const BUTTON_CACHE_TTL = 7 * 86400; // re-fetch each button at most once a week + +/** + * Fetch a remote image, cache it locally, and return it as a base64 data: URI. + * Returns null on failure so the caller can fall back to hotlinking. + */ +function button_data_uri(string $url): ?string +{ + $dir = DATA_DIR . '/buttons'; + @mkdir($dir, 0775, true); + $file = $dir . '/' . md5($url); // stores "content-type\n" + + if (!is_file($file) || (time() - filemtime($file)) > BUTTON_CACHE_TTL) { + $ctx = stream_context_create(['http' => [ + 'timeout' => 5, + 'user_agent' => 'Mozilla/5.0 (0xa0.dev button fetcher)', + ]]); + $bytes = @file_get_contents($url, false, $ctx); + if ($bytes !== false && $bytes !== '') { + $type = 'image/png'; + foreach ($http_response_header ?? [] as $h) { + if (stripos($h, 'content-type:') === 0) { + $type = trim(substr($h, strlen('content-type:'))); + break; + } + } + file_put_contents($file, $type . "\n" . $bytes); + } + } + + if (!is_file($file)) { + return null; + } + + $raw = (string) file_get_contents($file); + $nl = strpos($raw, "\n"); + if ($nl === false) { + return null; + } + + $type = substr($raw, 0, $nl); + $data = substr($raw, $nl + 1); + return 'data:' . $type . ';base64,' . base64_encode($data); +} + +/** Render the whole 88x31 button row as HTML. */ +function render_buttons_88x31(): string +{ + $html = ''; + foreach (BUTTONS_88x31 as $b) { + $uri = button_data_uri($b['src']) ?? $b['src']; // fall back to hotlink if fetch failed + $img = '' . htmlspecialchars($b['alt']) . ''; + $href = $b['href'] ?? ''; + if ($href !== '') { + $html .= '' . $img . '' . "\n"; + } else { + $html .= $img . "\n"; // no href → just the image, not clickable + } + } + return $html; +} diff --git a/inc/footer.php b/inc/footer.php index fe3771d..2e797fd 100644 --- a/inc/footer.php +++ b/inc/footer.php @@ -1,6 +1,10 @@ +
+
+ +
© · source · build:
diff --git a/inc/header.php b/inc/header.php index 104d702..f31977d 100644 --- a/inc/header.php +++ b/inc/header.php @@ -4,14 +4,16 @@ - <?= htmlspecialchars(SITE_TITLE) ?> + <?= htmlspecialchars($page_title ?? SITE_TITLE) ?> +
diff --git a/inc/posts.php b/inc/posts.php new file mode 100644 index 0000000..450af55 --- /dev/null +++ b/inc/posts.php @@ -0,0 +1,87 @@ + $slug, + 'title' => $meta['title'] ?? $slug, + 'date' => $meta['date'] ?? date('Y-m-d', filemtime($file)), + 'body' => $body, + ]; +} + +function all_posts(): array { + $posts = []; + foreach (glob(POSTS_DIR . '/*.md') ?: [] as $file) { + if ($p = load_post(basename($file, '.md'))) { + $posts[] = $p; + } + } + usort($posts, fn($a, $b) => strcmp($b['date'], $a['date'])); + return $posts; +} + +function render_markdown(string $md): string { + static $pd = null; + if ($pd === null) { + $pd = new Parsedown(); + $pd->setSafeMode(true); + } + return $pd->text($md); +} + +function post_date_display(string $date): string { + $ts = strtotime($date); + return $ts ? date('m/d/Y', $ts) : htmlspecialchars($date); +} + +function render_post_list(array $posts): void { + if (!$posts) { + echo '

no posts yet.

'; + return; + } + echo '
    '; + foreach ($posts as $p) { + printf( + '
  • %s%s
  • ', + urlencode($p['slug']), + htmlspecialchars($p['title']), + post_date_display($p['date']) + ); + } + echo '
'; +} diff --git a/index.php b/index.php index e7af627..f27070f 100644 --- a/index.php +++ b/index.php @@ -1,13 +1,29 @@ -

hey, I'm 0x

-

welcome to my corner of the web. no framework, no build step — just PHP printing HTML.

+

hey, I'm 0x /ˈzɪəroʊ ɛks/

+

welcome to my corner of the web. just PHP printing HTML as God intended.

status

-
+
+diff(new DateTime('today'))->y; ?> +

I'm a -year-old developer (I know I'm unc), I hold a bachelor's degree in computer science (BUT Informatique) and I'm also a 42 school student.

+ +

if you wanna see what I'm building, feel free to check out my GitHub !

+ +
+

recent posts

+ + +

all posts →

+
+ +

If you want to leave a message, feel free to write something on my guestbook !

+ + + diff --git a/js/idiomorph-ext.min.js b/js/idiomorph-ext.min.js new file mode 100644 index 0000000..f6e8883 --- /dev/null +++ b/js/idiomorph-ext.min.js @@ -0,0 +1 @@ +var Idiomorph=function(){"use strict";const e=()=>{};const n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:e,afterNodeAdded:e,beforeNodeMorphed:e,afterNodeMorphed:e,beforeNodeRemoved:e,afterNodeRemoved:e,beforeAttributeUpdated:e},head:{style:"merge",shouldPreserve:e=>e.getAttribute("im-preserve")==="true",shouldReAppend:e=>e.getAttribute("im-re-append")==="true",shouldRemove:e,afterHeadMorphed:e},restoreFocus:true};function t(t,e,n={}){t=d(t);const r=f(e);const o=u(t,r,n);const i=a(o,()=>{return c(o,t,r,e=>{if(e.morphStyle==="innerHTML"){s(e,t,r);return Array.from(t.childNodes)}else{return l(e,t,r)}})});o.pantry.remove();return i}function l(e,t,n){const r=f(t);s(e,r,n,t,t.nextSibling);return Array.from(r.childNodes)}function a(e,t){if(!e.config.restoreFocus)return t();let n=document.activeElement;if(!(n instanceof HTMLInputElement||n instanceof HTMLTextAreaElement)){return t()}const{id:r,selectionStart:o,selectionEnd:i}=n;const l=t();if(r&&r!==document.activeElement?.id){n=e.target.querySelector(`[id="${r}"]`);n?.focus()}if(n&&!n.selectionEnd&&i){n.setSelectionRange(o,i)}return l}const s=function(){function e(e,t,n,r=null,o=null){if(t instanceof HTMLTemplateElement&&n instanceof HTMLTemplateElement){t=t.content;n=n.content}r||=t.firstChild;for(const i of n.childNodes){if(r&&r!=o){const a=d(e,i,r,o);if(a){if(a!==r){h(e,r,a)}p(a,i,e);r=a.nextSibling;continue}}if(i instanceof Element&&e.persistentIds.has(i.id)){const s=m(t,i.id,r,e);p(s,i,e);r=s.nextSibling;continue}const l=u(t,i,r,e);if(l){r=l.nextSibling}}while(r&&r!=o){const c=r;r=r.nextSibling;f(e,c)}}function u(e,t,n,r){if(r.callbacks.beforeNodeAdded(t)===false)return null;if(r.idMap.has(t)){const o=document.createElement(t.tagName);e.insertBefore(o,n);p(o,t,r);r.callbacks.afterNodeAdded(o);return o}else{const i=document.importNode(t,true);e.insertBefore(i,n);r.callbacks.afterNodeAdded(i);return i}}const d=function(){function e(e,t,n,r){let o=null;let i=t.nextSibling;let l=0;let a=n;while(a&&a!=r){if(c(a,t)){if(s(e,a,t)){return a}if(o===null){if(!e.idMap.has(a)){o=a}}}if(o===null&&i&&c(a,i)){l++;i=i.nextSibling;if(l>=2){o=undefined}}if(a.contains(document.activeElement))break;a=a.nextSibling}return o||null}function s(e,t,n){let r=e.idMap.get(t);let o=e.idMap.get(n);if(!o||!r)return false;for(const i of r){if(o.has(i)){return true}}return false}function c(e,t){const n=e;const r=t;return n.nodeType===r.nodeType&&n.tagName===r.tagName&&(!n.id||n.id===r.id)}return e}();function f(e,t){if(e.idMap.has(t)){l(e.pantry,t,null)}else{if(e.callbacks.beforeNodeRemoved(t)===false)return;t.parentNode?.removeChild(t);e.callbacks.afterNodeRemoved(t)}}function h(t,e,n){let r=e;while(r&&r!==n){let e=r;r=r.nextSibling;f(t,e)}return r}function m(e,t,n,r){const o=r.target.id===t&&r.target||r.target.querySelector(`[id="${t}"]`)||r.pantry.querySelector(`[id="${t}"]`);i(o,r);l(e,o,n);return o}function i(t,n){const r=t.id;while(t=t.parentNode){let e=n.idMap.get(t);if(e){e.delete(r);if(!e.size){n.idMap.delete(t)}}}}function l(t,n,r){if(t.moveBefore){try{t.moveBefore(n,r)}catch(e){t.insertBefore(n,r)}}else{t.insertBefore(n,r)}}return e}();const p=function(){function e(e,t,n){if(n.ignoreActive&&e===document.activeElement){return null}if(n.callbacks.beforeNodeMorphed(e,t)===false){return e}if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){h(e,t,n)}else{r(e,t,n);if(!f(e,n)){s(n,e,t)}}n.callbacks.afterNodeMorphed(e,t);return e}function r(e,t,n){let r=t.nodeType;if(r===1){const o=e;const i=t;const l=o.attributes;const a=i.attributes;for(const s of a){if(d(s.name,o,"update",n)){continue}if(o.getAttribute(s.name)!==s.value){o.setAttribute(s.name,s.value)}}for(let e=l.length-1;0<=e;e--){const c=l[e];if(!c)continue;if(!i.hasAttribute(c.name)){if(d(c.name,o,"remove",n)){continue}o.removeAttribute(c.name)}}if(!f(o,n)){u(o,i,n)}}if(r===8||r===3){if(e.nodeValue!==t.nodeValue){e.nodeValue=t.nodeValue}}}function u(n,r,o){if(n instanceof HTMLInputElement&&r instanceof HTMLInputElement&&r.type!=="file"){let e=r.value;let t=n.value;i(n,r,"checked",o);i(n,r,"disabled",o);if(!r.hasAttribute("value")){if(!d("value",n,"remove",o)){n.value="";n.removeAttribute("value")}}else if(t!==e){if(!d("value",n,"update",o)){n.setAttribute("value",e);n.value=e}}}else if(n instanceof HTMLOptionElement&&r instanceof HTMLOptionElement){i(n,r,"selected",o)}else if(n instanceof HTMLTextAreaElement&&r instanceof HTMLTextAreaElement){let e=r.value;let t=n.value;if(d("value",n,"update",o)){return}if(e!==t){n.value=e}if(n.firstChild&&n.firstChild.nodeValue!==e){n.firstChild.nodeValue=e}}}function i(e,t,n,r){const o=t[n],i=e[n];if(o!==i){const l=d(n,e,"update",r);if(!l){e[n]=t[n]}if(o){if(!l){e.setAttribute(n,"")}}else{if(!d(n,e,"remove",r)){e.removeAttribute(n)}}}}function d(e,t,n,r){if(e==="value"&&r.ignoreActiveValue&&t===document.activeElement){return true}return r.callbacks.beforeAttributeUpdated(e,t,n)===false}function f(e,t){return!!t.ignoreActiveValue&&e===document.activeElement&&e!==document.body}return e}();function c(t,e,n,r){if(t.head.block){const o=e.querySelector("head");const i=n.querySelector("head");if(o&&i){const l=h(o,i,t);return Promise.all(l).then(()=>{const e=Object.assign(t,{head:{block:false,ignore:true}});return r(e)})}}return r(t)}function h(e,t,r){let o=[];let i=[];let l=[];let a=[];let s=new Map;for(const n of t.children){s.set(n.outerHTML,n)}for(const u of e.children){let e=s.has(u.outerHTML);let t=r.head.shouldReAppend(u);let n=r.head.shouldPreserve(u);if(e||n){if(t){i.push(u)}else{s.delete(u.outerHTML);l.push(u)}}else{if(r.head.style==="append"){if(t){i.push(u);a.push(u)}}else{if(r.head.shouldRemove(u)!==false){i.push(u)}}}}a.push(...s.values());let c=[];for(const d of a){let n=document.createRange().createContextualFragment(d.outerHTML).firstChild;if(r.callbacks.beforeNodeAdded(n)!==false){if("href"in n&&n.href||"src"in n&&n.src){let t;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});c.push(e)}e.appendChild(n);r.callbacks.afterNodeAdded(n);o.push(n)}}for(const f of i){if(r.callbacks.beforeNodeRemoved(f)!==false){e.removeChild(f);r.callbacks.afterNodeRemoved(f)}}r.head.afterHeadMorphed(e,{added:o,kept:l,removed:i});return c}const u=function(){function e(e,t,n){const{persistentIds:r,idMap:o}=d(e,t);const i=a(n);const l=i.morphStyle||"outerHTML";if(!["innerHTML","outerHTML"].includes(l)){throw`Do not understand how to morph style ${l}`}return{target:e,newContent:t,config:i,morphStyle:l,ignoreActive:i.ignoreActive,ignoreActiveValue:i.ignoreActiveValue,restoreFocus:i.restoreFocus,idMap:o,persistentIds:r,pantry:s(),callbacks:i.callbacks,head:i.head}}function a(e){let t=Object.assign({},n);Object.assign(t,e);t.callbacks=Object.assign({},n.callbacks,e.callbacks);t.head=Object.assign({},n.head,e.head);return t}function s(){const e=document.createElement("div");e.hidden=true;document.body.insertAdjacentElement("afterend",e);return e}function c(e){let t=Array.from(e.querySelectorAll("[id]"));if(e.id){t.push(e)}return t}function u(n,e,r,t){for(const o of t){if(e.has(o.id)){let t=o;while(t){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(o.id);if(t===r)break;t=t.parentElement}}}}function d(e,t){const n=c(e);const r=c(t);const o=f(n,r);let i=new Map;u(i,o,e,n);const l=t.__idiomorphRoot||t;u(i,o,l,r);return{persistentIds:o,idMap:i}}function f(e,t){let n=new Set;let r=new Map;for(const{id:i,tagName:l}of e){if(r.has(i)){n.add(i)}else{r.set(i,l)}}let o=new Set;for(const{id:i,tagName:l}of t){if(o.has(i)){n.add(i)}else if(r.get(i)===l){o.add(i)}}for(const i of n){o.delete(i)}return o}return e}();const{normalizeElement:d,normalizeParent:f}=function(){const o=new WeakSet;function e(e){if(e instanceof Document){return e.documentElement}else{return e}}function r(e){if(e==null){return document.createElement("div")}else if(typeof e==="string"){return r(l(e))}else if(o.has(e)){return e}else if(e instanceof Node){if(e.parentNode){return new i(e)}else{const t=document.createElement("div");t.append(e);return t}}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}class i{constructor(e){this.originalNode=e;this.realParentNode=e.parentNode;this.previousSibling=e.previousSibling;this.nextSibling=e.nextSibling}get childNodes(){const e=[];let t=this.previousSibling?this.previousSibling.nextSibling:this.realParentNode.firstChild;while(t&&t!=this.nextSibling){e.push(t);t=t.nextSibling}return e}querySelectorAll(r){return this.childNodes.reduce((t,e)=>{if(e instanceof Element){if(e.matches(r))t.push(e);const n=e.querySelectorAll(r);for(let e=0;e]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=r.parseFromString(n,"text/html");if(e.match(/<\/html>/)){o.add(t);return t}else{let e=t.firstChild;if(e){o.add(e)}return e}}else{let e=r.parseFromString("","text/html");let t=e.body.querySelector("template").content;o.add(t);return t}}return{normalizeElement:e,normalizeParent:r}}();return{morph:t,defaults:n}}();(function(){function o(e){if(e==="morph"||e==="morph:outerHTML"){return{morphStyle:"outerHTML"}}else if(e==="morph:innerHTML"){return{morphStyle:"innerHTML"}}else if(e.startsWith("morph:")){return Function("return ("+e.slice(6)+")")()}}htmx.defineExtension("morph",{isInlineSwap:function(e){let t=o(e);return t?.morphStyle==="outerHTML"||t?.morphStyle==null},handleSwap:function(e,t,n){let r=o(e);if(r){return Idiomorph.morph(t,n.children,r)}}})})(); \ No newline at end of file diff --git a/post.php b/post.php new file mode 100644 index 0000000..b54f50f --- /dev/null +++ b/post.php @@ -0,0 +1,31 @@ + + +

404

+

no such post.

+ + + +
+

[ ]

+ +
+
+ + + + diff --git a/posts/42-piscine.md b/posts/42-piscine.md new file mode 100644 index 0000000..8fb7f5a --- /dev/null +++ b/posts/42-piscine.md @@ -0,0 +1,9 @@ +--- +title: 42 Piscine +date: 2025-08-30 +--- +During the month of July 2025, I had the opportunity to participate in the 42 Piscine in Perpignan, France. This intensive coding bootcamp was a unique experience that challenged my skills and pushed me to my limits. After an entire month of rigorous work, I managed to be selected among the candidates to be part of the 42 school. + +it was an unique experience and I learned a lot of things in the short amount of time, I look forward to continue to learn during my time at 42 and apply that knowledge in real-world projects. + +![42 Perpignan Occitanie](/img/42-perpignan.png) diff --git a/status.php b/status.php index 2e95a43..13da74d 100644 --- a/status.php +++ b/status.php @@ -2,23 +2,43 @@ require_once __DIR__ . '/config.php'; $cacheFile = DATA_DIR . '/lanyard.json'; -$ttl = 4; +$ttl = 15; -if (!is_file($cacheFile) || time() - filemtime($cacheFile) > $ttl) { - $ch = curl_init('https://api.lanyard.rest/v1/users/' . DISCORD_ID); - curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 4]); - $res = curl_exec($ch); - curl_close($ch); - if ($res) { - @mkdir(DATA_DIR, 0755, true); - file_put_contents($cacheFile, $res); +if (!function_exists('lanyard_refresh')) { + function lanyard_refresh(string $cacheFile): void { + $ch = curl_init('https://api.lanyard.rest/v1/users/' . DISCORD_ID); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 3, + ]); + $res = curl_exec($ch); + curl_close($ch); + if ($res) { + @mkdir(DATA_DIR, 0755, true); + file_put_contents($cacheFile, $res, LOCK_EX); + } } } +$haveCache = is_file($cacheFile); +$stale = !$haveCache || (time() - filemtime($cacheFile) > $ttl); + +if ($stale && !$haveCache) { + lanyard_refresh($cacheFile); + $stale = false; +} + $data = json_decode(@file_get_contents($cacheFile), true)['data'] ?? null; $status = $data['discord_status'] ?? 'offline'; $spotify = ($data['listening_to_spotify'] ?? false) ? $data['spotify'] : null; +// Discord can lag clearing the Spotify activity after playback stops, leaving a +// stale "ghost" track. If the song's end time has already passed, treat it as stopped. +if ($spotify && !empty($spotify['timestamps']['end']) && $spotify['timestamps']['end'] < time() * 1000) { + $spotify = null; +} + $user = $data['discord_user'] ?? []; $did = $user['id'] ?? DISCORD_ID; $name = $user['global_name'] ?? $user['username'] ?? ''; @@ -33,13 +53,41 @@ $banner = $bannerHash ? "https://cdn.discordapp.com/banners/$did/$bannerHash." . (str_starts_with($bannerHash, 'a_') ? 'gif' : 'png') . '?size=480' : null; ?> -
> - -
- - - - - +
+
> + +
+ discord + + +
+ + + + +
+ ♪ listening on spotify + + +
+
+ +
+
+
+ spotify + nothing playing + not listening right now +
+
+
+