From 2a4cd8fe6cc9f30ae1ec17da7e8bc0e4f8924abc Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Sat, 13 Apr 2024 11:25:46 -0400 Subject: [PATCH] Squash changes from failed Git experiment --- .gitattributes | 1 - libs/nsapi4j.jar | Bin 1533519 -> 1533117 bytes .../kotlin/info/mechyrdia/Configuration.kt | 2 +- .../{session_storage.kt => SessionStorage.kt} | 0 .../auth/{views_login.kt => ViewsLogin.kt} | 0 .../data/{data_files.kt => DataFiles.kt} | 34 +++-- .../info/mechyrdia/data/MigrateFiles.kt | 36 +++-- .../info/mechyrdia/data/MigrateFilesSerial.kt | 79 ++--------- .../{view_comments.kt => ViewComments.kt} | 2 +- .../{views_comment.kt => ViewsComment.kt} | 0 .../data/{views_files.kt => ViewsFiles.kt} | 44 +++--- .../data/{views_user.kt => ViewsUser.kt} | 0 .../kotlin/info/mechyrdia/data/data.kt | 19 +++ .../kotlin/info/mechyrdia/data/data_flow.kt | 21 --- .../kotlin/info/mechyrdia/data/data_utils.kt | 22 --- .../lore/{april_1st.kt => April1st.kt} | 0 .../{article_listing.kt => ArticleListing.kt} | 15 ++- .../{article_titles.kt => ArticleTitles.kt} | 2 +- .../{asset_caching.kt => AssetCaching.kt} | 0 ...set_compression.kt => AssetCompression.kt} | 0 .../{asset_hashing.kt => AssetHashing.kt} | 0 .../lore/{file_data.kt => FileData.kt} | 0 .../lore/{http_utils.kt => HttpUtils.kt} | 0 .../{parser_builder.kt => ParserBuilder.kt} | 0 .../lore/{parser_html.kt => ParserHtml.kt} | 0 .../kotlin/info/mechyrdia/lore/ParserJson.kt | 125 ++++++++++++++++++ .../lore/{parser_lexer.kt => ParserLexer.kt} | 0 ...ser_lexer_async.kt => ParserLexerAsync.kt} | 0 .../lore/{parser_plain.kt => ParserPlain.kt} | 0 ...rser_preprocess.kt => ParserPreprocess.kt} | 0 ..._include.kt => ParserPreprocessInclude.kt} | 0 ...rocess_json.kt => ParserPreprocessJson.kt} | 0 ...rocess_math.kt => ParserPreprocessMath.kt} | 0 .../lore/{parser_raw.kt => ParserRaw.kt} | 0 .../lore/{parser_tree.kt => ParserTree.kt} | 0 .../lore/{parser_utils.kt => ParserUtils.kt} | 0 .../lore/{view_bar.kt => ViewBar.kt} | 0 .../lore/{view_map.kt => ViewMap.kt} | 0 .../lore/{view_nav.kt => ViewNav.kt} | 0 .../mechyrdia/lore/{view_og.kt => ViewOg.kt} | 0 .../lore/{view_tpl.kt => ViewTpl.kt} | 0 .../lore/{views_error.kt => ViewsError.kt} | 0 .../lore/{views_lore.kt => ViewsLore.kt} | 14 +- .../lore/{views_prefs.kt => ViewsPrefs.kt} | 0 .../lore/{views_quote.kt => ViewsQuote.kt} | 0 .../lore/{views_robots.kt => ViewsRobots.kt} | 0 .../lore/{views_rss.kt => ViewsRss.kt} | 10 +- .../{resource_bodies.kt => ResourceBodies.kt} | 0 .../{resource_csrf.kt => ResourceCsrf.kt} | 0 ...resource_handler.kt => ResourceHandler.kt} | 0 ...urce_multipart.kt => ResourceMultipart.kt} | 0 .../{resource_types.kt => ResourceTypes.kt} | 0 52 files changed, 240 insertions(+), 186 deletions(-) delete mode 100644 .gitattributes rename src/jvmMain/kotlin/info/mechyrdia/auth/{session_storage.kt => SessionStorage.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/auth/{views_login.kt => ViewsLogin.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/data/{data_files.kt => DataFiles.kt} (93%) rename src/jvmMain/kotlin/info/mechyrdia/data/{view_comments.kt => ViewComments.kt} (99%) rename src/jvmMain/kotlin/info/mechyrdia/data/{views_comment.kt => ViewsComment.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/data/{views_files.kt => ViewsFiles.kt} (92%) rename src/jvmMain/kotlin/info/mechyrdia/data/{views_user.kt => ViewsUser.kt} (100%) delete mode 100644 src/jvmMain/kotlin/info/mechyrdia/data/data_flow.kt delete mode 100644 src/jvmMain/kotlin/info/mechyrdia/data/data_utils.kt rename src/jvmMain/kotlin/info/mechyrdia/lore/{april_1st.kt => April1st.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{article_listing.kt => ArticleListing.kt} (85%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{article_titles.kt => ArticleTitles.kt} (87%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{asset_caching.kt => AssetCaching.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{asset_compression.kt => AssetCompression.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{asset_hashing.kt => AssetHashing.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{file_data.kt => FileData.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{http_utils.kt => HttpUtils.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_builder.kt => ParserBuilder.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_html.kt => ParserHtml.kt} (100%) create mode 100644 src/jvmMain/kotlin/info/mechyrdia/lore/ParserJson.kt rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_lexer.kt => ParserLexer.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_lexer_async.kt => ParserLexerAsync.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_plain.kt => ParserPlain.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_preprocess.kt => ParserPreprocess.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_preprocess_include.kt => ParserPreprocessInclude.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_preprocess_json.kt => ParserPreprocessJson.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_preprocess_math.kt => ParserPreprocessMath.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_raw.kt => ParserRaw.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_tree.kt => ParserTree.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{parser_utils.kt => ParserUtils.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{view_bar.kt => ViewBar.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{view_map.kt => ViewMap.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{view_nav.kt => ViewNav.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{view_og.kt => ViewOg.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{view_tpl.kt => ViewTpl.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{views_error.kt => ViewsError.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{views_lore.kt => ViewsLore.kt} (92%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{views_prefs.kt => ViewsPrefs.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{views_quote.kt => ViewsQuote.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{views_robots.kt => ViewsRobots.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/lore/{views_rss.kt => ViewsRss.kt} (97%) rename src/jvmMain/kotlin/info/mechyrdia/route/{resource_bodies.kt => ResourceBodies.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/route/{resource_csrf.kt => ResourceCsrf.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/route/{resource_handler.kt => ResourceHandler.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/route/{resource_multipart.kt => ResourceMultipart.kt} (100%) rename src/jvmMain/kotlin/info/mechyrdia/route/{resource_types.kt => ResourceTypes.kt} (100%) diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 841b1e0..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.pdn binary diff --git a/libs/nsapi4j.jar b/libs/nsapi4j.jar index 4d7b198d75f25fc19bd5609a20476322f9c4a590..a80a0c13c6987c49d7c142a8b7131434a467892c 100644 GIT binary patch delta 21732 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 delta 22209 zcmZu(2Ut@{*EW+72sMP<8$p^4DJmd>h$7eDW$oQvQBjU-#a&%{TYK+o z$GY}{y`iXUSsVWE+)2XC_xwIj4Cg)XoH=u*+-W!aPPRXHrv2Ehn$Y@VD5i9S7K<{X zOK1ue`ht&lp+bfF^y@mfcegS<`u@6j(wj<02R z|J5O-Q`$W{+^17`vniR$MIU6}eRMb|C%xQ|@Dla5HrW4R$;c5mMvvORPa9vmlyj}n zJHK6D9=RtxbK>U{HMX=`68f$C_)^hhEE99rQ{ng{aYYAuFVg?$?lC!Fa{I|4#{-X7 zIj%Wg^tgBJh$(T)-W8voTKCBKu4d=N6~(I^x|$e0n67;7@NWGAx3#~7JfyzWuJ`K_ zQ#drSRp#tPRocdcG`m#JusG&xcAtE?+Oln%XIy?TX5_M|N3Wea`7XUsubN}x=iD&) zMeJKv|Ci(W#v2&^wn>=pk1)etI4`L}!=^0e0%4R|U^ zG;4vRqd&Jxer!MrRow%mQGJbx_GZZLLidU5q8n1DZBh`uN|49s$6xwuOT)V6A^p|pikASJibIMXlPY(9qW zMsDNV3LpDlku1XX(HuU|ju%U&O}nLdwl+<0411(F=GTR5TCuKO1(?!B<4RW-N=CZ8 z2MMor6Y#;Vn!U+!%v{I8Z(5c($h7b^jj;7 zHNF*RXp9~O^*Z_&B`^W>%YG?JdvX<|6_PKtU4#03xgTbt*9#y0)@i(GI%koRFJl`T7IH)*74hY^pnXOu>m z(TJRBXEBS3UY|hwd~z5?aKA){j!HiC^n%8ZqK<&s54(k3y$;!ga+c{V1CI z@#ow~Lw52N2b;0WuZ7V~c^V)3^%$Hv{YD^Xjv;0qAB7P1x0J|AeHH@TbR}{}Yfv&- z__ySzsZQRJW@F-b(m~$G}CM=S=-Z~HmhGi^`rx@(IW~Ne}Tb{ zrBL>1R6*sYBIDX;q-gDr%@xNYXtjsU+|?FL-9`Lc6}ax4*^=o+H&CiVdlq*8N#pjK zX`6IqeDH~*b!TDtc`vK#n22y~(5G>uCSp-}rYheg1-8lzkmTkh?RDdyWw@ zE#FB;eQ!u6%6HPbP{a-BW*8iG)!1w|N5n)oZ(wxD@le4B-L*b+<_}wgt9ujm_0Cfe z&U--Dzo;z|3%RbSkI)7GqBYQ|H}LLX4mY(}ZQj9Hm%hzb=%%-z%Lq{EzU`F#ML@Q5 zopTAHOC6+jqt~}k_-+w)whwKrs&`X)(C0Ri)Qh$g%(Uq?l1!_hGSWXzYoV;Wh-~y7 zu>VW2v+J``RfTZyj#Pv7Z(!G$DJ@0oL1!6yYrVTjb+xI=;{GODe{M0HJ#VH;vT28{ zl*Sw+S=UyTq+X7c#B$o(*$87dy`Iw6*y4ODO>1;7c)ELawo}dY9Xd*iwuL?X9;{F3 zt|IigCsklO`>K$$92qoNAc&lMlCD*x46FQ|fUr#{{ z606wq{mKb7ezg;15ue5n01ukYVVRN_xJ%UVqHLgL4=^1t&--84_z;iQ`k4xx5#{Vc zX%D69Y{&cpU_}yH5m|{hJBG+j#hS}Q%@vccJvt6g^}k|jE;{^2%_pynHM9BkeMep_;k^6X+ z>sqte>w*(>O=}{r7ihbqZwRE_3*>tCrV#eLK&{=sEd;pWLn*y&_7ZYougG1Y8=5P{ zknSaB0cUejj{JuLF7*ia|pqh6xXKYuC&rG!d(zPyB;CeJzSKQCA=OpTstovC6A zFJiLTRZ0C4aL{rZI02cdx> zJ^J4Ans&A?ox5lw9^%A5YYW}$_vo=#>I-2>ijy0qen3$Z8VF?Z2dTO?sHu+bp3p^- z_Oeu%!aw36xVO0i`BK_PDM9C2VEih9wb{RpuNtW@`16S-W2``Q~FnZNW&*r=X>i)+b4KBI`;`aZ|qB1 zdbu10d`8de+>Z+-s8L^?kBYe9Gl-u3?K%T(kFa=Ava_tC5723!4c75VpFc%@kt(s6 zalD)|rJltz?bk0-RW^3K3h6mcXQB-nJZdJ5cW73pCfHdf+A|(??cc{1--L+{)G?Fn zR5#l275#4GWEFzZr!q^Qrb7C03%HZM~!nw6ysoc9&LvgHC8`v(G7M+3zFPk|_%&0~d#dN`}I(hk>V z!&cb^6ZKfBGgIZa=opij?8ok}=Dn!}tFwu7l61?O z)4sW;H2D&A_bLW~eKPR+cO8t+%Avkaay?e`A?#z}zNT-*ZvQJN_wO+2t>OJGF zxFf6ensbCz4Nbs8GyA{V8b&T?f<5RG+_i&26L5|no0iXYe+J^wC7ZU#A? zy~Q+?vv290Ov1gX1_bP}3y1MUHK5WCV)Dra+i>hoU*4fpG;jrRv8PHm%@x^flN3a0 zN;)?=n&n_ZiR21!TXu4$`E6`XWV{>#tOlY0E!<$)Y! zOi8WD4ScV}Tl5oyCv>iHR%|`D@otI zQ2OXb6k(JbF^|SH%807#-h_@D(Rh-YlD9V+$(rW;JCJ62%XPI?TRD+iOQ+JJ-enV- zW$iXjZlWNwyZ7vY?7O4Cv|WlW1g-sAApOIr;l7lyQHfEMN%Ihxjakl zMjA8L%=>)gYHTq^YqQ*jy%{3Fp=P-%Ydb;+RfaoRSJfYpfJOhz;c!Zn(a@Ax_hkz#UEj!I%%`q6d6T&?T6Ebh0%=nimGBd`+PJ#mHZ;M$ zFHrqw3M19A69WEHSdL>GPpM3_^o5CrX9cp@SB_<|=Y;UY7j5?3c_FMl=Mu@s~BP_9B7GSK*r1)?Y;UUOtLoBNj2CbQq(b2*U4zHl+QI>LpRe4+OGhG_g8jPW$QD6*OOU33vFvn_o2 z8P47oq5&b8Tx7ZE4Yao?YVZf9LKHT^YE4a8^L6bzAi({Z|6D0@GNEZk4L4%#_O9yYG>^a3SA$f0bU?rf~cE`(>z|&@* zy9&VyD45oifX|7Z0=``W-R+x~Rq)jNC_6TpC1JWvN$~3W3DvJ9(d?Z4h491=yq&vI zetify4~lU(fRaMwC^iqX*ATfGtBy%3_vB89T!}r45(47sLJ6fnD_&k86G|am<_bc% zR0<7!eI+6MnxHRCBjYXZiZgXXkaYx%d1a3FjdDF~6#XvprPg@87kz{19Ip%mpu%e7g%aYE=@ z4g*o033kDQFp0I$r*gb-lT!;Jb7`X?# z{)5adF8>)oFh1sXY0*;uh2*>Vgc8jC*a>=;n_!01S}z| zvt~=V?QrTBCs$|i=k*m7ibRzL>j z*9i9cN;qyZ#v^^{^#Tc62TzdHNb}-BoV1ZP$IIPW)xWs9JTND`Bdi023?r3Ej*DJj3T66xrYaF_~33A~dKCXX;Q%uEjQF z3pM;oVrdu1yRzJqb-v7L{+XZ9QVQDMKX)AJ+z)3!g5*gacfrKotdahE;Xq1!SwM=) z1xXo88>5K3kENfSN&Q&d(sZ%nB*JQo3%sXf}D`B=H20GmYhO#((xR?r7 z2ICo#&gzl-e3T%hCZR4zB&yg4lF&MD)>9$bb)AirHW-DdnG7d}x8k}1ETWy@yibOoUs)8T?gHWBTih2H(zCt+I$GI?hHbd#ss$rxnHoy)WmE?4A z54KuPRFi$#)Ja?yN@-J_J;}E^{Ovc*juhi{Y^0slkd&GrCb83e|_ZEs2-enmPlC*u!{P%9-=|j>3lu8 zv-Wrm7n)t&r4r}i@VvI5?5~g6t4BS%fX&tvSpK!8l21_@*21N*u)H?~b8xAn3w5gI z5=`IQyLeNt;udcz(m?jrmg}l`Qi=|C#^_e6i;Ibp>cO3f4d6~o>}fQRyRhE|^6zNs z+W^r@X$XyB3`a`RuMOqO+DqfCrP)8$MG|?gnd+iGddr(;%_V25OBju5gn6BNdI97` z73;zBe~nBm&)t}S%bxrlTpq0_F~V72GbSWeWA-E1vxA1=l1 zuqlC$C!Z^rmuxcK=O9s!T4;kS6yw>Y}lsG7aTY!+Tcdqh@lncIiW_)T}wAyeC%a z2$vq@TBUL=AeDV>m6mZS?VVLJwuI#SpH&*eCH*%=s!Vw;X1h2nV^~ z86$+Kb`V;}2_cIMCo2lU*dBr@Q3zwXP&!!%ues2tx)9PjK}Vo{;4}ztn+ai78n!K(v{nQ>K%A&@D}%Ldm+6GIRJy(3oxkXUn;4z&gc{jN zAZclalA;rg7-CQl;s=HnSX({B;44U@I?H~V+N9}%hb=l$GD{sJ?Bt|7yHj`<%w|_% z#mR$jZ0oG2XxKl+u;zp@rPw3?Fu2p*#ReC?8*8LRTz@v}fAn{zV&DCnqWb-RRDHkW z#7=ppclZHGXD=_%FZ=y$&P{L@xKFRUq! zW7*Bw=X)U@J+bVzCRI*L!dcPOQ~-f|3UcUh=YFQM$%zYS+|uhjp1zLF8axJTEKVQdtaKgZdBbho|%3 zw>cb1WBS35^t*y{x1XG(IZU(r%cZG)e-tL_zEJGzkBQi)2SRvx-(cZKN#ShQBaX<_ z|1p~3!U32+&VFJ?N|D!3;Ea7{NBjuufeNh03l$PE)?GPEwJnNr9U|NArHYz35Q%H% zsgNqK49c9#NU^US(v5hdVtEZhx^@4mkb)({{6Prgg7+$&>pjUob1(w8^rH$*9gKdu z`IA6q{f8czKit}cE)0f=me?pjBr=kX-c z_rirhUPh!UO1*rs>7+QZqlK$_`2C8zfmW78Gk->lp4d$wN^D;KjLpG8Jvq{oEf{F$ z>S@+buD&7&Q^z`3JUh;jqL!54*GqSxrq7~rW3VwU%@s+;jKwZraE2@9g=0~J zE9cwc`fT?fR>2ACFJm#DB>t&Fx{X8Qys=Rrr#86yhzxzVJ7m~lhafcE?&?7i*K7-` zuw4$UzPkmh!!B1h`tLM$GJYSAhFszRhXW~RJnCfiAtB5<=o%;%M*}CI@?wtKVQ<<$ z0X}W}TZODYhI|XwL+_6}T}K2mDbL-cGb8kNM7xh z(D$Drr?Rzogpe~uZpUWb6GGH9od3{RNAp$)5_4%y~G5KJg2N^b{vLv`7xgNcPfE zlFrSQ5`RVe9qCel_H}kM&?1hmF(@b{w{SCM&P3jx?gg~8rf%31Y2c=(5i>D?x#KCg z!Eex@b39=9**?^-(vb~DH+4sD=>7j$X=8L#N*~S!nG3R|!9Ivy>FEf{+jIowo-B0d zB{zRcodqY~6;@?DeF`QrN}eVB9P;c@j`IFi~fHp`Uj&% zagJf6xH)jALWu$rwxL~U;v6h^Ux)k;`kbR{!u|(M%>a6;v;!K?3{?sM#MqBY&xMm4 zt175c6|^ZCp-P+$Cw9+;6NPKoQ=p!ku-y&qGTCa$$O~np8mVqZs+@^Kq-l*ENPgs< zi3;x1%mFP)3wh*MW+IoyEfp2_pa_q=k<0|RPHQ`CqGvPFu)DW$i2VAtf}k{vjcsh6 zzvGFEcTnl-&W8hY(iB8#o=WMLc0`3_&qsv}?qVhXhOShvn*$G9yj|Tep~{3?yMBXZ z-|qjTS6XV}1<2xQ&jM9YkQ_ow7a+mg{=zzM>-6seguE>_D$!G@-cN2plow(hP=*e6 zDBa#6oK%teFG2^qyAa306GqvQM$9sv3zONY$^5$+r7yzVY0F~dH)j?{;;Gj{AOTB& zoF^)}6!Ww0OVCYAE)_z+61M>Qc?pW;v=kie?;IY?&adI$v6Q{q%`DV&*E#rgeZ5Fu zIZLj=E^ZRSyW_HtvJrA33$||kB~ToXMzgGKTqsM=LU8j0I~GnfV;MGsx*g(PhESvB z*r-eT9fzWKmZ8zc9(9B(vEe6GNWT-PvwE9s6Yf(Ep5>iZF@OF8CG-CSCi`YP!pVH9 zY|Gn&-u{7_>w43UB(lrc_p`d@LFezfdD6M%XeDh`b`| zIudFy_Y@VfX%iY!g~kHO*^I--GO6wXbY{P8hV`MTLuFKIuA)xgj7{+1wsxd8^XR0q zZA^1_r+$ASyfwNy!cLUZ#odS3b~r2kqoYnoEw^AY+qkz1p4k^yc~5OYBxdw;g#AU6 z2_N9#Wxt;sS$aCK6*dA{vxA=&>I&=#VL~ScEqjhI01J|!5zke ziQijkO&k*p&9ms9dOsjBP3J|#^?gVG+YyoeY zur6~KTK?$y9EhN^yJT1Dy9+I+(?UCft1&BtP<}VK`KyKCwMUj{ z{chBA=Ph<5g>~O<7bIG|2Wzv0y(%PlFODIMhg8U_y@-F=!*;|>e)|yAU<@4lF!lyM zbvM)W#klgp4?kDzgZuUHlE^-}8%xO*iIQKSL3G@YN-zJP-Ha<8+>d5d)rT%z!m9hX z188|s2e3_4MzSM`EK#-#*xfjYeD?%X)q`?p*1ZJ(#&*L&IbO4k1{{Ks)g?TlwJQr;w)~2Kj50UFAtR zhvCD6YQkvZ5%^oJrV!$4c=*#R1VYTbCfD~+k7lRwRvCBqKz$E0H9Ufvu6z`>E2av# zOe1)%EGd_B_-YdYpK0Y`pm#?Rr)#b4h?gJ^IR;{rP6AHbj#-y-ds-QMZ*tv(Zo=

8nc4;=d+$AiFs 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 { init { @@ -62,6 +56,10 @@ value class StoragePath(val elements: List) { operator fun div(element: String) = this / element.split('/') operator fun div(elementCollection: Iterable) = StoragePath(elements + elementCollection.filterNot(String::isEmpty)) + operator fun contains(path: StoragePath) = elements.mapIndexed { i, element -> + path.elements.getOrNull(i) == element + }.all { it } + override fun toString(): String { return elements.joinToString(separator = "/") } @@ -83,8 +81,6 @@ enum class StoredFileType { FILE, } -data class StoredFileEntry(val name: String, val type: StoredFileType) - data class StoredFileStats( val updated: Instant, val size: Long, @@ -97,7 +93,7 @@ interface FileStorage { suspend fun createDir(dir: StoragePath): Boolean - suspend fun listDir(dir: StoragePath): List? + suspend fun listDir(dir: StoragePath): Map? suspend fun deleteDir(dir: StoragePath): Boolean @@ -154,7 +150,7 @@ interface FileStorage { private class FlatFileStorage(val root: File) : FileStorage { private fun resolveFile(path: StoragePath) = if (path.isRoot) root else root.combineSafe(path.toString()) - private fun renderEntry(file: File) = StoredFileEntry(file.name, if (file.isFile) StoredFileType.FILE else StoredFileType.DIRECTORY) + private fun renderEntry(file: File) = file.name to (if (file.isFile) StoredFileType.FILE else StoredFileType.DIRECTORY) private fun createDir(file: File): Boolean { if (file.isFile) return false @@ -199,8 +195,8 @@ private class FlatFileStorage(val root: File) : FileStorage { return withContext(Dispatchers.IO) { createDir(resolveFile(dir)) } } - override suspend fun listDir(dir: StoragePath): List? { - return withContext(Dispatchers.IO) { resolveFile(dir).listFiles()?.map { renderEntry(it) } } + override suspend fun listDir(dir: StoragePath): Map? { + return withContext(Dispatchers.IO) { resolveFile(dir).listFiles()?.associate { renderEntry(it) } } } override suspend fun deleteDir(dir: StoragePath): Boolean { @@ -363,20 +359,20 @@ private class GridFsStorage(val table: DocumentTable, val bucket: G return true } - override suspend fun listDir(dir: StoragePath): List? { + override suspend fun listDir(dir: StoragePath): Map? { val prefixPath = toPrefixPath(dir) val allEntries = getPrefix(dir).map { val subPath = it.path.removePrefix(prefixPath) if (subPath.contains('/')) - StoredFileEntry(subPath.substringBefore('/'), StoredFileType.DIRECTORY) + subPath.substringBefore('/') to StoredFileType.DIRECTORY else - StoredFileEntry(subPath, StoredFileType.FILE) - }.toList().distinctBy { it.name } + subPath to StoredFileType.FILE + }.toList().toMap() if (allEntries.isEmpty()) return null - return allEntries.filter { it.name != GRID_FS_KEEP } + return allEntries.filterKeys { it != GRID_FS_KEEP } } override suspend fun deleteDir(dir: StoragePath): Boolean { diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt b/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt index eaf60e0..21de217 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import kotlin.system.exitProcess -private fun printUsage(): Nothing { +fun printUsage(): Nothing { println("Usage: ") println("Both arguments are of either following format:") println(" gridfs - use GridFS (database connection indicated by config.json)") @@ -19,7 +19,7 @@ private fun printUsage(): Nothing { exitProcess(-1) } -private fun String.parseStorage(): FileStorageConfig { +fun String.parseStorage(): FileStorageConfig { val configuration = Configuration.Current return if (this == "config") @@ -49,12 +49,12 @@ private suspend fun migrateDir(path: StoragePath, from: FileStorage, into: FileS val inDir = from.listDir(path) ?: return listOf("[Source Error] Directory at /$path does not exist") return coroutineScope { - inDir.map { entry -> + inDir.map { (name, type) -> async { - val entryPath = path / entry.name - when (entry.type) { - StoredFileType.FILE -> migrateFile(entryPath, from, into) - StoredFileType.DIRECTORY -> migrateDir(entryPath, from, into) + val subPath = path / name + when (type) { + StoredFileType.FILE -> migrateFile(subPath, from, into) + StoredFileType.DIRECTORY -> migrateDir(subPath, from, into) } } }.awaitAll().flatten() @@ -66,19 +66,23 @@ private suspend fun migrateRoot(from: FileStorage, into: FileStorage): List + inRoot.map { (name, type) -> async { - val entryPath = StoragePath.Root / entry.name - when (entry.type) { - StoredFileType.FILE -> migrateFile(entryPath, from, into) - StoredFileType.DIRECTORY -> migrateDir(entryPath, from, into) + val subPath = StoragePath.Root / name + when (type) { + StoredFileType.FILE -> migrateFile(subPath, from, into) + StoredFileType.DIRECTORY -> migrateDir(subPath, from, into) } } }.awaitAll().flatten() } } -fun main(args: Array) { +fun interface FileStorageMigrator { + suspend fun migrateRoot(from: FileStorage, into: FileStorage): List +} + +fun doMigration(args: Array, migrator: FileStorageMigrator) { if (args.size != 2) { println("Invalid number of arguments ${args.size}, expected 2") printUsage() @@ -98,7 +102,7 @@ fun main(args: Array) { val fromStorage = FileStorage(from) val intoStorage = FileStorage(into) - migrateRoot(fromStorage, intoStorage) + migrator.migrateRoot(fromStorage, intoStorage) } if (errors.isEmpty()) @@ -109,3 +113,7 @@ fun main(args: Array) { println(error) } } + +fun main(args: Array) { + doMigration(args, ::migrateRoot) +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFilesSerial.kt b/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFilesSerial.kt index 784b6bc..231c609 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFilesSerial.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFilesSerial.kt @@ -2,35 +2,6 @@ package info.mechyrdia.data -import info.mechyrdia.Configuration -import info.mechyrdia.FileStorageConfig -import kotlinx.coroutines.runBlocking -import kotlin.system.exitProcess - -private fun printUsage(): Nothing { - println("Usage: ") - println("Both arguments are of either following format:") - println(" gridfs - use GridFS (database connection indicated by config.json)") - println(" config - storage indicated in config file") - println(" file: - use flat-file storage") - exitProcess(-1) -} - -private fun String.parseStorage(): FileStorageConfig { - val configuration = Configuration.Current - - return if (this == "config") - configuration.storage - else if (this == "gridfs") - FileStorageConfig.GridFs - else if (startsWith("file:")) - FileStorageConfig.Flat(removePrefix("file:")) - else { - println("Invalid format for argument value $this") - printUsage() - } -} - private suspend fun migrateFile(path: StoragePath, from: FileStorage, into: FileStorage): List { println("[Message] Starting transfer of /$path") @@ -48,11 +19,11 @@ private suspend fun migrateDir(path: StoragePath, from: FileStorage, into: FileS val inDir = from.listDir(path) ?: return listOf("[Source Error] Directory at /$path does not exist") - return inDir.flatMap { entry -> - val entryPath = path / entry.name - when (entry.type) { - StoredFileType.FILE -> migrateFile(entryPath, from, into) - StoredFileType.DIRECTORY -> migrateDir(entryPath, from, into) + return inDir.flatMap { (name, type) -> + val subPath = path / name + when (type) { + StoredFileType.FILE -> migrateFile(subPath, from, into) + StoredFileType.DIRECTORY -> migrateDir(subPath, from, into) } } } @@ -61,43 +32,15 @@ private suspend fun migrateRoot(from: FileStorage, into: FileStorage): List - val entryPath = StoragePath.Root / entry.name - when (entry.type) { - StoredFileType.FILE -> migrateFile(entryPath, from, into) - StoredFileType.DIRECTORY -> migrateDir(entryPath, from, into) + return inRoot.flatMap { (name, type) -> + val subPath = StoragePath.Root / name + when (type) { + StoredFileType.FILE -> migrateFile(subPath, from, into) + StoredFileType.DIRECTORY -> migrateDir(subPath, from, into) } } } fun main(args: Array) { - if (args.size != 2) { - println("Invalid number of arguments ${args.size}, expected 2") - printUsage() - } - - val (from, into) = args.map { it.parseStorage() } - if (from == into) { - println("Cannot migrate storage to itself") - printUsage() - } - - val errors = runBlocking { - System.setProperty("logback.statusListenerClass", "ch.qos.logback.core.status.NopStatusListener") - - ConnectionHolder.initialize(Configuration.Current.dbConn, Configuration.Current.dbName) - - val fromStorage = FileStorage(from) - val intoStorage = FileStorage(into) - - migrateRoot(fromStorage, intoStorage) - } - - if (errors.isEmpty()) - println("Successful migration! No errors encountered!") - else { - println("Migration encountered ${errors.size} ${errors.size.pluralize("error")}") - for (error in errors) - println(error) - } + doMigration(args, ::migrateRoot) } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/view_comments.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt similarity index 99% rename from src/jvmMain/kotlin/info/mechyrdia/data/view_comments.kt rename to src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt index 09e1e03..e76dfd1 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/view_comments.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt @@ -35,7 +35,7 @@ data class CommentRenderData( comments.map { comment -> async { val nationDataAsync = async { nations.getNation(comment.submittedBy) } - val pageTitleAsync = async { (StoragePath.articleDir / comment.submittedIn.split('/')).toFriendlyPathTitle() } + val pageTitleAsync = async { (StoragePath.articleDir / comment.submittedIn).toFriendlyPathTitle() } val htmlResult = comment.contents.parseAs(ParserTree::toCommentHtml) CommentRenderData( diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt rename to src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/views_files.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt similarity index 92% rename from src/jvmMain/kotlin/info/mechyrdia/data/views_files.kt rename to src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt index 3c672a9..3384d4c 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/views_files.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt @@ -13,22 +13,35 @@ import io.ktor.server.application.* import io.ktor.server.html.* import io.ktor.server.plugins.* import io.ktor.server.response.* -import io.ktor.utils.io.jvm.javaio.* import kotlinx.coroutines.* import kotlinx.html.* +private fun Map.sortedAsFiles() = toList() + .sortedBy { (name, _) -> name } + .sortedBy { (_, type) -> type } + private sealed class TreeNode { data class FileNode(val stats: StoredFileStats) : TreeNode() data class DirNode(val children: Map) : TreeNode() } +private val TreeNode.sortIndex: Int + get() = when (this) { + is TreeNode.FileNode -> 1 + is TreeNode.DirNode -> 0 + } + +private fun Map.sortedAsFiles() = toList() + .sortedBy { (name, _) -> name } + .sortedBy { (_, node) -> node.sortIndex } + private suspend fun fileTree(path: StoragePath): TreeNode? { return FileStorage.instance.statFile(path)?.let { TreeNode.FileNode(it) } ?: coroutineScope { - FileStorage.instance.listDir(path)?.map { entry -> + FileStorage.instance.listDir(path)?.map { (name, _) -> async { - fileTree(path / entry.name)?.let { entry.name to it } + fileTree(path / name)?.let { name to it } } }?.awaitAll() ?.filterNotNull() @@ -39,12 +52,7 @@ private suspend fun fileTree(path: StoragePath): TreeNode? { context(ApplicationCall) private fun UL.render(path: StoragePath, childNodes: Map) { - val sortedChildren = childNodes.toList().sortedBy { it.first }.sortedBy { - when (it.second) { - is TreeNode.FileNode -> 1 - is TreeNode.DirNode -> 0 - } - } + val sortedChildren = childNodes.sortedAsFiles() for ((name, child) in sortedChildren) render(path / name, child) @@ -92,13 +100,13 @@ private fun UL.render(path: StoragePath, node: TreeNode) { when (node) { is TreeNode.FileNode -> li { a(href = href(Root.Admin.Vfs.View(path.elements))) { - +path.elements.last() + +path.name } } is TreeNode.DirNode -> li { a(href = href(Root.Admin.Vfs.View(path.elements))) { - +path.elements.last() + +path.name } ul { render(path, node.children) @@ -225,9 +233,9 @@ suspend fun ApplicationCall.adminPreviewFile(path: StoragePath) { private suspend fun fileTreeForCopy(path: StoragePath): TreeNode.DirNode? { return coroutineScope { - FileStorage.instance.listDir(path)?.map { entry -> + FileStorage.instance.listDir(path)?.map { (name, _) -> async { - fileTreeForCopy(path / entry.name)?.let { entry.name to it } + fileTreeForCopy(path / name)?.let { name to it } } }?.awaitAll() ?.filterNotNull() @@ -255,7 +263,7 @@ private fun UL.renderForCopy(fromPath: StoragePath, intoPath: StoragePath, node: suspend fun ApplicationCall.adminShowCopyFile(from: StoragePath): HTML.() -> Unit { if (FileStorage.instance.statFile(from) == null) throw NoSuchElementException("File does not exist") - + val tree = fileTreeForCopy(StoragePath.Root)!! return adminPage("Copy File /$from") { @@ -357,7 +365,7 @@ suspend fun ApplicationCall.adminMakeDirectory(path: StoragePath, name: String) } suspend fun ApplicationCall.adminConfirmRemoveDirectory(path: StoragePath) { - val entries = FileStorage.instance.listDir(path)?.sortedBy { it.name }?.sortedBy { it.type } + val entries = FileStorage.instance.listDir(path)?.sortedAsFiles() if (entries == null) respond(HttpStatusCode.Conflict) else @@ -368,10 +376,10 @@ suspend fun ApplicationCall.adminConfirmRemoveDirectory(path: StoragePath) { strong { +"It, and all of its contents, will be gone forever!" } } ul { - for (entry in entries) + for ((name, type) in entries) li { - +entry.name - if (entry.type == StoredFileType.DIRECTORY) + +name + if (type == StoredFileType.DIRECTORY) +"/" } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/views_user.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsUser.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/views_user.kt rename to src/jvmMain/kotlin/info/mechyrdia/data/ViewsUser.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/data.kt b/src/jvmMain/kotlin/info/mechyrdia/data/data.kt index c82a623..02a9671 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/data.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/data.kt @@ -28,7 +28,9 @@ import org.bson.codecs.kotlinx.KotlinSerializerCodecProvider import org.bson.conversions.Bson import java.security.SecureRandom import kotlin.reflect.KClass +import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 +import kotlin.reflect.full.findAnnotations import com.mongodb.reactivestreams.client.MongoDatabase as JMongoDatabase @Serializable(IdSerializer::class) @@ -198,6 +200,23 @@ class DocumentTable>(private val kClass: KClass) { suspend inline fun , reified R : Any> DocumentTable.aggregate(pipeline: List) = aggregate(pipeline, R::class) +suspend inline fun > DocumentTable.getOrPut(id: Id, defaultValue: () -> T): T { + val value = get(id) + return if (value == null) { + val answer = defaultValue() + if (answer.id != id) { + throw IllegalArgumentException("Default value $answer has different Id than provided: $id") + } + put(answer) + answer + } else { + value + } +} + +val KProperty.serialName: String + get() = findAnnotations(SerialName::class).singleOrNull()?.value ?: name + inline fun > DocumentTable() = DocumentTable(T::class) interface TableHolder> { diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/data_flow.kt b/src/jvmMain/kotlin/info/mechyrdia/data/data_flow.kt deleted file mode 100644 index 80f541c..0000000 --- a/src/jvmMain/kotlin/info/mechyrdia/data/data_flow.kt +++ /dev/null @@ -1,21 +0,0 @@ -package info.mechyrdia.data - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector - -fun Flow.distinct(): Flow = DistinctSetFlow(this) { it } -fun Flow.distinctBy(keySelector: (T) -> K): Flow = DistinctSetFlow(this, keySelector) - -private class DistinctSetFlow( - private val upstream: Flow, - private val keySelector: (T) -> K -) : Flow { - override suspend fun collect(collector: FlowCollector) { - val previousKeys = mutableSetOf() - upstream.collect { value -> - val key = keySelector(value) - if (previousKeys.add(key)) - collector.emit(value) - } - } -} diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/data_utils.kt b/src/jvmMain/kotlin/info/mechyrdia/data/data_utils.kt deleted file mode 100644 index 274b065..0000000 --- a/src/jvmMain/kotlin/info/mechyrdia/data/data_utils.kt +++ /dev/null @@ -1,22 +0,0 @@ -package info.mechyrdia.data - -import kotlinx.serialization.SerialName -import kotlin.reflect.KProperty -import kotlin.reflect.full.findAnnotations - -suspend inline fun > DocumentTable.getOrPut(id: Id, defaultValue: () -> T): T { - val value = get(id) - return if (value == null) { - val answer = defaultValue() - if (answer.id != id) { - throw IllegalArgumentException("Default value $answer has different Id than provided: $id") - } - put(answer) - answer - } else { - value - } -} - -val KProperty.serialName: String - get() = findAnnotations(SerialName::class).singleOrNull()?.value ?: name diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/april_1st.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/April1st.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/april_1st.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/April1st.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt similarity index 85% rename from src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt index 9a90123..e2ea11b 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt @@ -14,20 +14,20 @@ import kotlinx.html.a import kotlinx.html.li import kotlinx.html.ul -data class ArticleNode(val name: String, val title: String, val subNodes: List) +data class ArticleNode(val name: String, val title: String, val subNodes: List?) -suspend fun rootArticleNodeList(): List = StoragePath.articleDir.toArticleNode().subNodes +suspend fun rootArticleNodeList(): List = StoragePath.articleDir.toArticleNode().subNodes.orEmpty() suspend fun StoragePath.toArticleNode(): ArticleNode = ArticleNode( name, toFriendlyPageTitle(), coroutineScope { val path = this@toArticleNode - FileStorage.instance.listDir(path)?.map { - val subPath = path / it.name + FileStorage.instance.listDir(path)?.map { (name, _) -> + val subPath = path / name async { subPath.toArticleNode() } }?.awaitAll().orEmpty() - }.sortedBy { it.name }.sortedBy { it.subNodes.isEmpty() } + }.sortedBy { it.name }.sortedBy { it.subNodes == null } ) private val String.isViewable: Boolean @@ -46,10 +46,11 @@ fun List.renderInto(list: UL, base: List = emptyList(), for list.li { val nodePath = base + node.name a(href = href(Root.LorePage(nodePath, format))) { +node.title } - if (node.subNodes.isNotEmpty()) + node.subNodes?.let { subNodes -> ul { - node.subNodes.renderInto(this, nodePath, format) + subNodes.renderInto(this, nodePath, format) } + } } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/article_titles.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt similarity index 87% rename from src/jvmMain/kotlin/info/mechyrdia/lore/article_titles.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt index 1bbf15b..337126f 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/article_titles.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt @@ -5,7 +5,7 @@ import info.mechyrdia.data.StoragePath object ArticleTitleCache : FileDependentCache() { override suspend fun processFile(path: StoragePath): String? { - if (path.elements[0] != StoragePath.articleDir.elements[0]) + if (path !in StoragePath.articleDir) return null val bytes = FileStorage.instance.readFile(path) ?: return null diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/asset_caching.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCaching.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/asset_caching.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/AssetCaching.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/asset_compression.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCompression.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/asset_compression.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/AssetCompression.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/asset_hashing.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/AssetHashing.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/asset_hashing.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/AssetHashing.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/file_data.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/FileData.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/file_data.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/FileData.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/http_utils.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/HttpUtils.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/http_utils.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/HttpUtils.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_builder.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserBuilder.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_builder.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserBuilder.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_html.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserJson.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserJson.kt new file mode 100644 index 0000000..e0344dc --- /dev/null +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserJson.kt @@ -0,0 +1,125 @@ +package info.mechyrdia.lore + +import info.mechyrdia.route.KeyedEnumSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable(with = DocTextColorSerializer::class) +@JvmInline +value class DocTextColor(val rgb: Int) { + constructor(rgb: String) : this(fromStringOrNull(rgb) ?: error("Expected string of 3 or 6 hex digits with optional # prefix, got $rgb")) + + override fun toString(): String { + return "#${rgb.toString(16).padStart(6, '0')}" + } + + companion object { + fun fromStringOrNull(rgb: String): Int? { + return repeatColorDigits(rgb.removePrefix("#"))?.toIntOrNull(16) + } + } +} + +object DocTextColorSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DocTextColorSerializer", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: DocTextColor) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): DocTextColor { + return DocTextColor(decoder.decodeString()) + } +} + +@Serializable(with = DocTextFontSerializer::class) +enum class DocTextFont { + NORMAL, CODE, IPA +} + +object DocTextFontSerializer : KeyedEnumSerializer(DocTextFont.entries) + +@Serializable +data class DocTextFormat( + val isBold: Boolean = false, + val isItalic: Boolean = false, + val isUnderline: Boolean = false, + val isStrikeOut: Boolean = false, + val isSubscript: Boolean = false, + val isSuperscript: Boolean = false, + val color: DocTextColor? = null, + val font: DocTextFont = DocTextFont.NORMAL +) + +@Serializable +data class DocText( + val text: String, + val format: DocTextFormat = DocTextFormat(), +) + +@Serializable +sealed class DocLayoutItem { + @Serializable + @SerialName("textLine") + data class TextLine(val text: List) : DocBlock() + + @Serializable + @SerialName("formatBlock") + data class FormatBlock(val blocks: List) : DocBlock() +} + +@Serializable(with = ListingTypeSerializer::class) +enum class ListingType { + ORDERED, + UNORDERED, +} + +object ListingTypeSerializer : KeyedEnumSerializer(ListingType.entries) + +@Serializable +@JvmInline +value class DocTableRow( + val cells: List +) + +@Serializable +data class DocTableCell( + val isHeading: Boolean = false, + val colSpan: Int = 1, + val rowSpan: Int = 1, + val contents: DocLayoutItem +) + +@Serializable +sealed class DocBlock { + @Serializable + @SerialName("paragraph") + data class Paragraph(val contents: List) : DocBlock() + + @Serializable + @SerialName("list") + data class Listing(val ordering: ListingType, val items: List) : DocBlock() + + @Serializable + @SerialName("table") + data class Table(val items: List) : DocBlock() +} + +@Serializable +data class DocSections( + val headingText: String, + val headContent: List, + val subSections: List, +) + +@Serializable +data class Document( + val ogData: OpenGraphData?, + val sections: DocSections, +) diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_lexer.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexer.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_lexer.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexer.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_lexer_async.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_lexer_async.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_plain.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPlain.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_plain.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserPlain.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_include.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_json.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_json.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_preprocess_math.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserRaw.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_raw.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserRaw.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tree.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserTree.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_tree.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserTree.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_utils.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserUtils.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/parser_utils.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ParserUtils.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/view_bar.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewBar.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/view_bar.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewBar.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/view_map.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewMap.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/view_map.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewMap.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/view_nav.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/view_nav.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/view_og.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewOg.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/view_og.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewOg.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_error.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsError.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/views_error.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewsError.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt similarity index 92% rename from src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt index 56098fe..86aea5e 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt @@ -75,11 +75,10 @@ suspend fun ApplicationCall.loreRawArticlePage(pagePath: List): HTML.() } } - val pageType = FileStorage.instance.getType(pageFile) - val isValid = pageType != null && pageFile.isViewable + val isValid = FileStorage.instance.getType(pageFile) != null && pageFile.isViewable if (isValid) { - if (pageType == StoredFileType.DIRECTORY) { + if (pageNode.subNodes != null) { return rawPage(pageNode.title) { breadCrumbs(parentPaths) h1 { +pageNode.title } @@ -90,7 +89,7 @@ suspend fun ApplicationCall.loreRawArticlePage(pagePath: List): HTML.() } val pageMarkup = FactbookLoader.loadFactbook(pagePath) - if (pageType == StoredFileType.FILE && pageMarkup != null) { + if (pageMarkup != null) { val pageHtml = pageMarkup.toRawHtml() val pageToC = TableOfContentsBuilder() @@ -132,11 +131,10 @@ suspend fun ApplicationCall.loreArticlePage(pagePath: List, format: Lore canCommentAs.await() to comments.await() } - val pageType = FileStorage.instance.getType(pageFile) - val isValid = pageType != null && pageFile.isViewable + val isValid = FileStorage.instance.getType(pageFile) != null && pageFile.isViewable if (isValid) { - if (pageType == StoredFileType.DIRECTORY) { + if (pageNode.subNodes != null) { val navbar = standardNavBar(pagePath.takeIf { it.isNotEmpty() }) val sidebar = PageNavSidebar( @@ -160,7 +158,7 @@ suspend fun ApplicationCall.loreArticlePage(pagePath: List, format: Lore } val pageMarkup = FactbookLoader.loadFactbook(pagePath) - if (pageType == StoredFileType.FILE && pageMarkup != null) { + if (pageMarkup != null) { val pageHtml = pageMarkup.toFactbookHtml() val pageToC = TableOfContentsBuilder() diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_prefs.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsPrefs.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/views_prefs.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewsPrefs.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_quote.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsQuote.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/views_quote.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewsQuote.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_robots.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRobots.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/views_robots.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRobots.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_rss.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt similarity index 97% rename from src/jvmMain/kotlin/info/mechyrdia/lore/views_rss.kt rename to src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt index 1a53259..c89d975 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/views_rss.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt @@ -18,27 +18,27 @@ import java.time.format.DateTimeFormatter data class StoragePathWithStat(val path: StoragePath, val stat: StoredFileStats) -private suspend fun ArticleNode.addPages(base: StoragePath): List { +private suspend fun ArticleNode.getPages(base: StoragePath): List { if (!this.isViewable) return emptyList() val path = base / name val stat = FileStorage.instance.statFile(path) return if (stat != null) listOf(StoragePathWithStat(path, stat)) - else coroutineScope { + else if (subNodes != null) coroutineScope { subNodes.map { subNode -> async { - subNode.addPages(path) + subNode.getPages(path) } }.awaitAll().flatten() - } + } else emptyList() } suspend fun allPages(): List { return coroutineScope { rootArticleNodeList().map { subNode -> async { - subNode.addPages(StoragePath.articleDir) + subNode.getPages(StoragePath.articleDir) } }.awaitAll().flatten() } diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/resource_bodies.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/route/resource_bodies.kt rename to src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/resource_csrf.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceCsrf.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/route/resource_csrf.kt rename to src/jvmMain/kotlin/info/mechyrdia/route/ResourceCsrf.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/resource_handler.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/route/resource_handler.kt rename to src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/resource_multipart.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/route/resource_multipart.kt rename to src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt rename to src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt -- 2.25.1