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$jRzB%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('/^\[(.+?)\]:[ ]*(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $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 .= ''.$Element['name'].'>';
+ }
+ 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 = '
';
+ $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 @@
+
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 = $age ?>-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 !
+
+
+
+
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(""+n+"","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 @@
+
+ ← go back
+ 404
+ no such post.
+
+← go back
+
+
+ [ = htmlspecialchars($post['title']) ?> ]
+ = post_date_display($post['date']) ?>
+ = render_markdown($post['body']) ?>
+
+
+← go back
+
+
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.
+
+
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;
?>
->
-
 ?>)
-
-
= htmlspecialchars($name) ?>
-
= htmlspecialchars($status) ?>
-
-
♪ = htmlspecialchars($spotify['song']) ?> — = htmlspecialchars($spotify['artist']) ?>
-
+
+
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 @@