From f29c639f7cff75fb9c902eace641720623c31194 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 3 Aug 2026 07:21:32 +0100 Subject: [PATCH 1/4] docs(examples): stop the examples describing the releases they were written for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight committed previews read as documents about 1.x. Three framed a current feature as "v1.6 Phase A/B/C" — a plan for a release that shipped, on the pages demonstrating nested lists, composed cells and the free canvas. One told the reader to tag v1.9.0 to publish a module that has been on Central since. Two signed off "Composed with GraphCompose v1.5", which dates itself again at every release, so the number is gone rather than moved. One badged a canvas-placement demo "v1.8" beside badges reading "charts" and "paint", where the version said nothing the other two did not say better. And the certificate on the free-canvas page was awarded for shipping v1.6; it is awarded for the page it is printed on. The hyperlink example was worse than dated. Both URLs it renders — a template-authoring page and a v1.6 roadmap — no longer exist, so the example that demonstrates links shipped two broken ones. They point at the preset cheatsheet and the extension guide, and both files are in the tree. Left alone: the inline-code demo still shows a Maven coordinate with a version in it, because a coordinate is what the demo is about — the subject matter, not a stamp. The eight previews are re-rendered, which is what the drift gate asked for by name. --- CHANGELOG.md | 16 +++++++ .../readme/examples/canvas-layer-showcase.pdf | Bin 1846 -> 1804 bytes .../examples/composed-table-cell-showcase.pdf | Bin 3149 -> 3122 bytes assets/readme/examples/cover-letter.pdf | Bin 3395 -> 3392 bytes assets/readme/examples/feature-catalog.pdf | Bin 215331 -> 215328 bytes .../examples/inline-highlight-chips.pdf | Bin 3773 -> 3704 bytes .../readme/examples/nested-list-showcase.pdf | Bin 2345 -> 2374 bytes assets/readme/examples/rich-text-showcase.pdf | Bin 3931 -> 3958 bytes assets/readme/examples/weekly-schedule.pdf | Bin 8344 -> 8336 bytes .../features/canvas/CanvasLayerExample.java | 14 +++--- .../features/lists/NestedListExample.java | 12 +++--- .../tables/ComposedTableCellExample.java | 10 ++--- .../features/text/InlineHighlightExample.java | 4 +- .../text/RichTextShowcaseExample.java | 8 ++-- .../flagships/FeatureCatalogExample.java | 4 +- .../support/WeeklyScheduleRenderer.java | 2 +- .../coverletter/CoverLetterFileExample.java | 4 +- .../examples/CommittedAssetDriftTest.java | 40 +++++++++++++++++- 18 files changed, 84 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b34368..14df4671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,6 +162,22 @@ follow semantic versioning; release dates are ISO 8601. ### Documentation +- **The examples stop describing the releases they were written for.** Eight committed + previews read as documents about 1.x: three framed a current feature as "v1.6 Phase + A/B/C" — a plan for a release that shipped — one told the reader to tag v1.9.0 to + publish a module that has been on Central since, two signed off "Composed with + GraphCompose v1.5", one badged a canvas demo "v1.8", and the certificate on the + free-canvas page was awarded for shipping v1.6. They describe what they demonstrate + now, so nothing in them dates again. The hyperlink example was worse than dated: both + URLs it rendered — a template-authoring page and a v1.6 roadmap — had been deleted, so + the example that demonstrates links shipped two of them broken. They point at the + preset cheatsheet and the extension guide. +- **The drift gate stops going red on a tree nobody touched.** `pdf-chrome.pdf` + demonstrates the `{date}` header token, which resolves from the clock, so its committed + copy differed from a fresh render every day but the one it was committed on. It is + named as exempt with the reason rather than tolerated, and the entry stays until that + date can be pinned the way both fixed backends already accept a `deterministic(...)` + instant. - **A committed preview can be reproduced from a branch that has moved past it.** The documents that print a version took it from the reactor, which between releases sits on the next patch — so a render from `develop` named a version nobody could depend on yet, diff --git a/assets/readme/examples/canvas-layer-showcase.pdf b/assets/readme/examples/canvas-layer-showcase.pdf index c8df82f9a2877aa7268502127569a97d47ca6997..d141794dae3bdfa0e11026380de5b5effd6f2ac5 100644 GIT binary patch delta 1173 zcmdnS*TXjChp9Y`D6DEWlGtg`@zs|x@H=8Zrtt#}{uXlGv zpXhZs9s0n$2XYLS-Ib!0a+RJh{R;_;L zk{x^>^~Ge?GJQAPXDsAySJ!^>y25Ohde-~=0!ulBkEL#E_GonRcYMOpJvm~@TP=;W zmJe(9Ei@T$MOtg$-wYPN=~j@H!IOd-st8t&<=GSZj+VRU28;curUryQHw+LpFX zF+6SQ9euu}QwhvIE(P)WzJaqO>u$CftjX@Kx4xRe?tNX>>h|*u$MsVr)T{q6Pmf;9 z`($gR;-XDv;u2<7?JIa+|C!*s_s7;tm!HXqZPlJ#v{bP1p2;l{z8uTh^7c3FY&2hO zW2Q*1N}Ltk8os%J1u!}>ZqXZ(;d$v_E{u3Y*-Vph zH+Cy#N-V6GzPa;Y`trLr?QNTvI8Xi~BX#bErb&hS6cxoy(MtsN_-&NcE6Rl4CCs?% zd+hq-hfh_SZ)fk2`Db{}F10?*e6sQUD=G^Wr^>K+JI@N5`A*3w;ZNuz={=jX4CSxg zpTM$9ytzwb$rKlsMG0UF`2VIEB7jIbedN5M!@9B9EHL+s#~dGc`IVyq(md*(7x1%;GRXx!b$V zSvVKZu$yLFYpHCP5$ceZ79pEk&!L?BzkOr%V|9bNEnmZX!%KIY*&T~@Pg}8HdFkhH z-zl@DtpB<`FJajI^uj0p-?IHSldt;Sp8Bo#)NS9>y?Qp>eRFvE=FD5XCoru(?5Cjj zXB~y@TgvRW{Fz|ESA8%2l-SfVy9-sXa<8Ab5qIqKCapE!0z*!T@axC7?P+Bjwj|#)e$eslazNRxYOEj?R%{>Hg2 zwXd%)*AkzX82H$~Z*96%okabg@2|eipM6p7{=C}0nO`cBx1IgN@OR_3Cfk30he|>p zFFv&F+P&@8Svvn2>&v^dHpjBeV63;`(hthdFHta5Fi;5M()V;xh_*2`cQSV~HgYv~ zG%<5?F>^98c64zzc5yT@F*3F^HFk6|G_tU?v@~!uG`27|HMKM{H+8izF#(bWmWHlI z26hTW8zF(qjLAK0p*r4t3<3f!2e0>>cX||2w(fPs7FSi)7(>q<|7l15ofivR6Sa9= b#0nX?Fdb&D$$ac`Tqb75T&k+B{%%|VIhP8% delta 1240 zcmeC-+r~E`iOIrbav_V@#473fzEkECgUoq6WR`xupc?xsH=c{9%f($s52e^Rfz|_w%Yt&d=S?`h0VPcYvoz(5w|4X|5mMO=3E_y7I$2r}aMT^6ln0 z=P?Tf^~oOyRcYC2I#GpfHv4{!lbYT;Q&g0UriBzr8Y)>RcNVQ)VibDm=24b<;d)6; zwV+DP*6O~alOC6@YDwDR_d(k8V8@gl{U18HD<3qf?mzSP>Y9TQ5h0Q5xKCJ}$ht7wHMQvS z!RZ&%cy8;fCuC)F@y4!HJpU>*t$ve$A)j>pgGmQ+H?QFizW?y3T8ro9EjvzIveI$NHtv)Qw`c2BU$YbzVe2ZbI}kMtLr6rQ^4wQ`beEYn4!#_Gl|t}V>2QPHwz z=9ez^5UD?25%cJgm)a}u^!IIQijL;1%A3FU?XKkPeNfo1_Rr9y>2q?%2U$a@_K4p` ziaTtsN!kXbyFctxdwDje%5v_Uvq3xdwQN3_fBDP%Fa2M#${vZEiWZ)j!fmXjRt@8N)$)Zb0l%sQ=l(4&E1np4 z;7!$w`9~95&Z;un2n9Wx^y;tsfqOGfy$-7QzFs_|>By@grM7~xlexiSZiUo~`4dc31gjsJSJrmc zT#01NtWD2ad;5Ru#Lw>&-^~BP*famuv(@LUcf?({I~F_tnW6QS{S}wz?5xk);B@c1 zpZkVrIktm`>%N!S3X9Fj3%_Nab}z3lHK;;N#zk7jnc2N^`cAKpYnIgsPKe$1wr{0~cD^6dQlv_Bv>LbB}kh=-O|AArXK2L-(Y|EliVBfAVB!@N1#cXOVX{ zeB-g$@z=!Y%B==|^K0=>o_w=(DmcgQ|6@gWEvxLpTV|)tzS}0(Uw(Vu&VFiSMVo7L zz00uYX_Ob6!4cB`>-y`uo1Oj>$jy zSCr1|+-$-!gR$O{OFt+-zeK@M!9XF1OW)H)A=<{wz|z#w&DGr8$k^Dz(b>Sn+|Ak4 z)Xmbwz|GOw(!j{lz{%3s&B)Ek!pzj%*uvNX$TT)GHgz>IG&D6fHMdhB+6W0;W=u|I z3k|)^%OD`)a&U7@KY!=BD~nR*Zm?66e9+~YRI^FtTK%>SNmpl;9(8;Afg!wvIfj=j oH7}*Oq$o8p7sSd>%Hk?6Nh~U^a}07*$Ds{jB1 diff --git a/assets/readme/examples/composed-table-cell-showcase.pdf b/assets/readme/examples/composed-table-cell-showcase.pdf index d3ff47c7388601b542c86048737e1623396d6c40..0f5647815ee99d0af623b55c25bd44ddc719f4ed 100644 GIT binary patch delta 2519 zcmbu-`6Cky1Hkdvn0wwS)I;Q!BgPm_j?IviM_X&oCdb@NEh?K^=E!~LTB1-E4|~X! zJg#SGcyfjix$kSy`@Da{``h=Y&re^L zq4uI?kr?kv$iVzMA-&JdV|qU(+&4I?T`;6z(uF$3EyS9MH@Z31kNIX=*VuYm@{jV- zz!-;6wJR(GYtSV9RlSqx^kPx_a4qbF1k{e#+xv7#3+4Q8O3wC+E^VC8D78d)co zn8Vl6`=8kksw1EUFBc~@mnq^8>xIiD;=S1y6V->!CM$kRN$r(rziUqpFq>^dfGJ^X z#Mefgr*wj&V9Lnl;X+<3j>2qp+y-GkNnmYSP;!M@7onpC)(x+#)I9~?;_^Nhfp)Uv zk)`@6e(fj#Ydr?2h_}|ydLijR@QI#ofNAS=aSfliu+ku{KCF6BxhCP1Kd2g>h$9agYtzCvf~L-OJuOn&JBC%Vf926ed^&nv2eRB54DbZomkxR3 z@5df#q_9d61S@+#x}kG}*tdsyiqv=a?CQo%489TZt9N`j^=}B*F-q@Lcvb&geXsQk zqwsxPeF#xKBP}Wl-`FCbN!DjFi~RwQPb21nRyT*Oy*DjTs9wPeXdCC!-+R}oGvi`orXX`P;B$w!BdSBjgQM&$KSKp#{ zHp7-rI`Y{ZQ$emJ<-YRcKO@xcBm<+8Z+IDw;JIqz8(i4S5ew#F@l7w!#Hp@XOk{|3 zTE;eIB=06FT)1L1I?kltkzuM(X>(7O_|D=L#%0FGE#Q9_i95v&T=?zU|*v2c=Wt3m!7=BYe?p zM?RMoB^iM6y+!#QI`F7}3399e!<|^Rujb)+@zhjGpzTMukbTbbe zwqyHy)saq{2W*Z!R@-Z?TkcYIDpj6%yGH(@IvWgXT(P#wpIJ?R?Oj}qw;z%A%deS> zE;c7uZdS<;{FLShXUGe?PpH${NAF)?tjK-6RMOlf_JzA@n<}MnniJgrD}-m5FAIz3 zD!m*Nn#CU_Bs#l)_pflcg>Y*f3O+8GmE+g^3Ns2uUIc1vhDu?BfXk&c^D51H+kqG2wX zS8egU;YwQ%Riqz%&mbC%g79)-0^(*+Hp*-%c*|y6ru_Hu=k>bf2l>yW(aN8mZ6^yS z{ku-TER!;`E!v;aqQQKm?Jqx)%5CsmN?1N}*mE9rML)2!H}&3Ns;bSRsN-_AUvM_u zS|CkfS8H7Hfh>CaiKWVjPTHZ*(wb;`($y_m@{I-Ry7HidjGpwDphEWRp@T%Veg7tn z2>$o^)A&B28pa~s4^VB@V>F@Q?3=`(rd01*w+=l*$|na(I_Dp9UEK$2-U?J1U+3NM zoUuQ91l7=Y6M_fXWyn{BI-!G{Y{DUz7;JJX={HysoDWart&AtBe!@4-1a6oCD? zM-5vcG6F9YS?@Og8HAd=j5eABlSZcm{Y@+_FJSNv z`zg%SHORxlHM?doVY}%e{ZifhwsE6isFTxq5>7MEQn78P+Tzu5O{yfD64bX<2PtTI<4S`^aI?qAW==SM<{LEhE;* zt579~XNqfl13A#OA&E%AS?sMt^7ul(=K;AQC!w7Ti94j3E|auRQR2g|LMf@O{#sNxRdikwupFoMb;eZm<+%YMvH((cVy8UY;1z$KhDuJOdup%M8XyB?<> z$qy10oNv6GAd$l)(^`m#*b_dLsg6z_qfe9Bs(UZU>8uLhU(*)(EiH3cf(hEQyp zggY=H<>maVX6S}jgj0s-?deCFCHFSQ`xwcaVdU=nJQHEwOEz-6mud z-4G3t(1YIsIaUrHQ1h&-@}%ExURe$RbVr?{9m{9GCLT|@j860B`0C47{OHDjKGd~- zPTY<28K3=NLc$#$M*^l@kD>0HJbkmjiB0aZ>xXXc(dEhw_y0mjtp(#T2aDdha#6L= z<}ht-GXtoZxfV-2L5xACTM$e*dcgS8svf2L#fC U5R$ux08}5YE1;-oehV$|UnlRkYXATM delta 2521 zcmbu-=_3;i0|0QE%#i!8kTq9w&9DbKhsYHYO>-nSBN3zJD!FoBbDznXV}_hruE(`J z8FDK7I;mT0^10%yqedJ3f=XH$VBHp;U? zlZ?Er#fF2}aY!!A0xi1Ra4BAkWx#a1u4|i~At4!TTlu8jOYi&lgG2U^PQi0H2CH(w z^Sx*|jB_Jt4$_G7L5kl9Y{@zI_ZB9RLyv!9_-vr>+J(>L2}9$YudbzY*GuPj{4?}| zNV3HC?%m{-w80GVC-U@yU`UOZCAG~ZKIXNh=q9ULE{||KW*3&``U0OxuKeSpOPjzy z1=IUJv|ySm(2^yQFOsp`Vf*$2(0J))6-S+s+k#FSS3G9MW-1~0l1@92F2e)#mwX4) zx0wbE*OdqSZ+Zo;`M8bE%fSYk>FjHCwu-PUG*i0@q-Rj2&Z*$dt?KDrEeGUyVEVU9 zG=HHrr&|-mn(k!k(XvK-D?NqFT&8WesmLPY-RtW)Ee;F5=8qZT7`Y!4Tf=I`C9?R> zxtH3^7)Rq`WR}ED=!p8h0* zr#-jHvA4>J@_x`ZKUrs+CLwdb%HFCU7pV2+e&fmxFKg_NC#h4CcjL}OoC_YoSl8aO9#y>B^JanZ%H+L2>hX6I4`A%C)meY3z5Kbvm*m;cenpEFA{F`t8v&MW zx9eD!pO#$mz7y(4jG6#<<@Pdqum_M5C8Urz=H7%K(U_>0br>X=8Lx3LNJ7!b zcJmw~So*OXH^epv2VO-RNk*}06aA1}oT!1%+#U1Xyj2Df+7QbO)0dCvyq2~yo7!B1 zEl^9cQr(V57V6%?mluD|7RGXAJF9Y(J zO@Ml`*ot2((7^o*_mnNGq}kd}<8BeQk4vOR(A{WKuyrP)^PV!z`y&-xh3B;+7TyiL(t035t!v8JoXnm7ozSE`z_d?G+mXaFzJN{HbF zcaK<^JG4U!ORPHrs_hKQ?v#iKK=>5U36{#r;@f2s_Q|X|USFZrEHbp;*|%+M+e$cs zEUXJymz?*s0ubnc@=|+}+v?e=g32oHGw`eQt%=K#>Jlo2hr8MYRG>C)k20EJa1}AJ z23#E>ecE=L?Nc{pdi3lMUw-pyc9(NkxL;|JS0+LM`+%0DPi>3&W9YV1T$gRzU9ApG z&|J$s(0ob{i@bB2FMk{GBghZF?N;xfU)0D(r8@XBK;NOYZ#!~m9R@O)HU)@{gm1C1BjHZ9 z9ZhBMjs?9f(L8hTH#KFY1}I52a`7a5SeI;hf?u2`65_24m{-*OmExHm79{^~#L-)X z$tSaGvVIvnx9_iO&p-kS^|AXHL&;_+>Bw_^iz78bZ76g zjTh50o8HYZy&v>L5#U8VcAM^=E?vp@=90@#_>4^|`)%As3M%F_P>oR$@9a<12p{IY z5sO#rITa-EAv%qQ?$M(W~RVcN0%Nd?ZWu#4bP{n>b2u2}Kfb3%u zCBm*EBwwv6`AfuAtjMl9&tJwlsDCSqT;3RhFyDZB-dAF11oqq=$ghI~vt`O|f{7zI z;RYH7vziQz#HXxng1F+qYFzCR{2h{APgI0-jM;oeklU7cY2HcvlvsiDqq>LWI|j5x zWjuqBneTKli#gzPb3%1U0GVZ#Ab01C#|6=;FMIBUm)5`%^XNW2MN+=x*0hDVZHO)1 ziH7Q}=IVxOkOd^8bAcW7;D=|gZaD1##c13>F;Y+bXd}SvnUQ%TQqtY_2Hoas@|jMw0P&ZS&FosfTW(K0M0l&NuhwP{x-`aNy{qrL^P$-P5mtb^lI*$cvYkCXQtF)USR6hN3Y9; zQ(Wib+pPLrNe}64>A^U~2!!Fo2CuOZjE@7#2pdzmqn5c_AG8Is?LWB(U%^MfG-1C9 zex5_k_XmnZ;f|qQsG1p-=Nndjuo*!3+e`9R9QoNb5Gpx|*42s^bUU}YVSc30$1 z7^QS&bSDia1*^YQK9iYH4M~gIY2J6J*@^jfaEA)b@B*r4o$NWC$4i~|9)5g1v_ChC zRZ}8FmU^NAyrdnrYl$-g?nv^Y`b~6=&Vr|DgGZohf7i#ZYohEY*ZuO|V$?Q*u#VX4 z4h?hhh2<%gY_Ht0C4!V7a?{t~@Ri!#!>jF`#NY3S7N2B%of0Rkmw8v@Z5SX-z14+Y zV4KEVEarC1{t9wXk+3&gmq4Y4zR?&Jo7qmt7Yc=*;49)A{vOBM6Nx@lRKthUopZ0p zm%yH6KzCypD4rgx3WKRA>%!p(9VK0PC4Fs0C{#rs2G!A*M<^py;Ce8aydqpdNg1lA zs;8r@Ew2rSB6Q>xl;L{1Fql4E<({be|3d)&Yox}p1h9B8>enu?(is>4B2$@Jg_%_q N6qq3pgqc3`e*hAIze)fA diff --git a/assets/readme/examples/cover-letter.pdf b/assets/readme/examples/cover-letter.pdf index 6c8ad84989f1d42c0500b105cd9be3d50d96e8b8..b455df94aaa5dfda041c0d53240147384db9ffbc 100644 GIT binary patch delta 261 zcmX>sbwFxD3Zv=dvpmWhJGOE9vKdc0=pYpETvYJ4C_~Dux|gx3_EzWLKRPyhMx%h= z&4wynF6}U;Z^2{N`WBD!1blclk}*?mF4L8GG;k<91$H9J#rV ztB0}P&Dqe>*xbm~z{14T+0D(|$-u?H(b3h((%I3#(#6Ht+1S9`($vM#+``4x$<@%n z!pX?U$;`#o%)r>y(Zbx)PQiu}BUnIYOrFbA%x%Nw_O6_V!R;k;_GD>Z1r8G~RaIAi GH!c8k@m4ng delta 264 zcmX>gby#Xb3Zup3Q#{HWJGOBKXdJSLbC`YI`At|uf99{H=2QPZu-V@!{XUV+$s+l@ zoXrBW)|Teik2jk9b8IZy|2rW6o_pYze@zO1_}8)8>^Q|;e$%$QPF8QuUdjL5HzmsY zH+OUOFxES{I2*Ybx;i;qT9}#`8<<-dxR|*(Svngy8=JaWIvTk+8@pMU8#eGcp%Wmf%(3Fy~TL Jb@g}S0swobRm1=Q diff --git a/assets/readme/examples/feature-catalog.pdf b/assets/readme/examples/feature-catalog.pdf index 0c0e2391719f745bb7d0dba3719c9eb8cca623e1..860559b4d0c1e587ce1743a729f59b9b3785b7a8 100644 GIT binary patch delta 2739 zcmbu9`#aMO1IAzHki*OfX-($bGK_8LFf$fL4s$A6DIrqM9(y6zwDpFY2Q?(1gcNYHa68m!~_Q1|Je6ZZq-g@D(f z=j{3;pYZ|{PZsamH1rlmdc_G*YZK?iPsy6QpC^=C+bgN|oc0W12YsxVnGhjM}+-&%k6tNzxl(B(J zS!kOJ*?G2av9o_R=O#yPOphJUWvZsjFT!q$;>SD$MbFWZ2^3uql4Yy3(!Gawb}~ zUzqf>(Kp~j|4{snREqhS{U0xTMHq08ORBEWmA%1MUd+x$>GzFfTIp83A^>*V3Xx~* z)^&IIkWOV+G+PL-1j{C192l`r+gPyNL#!s{ssGojo;9%2xJ+9g9hO zc4?5euFL_>sMXC>nw{Dhpa|0rSk$Po>wo*##cMXMn#N^RB#OZ;z91rj=p+tjj!qgQuB^^pi1>ev0Q4jO1gwjbv}0c0-_6Ms`F6wSMxaX zv4~z(kmryTJh-CE4r8h@QOK`5_tg2IbHuEztG!^+-}zT0a%T--c4K**yyej8hddLF?0w86|RaqDvLGM28e z@upg|A*kf=@a{`2?x*h^o~$gys>Q3PGOsqbfP?+DVthk(yor^>pnB8$J}=H#R0F1v zU1lbT()=dqMg}649qhHnv1bVDrzer28s`$%z>100H2?1A7loI#u1upRiJa(ee%N&o zevfE~Rx<8=@S=0u)!V?zn|}==lwahiqxVSIuCL2>lzYZtrl9rv{&(24H-1h@=ALCOWSZJM8T=r7QPxzibI__*;;KFlb0XxuxdO|3+n9Sim zWd<00J$Tod!4d7L6o}%RTNTZV8`IjOubka@Qm!IfdNot{K`H3@A3JWW)<4UO1!xwQ z{+|PZ#<)_fq3!PVoX<#@ zMPv{O(Am3R@5}B;H!yNU}ELS6hP} z`KJ3*AmZK-k}y@Ue#>U|7i-qP!wAD>Ob%V(x{ z4LwSQqWu?t?x1+IW~X2bwd>bFKoLm<2u;(EG+~X8oRD;C&?W;HkFMl$j*>#^mL}%k zH^^X1FGi{-P9R=;)eUHl_ZloD?+$9ao>+mm6x0%O*Dwy^hSLi^WQx#% zwVcw>l!JB2-shI4g`&Kk>EZAA8x zghD952CHwYFN_~8GL*nDKs*rujvJrSd$MQNd`C?@=k8 zQKf&3trMUDd3W#9?zIG4083z+vcYm}dDiP0hq-9i|G48PNA8;0ee9aU=L^g5VEN%a&F7 zc7vA}EMFDA+`o%WUhc~!@tKs@=Lh>eIeAA_BgK-o4W~MEu$!~aA}-$;UfMmf+ss&z zJ)UR)cr&Jz{~m|1r}#&L!}RChO%=)!`*Tt^uX4LG!fCsVdOPVC@@1^VKDh|F5E|q! ztSh-N>-nc~@UivYi^N|>PlEOBgD65+$@wE02bOESE~)Y7##17Q{%SQIENg$QKU0tV zAa&8b|;HZTvUL)`|-4k)$ zqV3?<@|WbvN!zf4xyOF7tERimSInQ2w;t!seOlg(3gR(mpWc0#Y9QHSxwxvI5zxAB zJ2x`8v5wzDA1xC0E-9e`u*FE1`^W|DJATcf;OV&6jTd%?_futtN{&xhTvHeP zhlyH|Zr04*R+8}`N^V34ad*0JvJE4$ASx#r6loC;5PZOxGw;9x^7YB~V zWhCP=_}Xw8{KXyoXjI;Nf&di2m?Kdr9PW4Ye>b!l25+QmW2E-F=j|TT|Gk-+RzAPh|)8_o8fThFsF~3{67f6e;IjH3jt@KjZ8f!o%BH=Mqi7H zQP)*>v;*m$NO8@|b#tAK5cOpo7c@1sgW`Mr_!;8hj549mI+|XhW}njy`V?E6%b*xd$#`k=V7Yd>>(br zS&HkV(le9pm@x&^hznnqPI8>cs8ksC=nOtWaJ=(lW;j~O7C;WYrD}n}UYHk5#6l@M`6@cj0UaO2jJ0A?sgcB< fXbp!4KvKB%t>5;Ba$Wg4llnCi)%p delta 2773 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 Iwx<050rIRG;s5{u diff --git a/assets/readme/examples/inline-highlight-chips.pdf b/assets/readme/examples/inline-highlight-chips.pdf index fd23b32c33247b86a6c9056c3401fac03e2b6c40..949063ab50356225be11c3489237e3da0178e517 100644 GIT binary patch delta 3070 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 delta 3127 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 diff --git a/assets/readme/examples/nested-list-showcase.pdf b/assets/readme/examples/nested-list-showcase.pdf index 68d1dbe68454e3c64c12535419a07e91b80939ab..b06f5b6d54492f0f6fba38102395885a371f55e1 100644 GIT binary patch delta 1741 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 ND18VLNwuOu{sjhlFc|;< diff --git a/assets/readme/examples/rich-text-showcase.pdf b/assets/readme/examples/rich-text-showcase.pdf index e2f3952f280e75a34e59edae3ebd71ab37d33acd..4c32dc87985c388df1df964297cffe09588de56a 100644 GIT binary patch delta 3626 zcmbu8_dnH-HT>-|+qIoacEwp1&Ou*D+U`W5zG=t>>YPfC6LqjQRe!^;^78H}3xZDT8!f zY%vydMTQ(_VX{K$3>qG9wWvYl)fip^*^y^LKG&L64hdjFaW4B>CZh~fBu*_;k@>*;bxq!ee_53qW zw!}h52_gH&DfFyIwMcH}3vC{!=c~aGY(Ac0@d1m?eqL0WZpT9!;m36yKuLy1&_mUkx#Z)D07Fy*Q>`vYZo4!)RPF1aIS4M^NT`@G6^8%(@83E z-(mW(>1=Ap?(B9It5mktoy8|qfpZJK!vieHcyM!azfx4T-4y@MUS++yL70d zL2}ldaU}+s%PfgbJVe+>F{{v#OZ(-5i}zGj!U}mB1K6u{0|RDt9|xV;nosDibPBknIhPxI>QvdBcvGH66hN+@v6F~*83S4<;Tr)Aq z66(VMT)$_(!N*@(iQu&Z_-j}ApDqXb#4k8)a39Tc%>YJ()?gS;V?pD zGEy?x^NAf7q5|sw+c<4ZebJd|p<;=yd4Wa0!sNVbjk4L_<4F_>?@hNh1W# z4g%M`$@yNX`JI;8$qB*B62)Y#1j|H3h$;w`Iwjcg^F=FT>yCRBCRJS0Nuov9=11j_ zY*Vx>HZ=c!;u8Z**Njm4@q)T9DQnLO+&qmh+XFmo(CbHygBylkvVZ8f}~mq|6%C=T}$4izjgj7WC-os7SV``w4J0jNIg0)5 zB&vrHM>E`!4(HuRJD;mfS(8oEyT$%nq`%<(QI(6n)8@v_a3NuO?weB)VPTXtH2mu{ z*lO26aL)qxX*s=1Tq#^g`r|M-If^t1_YYrbJDHEkx=x~*g2(;U4wJp(jUZa&qeX3hvr-4bpJsW^X#Ek9q9 zGM0GU`{Dzm+!lfXu_jD6ukhtLIQBB`sgwf0GrvqqxKHqdnFrMVKf>ZY~tr>1Zn+8}PS|<;j z{pjx^S27rQs1r#STo?hHg{^PX4B*^Ywfq=(yk)9oDY^+2jq=2auOzOGZBB#L^l`SD zhkp&q9&j=cBl4N@Tpo&K%!}9c5c5a-oAtk@b@mr+K}uWjKIZhqr$$qLgsEze1;CJ$ zvQ2zD*Y)+tf6#3M#cTQIYY5Q!g?{~_(ts-W3y+((Lw_)8f(WVr3@4y=p_RsI?uCXC+Q7s z$LEIuL4A3f@jmS_mWgvg3vk;gz_pN*np7yVXU~}9-wtVIdRZJSxp2%hZ`R^*G`81r zn1qsj^e7$=L>X0oN=14Vp(5|aGo{eKXPkrX9vnTix@ZY`g_)k6DL$m9w|P1YGqxRm z*BAEf8{{EOuSsl~BNllp&j4fh6U*C*a)G9((YK1p&bxzz{A>GD7W9q)tlFEn$BF3w zq8VM*(zE`unjNfB@-|QOPK&mwgx`fIUhf+uuQ)&?h(VOEYp{$BcMg*mT~l|AG!EvO zCMT+2Mjt>jIiOQwKfW%mI&#?X@-A6(IMs^Gbgt_X9m?@+Wq#k@LbH3$FI7U zNoQ|lnJ79Zj#!lX0@;pgBL*V{gO;WLaDhMw=XWMC$)!W}3!hR!ePh3+NY&k|fRIJilE2@}+IM!m1(6L4l?V{zQI z$*y>k!D8lyz;)7PYClo*@ek;tg_N$mHK~BFm?<}DMMfH|NR+aX7GpeVq*MX){;O3j;mZdLe zofD!Cb!Y4}xc=DCgOe<-)u^m3Vcsd`{rIOe`Vn++hjL=z;`fg!bHu;B2~(jEj<3h? z4V==~u-9A+w$gSvPQg2#`tn1sofk=0V%@E`y?6PqZcGWu>}E891u5huD*K!6ZFU{HRAVZlxRp3_`u?pMBq|8IC+<8qZA3Ex!WlWnPvH$ zyQ0E3V$ocZ#xJb)l=6n~ZC)8y#zbD^Q^8E*a`gj(1!TQEh5xP)?M1}0k#^H zWWkiW@^rZmrK>=11KOgzt3dVJ4`2l)JP<@+WLU##es?-Xg~QZc=sK2Gzoq4n8L%eb z8}UkHOixQs$l$O}%VB1P3JNsq%|h8llgB3612$aCHonJ%1G5&GrHR(0m3;`N1*IFuGd5dRRG@>QV_}0KZ1n00Fl% zCqkin6z6L$w?D&twB>54_zK%@owu9kXU+l0??-G<1s4mOlVZ-5#d)4eg-lNOgrgOE zV)x(vtg;RM<7$qNU_XN$O%e=4b1yGDm=kSV4lAtzk-Y z8P|@Pugxy?yZ!QOjg{U4<%t)qf5sl!-O0zAbfa+(WQGE`96gDPbNX&mLj3MGjYOH2h&i4^T=1USbzP9 zF+_JmRadAt@Ri7#2ZeIu$tlSU2{gDQ&3BdmvSA0vDw`4$GKlobfSj6|l$54|oUD|r zv=$OBp{AgwAg3iOFCil#@oz$EAf+|ovQi3KlIl{jQd&})S~7A-X$4Jn1t}>RH6;8V zkMjRVLHR#NMlyqGh%O7#m|1iD9J2bd>^3cXrNgvVRvbslyXkDdTyXH57xk#${0Ve9 z5&|tsSV`m2jN_ovLj~crqFFdY=vXDupZ?xlQN5J z8E1vh`};S1e|`P%e7uNUi(F_2fMm)c6-&w<9WAQZe9>o7gDz;^v~7q1=Fz zAB!xB9Q5vB(;SfM0uWvtt;JE8iUNzoz$5vSz=cJY)MZ_|wP5;$kzf^0e5j9xLTAFt zyJVVsWbVzfLdsbFZehkzr7X2rv&OTW>+UX@t1xPWfwy=J8g~Q1X7Y!x+2o;nfNE)f zEeD=fP=>UMjcfP#gctIIUUIZoC@{_UTP=I67UfpdRGVnNA|{vhO*velDltT-ijwK` zT56d~a__GS@;NUiQLirYd0~TfLDE8$K6yp+oqDC~FPcYc2Ac!l2tvtx58OL#yI8}S zdw|~nRGVb|=vaWGsP46M{y3H_-q;`Z9#vY z1}ile1ia+GZCr&UnBqE6tx_IMq z!4l2&mMt=})O$)#*>crE+TI$GL+;fmxd%r5Ki8ZCSEvK7qXkPbWfA#)kV4)`zTr*{1OYh@%-Ssg}KAX`KNe= zoN0=2>z4}!e`C)}Og2k0pKfXzAi|Ua;l3BGOL9@YQV`wz@J(_&%~594H<5MivjR=e z+JKkdw~b~aWr$Cx6%{-aM*>eH!tY4|HSm0oPxlA%`VQ7=w&~RlCUs-1;`HxsuBV*^8{N|9u(FNoEk?db0`UbaqRp4HZ4x)LAMwcJ zi`VUIm&i1^jH@nd#G?N!USjs9`Z;(oQ5+45?QXMutfPbq8PFmT`&d}@9K_p{!i&Ef z!=?<3Q5XqM5WU|OGTC2-hlKLPFx#>|>f;9OQWD%>o2thEm2BSXy0g6Z%4Xx&65LRr z2n2qQUvC(vzUK!jyCL#;yWh6t<;9yvbelphcIsDq@ed5%XYo}*O!jUF^ag&d;zFmr zmwIjTGI^-|5=SRlIs0N~v$vvANJ-Yy($rEU%pk}&Xtg+S%anGuSC@0TeEaK8{*pXO zbbsmD*aLjDU;<5PfDLa1qc(%s?lHfFSW{Yfh;dh^VJMlpI=^lljJZkn?qe;IYc@1= zH|#ez$m}QdRkQVg5dB1*DKw6lXU0XdyE}EYVuT7bNnvx8FU4p4)vlC6Jp1gcI=IpI zaR2cdKHu(#tbv*corit~A2kbgXh-iqlu)a8BM+PD6^ujAj+%^#R%MKfpt% zh{AjVze@h{9BE2`Wd53=0Ykvb_XlZ{T!|H!ZDe_Tgc zdz{Tet4l=qhil>o%4HJ0YC2O1$Qs&glo_1BcB7ZA7gpv~8mC4T-OcVtD!fB$n^DmFC6&wMpM@A0we zwsrEpBI9`PEf)9OUm2Q2{IMu5L1}~LoSMBvQX!cA(AX?hD_J7 zQnV1mWBNYX0<$4&AVn@zrIrIP!}EH8B3F!?+Qn(<8z5Iq#X0`JcJjF3RlW*%&N?EG zeiZlCm)20;$ zF*QfW!4%zw@-=HFLD;#tIt?+=#F{;RzLM}KP2DBLCLJu8ZDW&6@#YgLt!-%3ZX${g z!8*Y>5_1tUIK~Y8&>yjPUO<3$jGcWqY%rUOmxNbjZ32$?I{`{ z%%9?|JIjv)i%ka1k8$*bL~7*dfuKSknc%#EN`-*Z3OzG0{qIe`Ms{u6aOFQu*%vJg zm4aaD`aN2FzkmHjWhk^0R6APV;XgfvI|{=l5uc6L)==ii4d32v{{Zz(si)$*D=96N z@|_BXN*ncD6lG9DIi~ORd^l$^A_e3$y2dIdpVW!gqaoZZZgDldqsCz-4P~;f3U`ykYN7xn!G!{(mSbQ?`>(RAdkp;CQCpr= z93#$6nKOizh2()hGCZ$f$z1mc2bUf3-it-`n$z;SQ^gevXpp=Sz>r$j^##wEcH3Eh z@A9&}Phf)Aaak~s>_7(ft1^_rtKpQCyqY;Wa00kkNb@(yW9w&l&`pznpQ2++%|l|d z5PO#9_aWvV4031L6%a&eDZNu@0#(-~;5ytbgydq6j$l>Am^6 zZkVlXR|G}V)&ocW7q?|95tq+7V`rX4!k1IFCYC*t38zj={>MniQ-vHMFV_M4`;fXG zi8%gA5+hh0H@3%KoV&2WTflj~5Ov78jvk7bmm zD^Ks)a0nY;lyf?a^U6^p{p;7GGTqsG?Gy8uh zXc37`X6u(j#TH9#&0`J2SZkKPmqu+OBireOmijDXoi^XUY1@-zd=EB9j|K-gt)+8r zht|$f<#3Y+r*17-y*|r$YI<`~FSxaG79pcj>iW=qvUz8N%)Z63oiJAPy_NuP8DU*G z5qxXqywl%iT^+Pec(=7#1@%UHZp~L62KJh_n2R*b>D24AA4+shRs`ITqC#(Z&N-9D zJ`&C9M&@q$h*HWkh@F(*wDPE@FjX~CDH&x6byYD5RY^%{Rb?@0H5C;Zn561cRVWk& zlYlBqLt#=dn3N+N3XV z?P4&W*CjL2HCx?@+rr*RSTmy>U3%?ws680;0Ki27T#aF)qttg8(koDnV09CBnJgR( n2Htn-_|wfwZS1~*Fe6Ssz*AGbPeIT>ppjlkv diff --git a/assets/readme/examples/weekly-schedule.pdf b/assets/readme/examples/weekly-schedule.pdf index d85aaaff1df1d7f2854ef4a1f9d6593773717bfc..0937022ccfd818d5672ecbd4d1cb8b56b525dce7 100644 GIT binary patch delta 7580 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 q1 .addItem("Document Engine", phaseA -> phaseA - .addItem("Nested lists landed in v1.6") - .addItem("Composed table cells landed in v1.6") + .addItem("Nested lists with a per-depth marker cascade") + .addItem("Table cells that compose a whole flow") .addItem("Templates v2 with visual parity gate")) .addItem("Backend SPI", phaseB -> phaseB .addItem("PdfFragmentRenderHandler is now public") .addItem("DOCX semantic backend skeleton"))) .addItem("Documentation", docs -> docs - .addItem("Migration guide v1.5 to v1.6") + .addItem("Migration guide for the layered presets") .addItem("ADRs 0011-0013 published"))) // 2) markerFor() per-depth override + per-item marker. @@ -130,7 +130,7 @@ public static Path generate() throws Exception { .addItem("Ran mvnw verify locally")) .addItem("Closed bug: marker double-space rendering") .addItem("Triaged backlog", triage -> triage - .addItem("Phase E.4 deferred to v1.7") + .addItem("Deferred: hanging indent on wrapped items") .addItem("CanvasLayerNode parked"))) // 4) Deep nesting (depth 4+) falls back to the · cascade. diff --git a/examples/src/main/java/com/demcha/examples/features/tables/ComposedTableCellExample.java b/examples/src/main/java/com/demcha/examples/features/tables/ComposedTableCellExample.java index 2a95be6f..1413e0ca 100644 --- a/examples/src/main/java/com/demcha/examples/features/tables/ComposedTableCellExample.java +++ b/examples/src/main/java/com/demcha/examples/features/tables/ComposedTableCellExample.java @@ -24,7 +24,7 @@ import java.nio.file.Path; /** - * Runnable showcase for the v1.6 Phase B composed table cell content: + * Runnable showcase for composed table cell content: * {@code DocumentTableCell.node(DocumentNode)} accepts any composable * canonical node and the table layout pipeline prepares the child * sub-tree against the cell's resolved inner width, sizes the row from @@ -33,8 +33,8 @@ * *

The generated PDF puts paragraphs (with markdown rich text) and a * nested list inside table cells, alongside plain-text cells, so the - * difference between the v1.5 line-only shape and the v1.6 composed - * shape is visible at a glance.

+ * difference between a line-only cell and a composed one is visible + * at a glance.

* * @author Artem Demchyshyn */ @@ -109,7 +109,7 @@ public static Path generate() throws Exception { document.pageFlow() .name("ComposedCellShowcase") .spacing(8) - .addParagraph("v1.6 Phase B — Composed table cell content", title) + .addParagraph("Composed table cell content", title) .addParagraph( "DocumentTableCell.node(DocumentNode) accepts any registered " + "canonical node — paragraphs (with markdown), nested lists, " @@ -223,7 +223,7 @@ public static Path generate() throws Exception { .addParagraph("3. Mixed composed and plain-text cells in the same row", sectionHeading) .addParagraph( "Plain-text cells continue to use the existing DocumentTableCell.text(...) " - + "factory and render through the v1.5 line-iteration code path. " + + "factory and render through the line-iteration code path. " + "Composed cells render via NodeDefinition recursion alongside.", caption) .build(); diff --git a/examples/src/main/java/com/demcha/examples/features/text/InlineHighlightExample.java b/examples/src/main/java/com/demcha/examples/features/text/InlineHighlightExample.java index 709a582e..66deb9cd 100644 --- a/examples/src/main/java/com/demcha/examples/features/text/InlineHighlightExample.java +++ b/examples/src/main/java/com/demcha/examples/features/text/InlineHighlightExample.java @@ -75,8 +75,8 @@ public static Path generate() throws Exception { .addSection("Code", section -> labelledRow(section, "code(text) — monospace on a light chip, engine defaults", rich -> rich - .plain("Run ").code("./mvnw verify").plain(" then tag ") - .code("v1.9.0").plain(" to publish ").code("graph-compose-emoji"))) + .plain("Run ").code("./mvnw verify").plain(" before pushing, and ") + .code("-Dtest=Name").plain(" to narrow it to one class"))) .addSection("Badges", section -> labelledRow(section, "chip(text, fg, bg) — a coloured status badge between words", rich -> rich diff --git a/examples/src/main/java/com/demcha/examples/features/text/RichTextShowcaseExample.java b/examples/src/main/java/com/demcha/examples/features/text/RichTextShowcaseExample.java index 23d3de73..0f216012 100644 --- a/examples/src/main/java/com/demcha/examples/features/text/RichTextShowcaseExample.java +++ b/examples/src/main/java/com/demcha/examples/features/text/RichTextShowcaseExample.java @@ -115,11 +115,11 @@ public static Path generate() throws Exception { "link", rich -> rich .plain("Read the ") - .link("template authoring cheatsheet", - "https://github.com/DemchaAV/GraphCompose/blob/develop/docs/template-authoring.md") + .link("preset authoring cheatsheet", + "https://github.com/DemchaAV/GraphCompose/blob/develop/docs/templates/v2-layered/authoring-presets.md") .plain(" or the ") - .link("v1.6 roadmap", - "https://github.com/DemchaAV/GraphCompose/blob/develop/docs/v1.6-roadmap.md") + .link("extension guide", + "https://github.com/DemchaAV/GraphCompose/blob/develop/docs/contributing/extension-guide.md") .plain(" for the next steps."))) .addSection("Composing runs", section -> labelledRow(section, "append", diff --git a/examples/src/main/java/com/demcha/examples/flagships/FeatureCatalogExample.java b/examples/src/main/java/com/demcha/examples/flagships/FeatureCatalogExample.java index a98f8b99..a7282ff1 100644 --- a/examples/src/main/java/com/demcha/examples/flagships/FeatureCatalogExample.java +++ b/examples/src/main/java/com/demcha/examples/flagships/FeatureCatalogExample.java @@ -395,12 +395,12 @@ public static Path generate() throws Exception { feature(flow, "Canvas — absolute (x, y) placement", """ section.addCanvas(220, 70, canvas -> canvas - .position(badge("v1.8"), 8, 8) + .position(badge("layout"), 8, 8) .position(badge("charts"), 84, 26) .position(badge("paint"), 160, 8))""", demo -> demo.addCanvas(220, 70, canvas -> canvas .clipPolicy(ClipPolicy.OVERFLOW_VISIBLE) - .position(badge("v1.8"), 8, 8) + .position(badge("layout"), 8, 8) .position(badge("charts"), 84, 26) .position(badge("paint"), 160, 8))); diff --git a/examples/src/main/java/com/demcha/examples/support/WeeklyScheduleRenderer.java b/examples/src/main/java/com/demcha/examples/support/WeeklyScheduleRenderer.java index 9ed1dce4..236c4a29 100644 --- a/examples/src/main/java/com/demcha/examples/support/WeeklyScheduleRenderer.java +++ b/examples/src/main/java/com/demcha/examples/support/WeeklyScheduleRenderer.java @@ -537,7 +537,7 @@ public static void renderTo(Path outputFile, .addSection("BuildFooter", section -> section .padding(new DocumentInsets(6, 0, 0, 0)) .addParagraph(p -> p - .text("Composed with GraphCompose v1.5 — examples/.../WeeklyScheduleRenderer.java") + .text("Composed with GraphCompose — examples/.../WeeklyScheduleRenderer.java") .textStyle(DocumentTextStyle.builder() .fontName(FontName.COURIER) .size(7.5) diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterFileExample.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterFileExample.java index 348ee011..f7c7031c 100644 --- a/examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterFileExample.java +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterFileExample.java @@ -17,7 +17,7 @@ /** * Modern cinematic cover letter rendered directly through the canonical * DSL — `BusinessTheme.modern()` drives colour and type, sections use - * v1.5 presets ({@code softPanel}, {@code accentLeft}, {@code accentTop}) + * presets ({@code softPanel}, {@code accentLeft}, {@code accentTop}) * for the visual hierarchy, and an opening rich-text strip highlights * the candidate's headline value proposition. */ @@ -214,7 +214,7 @@ public static Path generate() throws Exception { .accentTop(THEME.palette().rule(), 0.6) .padding(new DocumentInsets(8, 0, 0, 0)) .addRich(rich -> rich - .plain("Composed with GraphCompose v1.5 — ") + .plain("Composed with GraphCompose — ") .style("examples/.../CoverLetterFileExample.java", DocumentTextStyle.builder() .fontName(FontName.COURIER) .size(8) diff --git a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java index e47ce952..bbbcaa38 100644 --- a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java +++ b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java @@ -108,6 +108,22 @@ static void generateEveryExample() throws Exception { GeneratedCatalogue.generateOnce(); } + /** + * Previews whose subject is the moment they were rendered. + * + *

{@code pdf-chrome.pdf} demonstrates the {@code {date}} header token, which + * {@code HeaderFooterConfig} resolves from the clock. Its committed copy therefore differs + * from a fresh render on every day but the one it was committed on — this guard would have + * gone red each morning on a tree nobody had touched, which is the fastest way to teach a + * team to ignore it.

+ * + *

Named rather than tolerated: the whole document is left out, and the entry stays until + * the date behind that token can be pinned the way both fixed backends already accept a + * {@code deterministic(...)} instant. Everything else about the file — that it exists, and + * that an example still renders it — is still checked below.

+ */ + private static final Set CLOCK_DEPENDENT_PREVIEWS = Set.of("pdf-chrome.pdf"); + @Test void everyCommittedPreviewMatchesWhatTheCatalogueRenders() throws Exception { Map generated = generatedByName(); @@ -118,7 +134,8 @@ void everyCommittedPreviewMatchesWhatTheCatalogueRenders() throws Exception { Path fresh = generated.get(name); if (fresh == null) { missing.add(name); - } else if (!AssetContent.digestOf(PREVIEWS.resolve(name)) + } else if (!CLOCK_DEPENDENT_PREVIEWS.contains(name) + && !AssetContent.digestOf(PREVIEWS.resolve(name)) .equals(AssetContent.digestOf(fresh))) { drifted.add(name); } @@ -192,6 +209,27 @@ void theOnlyAssetsThisCannotCompareAreTheOnesWrittenDown() throws Exception { * catches its opposites too — a new example nobody decided to publish, a preview added * without a source, a rename that lands as one of each.

*/ + /** + * A preview left out of the comparison is still a preview somebody has to keep. + * + *

An entry in {@link #CLOCK_DEPENDENT_PREVIEWS} turns the content check off for a whole + * file, so the two things that remain knowable about it are asserted here: it is committed, + * and an example still renders it. A name that stops matching either is an exemption for + * nothing — the same hole a mistyped part or shape key would open.

+ */ + @Test + void everyPreviewLeftOutOfTheComparisonStillExists() throws Exception { + Map generated = generatedByName(); + for (String name : new TreeSet<>(CLOCK_DEPENDENT_PREVIEWS)) { + assertThat(PREVIEWS.resolve(name)) + .describedAs("%s is exempt from the comparison but is not committed", name) + .exists(); + assertThat(generated.get(name)) + .describedAs("%s is exempt from the comparison but nothing renders it", name) + .isNotNull(); + } + } + @Test void theCatalogueIsExactlyThePublishedPreviewsPlusTheUnpublishedOnes() throws Exception { Set committed = new TreeSet<>(committedPreviews()); From d4663e24233c2715eeb061a0f0fd0b2d68d2b5f5 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 3 Aug 2026 07:47:19 +0100 Subject: [PATCH 2/4] fix(examples): compare the clock-dependent preview instead of dropping it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leaving pdf-chrome.pdf out of the comparison absorbed eight characters and gave up everything else: the page demonstrates a watermark behind the content, an information dictionary, a header, a footer and bookmarks, and none of it was being checked. That is a larger hole than the one it closed, and a green build proved nothing about the one file it stopped reading. The date is inside a compressed stream, so masking bytes was never available — it is decompressed instead. That one file is compared by what it draws and declares: page count, each page's operators with any ISO date replaced, the metadata it sets, and the bookmark titles with the pages they land on. Everything the example exists to show is back under the guard, and what changes by itself is not. The workaround goes when the render date can be pinned the way both fixed backends already accept a deterministic instant. Two things these previews had only ever been read for are now read by a test. A preview naming a release the project has moved past — the whole subject of this branch, and a check the branch itself did by hand. And an example rendering a link to a repository path that is not in the tree, which is how the hyperlink example came to ship two dead ones: the URLs point into a branch that moves, so a rename breaks them again in silence. Both were mutated to confirm they fail: the pre-fix preview reports [v1.5, v1.6, v1.7] and a redirected link reports the path. --- CHANGELOG.md | 15 ++- .../examples/CommittedAssetDriftTest.java | 32 ++++-- .../examples/ExampleContentGuardTest.java | 103 ++++++++++++++++++ .../demcha/examples/support/AssetContent.java | 74 +++++++++++++ 4 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 examples/src/test/java/com/demcha/examples/ExampleContentGuardTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 14df4671..bf7ca7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -174,10 +174,17 @@ follow semantic versioning; release dates are ISO 8601. preset cheatsheet and the extension guide. - **The drift gate stops going red on a tree nobody touched.** `pdf-chrome.pdf` demonstrates the `{date}` header token, which resolves from the clock, so its committed - copy differed from a fresh render every day but the one it was committed on. It is - named as exempt with the reason rather than tolerated, and the entry stays until that - date can be pinned the way both fixed backends already accept a `deterministic(...)` - instant. + copy differed from a fresh render every day but the one it was committed on. The date + lives inside a compressed stream, so it is decompressed and replaced: that one file is + compared by what it draws and declares — page content, information dictionary, + bookmarks — rather than by its bytes. Dropping it from the comparison instead would + have left the page that demonstrates a watermark, metadata, a header, a footer and + bookmarks checked by nothing. The workaround goes away when that date can be pinned the + way both fixed backends already accept a `deterministic(...)` instant. +- **Two things the previews used to be read for are now checked.** A preview naming a + release the project has moved past, and an example rendering a link to a repository + path that no longer exists — both render perfectly, so only reading caught them, and + both had been true for six releases. - **A committed preview can be reproduced from a branch that has moved past it.** The documents that print a version took it from the reactor, which between releases sits on the next patch — so a render from `develop` named a version nobody could depend on yet, diff --git a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java index bbbcaa38..674a1231 100644 --- a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java +++ b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java @@ -117,10 +117,13 @@ static void generateEveryExample() throws Exception { * gone red each morning on a tree nobody had touched, which is the fastest way to teach a * team to ignore it.

* - *

Named rather than tolerated: the whole document is left out, and the entry stays until - * the date behind that token can be pinned the way both fixed backends already accept a - * {@code deterministic(...)} instant. Everything else about the file — that it exists, and - * that an example still renders it — is still checked below.

+ *

Named rather than tolerated, and still compared: the date is replaced and the document + * is read by what it draws and declares — see + * {@link AssetContent#clockIndependentPdfDigest(Path)}. Dropping the file instead would have + * left a page that demonstrates a watermark, an information dictionary, a header, a footer + * and bookmarks checked by nothing, to absorb eight characters. The entry stays until the + * date behind that token can be pinned the way both fixed backends already accept a + * {@code deterministic(...)} instant; with that, this list and its comparison go away.

*/ private static final Set CLOCK_DEPENDENT_PREVIEWS = Set.of("pdf-chrome.pdf"); @@ -134,9 +137,7 @@ void everyCommittedPreviewMatchesWhatTheCatalogueRenders() throws Exception { Path fresh = generated.get(name); if (fresh == null) { missing.add(name); - } else if (!CLOCK_DEPENDENT_PREVIEWS.contains(name) - && !AssetContent.digestOf(PREVIEWS.resolve(name)) - .equals(AssetContent.digestOf(fresh))) { + } else if (!sameDocument(PREVIEWS.resolve(name), fresh, name)) { drifted.add(name); } } @@ -262,6 +263,23 @@ void theCatalogueIsExactlyThePublishedPreviewsPlusTheUnpublishedOnes() throws Ex * The site reads this folder flat, so a folder inside it is a mistake worth naming rather * than passing over.

*/ + /** + * Compares two renders of one preview, by content unless the content carries a clock. + * + *

A preview in {@link #CLOCK_DEPENDENT_PREVIEWS} is still compared — by what it draws and + * declares, with the date it prints replaced. Skipping it instead would have left a document + * that demonstrates a watermark, an information dictionary, a header, a footer and bookmarks + * checked by nothing at all, to absorb eight characters.

+ */ + private static boolean sameDocument(Path committed, Path fresh, String name) + throws IOException { + if (CLOCK_DEPENDENT_PREVIEWS.contains(name)) { + return AssetContent.clockIndependentPdfDigest(committed) + .equals(AssetContent.clockIndependentPdfDigest(fresh)); + } + return AssetContent.digestOf(committed).equals(AssetContent.digestOf(fresh)); + } + private static Set committedPreviews() throws IOException { Set names = new TreeSet<>(); Set folders = new TreeSet<>(); diff --git a/examples/src/test/java/com/demcha/examples/ExampleContentGuardTest.java b/examples/src/test/java/com/demcha/examples/ExampleContentGuardTest.java new file mode 100644 index 00000000..0ea5db81 --- /dev/null +++ b/examples/src/test/java/com/demcha/examples/ExampleContentGuardTest.java @@ -0,0 +1,103 @@ +package com.demcha.examples; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Holds the examples to what they can still claim. + * + *

{@link CommittedAssetDriftTest} catches a preview falling behind its example. It cannot catch + * an example that renders faithfully and reads as a document about a release that shipped, or one + * that links to a page somebody has since deleted — both render perfectly. Those went unnoticed + * for six releases and were found by reading, so they are checked here rather than read for again. + *

+ */ +class ExampleContentGuardTest { + + private static final Path REPO_ROOT = Path.of("..").toAbsolutePath().normalize(); + private static final Path PREVIEWS = REPO_ROOT.resolve("assets/readme/examples"); + private static final Path SOURCES = Path.of("src", "main", "java"); + + /** + * A release the project has left behind, named in a rendered page. + * + *

Matches the {@code v}-prefixed form, which is how prose names a release — "v1.6 Phase A", + * "Composed with GraphCompose v1.5", "tag v1.9.0". A bare {@code 1.9.0} is deliberately not + * matched: a Maven coordinate is the subject of the inline-code demo, not a stamp on it.

+ */ + private static final Pattern PAST_RELEASE = Pattern.compile("\\bv[01]\\.\\d+(\\.\\d+)?\\b"); + + @Test + void noCommittedPreviewNamesAReleaseTheProjectHasLeftBehind() throws IOException { + List dated = new ArrayList<>(); + try (var files = Files.list(PREVIEWS)) { + for (Path pdf : files.filter(p -> p.toString().endsWith(".pdf")).sorted().toList()) { + Set found = new TreeSet<>(); + try (PDDocument document = Loader.loadPDF(pdf.toFile())) { + Matcher match = PAST_RELEASE.matcher(new PDFTextStripper().getText(document)); + while (match.find()) { + found.add(match.group()); + } + } + if (!found.isEmpty()) { + dated.add(pdf.getFileName() + " " + found); + } + } + } + + assertThat(dated) + .describedAs("a committed preview reads as a document about a release the project " + + "has moved past. Describe what the example demonstrates instead of the " + + "release it shipped in — a version in prose dates the page, and nothing " + + "re-reads these once they are committed") + .isEmpty(); + } + + private static final Pattern REPOSITORY_LINK = Pattern.compile( + "https://github\\.com/DemchaAV/GraphCompose/blob/[^/\"]+/([^\"\\s)]+)"); + + /** + * Every repository page an example links to is a page that exists. + * + *

The rich-text example rendered two links into a public preview and both had been deleted: + * the example that demonstrates hyperlinks shipped broken ones. They point into + * {@code blob/develop}, which is a branch that moves, so a file renamed a year from now breaks + * them again — silently, since a PDF is not a page anybody crawls. Resolving the path against + * this checkout costs nothing and catches the rename in the commit that makes it.

+ */ + @Test + void everyRepositoryLinkAnExampleRendersResolvesToAFileInTheTree() throws IOException { + List broken = new ArrayList<>(); + try (var sources = Files.walk(SOURCES)) { + for (Path source : sources.filter(p -> p.toString().endsWith(".java")).sorted().toList()) { + Matcher link = REPOSITORY_LINK.matcher(Files.readString(source)); + while (link.find()) { + if (!Files.exists(REPO_ROOT.resolve(link.group(1)))) { + broken.add(source.getFileName() + " -> " + link.group(1)); + } + } + } + } + + assertThat(broken) + .describedAs("an example renders a link to a repository path that is not in the " + + "tree. A reader clicking it in the committed preview gets a 404, and the " + + "example demonstrating links is the worst place to ship one") + .isEmpty(); + } +} diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContent.java b/examples/src/test/java/com/demcha/examples/support/AssetContent.java index 8811163d..961463aa 100644 --- a/examples/src/test/java/com/demcha/examples/support/AssetContent.java +++ b/examples/src/test/java/com/demcha/examples/support/AssetContent.java @@ -1,5 +1,12 @@ package com.demcha.examples.support; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentInformation; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; + import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -13,6 +20,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; @@ -180,6 +188,72 @@ private static byte[] raster(String key, byte[] content) throws IOException { return pixels.getBytes(StandardCharsets.UTF_8); } + /** + * A PDF read as the document it draws rather than as the bytes it was written to. + * + *

For a PDF whose content carries a clock — {@code pdf-chrome.pdf} prints the + * {@code {date}} header token, which the engine resolves from {@code LocalDate.now()} — the + * byte comparison is red on every day but the one the file was committed on. The date is + * inside a compressed stream, so nothing can be masked without decompressing first.

+ * + *

So it is decompressed: page count, then each page's operators with any ISO date + * replaced, then the metadata the document declares and the outline it builds. That covers + * what {@code pdf-chrome.pdf} exists to demonstrate — a watermark drawn behind the content, + * a header and footer, an information dictionary, bookmarks — and it stops covering only the + * eight characters that change by themselves. Excluding the file instead would have left all + * of that unchecked, which is a larger hole than the one being closed.

+ * + *

This is the second-best fix. The first is a render date the caller can pin, the way both + * fixed backends already accept a {@code deterministic(...)} instant; with that, this method + * and its caller go away and the file rejoins the ordinary comparison.

+ * + * @param pdf the file to read + * @return a hex SHA-256 over what the document draws and declares + * @throws IOException if the file cannot be read or parsed + */ + public static String clockIndependentPdfDigest(Path pdf) throws IOException { + MessageDigest digest = sha256(); + try (PDDocument document = Loader.loadPDF(pdf.toFile())) { + digest.update("pages=%d".formatted(document.getNumberOfPages()) + .getBytes(StandardCharsets.UTF_8)); + for (PDPage page : document.getPages()) { + digest.update(withoutDates(page.getContents().readAllBytes())); + } + PDDocumentInformation information = document.getDocumentInformation(); + for (String key : new TreeSet<>(information.getMetadataKeys())) { + digest.update(key.getBytes(StandardCharsets.UTF_8)); + digest.update(withoutDates( + String.valueOf(information.getCustomMetadataValue(key)) + .getBytes(StandardCharsets.UTF_8))); + } + digest.update(outline(document).getBytes(StandardCharsets.UTF_8)); + } + return HexFormat.of().formatHex(digest.digest()); + } + + /** Every bookmark title, in order, with the page each one lands on. */ + private static String outline(PDDocument document) throws IOException { + PDDocumentOutline root = document.getDocumentCatalog().getDocumentOutline(); + if (root == null) { + return "no outline"; + } + StringBuilder bookmarks = new StringBuilder(); + for (PDOutlineItem item : root.children()) { + bookmarks.append(item.getTitle()).append('@') + .append(document.getPages().indexOf(item.findDestinationPage(document))) + .append(';'); + } + return bookmarks.toString(); + } + + private static byte[] withoutDates(byte[] content) { + return ISO_DATE.matcher(new String(content, StandardCharsets.ISO_8859_1)) + .replaceAll("") + .getBytes(StandardCharsets.ISO_8859_1); + } + + private static final Pattern ISO_DATE = Pattern.compile("\\d{4}-\\d{2}-\\d{2}"); + /** A PDF with the one thing in it that the clock writes taken out. */ static byte[] withoutPdfId(byte[] content) { String text = new String(content, StandardCharsets.ISO_8859_1); From ffaa0610bb64fec5ca76783387b438a1eb4a263b Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 3 Aug 2026 08:21:48 +0100 Subject: [PATCH 3/4] feat(engine): let a build pin the date the {date} token resolves to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The token read the clock with no way to say otherwise, so any document using it was a different file every morning. The example demonstrating it could not be held to its committed preview at all, and two attempts to work around that from the test side both left holes: the first dropped the file from the comparison, the second rebuilt "what a PDF means" by hand and missed the resources a watermark's opacity lives in, the nested bookmarks that are the point of that page, and the page boxes. Each patch would have added surface to maintain, to avoid one call to LocalDate.now(). -Dgraphcompose.renderDate=YYYY-MM-DD fixes what the token resolves to — the need SOURCE_DATE_EPOCH answers for archives. Unset, it is the clock, so nothing changes for anyone who has not asked. A value that is not a date falls back to the clock rather than failing the render: a typo in a build flag should not stop a document being produced, and the drift it causes surfaces where drift is checked. The examples module pins it for both the tests and GenerateAllExamples, so the release renders the same date the gate compares against, and that preview goes back through the ordinary byte comparison with the hand-rolled one deleted. The content guard also stopped promising more than it checked. It read only the PDFs while its name said every preview, leaving the six committed decks — the most-linked of them — outside it; decks and Word documents are read through their packages now. And which majors count as stale is derived from the version being built rather than written down, so it still works when today's previews are the dated ones. --- CHANGELOG.md | 16 ++-- assets/readme/examples/pdf-chrome.pdf | Bin 4210 -> 4210 bytes .../header_footer/HeaderFooterConfig.java | 31 +++++++- examples/pom.xml | 31 ++++++++ .../examples/CommittedAssetDriftTest.java | 60 +------------- .../examples/ExampleContentGuardTest.java | 70 ++++++++++++++--- .../demcha/examples/support/AssetContent.java | 74 ------------------ .../HeaderFooterRenderDateTest.java | 72 +++++++++++++++++ 8 files changed, 201 insertions(+), 153 deletions(-) create mode 100644 qa/src/test/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterRenderDateTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7ca7e5..0ab27287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -172,15 +172,13 @@ follow semantic versioning; release dates are ISO 8601. URLs it rendered — a template-authoring page and a v1.6 roadmap — had been deleted, so the example that demonstrates links shipped two of them broken. They point at the preset cheatsheet and the extension guide. -- **The drift gate stops going red on a tree nobody touched.** `pdf-chrome.pdf` - demonstrates the `{date}` header token, which resolves from the clock, so its committed - copy differed from a fresh render every day but the one it was committed on. The date - lives inside a compressed stream, so it is decompressed and replaced: that one file is - compared by what it draws and declares — page content, information dictionary, - bookmarks — rather than by its bytes. Dropping it from the comparison instead would - have left the page that demonstrates a watermark, metadata, a header, a footer and - bookmarks checked by nothing. The workaround goes away when that date can be pinned the - way both fixed backends already accept a `deterministic(...)` instant. +- **A document that prints the date can be rendered twice and come out the same.** The + `{date}` header token resolved from the clock with no way to pin it, so any document + using it was a different file every morning — and the example demonstrating it could + not be held to its committed preview at all. `-Dgraphcompose.renderDate=YYYY-MM-DD` + fixes what the token resolves to, the way `SOURCE_DATE_EPOCH` does for archives; unset, + it is the clock as before. The examples module pins it, so that preview is now compared + like every other one instead of being trusted. - **Two things the previews used to be read for are now checked.** A preview naming a release the project has moved past, and an example rendering a link to a repository path that no longer exists — both render perfectly, so only reading caught them, and diff --git a/assets/readme/examples/pdf-chrome.pdf b/assets/readme/examples/pdf-chrome.pdf index c6253c1d8eb643310f9f72ca8a8f83a8d96fa7de..040e5425fb3cbfa768141c5633abf867df72bbd8 100644 GIT binary patch delta 165 zcmeyQ@JV6A3T|c(?NgIib9=FfbcQXN*eJZ2hewgQ-pSR}%*D~k$k^D}$i>;w!pzv& z+|<p5~$3~z|q3O+0fCzz`)hb(9*!r#o5x;PQiu}BUnIY JOy0nM2mpV`Di;6% delta 165 zcmeyQ@JV6A3U20zuoIJ4b9=FfJBKZw*eJZ2hewgQ-qp~|#lpqa(8$@;(ap`+&A`Oj z&Be&k#n{==+{MV$(ahY;)zZ?{#KqLo(Z$i#+|kj})YQe;$k5c-#n9Q@*iONQ5+hhZ KW=!6|e+U4_04n?d diff --git a/core/src/main/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterConfig.java b/core/src/main/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterConfig.java index 7f6b86bc..81ffc567 100644 --- a/core/src/main/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterConfig.java +++ b/core/src/main/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterConfig.java @@ -98,7 +98,34 @@ public String resolveTokens(String text, int physicalPage, int totalPages) { return text .replace("{page}", numberStyle.format(counted)) .replace("{pages}", numberStyle.format(countedTotal)) - .replace("{date}", java.time.LocalDate.now().toString()); + .replace("{date}", renderDate().toString()); + } + + /** + * The date the {@code {date}} token resolves to. + * + *

The clock by default. A build that has to produce the same bytes twice can pin it with + * {@code -Dgraphcompose.renderDate=YYYY-MM-DD} — the same need {@code SOURCE_DATE_EPOCH} + * answers for archives, and the reason this repository can hold its committed example + * previews to a byte comparison: a document that prints today re-renders differently every + * morning, and a guard that reports that is a guard people learn to ignore.

+ * + *

An unparseable value is the clock again rather than a failed render: a mistyped property + * should not stop a document being produced, and the drift it causes surfaces where drift is + * checked.

+ * + * @return the pinned date, or today + */ + static java.time.LocalDate renderDate() { + String pinned = System.getProperty("graphcompose.renderDate"); + if (pinned == null || pinned.isBlank()) { + return java.time.LocalDate.now(); + } + try { + return java.time.LocalDate.parse(pinned.trim()); + } catch (java.time.format.DateTimeParseException notADate) { + return java.time.LocalDate.now(); + } } /** @@ -129,6 +156,6 @@ public static String resolvePlaceholders(String text, int currentPage, int total return text .replace("{page}", String.valueOf(currentPage)) .replace("{pages}", String.valueOf(totalPages)) - .replace("{date}", java.time.LocalDate.now().toString()); + .replace("{date}", renderDate().toString()); } } diff --git a/examples/pom.xml b/examples/pom.xml index 90715395..abd75516 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -36,6 +36,15 @@ --> 2.1.0 + + 2026-01-15 + 6.1.2 3.27.7 @@ -202,6 +211,14 @@ ${graphcompose.examples.assetVersion} + + ${graphcompose.examples.renderDate} @@ -210,6 +227,20 @@ org.codehaus.mojo exec-maven-plugin 3.6.3 + + + + + graphcompose.renderDate + ${graphcompose.examples.renderDate} + + + diff --git a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java index 674a1231..e47ce952 100644 --- a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java +++ b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java @@ -108,25 +108,6 @@ static void generateEveryExample() throws Exception { GeneratedCatalogue.generateOnce(); } - /** - * Previews whose subject is the moment they were rendered. - * - *

{@code pdf-chrome.pdf} demonstrates the {@code {date}} header token, which - * {@code HeaderFooterConfig} resolves from the clock. Its committed copy therefore differs - * from a fresh render on every day but the one it was committed on — this guard would have - * gone red each morning on a tree nobody had touched, which is the fastest way to teach a - * team to ignore it.

- * - *

Named rather than tolerated, and still compared: the date is replaced and the document - * is read by what it draws and declares — see - * {@link AssetContent#clockIndependentPdfDigest(Path)}. Dropping the file instead would have - * left a page that demonstrates a watermark, an information dictionary, a header, a footer - * and bookmarks checked by nothing, to absorb eight characters. The entry stays until the - * date behind that token can be pinned the way both fixed backends already accept a - * {@code deterministic(...)} instant; with that, this list and its comparison go away.

- */ - private static final Set CLOCK_DEPENDENT_PREVIEWS = Set.of("pdf-chrome.pdf"); - @Test void everyCommittedPreviewMatchesWhatTheCatalogueRenders() throws Exception { Map generated = generatedByName(); @@ -137,7 +118,8 @@ void everyCommittedPreviewMatchesWhatTheCatalogueRenders() throws Exception { Path fresh = generated.get(name); if (fresh == null) { missing.add(name); - } else if (!sameDocument(PREVIEWS.resolve(name), fresh, name)) { + } else if (!AssetContent.digestOf(PREVIEWS.resolve(name)) + .equals(AssetContent.digestOf(fresh))) { drifted.add(name); } } @@ -210,27 +192,6 @@ void theOnlyAssetsThisCannotCompareAreTheOnesWrittenDown() throws Exception { * catches its opposites too — a new example nobody decided to publish, a preview added * without a source, a rename that lands as one of each.

*/ - /** - * A preview left out of the comparison is still a preview somebody has to keep. - * - *

An entry in {@link #CLOCK_DEPENDENT_PREVIEWS} turns the content check off for a whole - * file, so the two things that remain knowable about it are asserted here: it is committed, - * and an example still renders it. A name that stops matching either is an exemption for - * nothing — the same hole a mistyped part or shape key would open.

- */ - @Test - void everyPreviewLeftOutOfTheComparisonStillExists() throws Exception { - Map generated = generatedByName(); - for (String name : new TreeSet<>(CLOCK_DEPENDENT_PREVIEWS)) { - assertThat(PREVIEWS.resolve(name)) - .describedAs("%s is exempt from the comparison but is not committed", name) - .exists(); - assertThat(generated.get(name)) - .describedAs("%s is exempt from the comparison but nothing renders it", name) - .isNotNull(); - } - } - @Test void theCatalogueIsExactlyThePublishedPreviewsPlusTheUnpublishedOnes() throws Exception { Set committed = new TreeSet<>(committedPreviews()); @@ -263,23 +224,6 @@ void theCatalogueIsExactlyThePublishedPreviewsPlusTheUnpublishedOnes() throws Ex * The site reads this folder flat, so a folder inside it is a mistake worth naming rather * than passing over.

*/ - /** - * Compares two renders of one preview, by content unless the content carries a clock. - * - *

A preview in {@link #CLOCK_DEPENDENT_PREVIEWS} is still compared — by what it draws and - * declares, with the date it prints replaced. Skipping it instead would have left a document - * that demonstrates a watermark, an information dictionary, a header, a footer and bookmarks - * checked by nothing at all, to absorb eight characters.

- */ - private static boolean sameDocument(Path committed, Path fresh, String name) - throws IOException { - if (CLOCK_DEPENDENT_PREVIEWS.contains(name)) { - return AssetContent.clockIndependentPdfDigest(committed) - .equals(AssetContent.clockIndependentPdfDigest(fresh)); - } - return AssetContent.digestOf(committed).equals(AssetContent.digestOf(fresh)); - } - private static Set committedPreviews() throws IOException { Set names = new TreeSet<>(); Set folders = new TreeSet<>(); diff --git a/examples/src/test/java/com/demcha/examples/ExampleContentGuardTest.java b/examples/src/test/java/com/demcha/examples/ExampleContentGuardTest.java index 0ea5db81..8a3113bb 100644 --- a/examples/src/test/java/com/demcha/examples/ExampleContentGuardTest.java +++ b/examples/src/test/java/com/demcha/examples/ExampleContentGuardTest.java @@ -1,12 +1,16 @@ package com.demcha.examples; +import com.demcha.examples.support.ExampleVersion; + import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.junit.jupiter.api.Test; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -15,6 +19,8 @@ import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import static org.assertj.core.api.Assertions.assertThat; @@ -34,28 +40,35 @@ class ExampleContentGuardTest { private static final Path SOURCES = Path.of("src", "main", "java"); /** - * A release the project has left behind, named in a rendered page. + * A release named in a rendered page, whatever line it belongs to. * *

Matches the {@code v}-prefixed form, which is how prose names a release — "v1.6 Phase A", * "Composed with GraphCompose v1.5", "tag v1.9.0". A bare {@code 1.9.0} is deliberately not * matched: a Maven coordinate is the subject of the inline-code demo, not a stamp on it.

+ * + *

Which majors are stale is read from the version being built rather than written down, so + * this keeps working when the project is on 3.x and today's previews become the dated ones. + * The current major is allowed — a hero's coordinate pill names it on purpose.

*/ - private static final Pattern PAST_RELEASE = Pattern.compile("\\bv[01]\\.\\d+(\\.\\d+)?\\b"); + private static final Pattern RELEASE = Pattern.compile("\\bv(\\d+)\\.\\d+(\\.\\d+)?\\b"); + + private static final int CURRENT_MAJOR = + Integer.parseInt(ExampleVersion.current().split("[.\\-]")[0]); @Test void noCommittedPreviewNamesAReleaseTheProjectHasLeftBehind() throws IOException { List dated = new ArrayList<>(); try (var files = Files.list(PREVIEWS)) { - for (Path pdf : files.filter(p -> p.toString().endsWith(".pdf")).sorted().toList()) { - Set found = new TreeSet<>(); - try (PDDocument document = Loader.loadPDF(pdf.toFile())) { - Matcher match = PAST_RELEASE.matcher(new PDFTextStripper().getText(document)); - while (match.find()) { - found.add(match.group()); + for (Path preview : files.filter(Files::isRegularFile).sorted().toList()) { + Set stale = new TreeSet<>(); + Matcher match = RELEASE.matcher(readableText(preview)); + while (match.find()) { + if (Integer.parseInt(match.group(1)) < CURRENT_MAJOR) { + stale.add(match.group()); } } - if (!found.isEmpty()) { - dated.add(pdf.getFileName() + " " + found); + if (!stale.isEmpty()) { + dated.add(preview.getFileName() + " " + stale); } } } @@ -68,6 +81,43 @@ void noCommittedPreviewNamesAReleaseTheProjectHasLeftBehind() throws IOException .isEmpty(); } + /** + * The words a reader sees, whichever of the three formats the preview is. + * + *

A deck and a Word document keep their text in XML inside the package, so they are read as + * the package rather than through a PDF stripper. Checking only the PDFs would have left the + * six committed decks outside a guard whose name says every preview — and they are the ones a + * reader is most likely to open.

+ */ + private static String readableText(Path preview) throws IOException { + String name = preview.getFileName().toString(); + if (name.endsWith(".pdf")) { + try (PDDocument document = Loader.loadPDF(preview.toFile())) { + return new PDFTextStripper().getText(document); + } + } + if (!name.endsWith(".pptx") && !name.endsWith(".docx")) { + return ""; + } + StringBuilder text = new StringBuilder(); + try (ZipInputStream zip = + new ZipInputStream(new ByteArrayInputStream(Files.readAllBytes(preview)))) { + for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { + if (entry.getName().endsWith(".xml")) { + Matcher run = TEXT_RUN.matcher( + new String(zip.readAllBytes(), StandardCharsets.UTF_8)); + while (run.find()) { + text.append(run.group(1)).append(' '); + } + } + } + } + return text.toString(); + } + + /** A run of text in a deck ({@code }) or a Word document ({@code }). */ + private static final Pattern TEXT_RUN = Pattern.compile("<[aw]:t[^>]*>([^<]*)"); + private static final Pattern REPOSITORY_LINK = Pattern.compile( "https://github\\.com/DemchaAV/GraphCompose/blob/[^/\"]+/([^\"\\s)]+)"); diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContent.java b/examples/src/test/java/com/demcha/examples/support/AssetContent.java index 961463aa..8811163d 100644 --- a/examples/src/test/java/com/demcha/examples/support/AssetContent.java +++ b/examples/src/test/java/com/demcha/examples/support/AssetContent.java @@ -1,12 +1,5 @@ package com.demcha.examples.support; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentInformation; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; -import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; - import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -20,7 +13,6 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; -import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; @@ -188,72 +180,6 @@ private static byte[] raster(String key, byte[] content) throws IOException { return pixels.getBytes(StandardCharsets.UTF_8); } - /** - * A PDF read as the document it draws rather than as the bytes it was written to. - * - *

For a PDF whose content carries a clock — {@code pdf-chrome.pdf} prints the - * {@code {date}} header token, which the engine resolves from {@code LocalDate.now()} — the - * byte comparison is red on every day but the one the file was committed on. The date is - * inside a compressed stream, so nothing can be masked without decompressing first.

- * - *

So it is decompressed: page count, then each page's operators with any ISO date - * replaced, then the metadata the document declares and the outline it builds. That covers - * what {@code pdf-chrome.pdf} exists to demonstrate — a watermark drawn behind the content, - * a header and footer, an information dictionary, bookmarks — and it stops covering only the - * eight characters that change by themselves. Excluding the file instead would have left all - * of that unchecked, which is a larger hole than the one being closed.

- * - *

This is the second-best fix. The first is a render date the caller can pin, the way both - * fixed backends already accept a {@code deterministic(...)} instant; with that, this method - * and its caller go away and the file rejoins the ordinary comparison.

- * - * @param pdf the file to read - * @return a hex SHA-256 over what the document draws and declares - * @throws IOException if the file cannot be read or parsed - */ - public static String clockIndependentPdfDigest(Path pdf) throws IOException { - MessageDigest digest = sha256(); - try (PDDocument document = Loader.loadPDF(pdf.toFile())) { - digest.update("pages=%d".formatted(document.getNumberOfPages()) - .getBytes(StandardCharsets.UTF_8)); - for (PDPage page : document.getPages()) { - digest.update(withoutDates(page.getContents().readAllBytes())); - } - PDDocumentInformation information = document.getDocumentInformation(); - for (String key : new TreeSet<>(information.getMetadataKeys())) { - digest.update(key.getBytes(StandardCharsets.UTF_8)); - digest.update(withoutDates( - String.valueOf(information.getCustomMetadataValue(key)) - .getBytes(StandardCharsets.UTF_8))); - } - digest.update(outline(document).getBytes(StandardCharsets.UTF_8)); - } - return HexFormat.of().formatHex(digest.digest()); - } - - /** Every bookmark title, in order, with the page each one lands on. */ - private static String outline(PDDocument document) throws IOException { - PDDocumentOutline root = document.getDocumentCatalog().getDocumentOutline(); - if (root == null) { - return "no outline"; - } - StringBuilder bookmarks = new StringBuilder(); - for (PDOutlineItem item : root.children()) { - bookmarks.append(item.getTitle()).append('@') - .append(document.getPages().indexOf(item.findDestinationPage(document))) - .append(';'); - } - return bookmarks.toString(); - } - - private static byte[] withoutDates(byte[] content) { - return ISO_DATE.matcher(new String(content, StandardCharsets.ISO_8859_1)) - .replaceAll("") - .getBytes(StandardCharsets.ISO_8859_1); - } - - private static final Pattern ISO_DATE = Pattern.compile("\\d{4}-\\d{2}-\\d{2}"); - /** A PDF with the one thing in it that the clock writes taken out. */ static byte[] withoutPdfId(byte[] content) { String text = new String(content, StandardCharsets.ISO_8859_1); diff --git a/qa/src/test/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterRenderDateTest.java b/qa/src/test/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterRenderDateTest.java new file mode 100644 index 00000000..5203ead7 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterRenderDateTest.java @@ -0,0 +1,72 @@ +package com.demcha.compose.engine.components.content.header_footer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The {@code {date}} token resolves from the clock, unless a build pins it. + * + *

A document that prints today is a different document tomorrow, which is why the repository + * could not hold the example preview that demonstrates this token to a byte comparison: the guard + * went red each morning on a tree nobody had touched. Pinning is the same need + * {@code SOURCE_DATE_EPOCH} answers for archives, and it is what lets that preview be compared + * like every other.

+ */ +class HeaderFooterRenderDateTest { + + private static final String PROPERTY = "graphcompose.renderDate"; + + @AfterEach + void releaseTheProperty() { + System.clearProperty(PROPERTY); + } + + @Test + void withoutThePropertyTheTokenResolvesToToday() { + System.clearProperty(PROPERTY); + + assertThat(HeaderFooterConfig.resolvePlaceholders("{date}", 1, 1)) + .describedAs("a render nobody pinned prints the day it ran") + .isEqualTo(LocalDate.now().toString()); + } + + @Test + void aPinnedDateIsWhatTheTokenPrints() { + System.setProperty(PROPERTY, "2026-01-15"); + + assertThat(HeaderFooterConfig.resolvePlaceholders("Issued {date}", 1, 1)) + .describedAs("the pinned date is what makes a document with a date in it " + + "reproducible") + .isEqualTo("Issued 2026-01-15"); + } + + /** + * A mistyped property renders rather than throws. + * + *

Failing the render would turn a typo in a build flag into a document that cannot be + * produced at all. Falling back to the clock produces the document and lets the drift it + * causes surface where drift is checked.

+ */ + @Test + void aValueThatIsNotADateFallsBackToTheClock() { + System.setProperty(PROPERTY, "last Tuesday"); + + assertThat(HeaderFooterConfig.resolvePlaceholders("{date}", 1, 1)) + .isEqualTo(LocalDate.now().toString()); + } + + @Test + void theZoneResolverPinsTheSameWay() { + System.setProperty(PROPERTY, "2026-01-15"); + + assertThat(HeaderFooterConfig.builder().build() + .resolveTokens("{date} · {page}/{pages}", 2, 7)) + .describedAs("both resolvers read the same date; one pinned and one not would " + + "print two days in one document") + .startsWith("2026-01-15"); + } +} From 58a693ff47101610b77404c076762e9269e4183c Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 3 Aug 2026 08:38:12 +0100 Subject: [PATCH 4/4] test(engine): put the render-date property back rather than clearing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The property is global. Clearing it in teardown removes what the build set — the examples module pins a date so its previews are reproducible — and whichever test ran first would take it away from everything after, with the failure landing somewhere that has nothing to do with the cause. --- .../HeaderFooterRenderDateTest.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/qa/src/test/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterRenderDateTest.java b/qa/src/test/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterRenderDateTest.java index 5203ead7..559f5fb7 100644 --- a/qa/src/test/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterRenderDateTest.java +++ b/qa/src/test/java/com/demcha/compose/engine/components/content/header_footer/HeaderFooterRenderDateTest.java @@ -1,6 +1,7 @@ package com.demcha.compose.engine.components.content.header_footer; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.time.LocalDate; @@ -20,9 +21,27 @@ class HeaderFooterRenderDateTest { private static final String PROPERTY = "graphcompose.renderDate"; + private String pinnedByTheBuild; + + /** + * The property is global, so this puts back whatever it found rather than clearing it. + * + *

A build that pins the date — the examples module does, so its previews are reproducible — + * would otherwise have it removed by whichever test ran first, and the failure would land + * somewhere else entirely.

+ */ + @BeforeEach + void rememberWhatTheBuildSet() { + pinnedByTheBuild = System.getProperty(PROPERTY); + } + @AfterEach - void releaseTheProperty() { - System.clearProperty(PROPERTY); + void putItBack() { + if (pinnedByTheBuild == null) { + System.clearProperty(PROPERTY); + } else { + System.setProperty(PROPERTY, pinnedByTheBuild); + } } @Test